From d077c201216d20d8152e9e5016dba6a93d6f243b Mon Sep 17 00:00:00 2001 From: Fred Porter Date: Wed, 19 Aug 2026 22:05:08 +0800 Subject: [PATCH] refactor(mcp): remove fake GridSmith server boundary --- README.md | 37 +- agents/gridsmith/README.md | 4 +- agents/gridsmith/dist/mcp/server.cjs | 2472 ------------------------ agents/gridsmith/dist/mcp/server.d.cts | 1 - agents/gridsmith/dist/mcp/server.d.ts | 1 - agents/gridsmith/dist/mcp/server.js | 280 --- agents/gridsmith/package.json | 7 +- agents/gridsmith/src/mcp/server.ts | 339 ---- config/mcp_config.json | 49 - config/openrouter.yaml | 1 - docs/AI_INTEGRATION.md | 203 -- docs/DEV_PLAN.md | 11 +- docs/GRIDSMITH_DEV_PLAN.md | 14 +- docs/UCODE_REPO_BOUNDARY.md | 32 +- docs/learning-pathway/README.md | 3 +- docs/specs/SKILLS_FRAMEWORK.md | 4 +- package-lock.json | 6 +- scripts/setup-ai-integration.sh | 328 ---- 18 files changed, 43 insertions(+), 3749 deletions(-) delete mode 100755 agents/gridsmith/dist/mcp/server.cjs delete mode 100644 agents/gridsmith/dist/mcp/server.d.cts delete mode 100644 agents/gridsmith/dist/mcp/server.d.ts delete mode 100755 agents/gridsmith/dist/mcp/server.js delete mode 100644 agents/gridsmith/src/mcp/server.ts delete mode 100644 config/mcp_config.json delete mode 100644 docs/AI_INTEGRATION.md delete mode 100755 scripts/setup-ai-integration.sh diff --git a/README.md b/README.md index 241e48a..eaf3c05 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ uDosGo code surfaces. It is designed to be consumed by host applications such as uCode internals. **uCode is not a full application GUI.** It provides the foundational runtime -packages, grid/code algebra, import/export tooling, CLI/MCP surfaces, and +packages, grid/code algebra, import/export tooling, CLI surfaces, and inspectable render targets such as terminal and teletext widgets. The advanced runtime layer is now tracked as **uCode2**. uCode2 is reserved for @@ -27,11 +27,11 @@ Application-specific runtime concerns that belong to HomeNest should live in | ---------------------------- | -------------------------------------------------- | | `packages/gridcore` | Grid algebra, code addressing, cell/layer model | | `packages/viewport-renderer` | Browser render surfaces, Terminal/Teletext widgets | -| `agents/gridsmith` | CLI/MCP/import/export/build tooling | +| `agents/gridsmith` | CLI/import/export/build tooling | | `runtimes/basic` | BASIC runtime bridge/package | | `runtimes/amos` | AMOS runtime bridge/package | | `shared` | Cross-runtime support (not host-app-specific) | -| `config/` | AI routing, MCP definitions, vault config | +| `config/` | Runtime and development configuration | | `docs/` | Specs, boundary docs, workflows | | `tests/` | Integration tests | @@ -43,8 +43,9 @@ Application-specific runtime concerns that belong to HomeNest should live in - User/session/application state - Long-running command-centre behaviour -uCore and other hosts should consume uCode through **package, CLI, MCP, or -runtime artifact boundaries**. +uCore and other hosts should consume uCode through **package, CLI, or runtime +artifact boundaries**. External MCP exposure belongs to uCore's canonical +`udos-mcp` gateway. --- @@ -56,12 +57,12 @@ uCode/ │ ├── gridcore/ Grid algebra and core grid/code primitives │ └── viewport-renderer/ Terminal/teletext/browser render surfaces ├── agents/ -│ └── gridsmith/ CLI, MCP, and import/export tooling +│ └── gridsmith/ CLI and import/export tooling ├── runtimes/ │ ├── basic/ BASIC runtime bridge/package │ └── amos/ AMOS runtime bridge/package ├── shared/ Cross-runtime support -├── config/ AI routing, MCP, vault configuration +├── config/ Runtime and development configuration ├── docs/ Specs, boundary docs, workflows ├── tests/ Integration tests └── README.md This file @@ -132,24 +133,12 @@ runtimes/ --- -## AI Integration +## External Tool Integration -uCode provides MCP tooling (via GridSmith) that can be consumed by AI agents. -For full AI provider routing (Ollama, OpenRouter, Hivemind), uCode delegates to -uCore's snackbar daemon: - -``` -GridSmith MCP → uCore Snackbar (port 8484) → LiteLLM → Ollama/OpenRouter -``` - -Configuration lives in `config/`: - -| File | Purpose | -| ------------------------ | ------------------------------- | -| `config/hivemind.env` | API keys for OpenRouter, Ollama | -| `config/openrouter.yaml` | Model routing tiers & costs | -| `config/mcp_config.json` | MCP server definitions | -| `config/vault.yaml` | Vault/secret paths | +GridSmith exposes deterministic package and CLI contracts. uCore invokes those +contracts through its GridSmith bridge and selectively exposes bounded reads +through the canonical `udos-mcp` gateway. uCode does not run an MCP server, +proxy providers, or own client configuration. ## Embedding uCode diff --git a/agents/gridsmith/README.md b/agents/gridsmith/README.md index 89df7bd..9f9b879 100644 --- a/agents/gridsmith/README.md +++ b/agents/gridsmith/README.md @@ -8,7 +8,7 @@ GridSmith is responsible for: - creating and managing grid worlds under `uCode/workspaces/gridcore` - importing BASIC and AMOS-oriented world inputs into grid artifacts -- exposing tool contracts that can later be surfaced via uCore MCP and REST +- exposing CLI tool contracts that uCore can adapt through owned APIs - consuming the live `@udos/gridcore` and `@udos/viewport-renderer` packages It is not a replacement for GridCore or GridUI. It is the orchestration layer for world-building on top of those packages. @@ -34,4 +34,4 @@ npm run build --workspace @udos/gridsmith node ./agents/gridsmith/dist/cli.js grid create --cols 80 --rows 24 node ./agents/gridsmith/dist/cli.js location latlon-to-ucode --lat -33.8688 --lon 151.2093 --level 340 node ./agents/gridsmith/dist/cli.js location ucode-to-latlon --coord L340-0A0B-0000-0 -``` \ No newline at end of file +``` diff --git a/agents/gridsmith/dist/mcp/server.cjs b/agents/gridsmith/dist/mcp/server.cjs deleted file mode 100755 index b35293e..0000000 --- a/agents/gridsmith/dist/mcp/server.cjs +++ /dev/null @@ -1,2472 +0,0 @@ -#!/usr/bin/env node -"use strict"; -var __create = Object.create; -var __defProp = Object.defineProperty; -var __getOwnPropDesc = Object.getOwnPropertyDescriptor; -var __getOwnPropNames = Object.getOwnPropertyNames; -var __getProtoOf = Object.getPrototypeOf; -var __hasOwnProp = Object.prototype.hasOwnProperty; -var __copyProps = (to, from, except, desc) => { - if (from && typeof from === "object" || typeof from === "function") { - for (let key of __getOwnPropNames(from)) - if (!__hasOwnProp.call(to, key) && key !== except) - __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); - } - return to; -}; -var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( - // If the importer is in node compatibility mode or this is not an ESM - // file that has been converted to a CommonJS file using a Babel- - // compatible transform (i.e. "__esModule" has not been set), then set - // "default" to the CommonJS "module.exports" for node compatibility. - isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, - mod -)); - -// src/mcp/server.ts -var import_node_http = require("http"); - -// src/index.ts -var import_gridcore6 = require("@udos/gridcore"); - -// src/tools/basic.ts -var import_promises = require("fs/promises"); -var import_node_path = __toESM(require("path"), 1); -function slugify(value) { - return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80); -} -function parseBasicLines(program) { - return program.split(/\r?\n/).map((line) => line.trimEnd()).filter(Boolean).map((line) => { - const match = line.match(/^(\d+)\s*(.*)$/); - if (!match) { - return { lineNumber: null, statement: line.trim() }; - } - return { - lineNumber: Number(match[1]), - statement: match[2].trim() - }; - }); -} -function classifyStatements(lines) { - const counts = { - print: 0, - data: 0, - goto: 0, - gosub: 0, - rem: 0, - other: 0 - }; - for (const line of lines) { - const statement = line.statement.toUpperCase(); - if (statement.startsWith("PRINT")) counts.print += 1; - else if (statement.startsWith("DATA")) counts.data += 1; - else if (statement.startsWith("GOTO")) counts.goto += 1; - else if (statement.startsWith("GOSUB")) counts.gosub += 1; - else if (statement.startsWith("REM")) counts.rem += 1; - else counts.other += 1; - } - return counts; -} -async function importBasicProgram(programOrPath, worldName) { - const maybeFile = import_node_path.default.resolve(programOrPath); - let source = programOrPath; - let sourceType = "inline"; - try { - source = await (0, import_promises.readFile)(maybeFile, "utf-8"); - sourceType = "file"; - } catch { - sourceType = "inline"; - } - const parsed = parseBasicLines(source); - const stats = classifyStatements(parsed); - const slug = slugify(worldName || "basic-world") || "basic-world"; - const workspaceRoot = import_node_path.default.resolve(process.cwd(), "workspaces/gridcore"); - const scriptsDir = import_node_path.default.join(workspaceRoot, "scripts/basic"); - const worldsDir = import_node_path.default.join(workspaceRoot, "worlds/libraries"); - const importsDir = import_node_path.default.join(workspaceRoot, "grids/imports"); - await (0, import_promises.mkdir)(scriptsDir, { recursive: true }); - await (0, import_promises.mkdir)(worldsDir, { recursive: true }); - await (0, import_promises.mkdir)(importsDir, { recursive: true }); - const scriptPath = import_node_path.default.join(scriptsDir, `${slug}.bas`); - const worldPath = import_node_path.default.join(worldsDir, `${slug}.json`); - const importPath = import_node_path.default.join(importsDir, `${slug}.json`); - const world = { - id: slug, - name: worldName, - type: "library", - source: "basic", - sourceType, - grid: { - cols: 80, - rows: Math.max(parsed.length, 1), - cellSize: 24 - }, - layers: ["terrain", "details", "entities"], - metrics: { - lineCount: parsed.length, - maxLineLength: parsed.reduce((max, line) => Math.max(max, line.statement.length), 0), - statements: stats - } - }; - const importArtifact = { - worldId: slug, - preview: parsed.slice(0, 24).map((line, index) => ({ - row: index, - lineNumber: line.lineNumber, - text: line.statement - })) - }; - await (0, import_promises.writeFile)(scriptPath, source, "utf-8"); - await (0, import_promises.writeFile)(worldPath, JSON.stringify(world, null, 2), "utf-8"); - await (0, import_promises.writeFile)(importPath, JSON.stringify(importArtifact, null, 2), "utf-8"); - return { - world, - files: { - script: scriptPath, - manifest: worldPath, - importArtifact: importPath - }, - summary: { - sourceType, - lineCount: parsed.length, - statements: stats - } - }; -} - -// src/tools/amos.ts -var import_promises2 = require("fs/promises"); -var import_node_path2 = __toESM(require("path"), 1); -function slugify2(value) { - return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80); -} -function parseAmosLine(line) { - const result = { - sprites: [], - bobs: [], - sounds: [], - moves: [], - rems: [], - otherLines: [] - }; - const trimmed = line.trim(); - const upper = trimmed.toUpperCase(); - const spriteMatch = upper.match(/^\s*SPRITE\s+(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*$/); - if (spriteMatch) { - result.sprites.push({ - number: Number(spriteMatch[1]), - x: Number(spriteMatch[2]), - y: Number(spriteMatch[3]), - image: Number(spriteMatch[4]) - }); - return result; - } - const bobMatch = trimmed.match(/^\s*BOB\s+(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*"([^"]*)"\s*$/i); - if (bobMatch) { - result.bobs.push({ - number: Number(bobMatch[1]), - x: Number(bobMatch[2]), - y: Number(bobMatch[3]), - image: bobMatch[4] - }); - return result; - } - const soundMatch = upper.match(/^\s*SOUND\s+(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*$/); - if (soundMatch) { - result.sounds.push({ - channel: Number(soundMatch[1]), - freq: Number(soundMatch[2]), - duration: Number(soundMatch[3]), - volume: Number(soundMatch[4]) - }); - return result; - } - const moveMatch = upper.match(/^\s*MOVE\s+(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*$/); - if (moveMatch) { - result.moves.push({ - spriteNumber: Number(moveMatch[1]), - targetX: Number(moveMatch[2]), - targetY: Number(moveMatch[3]), - steps: Number(moveMatch[4]) - }); - return result; - } - if (upper.startsWith("REM") || trimmed.startsWith("'")) { - result.rems.push(trimmed); - return result; - } - result.otherLines.push(trimmed); - return result; -} -function parseAmosProgram(program) { - const lines = program.split(/\r?\n/).map((l) => l.trimEnd()).filter(Boolean); - const merged = { - sprites: [], - bobs: [], - sounds: [], - moves: [], - rems: [], - otherLines: [] - }; - for (const line of lines) { - const parsed = parseAmosLine(line); - merged.sprites.push(...parsed.sprites); - merged.bobs.push(...parsed.bobs); - merged.sounds.push(...parsed.sounds); - merged.moves.push(...parsed.moves); - merged.rems.push(...parsed.rems); - merged.otherLines.push(...parsed.otherLines); - } - return merged; -} -async function importAmosProgram(programOrPath, worldName) { - const maybeFile = import_node_path2.default.resolve(programOrPath); - let source = programOrPath; - let sourceType = "inline"; - try { - source = await (0, import_promises2.readFile)(maybeFile, "utf-8"); - sourceType = "file"; - } catch { - sourceType = "inline"; - } - const parsed = parseAmosProgram(source); - const totalAssets = parsed.sprites.length + parsed.bobs.length + parsed.sounds.length + parsed.moves.length; - const slug = slugify2(worldName || "amos-world") || "amos-world"; - const workspaceRoot = import_node_path2.default.resolve(process.cwd(), "workspaces/gridcore"); - const scriptsDir = import_node_path2.default.join(workspaceRoot, "scripts/amos"); - const worldsDir = import_node_path2.default.join(workspaceRoot, "worlds/libraries"); - const importsDir = import_node_path2.default.join(workspaceRoot, "grids/imports"); - await (0, import_promises2.mkdir)(scriptsDir, { recursive: true }); - await (0, import_promises2.mkdir)(worldsDir, { recursive: true }); - await (0, import_promises2.mkdir)(importsDir, { recursive: true }); - const scriptPath = import_node_path2.default.join(scriptsDir, `${slug}.amos`); - const worldPath = import_node_path2.default.join(worldsDir, `${slug}.json`); - const importPath = import_node_path2.default.join(importsDir, `${slug}.json`); - const world = { - id: slug, - name: worldName, - type: "dungeon", - source: "amos", - sourceType, - grid: { - cols: 80, - rows: Math.max(totalAssets, 1), - cellSize: 24 - }, - layers: ["terrain", "details", "entities"], - assets: { - sprites: parsed.sprites, - bobs: parsed.bobs, - sounds: parsed.sounds, - moves: parsed.moves, - commentCount: parsed.rems.length - }, - metrics: { - totalLines: parsed.sprites.length + parsed.bobs.length + parsed.sounds.length + parsed.moves.length + parsed.rems.length + parsed.otherLines.length, - spriteCount: parsed.sprites.length, - bobCount: parsed.bobs.length, - soundCount: parsed.sounds.length, - moveCount: parsed.moves.length, - commentCount: parsed.rems.length - } - }; - const importArtifact = { - worldId: slug, - spriteTable: parsed.sprites, - bobTable: parsed.bobs, - soundTable: parsed.sounds, - moveTable: parsed.moves - }; - await (0, import_promises2.writeFile)(scriptPath, source, "utf-8"); - await (0, import_promises2.writeFile)(worldPath, JSON.stringify(world, null, 2), "utf-8"); - await (0, import_promises2.writeFile)(importPath, JSON.stringify(importArtifact, null, 2), "utf-8"); - return { - world, - files: { - script: scriptPath, - manifest: worldPath, - importArtifact: importPath - }, - summary: { - sourceType, - totalAssets, - sprites: parsed.sprites.length, - bobs: parsed.bobs.length, - sounds: parsed.sounds.length, - moves: parsed.moves.length - } - }; -} - -// src/tools/cell.ts -var import_gridcore = require("@udos/gridcore"); -function editCell(grid, x, y, layer, data) { - const existing = (0, import_gridcore.getCell)(grid, x, y, layer); - const coord = existing?.coord ?? `L340-${x.toString(36).toUpperCase().padStart(2, "0")}${y.toString(36).toUpperCase().padStart(2, "0")}-0000-0`; - const cell = (0, import_gridcore.createCell)(coord, x, y, layer); - if (data.char !== void 0) cell.char = data.char; - if (data.fg !== void 0) cell.fg = data.fg; - if (data.bg !== void 0) cell.bg = data.bg; - (0, import_gridcore.setCell)(grid, cell); - return { - cell: { x: cell.x, y: cell.y, layer: cell.layer, char: cell.char, fg: cell.fg, bg: cell.bg }, - previous: existing ? { x: existing.x, y: existing.y, layer: existing.layer, char: existing.char, fg: existing.fg, bg: existing.bg } : null - }; -} - -// src/tools/layers.ts -var import_gridcore2 = require("@udos/gridcore"); -function composeGridLayers(grid, layerIndices) { - const layers = []; - for (const z of layerIndices) { - const cells = []; - for (let y = 0; y < grid.rows; y++) { - for (let x = 0; x < grid.cols; x++) { - const cell = (0, import_gridcore2.getCell)(grid, x, y, z); - if (cell) { - cells.push(cell); - } - } - } - layers.push((0, import_gridcore2.createLayer)(z, cells)); - } - const composed = (0, import_gridcore2.composeLayers)(layers); - return { - layers: layers.map((l) => ({ z: l.z, cellCount: l.cells.length })), - composed, - cellCount: composed.length - }; -} - -// src/tools/uvox.ts -var import_promises3 = require("fs/promises"); -var import_node_path3 = __toESM(require("path"), 1); -var import_gridcore3 = require("@udos/gridcore"); -async function exportUvox(grid, gridId, outputPath) { - const resolved = import_node_path3.default.resolve(outputPath); - await (0, import_promises3.mkdir)(import_node_path3.default.dirname(resolved), { recursive: true }); - const cells = (0, import_gridcore3.listCells)(grid).map((cell) => ({ - x: cell.x, - y: cell.y, - layer: cell.layer, - char: cell.char ?? " ", - fg: cell.fg ?? 7, - bg: cell.bg ?? 0, - coord: cell.coord - })); - const manifest = { - format: "uvox/1.0", - gridId, - cols: grid.cols, - rows: grid.rows, - createdAt: (/* @__PURE__ */ new Date()).toISOString(), - cells - }; - const json = JSON.stringify(manifest, null, 2); - await (0, import_promises3.writeFile)(resolved, json, "utf-8"); - return { - path: resolved, - bytes: Buffer.byteLength(json, "utf-8"), - cellCount: cells.length - }; -} - -// src/tools/pathfind.ts -var import_gridcore4 = require("@udos/gridcore"); -function heuristic(a, b) { - return Math.abs(a.x - b.x) + Math.abs(a.y - b.y); -} -function neighbors(node, grid, layer) { - const dirs = [ - { dx: 0, dy: -1 }, - // up - { dx: 0, dy: 1 }, - // down - { dx: -1, dy: 0 }, - // left - { dx: 1, dy: 0 } - // right - ]; - const result = []; - for (const { dx, dy } of dirs) { - const nx = node.x + dx; - const ny = node.y + dy; - if (nx >= 0 && nx < grid.cols && ny >= 0 && ny < grid.rows) { - const cell = (0, import_gridcore4.getCell)(grid, nx, ny, layer); - if (cell && cell.char !== "#") { - result.push({ x: nx, y: ny, layer }); - } - } - } - return result; -} -function reconstructPath(cameFrom, current) { - const path5 = [current]; - let key = `${current.x},${current.y},${current.layer}`; - while (cameFrom.has(key)) { - current = cameFrom.get(key); - key = `${current.x},${current.y},${current.layer}`; - path5.unshift(current); - } - return path5; -} -function findPath(grid, startX, startY, endX, endY, layer = 0) { - const start = { x: startX, y: startY, layer }; - const goal = { x: endX, y: endY, layer }; - if (startX < 0 || startX >= grid.cols || startY < 0 || startY >= grid.rows || endX < 0 || endX >= grid.cols || endY < 0 || endY >= grid.rows) { - return { path: [], steps: 0, found: false }; - } - const openSet = [{ node: start, f: heuristic(start, goal) }]; - const cameFrom = /* @__PURE__ */ new Map(); - const gScore = /* @__PURE__ */ new Map(); - gScore.set(`${start.x},${start.y},${start.layer}`, 0); - while (openSet.length > 0) { - openSet.sort((a, b) => a.f - b.f); - const current = openSet.shift(); - if (current.node.x === goal.x && current.node.y === goal.y) { - const path5 = reconstructPath(cameFrom, current.node); - return { path: path5, steps: path5.length - 1, found: true }; - } - const currentKey = `${current.node.x},${current.node.y},${current.node.layer}`; - const currentG = gScore.get(currentKey) ?? Infinity; - for (const neighbor of neighbors(current.node, grid, layer)) { - const tentG = currentG + 1; - const nKey = `${neighbor.x},${neighbor.y},${neighbor.layer}`; - if (tentG < (gScore.get(nKey) ?? Infinity)) { - cameFrom.set(nKey, current.node); - gScore.set(nKey, tentG); - openSet.push({ node: neighbor, f: tentG + heuristic(neighbor, goal) }); - } - } - } - return { path: [], steps: 0, found: false }; -} - -// src/tools/world.ts -var import_promises4 = require("fs/promises"); -var import_node_path4 = __toESM(require("path"), 1); -var import_gridcore5 = require("@udos/gridcore"); -function createWorldManifest(id, name, type, seed, source = "generated") { - return { id, name, type, seed, source }; -} -async function createWorld(options) { - const cols = options.cols ?? 80; - const rows = options.rows ?? 24; - const slug = options.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 80); - const grid = (0, import_gridcore5.createGrid)(cols, rows); - if (options.terrain) { - for (const [coord, char] of Object.entries(options.terrain)) { - const parts = coord.split(","); - if (parts.length === 2) { - const x = Number(parts[0]); - const y = Number(parts[1]); - if (!isNaN(x) && !isNaN(y)) { - editCell(grid, x, y, 0, { char }); - } - } - } - } - const manifest = createWorldManifest(slug, options.name, options.type, options.seed); - const workspaceRoot = import_node_path4.default.resolve(process.cwd(), "workspaces/gridcore"); - const worldDir = import_node_path4.default.join(workspaceRoot, "worlds", slug); - const manifestPath = import_node_path4.default.join(worldDir, "manifest.json"); - const gridPath = import_node_path4.default.join(worldDir, "grid.json"); - await (0, import_promises4.mkdir)(worldDir, { recursive: true }); - const gridExport = { - id: slug, - cols: grid.cols, - rows: grid.rows, - cells: [...grid.cells.values()].map((c) => ({ - x: c.x, - y: c.y, - layer: c.layer, - char: c.char, - fg: c.fg, - bg: c.bg, - coord: c.coord - })) - }; - await (0, import_promises4.writeFile)(manifestPath, JSON.stringify(manifest, null, 2), "utf-8"); - await (0, import_promises4.writeFile)(gridPath, JSON.stringify(gridExport, null, 2), "utf-8"); - return { - manifest, - grid: { cols: grid.cols, rows: grid.rows, cellCount: grid.cells.size }, - files: { - manifest: manifestPath, - grid: gridPath - } - }; -} - -// src/tools/source-miner.ts -var import_node_fs = require("fs"); -var import_node_path5 = require("path"); -var import_node_fs2 = require("fs"); -var ASM_EXTENSIONS = /* @__PURE__ */ new Set([".asm", ".s", ".6502", ".a65", ".inc", ".eq", ".bbc"]); -var KNOWN_HARDWARE = { - "0xfe00": "VIA port B (user port)", - "0xfe01": "VIA port A (keyboard/sound)", - "0xfe04": "VIA timer 1 counter low", - "0xfe05": "VIA timer 1 counter high", - "0xfe08": "VIA ACR (auxiliary control register)", - "0xfe09": "VIA PCR (peripheral control register)", - "0xfe0a": "VIA IFR (interrupt flag register)", - "0xfe0b": "VIA IER (interrupt enable register)", - "0xfe40": "6845 CRTC address register", - "0xfe41": "6845 CRTC data register", - "0xfe60": "ACIA 6850 status/control", - "0xfe61": "ACIA 6850 data", - "0xfe80": "INTON (interrupt enable flip-flop)", - "0xfe84": "ROMSEL (paging register)", - "0xfea0": "System VIA IER", - "0xfec0": "ADC data high", - "0xfec1": "ADC data low", - "0xfee0": "Tube ULA data", - "0xfee1": "Tube ULA status", - "0xff00": "OSBYTE entry point", - "0xffe3": "OSBYTE indirect", - "0xffe7": "OSWORD entry point", - "0xfff4": "OSCLI entry point" -}; -var ASSET_PATH_PATTERNS = [ - { regex: /(?:^|[./])gfx[/\\]/i, type: "sprite_data" }, - { regex: /(?:^|[./])sprites?[/\\]/i, type: "sprite_data" }, - { regex: /(?:^|[./])graphics?[/\\]/i, type: "sprite_data" }, - { regex: /(?:^|[./])text[/\\]/i, type: "teletext_pages" }, - { regex: /(?:^|[./])data[/\\]/i, type: "game_data" }, - { regex: /(?:^|[./])levels?[/\\]/i, type: "level_data" }, - { regex: /(?:^|[./])maps?[/\\]/i, type: "map_data" }, - { regex: /(?:^|[./])sound(s)?[/\\]/i, type: "audio_data" }, - { regex: /(?:^|[./])music[/\\]/i, type: "audio_data" }, - { regex: /(?:^|[./])chars?[/\\]/i, type: "character_data" }, - { regex: /\.(?:bin|raw|dat|spr)$/i, type: "binary_asset" }, - { regex: /\.(?:png|bmp|gif)$/i, type: "image_asset" }, - { regex: /\.(?:wav|ogg|mp3)$/i, type: "audio_asset" } -]; -function isAsmFile(path5) { - const ext = (0, import_node_path5.extname)(path5).toLowerCase(); - if (ASM_EXTENSIONS.has(ext)) return true; - const base = path5.split("/").pop() || ""; - if (!base.includes(".") && !ext) { - return true; - } - return false; -} -function matchesTarget(path5, patterns) { - if (patterns.length === 0) return isAsmFile(path5); - return patterns.some((p) => { - try { - return new RegExp( - "^" + p.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".") + "$", - "i" - ).test(path5); - } catch { - return path5.toLowerCase().includes(p.toLowerCase().replace(/\*/g, "")); - } - }); -} -function matchesExclude(path5, patterns) { - if (patterns.length === 0) return false; - return patterns.some((p) => { - try { - return new RegExp( - "^" + p.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".") + "$", - "i" - ).test(path5); - } catch { - return path5.toLowerCase().includes(p.toLowerCase().replace(/\*/g, "")); - } - }); -} -function scanFile(filePath, fileContent, currentOrg) { - const lines = fileContent.split("\n"); - const memoryEntries = []; - const functions = []; - const structures = []; - let org = currentOrg; - let prevLabel = null; - let inDataBlock = false; - let dataBlockFields = []; - let dataBlockName = ""; - let dataBlockOffset = 0; - let addressCounter = 0; - let commentBuffer = ""; - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); - const lineNo = i + 1; - if (/^[;*\\]/.test(line)) { - commentBuffer = line.replace(/^[;*\\]+\s*/, ""); - continue; - } - const orgMatch = line.match(/^(?:ORG|\*)\s*(?:=)?\s*[$&]?([0-9A-Fa-f]+)/i); - if (orgMatch) { - org = parseInt(orgMatch[1], 16); - addressCounter = 0; - continue; - } - const equMatch = line.match(/^(\w+)\s+(?:EQU|=)\s*[$&]?([0-9A-Fa-f]+)\s*(?:[;\\]\s*(.*))?/i); - if (equMatch) { - const label = equMatch[1]; - const addr = parseInt(equMatch[2], 16); - const inlineComment = equMatch[3]?.trim(); - if (inlineComment) commentBuffer = inlineComment; - const addrHex = "0x" + addr.toString(16).padStart(4, "0"); - if (/^(PAGE|OS|VIA|SHEILA|CRTC|ACIA|ADC|TUBE|ULA|USER|INTON|ROMSEL|SYSTEM)/i.test(label)) { - commentBuffer = ""; - continue; - } - const hwDesc = KNOWN_HARDWARE[addrHex]; - if (hwDesc) { - memoryEntries.push({ - label, - address: addrHex, - type: "byte", - description: hwDesc, - confidence: 0.95 - }); - } else { - memoryEntries.push({ - label, - address: addrHex, - type: "byte", - description: commentBuffer || `${label} constant`, - confidence: commentBuffer ? 0.85 : 0.7 - }); - } - commentBuffer = ""; - continue; - } - const labelMatch = line.match(/^\.(\w+)(?:\s*$|\s*;|\s*[\\*;])/); - if (labelMatch) { - prevLabel = labelMatch[1]; - if (org !== null) { - const addr = org + addressCounter; - const addrHex = "0x" + addr.toString(16).padStart(4, "0"); - const prevLine = i > 0 ? lines[i - 1].trim() : ""; - const nextLine = i + 1 < lines.length ? lines[i + 1].trim() : ""; - const looksLikeFunction = nextLine && /^(?:LDA|LDX|LDY|STA|STX|STY|JSR|JMP|PHA|PHP|PLA|PLP|TXA|TYA|TAX|TAY|CLC|SEC|CLI|SEI|CLD|SED|CLV|INX|INY|DEX|DEY|RTS|RTI|BIT)/i.test(nextLine); - if (looksLikeFunction) { - functions.push({ - name: prevLabel, - address: addrHex, - description: commentBuffer || `${prevLabel} subroutine`, - parameters: extractParameters(lines, i + 1) - }); - } else { - memoryEntries.push({ - label: prevLabel, - address: addrHex, - type: "byte", - description: commentBuffer || `${prevLabel} data label`, - confidence: 0.75 - }); - } - } - commentBuffer = ""; - continue; - } - if (/^(?:EQUB|EQUD|EQUW|DEFB|DEFW|DEFM|EQUS)\s/i.test(line)) { - if (prevLabel && inDataBlock) { - const sizeHint = /^(EQUB|DEFB|DEFM|EQUS)/i.test(line) ? 1 : /^(EQUW|DEFW)/i.test(line) ? 2 : 4; - dataBlockFields.push({ offset: dataBlockOffset, name: prevLabel, size: sizeHint }); - dataBlockOffset += sizeHint; - } - const dataMatch = line.match(/^(?:EQUB|EQUD|EQUW|DEFB|DEFW|DEFM|EQUS)\s+(.+)/i); - if (dataMatch) { - const data = dataMatch[1]; - const quotedMatch = data.match(/^"([^"]*)"/); - if (quotedMatch) { - addressCounter += quotedMatch[1].length; - } else { - addressCounter += data.split(",").length * (/^(EQUD)/i.test(line) ? 4 : /^(EQUW|DEFW)/i.test(line) ? 2 : 1); - } - } - prevLabel = null; - commentBuffer = ""; - continue; - } - if (/^(?:SKIP|RES|DS|RMB)\s+(?:[$&])?([0-9A-Fa-f]+)/i.test(line)) { - const skipMatch = line.match(/^(?:SKIP|RES|DS|RMB)\s+(?:[$&])?([0-9A-Fa-f]+)/i); - const rawVal = skipMatch[1]; - const isHex = /^[$&]/.test(line.match(/^(?:SKIP|RES|DS|RMB)\s+([$&])/i)?.[1] || ""); - const skipBytes = isHex ? parseInt(rawVal, 16) : parseInt(rawVal, 10); - if (prevLabel) { - structures.push({ - name: prevLabel, - size: skipBytes, - fields: [] - }); - memoryEntries.push({ - label: prevLabel, - address: org !== null ? "0x" + (org + addressCounter).toString(16).padStart(4, "0") : "unknown", - type: "struct", - description: commentBuffer || `${prevLabel} data structure`, - confidence: 0.6, - length: skipBytes - }); - } - addressCounter += skipBytes; - prevLabel = null; - commentBuffer = ""; - continue; - } - if (prevLabel && !/^(?:LDA|LDX|LDY|STA|STX|STY|JSR|JMP|RTS|RTI|BNE|BEQ|BCC|BCS|BPL|BMI|BVC|BVS|CMP|CPX|CPY|ADC|SBC|AND|ORA|EOR|ASL|LSR|ROL|ROR|INC|DEC|INX|INY|DEX|DEY|PHA|PHP|PLA|PLP|TXA|TYA|TAX|TAY|CLC|SEC|CLI|SEI|CLD|SED|CLV|NOP|BRK)/i.test(line)) { - if (!inDataBlock) { - dataBlockName = prevLabel; - dataBlockFields = []; - dataBlockOffset = 0; - inDataBlock = true; - } - commentBuffer = ""; - continue; - } - if (/^(?:LDA|LDX|LDY|STA|STX|STY|JSR|JMP|RTS|RTI|BNE|BEQ|BCC|BCS|BPL|BMI|BVC|BVS|CMP|CPX|CPY|ADC|SBC|AND|ORA|EOR|ASL|LSR|ROL|ROR|INC|DEC|INX|INY|DEX|DEY|PHA|PHP|PLA|PLP|TXA|TYA|TAX|TAY|CLC|SEC|CLI|SEI|CLD|SED|CLV|NOP|BRK)/i.test(line)) { - addressCounter += line.startsWith("JSR") || line.startsWith("JMP") ? 3 : line.startsWith("BNE") || line.startsWith("BEQ") || line.startsWith("BCC") || line.startsWith("BCS") || line.startsWith("BPL") || line.startsWith("BMI") || line.startsWith("BVC") || line.startsWith("BVS") ? 2 : 1; - if (inDataBlock && dataBlockName) { - structures.push({ - name: dataBlockName, - size: dataBlockOffset, - fields: dataBlockFields - }); - inDataBlock = false; - dataBlockName = ""; - dataBlockFields = []; - } - prevLabel = null; - commentBuffer = ""; - continue; - } - prevLabel = null; - commentBuffer = ""; - } - if (inDataBlock && dataBlockName) { - structures.push({ - name: dataBlockName, - size: dataBlockOffset, - fields: dataBlockFields - }); - } - return { memoryEntries, functions, structures, org }; -} -function extractParameters(lines, startIndex) { - const params = []; - for (let i = startIndex; i < Math.min(startIndex + 20, lines.length); i++) { - const line = lines[i].trim(); - const ldaMatch = line.match(/LDA\s+#?\$?\w+/i); - if (ldaMatch && !params.some((p) => p.register === "A")) { - params.push({ register: "A", description: line }); - } - const ldxMatch = line.match(/LDX\s+#?\$?\w+/i); - if (ldxMatch && !params.some((p) => p.register === "X")) { - params.push({ register: "X", description: line }); - } - const ldyMatch = line.match(/LDY\s+#?\$?\w+/i); - if (ldyMatch && !params.some((p) => p.register === "Y")) { - params.push({ register: "Y", description: line }); - } - if (line.startsWith("RTS") || line.startsWith("JMP")) break; - } - return params; -} -function scanDirectory(rootPath, patterns, excludePatterns) { - const results = []; - function walk(dir) { - try { - const entries = (0, import_node_fs2.readdirSync)(dir); - for (const entry of entries) { - const fullPath = (0, import_node_path5.join)(dir, entry); - try { - const st = (0, import_node_fs2.statSync)(fullPath); - if (st.isDirectory()) { - if (!entry.startsWith(".") && entry !== "node_modules" && entry !== ".git") { - walk(fullPath); - } - } else { - const relPath = fullPath.replace(rootPath + "/", ""); - if (!matchesExclude(relPath, excludePatterns)) { - if (patterns.length === 0) { - if (isAsmFile(fullPath)) results.push(fullPath); - } else if (matchesTarget(relPath, patterns)) { - results.push(fullPath); - } - } - } - } catch { - } - } - } catch { - } - } - walk(rootPath); - return results; -} -function detectAssetReferences(rootPath) { - const refs = []; - const seen = /* @__PURE__ */ new Set(); - function walk(dir) { - try { - const entries = (0, import_node_fs2.readdirSync)(dir); - for (const entry of entries) { - const fullPath = (0, import_node_path5.join)(dir, entry); - try { - const st = (0, import_node_fs2.statSync)(fullPath); - if (st.isDirectory()) { - if (!entry.startsWith(".") && entry !== "node_modules" && entry !== ".git") { - walk(fullPath); - } - } else { - const relPath = fullPath.replace(rootPath + "/", ""); - for (const { regex, type } of ASSET_PATH_PATTERNS) { - if (regex.test(relPath) && !seen.has(relPath)) { - seen.add(relPath); - refs.push({ - path: relPath, - type, - description: `${type.replace(/_/g, " ")}: ${entry}` - }); - } - } - } - } catch { - } - } - } catch { - } - } - walk(rootPath); - const collapsed = {}; - for (const ref of refs) { - if (!collapsed[ref.type]) { - collapsed[ref.type] = { count: 0, type: ref.type, examples: [] }; - } - collapsed[ref.type].count++; - if (collapsed[ref.type].examples.length < 3) { - collapsed[ref.type].examples.push(ref.path); - } - } - return Object.values(collapsed).map((c) => ({ - path: c.examples[0] || rootPath, - type: c.type, - count: c.count, - description: `${c.count} ${c.type.replace(/_/g, " ")} file(s)` - })); -} -function generateRecommendations(findings, sourcePath) { - const recs = []; - const stateLabels = findings.memory_map.filter( - (m) => /score|status|state|lives|level|time|health|energy|position|x_pos|y_pos|inventory|ship/i.test( - m.label - ) && m.confidence >= 0.7 - ); - for (const entry of stateLabels.slice(0, 5)) { - recs.push({ - action: "create_lens_extractor", - target: entry.label, - priority: entry.confidence >= 0.85 ? "high" : "medium", - rationale: `Game state variable for save/load and telemetry (${entry.description})` - }); - } - const commandLabels = ["MainLoop", "Dock", "Jump", "Launch", "Start", "Init", "Title", "Death", "Win", "Lose", "Pause", "Save", "Load"]; - for (const fn of findings.functions) { - if (commandLabels.some((c) => fn.name.toLowerCase().includes(c.toLowerCase()))) { - recs.push({ - action: "create_mcp_command", - target: fn.name, - priority: "medium", - rationale: `${fn.name} is a game event trigger point` - }); - } - } - if (findings.asset_references.length > 0) { - recs.push({ - action: "create_skin_manifest", - target: sourcePath, - priority: "high", - rationale: `${findings.asset_references.length} asset type(s) found \u2014 create SKIN theme` - }); - } - if (findings.data_structures.length > 0) { - recs.push({ - action: "run_lens_craft", - target: findings.data_structures.map((s) => s.name).join(", "), - priority: "medium", - rationale: `${findings.data_structures.length} data structure(s) found \u2014 generate LENS extractors` - }); - } - return recs; -} -function sourceMiner(input) { - const patterns = input.options.target_patterns || []; - const excludePatterns = input.options.exclude_patterns || []; - const sourcePath = input.source.url; - if (!(0, import_node_fs.existsSync)(sourcePath)) { - throw new Error(`Source path not found: ${sourcePath}`); - } - const st = (0, import_node_fs2.statSync)(sourcePath); - const files = st.isDirectory() ? scanDirectory(sourcePath, patterns, excludePatterns) : [sourcePath]; - const allMemory = []; - const allFunctions = []; - const allStructures = []; - let currentOrg = null; - for (const file of files) { - try { - const content = (0, import_node_fs.readFileSync)(file, "utf-8"); - const result = scanFile(file, content, currentOrg); - allMemory.push(...result.memoryEntries); - allFunctions.push(...result.functions); - allStructures.push(...result.structures); - if (result.org !== null) currentOrg = result.org; - } catch { - } - } - const rootPath = st.isDirectory() ? sourcePath : sourcePath.split("/").slice(0, -1).join("/") || "."; - const assets = detectAssetReferences(rootPath); - const findings = { - memory_map: allMemory, - functions: allFunctions, - data_structures: allStructures, - asset_references: assets - }; - const recommendations = generateRecommendations(findings, sourcePath); - return { - skill: "Source-Miner", - version: "1.0", - executed_at: (/* @__PURE__ */ new Date()).toISOString(), - source: sourcePath, - findings, - recommendations - }; -} - -// src/tools/lens-craft.ts -var import_node_fs3 = require("fs"); -var import_node_path6 = require("path"); -function pascalCase(name) { - return name.replace(/[^a-zA-Z0-9]/g, " ").split(/[\s_]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(""); -} -function snakeCase(name) { - return name.replace(/[^a-zA-Z0-9]/g, "_").replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase(); -} -function pythonType(ext) { - switch (ext.type) { - case "bitmask": - case "uint8": - return "int"; - case "uint16": - case "int16": - return "int"; - case "uint32": - case "int32": - return "int"; - case "array": - return "dict"; - case "struct": - return "dict"; - default: - return "int"; - } -} -function pythonDocstring(description) { - return `"""${description}"""`; -} -function generateBitmaskGetter(ext, addr) { - const cases = ext.labels ? Object.entries(ext.labels).map(([val, label]) => ` ${val}: "${label}"`).join(",\n") : ""; - const classLabel = ext.labels ? ` labels = {${cases ? "\n" + cases + "\n" : ""} } - return labels.get(val, "unknown")` : "return val"; - return [ - ` @property`, - ` def ${snakeCase(ext.name)}(self) -> str:`, - ` ${pythonDocstring(ext.description)}`, - ` val = self._emu.read_byte(0x${addr.toString(16)})`, - classLabel - ].join("\n"); -} -function generateSimpleGetter(ext, addr) { - const method = ext.type === "uint32" || ext.type === "int32" ? `read_uint32(0x${addr.toString(16)})` : ext.type === "uint16" || ext.type === "int16" ? `read_uint16(0x${addr.toString(16)})` : `read_byte(0x${addr.toString(16)})`; - return [ - ` @property`, - ` def ${snakeCase(ext.name)}(self) -> ${pythonType(ext)}:`, - ` ${pythonDocstring(ext.description)}`, - ` return self._emu.${method}` - ].join("\n"); -} -function generateStructGetter(ext, addr) { - const fieldLines = ext.fields?.map((f) => { - const readMethod = f.type === "uint32" ? `read_uint32(0x${(addr + f.offset).toString(16)})` : f.type === "uint16" ? `read_uint16(0x${(addr + f.offset).toString(16)})` : `read_byte(0x${(addr + f.offset).toString(16)})`; - return ` "${f.name}": self._emu.${readMethod}`; - }) || []; - return [ - ` @property`, - ` def ${snakeCase(ext.name)}(self) -> dict:`, - ` ${pythonDocstring(ext.description)}`, - " return {", - ...fieldLines, - " }" - ].join("\n"); -} -function generateArrayGetter(ext, addr) { - const size = ext.size || 1; - const labels = ext.labels ? Object.entries(ext.labels).map(([idx, label]) => ` items = ${JSON.stringify(Object.values(ext.labels))}`).join("\n") : ""; - const labelBlock = labels ? ` -${labels} - result = {} - for i, label in enumerate(items): - result[label] = self._emu.read_byte(0x${addr.toString(16)} + i) - return result` : ` result = {} - for i in range(${size}): - result[i] = self._emu.read_byte(0x${addr.toString(16)} + i) - return result`; - return [ - ` @property`, - ` def ${snakeCase(ext.name)}(self) -> dict:`, - ` ${pythonDocstring(ext.description)}`, - labelBlock - ].join("\n"); -} -function generateProperty(ext) { - const addr = parseInt(ext.address, 16); - switch (ext.type) { - case "bitmask": - return generateBitmaskGetter(ext, addr); - case "struct": - return generateStructGetter(ext, addr); - case "array": - return generateArrayGetter(ext, addr); - case "uint8": - case "uint16": - case "uint32": - case "int16": - case "int32": - return generateSimpleGetter(ext, addr); - default: - return generateSimpleGetter(ext, addr); - } -} -function extractorToProvider(mem, programName) { - const providers = []; - for (const entry of mem) { - if (entry.confidence < 0.6) continue; - const snaked = snakeCase(entry.label); - const name = `${programName}_${snaked}`.replace(/^[^a-z_]+/, ""); - switch (entry.type) { - case "byte": - if (entry.description && /stat(e|us)|flag|mode|bit/i.test(entry.description) || /status|flag|mode/i.test(entry.label)) { - providers.push({ - name, - type: "bitmask", - address: entry.address, - size: 1, - labels: guessBitmaskLabels(entry.label, entry.description), - description: entry.description - }); - } else if (entry.length && entry.length > 1) { - providers.push({ - name, - type: "array", - address: entry.address, - size: entry.length, - description: entry.description - }); - } else { - providers.push({ - name, - type: "uint8", - address: entry.address, - size: 1, - description: entry.description - }); - } - break; - case "struct": - providers.push({ - name, - type: "struct", - address: entry.address, - size: entry.length || 4, - fields: [], - description: entry.description - }); - break; - default: - providers.push({ - name, - type: "uint8", - address: entry.address, - size: 1, - description: entry.description - }); - } - } - return providers; -} -function guessBitmaskLabels(label, description) { - const lower = description.toLowerCase(); - if (/docked|docking|flight|land/.test(lower)) { - return { "0": "docked", "1": "in_flight", "2": "docking", "3": "jumping" }; - } - if (/paused|running|stopped|title/i.test(lower)) { - return { "0": "title_screen", "1": "running", "2": "paused", "3": "game_over" }; - } - if (/alive|dead|hurt|invincible/i.test(lower)) { - return { "0": "alive", "1": "dead", "2": "hurt", "3": "invincible" }; - } - return { "0": "off", "1": "on" }; -} -function generatePythonCode(extractors, moduleName, sourcePath) { - const className = pascalCase(moduleName) + "Extractor"; - const propertyBlocks = extractors.map(generateProperty).join("\n\n"); - const captureItems = extractors.map((e) => ` "${snakeCase(e.name)}": self.${snakeCase(e.name)}`).join(",\n"); - const header = [ - "# Auto-generated by LENS-Craft v1.0", - `# Source: ${sourcePath}`, - `# ${extractors.length} extractors generated`, - "", - "", - `class ${className}:` - ].join("\n"); - const init = [ - " def __init__(self, emu):", - ' """Initialize with a 6502 emulator instance.', - "", - " Args:", - " emu: Emulator providing read_byte(addr), read_uint16(addr),", - " read_uint32(addr) in little-endian byte order.", - ' """', - " self._emu = emu" - ].join("\n"); - const captureAll = [ - "", - " def capture_all(self) -> dict:", - ' """Capture all known state in a single snapshot."""', - " return {", - captureItems, - " }" - ].join("\n"); - return [header, init, propertyBlocks, captureAll, ""].join("\n\n"); -} -function lensCraft(input) { - const memoryMap = input.source_miner_report.findings.memory_map || []; - const sourcePath = input.source_miner_report.source || "unknown"; - const pathSegments = sourcePath.replace(/\/$/, "").split("/"); - const programName = pathSegments[pathSegments.length - 1] || "unknown"; - const extractors = extractorToProvider(memoryMap, programName); - const moduleName = input.output.module_name; - const generatedCode = generatePythonCode(extractors, moduleName, sourcePath); - let writtenTo; - if (input.output.path) { - const outPath = input.output.path.endsWith(".py") ? input.output.path : (0, import_node_path6.join)(input.output.path, `${moduleName}.py`); - const dir = (0, import_node_path6.dirname)(outPath); - if (!(0, import_node_fs3.existsSync)(dir)) { - (0, import_node_fs3.mkdirSync)(dir, { recursive: true }); - } - (0, import_node_fs3.writeFileSync)(outPath, generatedCode, "utf-8"); - writtenTo = outPath; - } - return { - skill: "LENS-Craft", - version: "1.0", - executed_at: (/* @__PURE__ */ new Date()).toISOString(), - module_path: input.output.path || `${moduleName}.py`, - extractors, - generated_code: generatedCode, - written_to: writtenTo - }; -} - -// src/tools/skin-weaver.ts -var import_node_fs4 = require("fs"); -var import_node_path7 = require("path"); -var PALETTES = { - bbc_mode7: { - "0": "#000000", - "1": "#ff0000", - "2": "#00ff00", - "3": "#ffff00", - "4": "#0000ff", - "5": "#ff00ff", - "6": "#00ffff", - "7": "#ffffff" - }, - teletext_classic: { - "0": "#000000", - "1": "#ff0000", - "2": "#00cc00", - "3": "#cccc00", - "4": "#0000cc", - "5": "#cc00cc", - "6": "#00cccc", - "7": "#cccccc" - }, - dark_fantasy: { - "0": "#0a0a0a", - "1": "#8b0000", - "2": "#2e8b57", - "3": "#b8860b", - "4": "#191970", - "5": "#4b0082", - "6": "#5f9ea0", - "7": "#a9a9a9" - }, - repton_classic: { - "0": "#000000", - "1": "#ff8800", - "2": "#00cc00", - "3": "#ffff00", - "4": "#0044cc", - "5": "#cc44cc", - "6": "#00cccc", - "7": "#cccccc" - }, - elite_wireframe: { - "0": "#000000", - "1": "#00ff00", - "2": "#00ff00", - "3": "#ffff00", - "4": "#0000ff", - "5": "#ff00ff", - "6": "#00ffff", - "7": "#00ff00" - } -}; -var CHAR_MAP = { - wireframe_model: { char: "@", fg: 2, bg: 0, desc: "Wireframe ship model" }, - sprite_data: { char: "*", fg: 3, bg: 0, desc: "Sprite character" }, - teletext_pages: { char: " ", fg: 7, bg: 0, desc: "Teletext page" }, - audio_data: { char: "~", fg: 5, bg: 0, desc: "Audio asset" }, - map_data: { char: "#", fg: 6, bg: 0, desc: "Map tile" }, - level_data: { char: "#", fg: 1, bg: 0, desc: "Level layout" }, - game_data: { char: "?", fg: 7, bg: 0, desc: "Game data file" }, - image_asset: { char: "&", fg: 4, bg: 0, desc: "Image asset" }, - binary_asset: { char: "%", fg: 5, bg: 0, desc: "Binary asset" }, - character_data: { char: "C", fg: 2, bg: 0, desc: "Character definition" } -}; -function guessCharMapping(asset) { - const type = asset.format || asset.type; - const mapping = CHAR_MAP[type] || { char: "?", fg: 7, bg: 0, desc: asset.type }; - return { - source: asset.path, - target_char: mapping.char, - target_fg: mapping.fg, - target_bg: mapping.bg, - description: mapping.desc - }; -} -function buildManifest(assets, skinName, paletteName, resolution) { - const palette = PALETTES[paletteName] || PALETTES.bbc_mode7; - const mappings = assets.map((a) => guessCharMapping(a)); - const seen = /* @__PURE__ */ new Set(); - const unique = mappings.filter((m) => { - const key = `${m.target_char}-${m.target_fg}`; - if (seen.has(key)) return false; - seen.add(key); - return true; - }); - const overrides = { - header_row: { fg: 2, bg: 0, bold: true }, - hud_row: { fg: 3, bg: 0, bold: false }, - alert_row: { fg: 1, bg: 0, bold: true } - }; - return { - name: skinName, - version: "1.0", - palette, - character_mappings: unique, - teletext_overrides: overrides - }; -} -function generateYamlManifest(manifest) { - const lines = [ - `# Auto-generated by SKIN-Weaver v1.0`, - `skin:`, - ` name: "${manifest.name}"`, - ` version: "${manifest.version}"`, - ``, - `palette:` - ]; - for (const [idx, color] of Object.entries(manifest.palette)) { - lines.push(` '${idx}': '${color}'`); - } - lines.push(""); - lines.push("character_mappings:"); - for (const m of manifest.character_mappings) { - lines.push(` - source: "${m.source}"`); - lines.push(` target_char: "${m.target_char}"`); - lines.push(` target_fg: ${m.target_fg}`); - lines.push(` target_bg: ${m.target_bg}`); - lines.push(` description: "${m.description}"`); - } - lines.push(""); - lines.push("teletext_overrides:"); - if (manifest.teletext_overrides.header_row) { - const h = manifest.teletext_overrides.header_row; - lines.push(` header_row:`); - lines.push(` fg: ${h.fg}`); - lines.push(` bg: ${h.bg}`); - lines.push(` bold: ${h.bold || false}`); - } - if (manifest.teletext_overrides.hud_row) { - const h = manifest.teletext_overrides.hud_row; - lines.push(` hud_row:`); - lines.push(` fg: ${h.fg}`); - lines.push(` bg: ${h.bg}`); - lines.push(` bold: ${h.bold || false}`); - } - if (manifest.teletext_overrides.alert_row) { - const h = manifest.teletext_overrides.alert_row; - lines.push(` alert_row:`); - lines.push(` fg: ${h.fg}`); - lines.push(` bg: ${h.bg}`); - lines.push(` bold: ${h.bold || false}`); - } - return lines.join("\n") + "\n"; -} -function generateJsonManifest(manifest) { - return JSON.stringify( - { - generated_by: "SKIN-Weaver v1.0", - skin: { - name: manifest.name, - version: manifest.version - }, - palette: manifest.palette, - character_mappings: manifest.character_mappings, - teletext_overrides: manifest.teletext_overrides - }, - null, - 2 - ); -} -function skinWeaver(input) { - const skinName = input.source_assets[0]?.path ? input.source_assets[0].path.split("/").filter((s) => !s.startsWith(".")).join("_").replace(/[_/]/g, "_").replace(/\.[^.]+$/, "").slice(0, 30) || "default_skin" : "default_skin"; - if (!/^[a-z]/i.test(skinName)) { - const idx = skinName.search(/[a-z]/i); - if (idx > 0) { - } - } - const manifest = buildManifest( - input.source_assets, - `${skinName} (${input.target.locale})`, - input.target.palette, - input.target.resolution - ); - const exportedAssets = input.source_assets.map((a) => ({ - source: a.path, - output: `skins/${skinName}/chars/${a.path.split("/").pop() || "asset"}.json` - })); - return { - skill: "SKIN-Weaver", - version: "1.0", - executed_at: (/* @__PURE__ */ new Date()).toISOString(), - skin_name: skinName, - manifest, - exported_assets: exportedAssets - }; -} -function writeSkinManifest(output, outputDir, format = "yaml") { - if (!(0, import_node_fs4.existsSync)(outputDir)) { - (0, import_node_fs4.mkdirSync)(outputDir, { recursive: true }); - } - const ext = format === "yaml" ? ".yaml" : ".json"; - const outPath = (0, import_node_path7.join)(outputDir, `${output.skin_name}.skin${ext}`); - const content = format === "yaml" ? generateYamlManifest(output.manifest) : generateJsonManifest(output.manifest); - (0, import_node_fs4.writeFileSync)(outPath, content, "utf-8"); - return outPath; -} - -// src/tools/mcp-scribe.ts -function generateStandardCommands(programName, memoryMap, input) { - const prefix = programName.toLowerCase().replace(/[^a-z]/g, "_"); - const commands = []; - const stateKeys = memoryMap.filter( - (m) => /score|status|state|lives|level|time|health|energy|position|inventory|credits|ship/i.test( - m.label - ) && m.confidence >= 0.7 - ).map((m) => { - return m.label.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase().replace(/^_/, ""); - }); - const uniqueKeys = [...new Set(stateKeys)].slice(0, 10); - commands.push({ - name: `${prefix}_save`, - description: `Save current game state via LENS capture`, - parameters: { - slot: { type: "string", description: "Save slot name", default: "autosave" } - }, - action: "lens_capture", - payload: { - target: "variables", - scope: `${prefix}_save`, - keys: uniqueKeys - } - }); - commands.push({ - name: `${prefix}_load`, - description: `Restore saved game state`, - parameters: { - slot: { type: "string", description: "Save slot to load", default: "autosave" } - }, - action: "lens_restore", - payload: { - target: "variables", - scope: `${prefix}_save`, - keys: uniqueKeys - } - }); - commands.push({ - name: `${prefix}_status`, - description: `Query current game state`, - parameters: {}, - action: "lens_query", - payload: { - extractor: `${prefix}_lens_extractor`, - method: "capture_all" - } - }); - commands.push({ - name: `${prefix}_pause`, - description: `Pause/resume game execution`, - parameters: {}, - action: "emulator_control", - payload: { - command: "toggle_pause" - } - }); - const functions = input.source_miner_report?.findings?.functions || []; - const injectableFunctions = functions.filter( - (f) => /Dock|Jump|Launch|Title|Death|Win|Lose|Reset/i.test(f.name) && f.address - ).slice(0, 5); - for (const fn of injectableFunctions) { - const actionName = fn.name.toLowerCase().replace(/[^a-z0-9]/g, "_"); - commands.push({ - name: `${prefix}_${actionName}`, - description: fn.description || `Trigger ${fn.name} routine`, - parameters: {}, - action: "mcp_inject", - payload: { - target: "6502_execute", - address: fn.address, - description: `Call ${fn.name} routine at ${fn.address}` - } - }); - } - return commands; -} -function mcpScribe(input) { - const memoryMap = input.source_miner_report.findings.memory_map || []; - const commands = generateStandardCommands(input.program_name, memoryMap, input); - return { - skill: "MCP-Scribe", - version: "1.0", - executed_at: (/* @__PURE__ */ new Date()).toISOString(), - program: input.program_name, - commands - }; -} - -// src/tools/inspire-engine.ts -var GAME_TEMPLATES = { - "knight orc": { - title: "Knight Orc (uCode Adaptation)", - genre: ["text_adventure", "fantasy"], - summary: "You are Grindleguts, an orc seeking revenge in a fantasy/sci-fi world. The game features a real-time NPC schedule system unique to the KAOS engine.", - core_mechanics: [ - { - name: "text_parser", - description: "Standard text adventure parser supporting VERB NOUN grammar", - constraints: ["max 2 words per command", "abbreviation support (N=North, X=Examine, I=Inventory)"], - vocabulary_size: "~200 words (60 verbs, 140 nouns/adjectives)" - }, - { - name: "npc_schedules", - description: "NPCs follow daily routines independent of player actions. Time advances with player actions.", - implementation: "Tick-based time system. Each NPC has a schedule array: [location, time_start, time_end, action]", - constraints: ["Clock resolution: 1 tick per command", "Day/night cycle: 24 ticks (1 tick = 1 in-game hour)"] - }, - { - name: "magic_system", - description: "Spell casting with mana cost and component requirements", - spells: [ - { name: "BLAST", cost: 5, effect: "damage_target", learn_location: "wizard_tower" }, - { name: "HEAL", cost: 3, effect: "restore_health", learn_location: "healer_hut" }, - { name: "SHIELD", cost: 4, effect: "temporary_defense", learn_location: "wizard_tower" }, - { name: "INVIS", cost: 8, effect: "avoid_combat", learn_location: "thieves_guild" }, - { name: "LIGHT", cost: 2, effect: "illuminate_dark_rooms", learn_location: "general_store" } - ] - }, - { - name: "world_model", - description: "Multi-location world with movable objects and NPCs", - locations: [ - { id: "orc_camp", name: "Orc Camp", exits: ["north=dark_forest"] }, - { id: "dark_forest", name: "Dark Forest", exits: ["south=orc_camp", "east=wizard_tower", "north=castle_gate"] }, - { id: "wizard_tower", name: "Wizard's Tower", exits: ["west=dark_forest"] }, - { id: "castle_gate", name: "Castle Gate", exits: ["south=dark_forest", "north=courtyard"] }, - { id: "courtyard", name: "Castle Courtyard", exits: ["south=castle_gate", "north=throne_room"] }, - { id: "throne_room", name: "Throne Room", exits: ["south=courtyard"] }, - { id: "healer_hut", name: "Healer's Hut", exits: ["east=orc_camp"] }, - { id: "thieves_guild", name: "Thieves' Guild", exits: ["west=dark_forest"] }, - { id: "general_store", name: "General Store", exits: ["north=orc_camp"] } - ] - }, - { - name: "combat_system", - description: "Turn-based combat with hit points, armor class, and weapon modifiers", - constraints: ["Player HP: 20-50 based on class", "Weapon damage: 1d4 (fists) to 3d6 (battle axe)", "Enemy AC reduces hit probability"] - } - ], - effort: { - total_weeks: 10, - breakdown: { - core_engine: 3, - parser_and_vocabulary: 1, - world_building: 2, - npc_system: 2, - magic_system: 1, - testing: 1 - } - } - }, - "apple panic": { - title: "Apple Panic (uCode Adaptation)", - genre: ["platformer", "arcade"], - summary: "Dig through platforms to trap enemies and collect treasure. Inspired by the classic 1981 Apple II game Space Panic. Single-screen arcade action with progressive difficulty.", - core_mechanics: [ - { - name: "digging_system", - description: "Player digs holes in platforms. Enemies fall through holes and are temporarily trapped. Fill holes to eliminate trapped enemies.", - constraints: ["Max 3 holes at once", "Holes auto-fill after 5 seconds", "Enemies trapped for 3 seconds"] - }, - { - name: "ladder_movement", - description: "Player moves between platform levels using ladders. 5 platform levels stacked vertically.", - constraints: ["Player can only climb when aligned with ladder", "Cannot dig while on ladder"] - }, - { - name: "enemy_ai", - description: "Simple patrol AI. Enemies walk left-right on platforms until they encounter a hole or the player.", - implementation: "State machine: PATROL \u2192 FALL \u2192 TRAPPED \u2192 ESCAPE \u2192 PATROL", - constraints: ["3 enemies at start, +1 per level", "Enemy speed increases by 10% per level"] - }, - { - name: "scoring", - description: "Points awarded for trapping enemies (100), filling holes with trapped enemy (200), and collecting treasure drops (500)." - }, - { - name: "world_model", - description: "Single screen with 5 horizontal platforms, 2 ladders, and 3-7 enemies" - } - ], - effort: { - total_weeks: 2, - breakdown: { - core_engine: 1, - enemy_ai: 0.5, - scoring_and_ui: 0.25, - testing: 0.25 - } - } - }, - uconstruct: { - title: "uConstruct (uCode Adaptation)", - genre: ["construction", "simulation"], - summary: "Build and manage a medieval castle using tile-based construction mechanics. Inspired by the 1984 game ACS (Adventure Construction Set). Design rooms, place furniture, and manage resources.", - core_mechanics: [ - { - name: "tile_editor", - description: "Place and remove tiles on a grid to construct rooms, walls, and terrain", - implementation: "Grid-based editor with cursor navigation. Tile types: WALL, FLOOR, DOOR, WATER, GRASS, ROAD.", - constraints: ["Grid size: 64x48 tiles", "16 tile types", "Undo/Redo stack (20 levels)"] - }, - { - name: "room_system", - description: "Enclosed areas become rooms with defined purposes", - constraints: ["Room requires: at least 1 door, minimum 4x4 tiles", "Room types: BEDROOM, KITCHEN, ARMORY, THRONE, DUNGEON, LIBRARY, STORAGE"] - }, - { - name: "resource_management", - description: "Track stone, wood, gold, and food. Resources consumed during construction.", - constraints: ["Starting: 500 stone, 300 wood, 100 gold, 200 food", "Wall costs 2 stone, Floor costs 1 stone, Door costs 3 wood"] - }, - { - name: "scoring", - description: "Castle score based on room count, room variety, defensive capability, and aesthetic appeal" - } - ], - effort: { - total_weeks: 10, - breakdown: { - core_engine: 3, - tile_editor: 2, - room_system: 1.5, - resource_system: 1.5, - ui_and_input: 1, - testing: 1 - } - } - } -}; -function getGameTemplate(name) { - const lower = name.toLowerCase(); - if (GAME_TEMPLATES[lower]) return GAME_TEMPLATES[lower]; - for (const [key, tmpl] of Object.entries(GAME_TEMPLATES)) { - if (lower.includes(key) || key.includes(lower)) return tmpl; - } - return null; -} -function buildGDD(targetGame, template) { - const uCodeIntegration = { - lens_extractors: buildLensTargets(template), - skin_themes: buildSkinThemes(template), - mcp_commands: buildMcpCommands(targetGame, template) - }; - return { - title: template.title, - genre: template.genre, - summary: template.summary, - core_mechanics: template.core_mechanics, - uCode_integration: uCodeIntegration - }; -} -function buildLensTargets(template) { - const targets = [ - { target: "game_state", type: "string", description: "Current game state (title, playing, paused, game_over)" }, - { target: "score", type: "uint32", description: "Player score" } - ]; - const genres = template.genre; - if (genres.includes("text_adventure")) { - targets.push( - { target: "player_location", type: "string", description: "Current location ID" }, - { target: "player_inventory", type: "array", description: "List of carried item IDs" }, - { target: "game_time", type: "uint16", description: "Current game time in ticks" }, - { target: "npc_statuses", type: "array", description: "Array of {npc_id, location, action} for each NPC" } - ); - } - if (genres.includes("platformer") || genres.includes("arcade")) { - targets.push( - { target: "player_x", type: "uint16", description: "Player X position" }, - { target: "player_y", type: "uint16", description: "Player Y position" }, - { target: "lives", type: "uint8", description: "Lives remaining" }, - { target: "level", type: "uint8", description: "Current level number" }, - { target: "enemies_remaining", type: "uint8", description: "Enemies still active" } - ); - } - if (genres.includes("construction") || genres.includes("simulation")) { - targets.push( - { target: "stone", type: "uint16", description: "Stone resource count" }, - { target: "wood", type: "uint16", description: "Wood resource count" }, - { target: "gold", type: "uint16", description: "Gold resource count" }, - { target: "food", type: "uint16", description: "Food resource count" }, - { target: "room_count", type: "uint8", description: "Number of rooms built" } - ); - } - return targets; -} -function buildSkinThemes(template) { - const themes = [{ name: "teletext_classic", description: "Standard MODE 7 teletext" }]; - const genres = template.genre; - if (genres.includes("fantasy") || genres.includes("text_adventure")) { - themes.push({ name: "dark_fantasy", description: "Dark background with muted colours for fantasy atmosphere" }); - } - if (genres.includes("platformer") || genres.includes("arcade")) { - themes.push({ name: "bbc_mode7", description: "Bright primary colours for arcade visibility" }); - } - if (genres.includes("construction") || genres.includes("simulation")) { - themes.push({ name: "repton_classic", description: "Warm earth tones for medieval construction theme" }); - } - return themes; -} -function buildMcpCommands(targetGame, template) { - const prefix = targetGame.toLowerCase().replace(/[^a-z]/g, "_"); - const commands = [ - { name: `${prefix}_save`, description: "Save game state" }, - { name: `${prefix}_load`, description: "Load game state" }, - { name: `${prefix}_status`, description: "Query current game state" }, - { name: `${prefix}_pause`, description: "Pause/resume game" } - ]; - const genres = template.genre; - if (genres.includes("text_adventure")) { - commands.push({ name: `${prefix}_time_skip`, description: "Advance game time by N ticks" }); - } - if (genres.includes("platformer") || genres.includes("arcade")) { - commands.push({ name: `${prefix}_level_select`, description: "Select level to play" }); - } - if (genres.includes("construction") || genres.includes("simulation")) { - commands.push({ name: `${prefix}_export_map`, description: "Export current castle map" }); - } - return commands; -} -function inspireEngine(input) { - const template = getGameTemplate(input.target_game); - if (!template) { - throw new Error( - `No built-in game design template found for "${input.target_game}". Supported games: ${Object.keys(GAME_TEMPLATES).join(", ")}. Use research_sources to provide context for a custom game.` - ); - } - const gdd = buildGDD(input.target_game, template); - return { - skill: "Inspire-Engine", - version: "1.0", - executed_at: (/* @__PURE__ */ new Date()).toISOString(), - target_game: input.target_game, - game_design_document: gdd, - effort_estimate: template.effort - }; -} - -// src/tools/ucode-weaver.ts -function generateInit(mechanics) { - const body = [ - ` 510 CLS`, - ` 520 PRINT "Loading..."`, - ` 530 REM Initialize game state from LENS variables` - ]; - for (const mech of mechanics) { - if (mech.name === "combat_system") { - body.push(` 540 player_hp% = 30`); - body.push(` 550 player_ac% = 10`); - } - if (mech.name === "magic_system") { - body.push(` 560 mana% = MAX_MANA%`); - } - if (mech.name === "scoring") { - body.push(` 570 score%(0) = 0 : score%(1) = 0`); - } - } - body.push(` 580 ENDPROC`); - return { - name: "PROCinit", - description: "Initialize game state", - body: body.join("\n") - }; -} -function generateInput(mechanics) { - const isTextAdventure = mechanics.some((m) => m.name === "text_parser"); - const isPlatformer = mechanics.some((m) => m.name === "digging_system" || m.name === "ladder_movement"); - if (isTextAdventure) { - return { - name: "PROCinput", - description: "Handle text parser input", - body: [ - ` 610 LOCAL K$`, - ` 620 INPUT "> " K$`, - ` 630 K$ = FNupper$(K$)`, - ` 640 PROCparse_command(K$)`, - ` 650 ENDPROC` - ].join("\n") - }; - } - if (isPlatformer) { - return { - name: "PROCinput", - description: "Handle keyboard input for movement and actions", - body: [ - ` 610 LOCAL K%`, - ` 620 K% = INKEY(0)`, - ` 630 IF K% = 138 THEN pd% = 0 : ENDPROC`, - ` 640 IF K% = 139 THEN pd% = 1 : ENDPROC`, - ` 650 IF K% = 140 THEN pd% = 2 : ENDPROC`, - ` 660 IF K% = 137 THEN pd% = 3 : ENDPROC`, - ` 670 IF K% = 32 THEN PROCaction : ENDPROC`, - ` 680 ENDPROC` - ].join("\n") - }; - } - return { - name: "PROCinput", - description: "Handle cursor-based input", - body: [ - ` 610 LOCAL K%`, - ` 620 K% = INKEY(0)`, - ` 630 IF K% = 138 THEN cy% = cy% - 1 : ENDPROC`, - ` 640 IF K% = 139 THEN cx% = cx% + 1 : ENDPROC`, - ` 650 IF K% = 140 THEN cy% = cy% + 1 : ENDPROC`, - ` 660 IF K% = 137 THEN cx% = cx% - 1 : ENDPROC`, - ` 670 IF K% = 32 THEN PROCplace_tile : ENDPROC`, - ` 680 ENDPROC` - ].join("\n") - }; -} -function generateUpdate(mechanics) { - const hasEnemies = mechanics.some((m) => m.name === "enemy_ai" || m.name === "combat_system"); - const hasNpcs = mechanics.some((m) => m.name === "npc_schedules"); - const hasTimer = mechanics.some((m) => m.name === "digging_system"); - const body = [ - ` 710 REM Main update loop` - ]; - if (hasEnemies) { - body.push(` 720 PROCmove_enemies`); - } - if (hasNpcs) { - body.push(` 730 PROCupdate_npcs`); - } - if (hasTimer) { - body.push(` 740 PROCupdate_timers`); - } - body.push(` 750 ENDPROC`); - return { - name: "PROCupdate", - description: "Update game state each frame", - body: body.join("\n") - }; -} -function generateRender(mechanics) { - const isTeletext = true; - const hasLocations = mechanics.some((m) => m.name === "world_model"); - const body = [ - ` 810 LOCAL x%, y%`, - ` 820 CLS` - ]; - if (hasLocations) { - const location = mechanics.find((m) => m.name === "world_model"); - const firstLoc = location?.locations?.[0]; - body.push(` 830 PRINT "Location: ${firstLoc?.name || "Unknown"}"`); - body.push(` 840 PRINT "Exits: ${firstLoc?.exits?.join(", ") || "none"}"`); - body.push(` 850 PRINT`); - } else { - body.push(` 830 REM Render grid-based display`); - body.push(` 840 FOR y% = 0 TO 23`); - body.push(` 850 FOR x% = 0 TO 39`); - body.push(` 860 PRINT ".";`); - body.push(` 870 NEXT`); - body.push(` 880 PRINT`); - body.push(` 890 NEXT`); - } - body.push(` 910 PRINT "Score: "; score%(1) * 256 + score%(0)`); - body.push(` 920 ENDPROC`); - return { - name: "PROCrender", - description: "Render the display", - body: body.join("\n") - }; -} -function generateProcedures(gdd) { - const mechanics = gdd.core_mechanics; - const procs = []; - procs.push(generateInit(mechanics)); - procs.push(generateInput(mechanics)); - procs.push(generateUpdate(mechanics)); - procs.push(generateRender(mechanics)); - procs.push({ - name: "FNgame_over", - description: "Check if game is over", - body: [ - ` REM Check lives or completion condition`, - ` = FALSE` - ].join("\n") - }); - procs.push({ - name: "PROCgame_over", - description: "Handle game over sequence", - body: [ - ` CLS`, - ` PRINT "GAME OVER"`, - ` PRINT "Score: "; score%(1) * 256 + score%(0)`, - ` END`, - `ENDPROC` - ].join("\n") - }); - procs.push({ - name: "FNupper$", - description: "Convert string to uppercase", - body: [ - ` LOCAL i%, c%, s$`, - ` s$ = ""`, - ` FOR i% = 1 TO LEN(K$)`, - ` c% = ASC(MID$(K$, i%, 1))`, - ` IF c% >= 97 AND c% <= 122 THEN c% = c% - 32`, - ` s$ = s$ + CHR$(c%)`, - ` NEXT`, - ` = s$` - ].join("\n") - }); - return procs; -} -function generateBBCBasic(gdd, programName, runtime) { - const procs = generateProcedures(gdd); - return procs; -} -function generateCode(gdd, programName, runtime) { - const mechanics = gdd.core_mechanics; - const isText = mechanics.some((m) => m.name === "text_parser"); - const isPlatformer = mechanics.some((m) => m.name === "digging_system"); - const lines = []; - lines.push(` 10 REM ====================================================`); - lines.push(` 20 REM ${programName} - uCode Adaptation`); - lines.push(` 30 REM BBC BASIC for SDL 2.0 (Generated by uCode-Weaver v1.0)`); - lines.push(` 40 REM Runtime: ${runtime}`); - lines.push(` 50 REM Genre: ${gdd.genre.join(", ")}`); - lines.push(` 60 REM ====================================================`); - lines.push(""); - const integr = gdd.uCode_integration; - let lineNum = 100; - lines.push(` ${lineNum} REM ---- Global State (LENS-extractable) ----`); - lineNum += 10; - for (const lens of integr?.lens_extractors || []) { - const n = lens.target; - if (lens.type === "string") { - lines.push(` ${lineNum} ${n}$ = ""`); - } else if (lens.type === "array") { - lines.push(` ${lineNum} DIM ${n}(10)`); - } else { - lines.push(` ${lineNum} ${n}% = 0`); - } - lineNum += 10; - } - lines.push(""); - lines.push(` ${lineNum} REM ---- Entry Point ----`); - lineNum += 10; - lines.push(` ${lineNum} PROCinit`); - lineNum += 10; - lines.push(` ${lineNum} REPEAT`); - lineNum += 10; - lines.push(` ${lineNum} IF FNgame_over THEN PROCgame_over`); - lineNum += 10; - lines.push(` ${lineNum} PROCinput`); - lineNum += 10; - lines.push(` ${lineNum} PROCupdate`); - lineNum += 10; - lines.push(` ${lineNum} PROCrender`); - lineNum += 10; - lines.push(` ${lineNum} UNTIL FALSE`); - lineNum += 10; - lines.push(` ${lineNum} END`); - lineNum += 10; - lines.push(""); - lines.push(` ${lineNum} DEF PROCinit`); - lineNum += 10; - lines.push(` ${lineNum} CLS : REM Initialize display`); - lineNum += 10; - lines.push(` ${lineNum} ENDPROC`); - lineNum += 10; - lines.push(""); - lines.push(` ${lineNum} DEF FNgame_over`); - lineNum += 10; - lines.push(` ${lineNum} REM Check win/lose conditions`); - lineNum += 10; - lines.push(` ${lineNum} = FALSE`); - lineNum += 10; - lines.push(""); - lines.push(` ${lineNum} DEF PROCinput`); - lineNum += 10; - lines.push(` ${lineNum} LOCAL K$`); - lineNum += 10; - if (isText) { - lines.push(` ${lineNum} INPUT "> " K$`); - lineNum += 10; - lines.push(` ${lineNum} K$ = FNupper$(K$)`); - lineNum += 10; - lines.push(` ${lineNum} PROCparse_command(K$)`); - } else if (isPlatformer) { - lines.push(` ${lineNum} K% = INKEY(0)`); - lineNum += 10; - lines.push(` ${lineNum} IF K% = 138 THEN pd% = 0 : ENDPROC : REM Up`); - lineNum += 10; - lines.push(` ${lineNum} IF K% = 139 THEN pd% = 1 : ENDPROC : REM Right`); - lineNum += 10; - lines.push(` ${lineNum} IF K% = 140 THEN pd% = 2 : ENDPROC : REM Down`); - lineNum += 10; - lines.push(` ${lineNum} IF K% = 137 THEN pd% = 3 : ENDPROC : REM Left`); - lineNum += 10; - lines.push(` ${lineNum} IF K% = 32 THEN PROCaction : ENDPROC : REM Space`); - } else { - lines.push(` ${lineNum} K% = INKEY(0)`); - lineNum += 10; - lines.push(` ${lineNum} REM Handle input`); - } - lineNum += 10; - lines.push(` ${lineNum} ENDPROC`); - lineNum += 10; - lines.push(""); - lines.push(` ${lineNum} DEF PROCupdate`); - lineNum += 10; - lines.push(` ${lineNum} REM Update game state each frame`); - lineNum += 10; - lines.push(` ${lineNum} ENDPROC`); - lineNum += 10; - lines.push(""); - lines.push(` ${lineNum} DEF PROCrender`); - lineNum += 10; - lines.push(` ${lineNum} CLS`); - lineNum += 10; - lines.push(` ${lineNum} PRINT "Score: 0"`); - lineNum += 10; - lines.push(` ${lineNum} ENDPROC`); - lineNum += 10; - lines.push(""); - lines.push(` ${lineNum} DEF PROCgame_over`); - lineNum += 10; - lines.push(` ${lineNum} CLS`); - lineNum += 10; - lines.push(` ${lineNum} PRINT "GAME OVER"`); - lineNum += 10; - lines.push(` ${lineNum} END`); - lineNum += 10; - lines.push(` ${lineNum} ENDPROC`); - if (isText) { - lineNum += 10; - lines.push(""); - lines.push(` ${lineNum} DEF FNupper$(s$)`); - lineNum += 10; - lines.push(` ${lineNum} LOCAL i%, c%, r$`); - lineNum += 10; - lines.push(` ${lineNum} r$ = ""`); - lineNum += 10; - lines.push(` ${lineNum} FOR i% = 1 TO LEN(s$)`); - lineNum += 10; - lines.push(` ${lineNum} c% = ASC(MID$(s$, i%, 1))`); - lineNum += 10; - lines.push(` ${lineNum} IF c% >= 97 AND c% <= 122 THEN c% = c% - 32`); - lineNum += 10; - lines.push(` ${lineNum} r$ = r$ + CHR$(c%)`); - lineNum += 10; - lines.push(` ${lineNum} NEXT`); - lineNum += 10; - lines.push(` ${lineNum} = r$`); - } - return lines.join("\n"); -} -function ucodeWeaver(input) { - const entry = input.entry_file || `${input.program_name.toLowerCase().replace(/[^a-z]/g, "_")}.bbc`; - const procedures = generateBBCBasic(input.gdd, input.program_name, input.runtime); - const generatedCode = generateCode(input.gdd, input.program_name, input.runtime); - return { - skill: "uCode-Weaver", - version: "1.0", - executed_at: (/* @__PURE__ */ new Date()).toISOString(), - program_name: input.program_name, - entry_file: entry, - procedures, - generated_code: generatedCode - }; -} - -// src/index.ts -var GRIDSMITH_TOOLS = [ - { - name: "create_world", - description: "Create a new world with optional terrain.", - parameters: { - name: { type: "string", description: "World name" }, - type: { type: "string", description: "World type: earth, dungeon, vault, or library" }, - cols: { type: "number", description: "Grid column count", default: 80 }, - rows: { type: "number", description: "Grid row count", default: 24 }, - seed: { type: "number", description: "Random seed", default: 0 }, - terrain: { type: "object", description: 'Map of "x,y" -> character for terrain' } - } - }, - { - name: "import_basic_program", - description: "Import a BASIC program as a grid world.", - parameters: { - program: { type: "string", description: "BASIC program code or file path" }, - world_name: { type: "string", description: "Name for the generated world" } - } - }, - { - name: "import_amos_program", - description: "Import an AMOS program as a grid world.", - parameters: { - program: { type: "string", description: "AMOS program code or file path" }, - world_name: { type: "string", description: "Name for the generated world" } - } - }, - { - name: "create_grid", - description: "Create a new grid.", - parameters: { - cols: { type: "number", description: "Grid column count", default: 80 }, - rows: { type: "number", description: "Grid row count", default: 24 }, - cell_size: { type: "number", description: "Logical cell size", default: 24 } - } - }, - { - name: "edit_cell", - description: "Edit a specific cell.", - parameters: { - grid_id: { type: "string", description: "Grid identifier" }, - x: { type: "number", description: "X cell coordinate" }, - y: { type: "number", description: "Y cell coordinate" }, - layer: { type: "number", description: "Layer index", default: 0 }, - data: { type: "object", description: "Cell payload (char, fg, bg)" } - } - }, - { - name: "compose_layers", - description: "Compose layers into a single view.", - parameters: { - grid_id: { type: "string", description: "Grid identifier" }, - layers: { - type: "array", - description: "Ordered layer list", - default: [0, 1, 2, 3, 4, 5], - items: { type: "number" } - } - } - }, - { - name: "export_uvox", - description: "Export a grid as a .uvox artifact.", - parameters: { - grid_id: { type: "string", description: "Grid identifier" }, - output_path: { type: "string", description: "Output file path" } - } - }, - { - name: "pathfind", - description: "Find path between two points on a grid (A*).", - parameters: { - grid_id: { type: "string", description: "Grid identifier" }, - start_x: { type: "number", description: "Start X coordinate" }, - start_y: { type: "number", description: "Start Y coordinate" }, - end_x: { type: "number", description: "End X coordinate" }, - end_y: { type: "number", description: "End Y coordinate" }, - layer: { type: "number", description: "Layer index", default: 0 } - } - }, - { - name: "latlon_to_ucode", - description: "Convert lat/lon to uCode.", - parameters: { - lat: { type: "number", description: "Latitude" }, - lon: { type: "number", description: "Longitude" }, - level: { type: "number", description: "uCode level", default: 340 } - } - }, - { - name: "ucode_to_latlon", - description: "Convert uCode to lat/lon.", - parameters: { - coord: { type: "string", description: "uCode coordinate" } - } - }, - { - name: "source_miner", - description: "Scan 6502 assembly source code for LENS-extractable integration points.", - parameters: { - source_path: { type: "string", description: "Path to source directory or file" }, - language: { type: "string", description: "Source language(s) as CSV", default: "6502" }, - target_patterns: { type: "string", description: "File patterns as CSV (e.g. *.asm,*.s)", default: "" }, - exclude_patterns: { type: "string", description: "Exclude patterns as CSV (e.g. test_*,*.tmp)", default: "" } - } - }, - { - name: "lens_craft", - description: "Generate Python LENS extractor code from a Source-Miner report.", - parameters: { - source_miner_json: { type: "string", description: "Source-Miner output as JSON string" }, - module_name: { type: "string", description: "Python module name (e.g. elite_lens)" }, - output_path: { type: "string", description: "Output file path (e.g. runtimes/basic/bridge/lens/extractors/)", default: "" } - } - }, - { - name: "skin_weaver", - description: "Convert original game assets to uCode SKIN manifest.", - parameters: { - assets_json: { type: "string", description: "JSON array of asset objects [{path, type, format?}]" }, - skin_name: { type: "string", description: "Name for the generated skin" }, - palette: { type: "string", description: "Palette: bbc_mode7, teletext_classic, dark_fantasy, repton_classic, elite_wireframe", default: "bbc_mode7" }, - output_dir: { type: "string", description: "Output directory for .skin.yaml manifest", default: "" } - } - }, - { - name: "mcp_scribe", - description: "Generate MCP command specifications from Source-Miner report.", - parameters: { - source_miner_json: { type: "string", description: "Source-Miner output as JSON string" }, - program_name: { type: "string", description: "Program name (e.g. Elite, Repton)" }, - program_type: { type: "string", description: "adapt-source, rewrite, port-c-to-basic, or rewrite_inspired_by", default: "adapt-source" } - } - }, - { - name: "inspire_engine", - description: "Generate game design documents for rewrite-inspired projects.", - parameters: { - target_game: { type: "string", description: "Game name (e.g. Knight Orc, Apple Panic, uConstruct)" }, - sources_json: { type: "string", description: "JSON array of research sources [{type, url, reliability}]", default: "[]" }, - runtime: { type: "string", description: "Target runtime", default: "bbc_basic_sdl" }, - display_mode: { type: "string", description: "Display mode", default: "teletext" } - } - }, - { - name: "ucode_weaver", - description: "Generate BBC BASIC skeleton code from a game design document.", - parameters: { - gdd_json: { type: "string", description: "Inspire-Engine GDD output as JSON string" }, - program_name: { type: "string", description: "Program name (e.g. Apple Panic, Knight Orc)" }, - runtime: { type: "string", description: "Target runtime", default: "bbc_basic_sdl" }, - display_mode: { type: "string", description: "Display mode", default: "teletext" } - } - } -]; -function createGridWorld(cols = 80, rows = 24) { - const grid = (0, import_gridcore6.createGrid)(cols, rows); - return { - grid, - cols: grid.cols, - rows: grid.rows, - cellCount: (0, import_gridcore6.listCells)(grid).length - }; -} -function convertLatLonToUCode(lat, lon, level = 340) { - return (0, import_gridcore6.latLonToUCode)(lat, lon, level); -} -function convertUCodeToLatLon(coord) { - return (0, import_gridcore6.uCodeToLatLon)(coord); -} - -// src/mcp/server.ts -var PORT = process.env.GRIDSMITH_MCP_PORT || "8670"; -var HOST = process.env.GRIDSMITH_MCP_HOST || "127.0.0.1"; -var gridRegistry = /* @__PURE__ */ new Map(); -function mcpResponse(id, result) { - return { jsonrpc: "2.0", id, result }; -} -function mcpError(id, code, message, data) { - return { jsonrpc: "2.0", id, error: { code, message, data } }; -} -function getOrCreateGrid(id, cols = 80, rows = 24) { - let grid = gridRegistry.get(id); - if (!grid) { - const created = createGridWorld(cols, rows); - grid = created.grid; - gridRegistry.set(id, grid); - } - return grid; -} -async function invokeTool(name, params) { - switch (name) { - case "tools/list": - return { tools: GRIDSMITH_TOOLS }; - case "create_world": - return createWorld({ - name: String(params.name || "New World"), - type: params.type || "earth", - cols: Number(params.cols) || 80, - rows: Number(params.rows) || 24, - seed: Number(params.seed) || void 0, - terrain: params.terrain || void 0 - }); - case "create_grid": { - const cols = Number(params.cols) || 80; - const rows = Number(params.rows) || 24; - const world = createGridWorld(cols, rows); - const gridId = String(params.grid_id || `grid-${Date.now()}`); - gridRegistry.set(gridId, world.grid); - return { gridId, cols: world.cols, rows: world.rows, cellCount: world.cellCount }; - } - case "edit_cell": { - const gridId = String(params.grid_id || "default"); - const grid = getOrCreateGrid(gridId); - return editCell( - grid, - Number(params.x), - Number(params.y), - Number(params.layer) || 0, - params.data || {} - ); - } - case "compose_layers": { - const gridId = String(params.grid_id || "default"); - const grid = getOrCreateGrid(gridId); - const layers = params.layers || [0, 1, 2, 3, 4, 5]; - const result = composeGridLayers(grid, layers); - return { cellCount: result.cellCount, layerCount: result.layers.length }; - } - case "export_uvox": { - const gridId = String(params.grid_id || "default"); - const grid = getOrCreateGrid(gridId); - const outputPath = String(params.output_path || "output.uvox"); - return exportUvox(grid, gridId, outputPath); - } - case "pathfind": { - const gridId = String(params.grid_id || "default"); - const grid = getOrCreateGrid(gridId); - return findPath( - grid, - Number(params.start_x), - Number(params.start_y), - Number(params.end_x), - Number(params.end_y), - Number(params.layer) || 0 - ); - } - case "import_basic_program": - return importBasicProgram( - String(params.program || ""), - String(params.world_name || "BASIC World") - ); - case "import_amos_program": - return importAmosProgram( - String(params.program || ""), - String(params.world_name || "AMOS World") - ); - case "latlon_to_ucode": - return { - coord: convertLatLonToUCode( - Number(params.lat), - Number(params.lon), - Number(params.level) || 340 - ) - }; - case "ucode_to_latlon": - return { - location: convertUCodeToLatLon(String(params.coord || "")) - }; - case "source_miner": { - const sourcePath = String(params.source_path || process.cwd()); - const langStr = String(params.language || "6502"); - const language = langStr.split(",").map((s) => s.trim()); - const targetStr = String(params.target_patterns || ""); - const excludeStr = String(params.exclude_patterns || ""); - const targetPatterns = targetStr ? targetStr.split(",").filter(Boolean) : []; - const excludePatterns = excludeStr ? excludeStr.split(",").filter(Boolean) : []; - return sourceMiner({ - source: { type: "local_path", url: sourcePath, language }, - options: { - scan_depth: "full", - target_patterns: targetPatterns.length > 0 ? targetPatterns : void 0, - exclude_patterns: excludePatterns.length > 0 ? excludePatterns : void 0 - } - }); - } - case "lens_craft": { - const minerJson = String(params.source_miner_json || "{}"); - const moduleName = String(params.module_name || "lens_extractor"); - const outputPath = String(params.output_path || ""); - const report = JSON.parse(minerJson); - return lensCraft({ - source_miner_report: report, - emulator: { type: "6502", endianness: "little" }, - output: { - language: "python", - module_name: moduleName, - path: outputPath || void 0 - } - }); - } - case "skin_weaver": { - const assetsJson = String(params.assets_json || "[]"); - const palette = String(params.palette || "bbc_mode7"); - const outputDir = String(params.output_dir || ""); - const assets = JSON.parse(assetsJson); - const result = skinWeaver({ - source_assets: assets, - target: { - locale: "teletext_grid", - resolution: { cols: 40, rows: 25 }, - palette - } - }); - if (outputDir) { - const writtenTo = writeSkinManifest(result, outputDir, "yaml"); - return { ...result, manifest_written_to: writtenTo }; - } - return result; - } - case "mcp_scribe": { - const minerJson = String(params.source_miner_json || "{}"); - const programName = String(params.program_name || "Unknown"); - const programType = params.program_type || "adapt-source"; - const report = JSON.parse(minerJson); - return mcpScribe({ - program_name: programName, - program_type: programType, - game_mechanics: { genre: [] }, - source_miner_report: report - }); - } - case "inspire_engine": { - const targetGame = String(params.target_game || ""); - const sourcesJson = String(params.sources_json || "[]"); - const runtime = String(params.runtime || "bbc_basic_sdl"); - const displayMode = String(params.display_mode || "teletext"); - if (!targetGame) throw new Error("Missing required parameter: target_game"); - const sources = JSON.parse(sourcesJson); - return inspireEngine({ - target_game: targetGame, - approach: "rewrite_inspired_by", - research_sources: sources, - design_constraints: { - target_runtime: runtime, - display_mode: displayMode - } - }); - } - case "ucode_weaver": { - const gddJson = String(params.gdd_json || "{}"); - const programName = String(params.program_name || "Weaver"); - const runtime = String(params.runtime || "bbc_basic_sdl"); - const displayMode = String(params.display_mode || "teletext"); - const gdd = JSON.parse(gddJson); - return ucodeWeaver({ - gdd: gdd.game_design_document || gdd, - program_name: programName, - runtime, - display_mode: displayMode - }); - } - default: - throw new Error(`Unknown tool: ${name}`); - } -} -async function handleRequest(body) { - let req; - try { - req = JSON.parse(body); - } catch { - return mcpError(null, -32700, "Parse error"); - } - if (req.jsonrpc !== "2.0") { - return mcpError(req.id, -32600, "Invalid Request"); - } - try { - switch (req.method) { - case "initialize": - return mcpResponse(req.id, { - protocolVersion: "2024-11-05", - capabilities: { tools: {} }, - serverInfo: { name: "gridsmith-mcp", version: "0.2.0" } - }); - case "tools/list": - return mcpResponse(req.id, { tools: GRIDSMITH_TOOLS }); - case "tools/call": { - const params = req.params; - if (!params?.name) { - return mcpError(req.id, -32602, "Missing tool name"); - } - const result = await invokeTool(params.name, params.arguments || {}); - return mcpResponse(req.id, { content: [{ type: "text", text: JSON.stringify(result) }] }); - } - case "ping": - return mcpResponse(req.id, {}); - default: - return mcpError(req.id, -32601, `Method not found: ${req.method}`); - } - } catch (err) { - return mcpError(req.id, -32e3, String(err)); - } -} -function main() { - const server = (0, import_node_http.createServer)(async (req, res) => { - if (req.method !== "POST") { - res.writeHead(405, { "Content-Type": "application/json" }); - res.end(JSON.stringify(mcpError(null, -32e3, "Method not allowed"))); - return; - } - const chunks = []; - for await (const chunk of req) { - chunks.push(chunk); - } - const body = Buffer.concat(chunks).toString("utf-8"); - const response = await handleRequest(body); - res.writeHead(200, { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": "*" - }); - res.end(JSON.stringify(response)); - }); - server.listen(Number(PORT), HOST, () => { - process.stderr.write(`GridSmith MCP server listening on ${HOST}:${PORT} -`); - }); -} -main(); diff --git a/agents/gridsmith/dist/mcp/server.d.cts b/agents/gridsmith/dist/mcp/server.d.cts deleted file mode 100644 index 908ba84..0000000 --- a/agents/gridsmith/dist/mcp/server.d.cts +++ /dev/null @@ -1 +0,0 @@ -#!/usr/bin/env node diff --git a/agents/gridsmith/dist/mcp/server.d.ts b/agents/gridsmith/dist/mcp/server.d.ts deleted file mode 100644 index 908ba84..0000000 --- a/agents/gridsmith/dist/mcp/server.d.ts +++ /dev/null @@ -1 +0,0 @@ -#!/usr/bin/env node diff --git a/agents/gridsmith/dist/mcp/server.js b/agents/gridsmith/dist/mcp/server.js deleted file mode 100755 index e20d026..0000000 --- a/agents/gridsmith/dist/mcp/server.js +++ /dev/null @@ -1,280 +0,0 @@ -#!/usr/bin/env node -import { - GRIDSMITH_TOOLS, - composeGridLayers, - convertLatLonToUCode, - convertUCodeToLatLon, - createGridWorld, - createWorld, - editCell, - exportUvox, - findPath, - importAmosProgram, - importBasicProgram, - inspireEngine, - lensCraft, - mcpScribe, - skinWeaver, - sourceMiner, - ucodeWeaver, - writeSkinManifest -} from "../chunk-4ASG7VW3.js"; - -// src/mcp/server.ts -import { createServer } from "http"; -var PORT = process.env.GRIDSMITH_MCP_PORT || "8670"; -var HOST = process.env.GRIDSMITH_MCP_HOST || "127.0.0.1"; -var gridRegistry = /* @__PURE__ */ new Map(); -function mcpResponse(id, result) { - return { jsonrpc: "2.0", id, result }; -} -function mcpError(id, code, message, data) { - return { jsonrpc: "2.0", id, error: { code, message, data } }; -} -function getOrCreateGrid(id, cols = 80, rows = 24) { - let grid = gridRegistry.get(id); - if (!grid) { - const created = createGridWorld(cols, rows); - grid = created.grid; - gridRegistry.set(id, grid); - } - return grid; -} -async function invokeTool(name, params) { - switch (name) { - case "tools/list": - return { tools: GRIDSMITH_TOOLS }; - case "create_world": - return createWorld({ - name: String(params.name || "New World"), - type: params.type || "earth", - cols: Number(params.cols) || 80, - rows: Number(params.rows) || 24, - seed: Number(params.seed) || void 0, - terrain: params.terrain || void 0 - }); - case "create_grid": { - const cols = Number(params.cols) || 80; - const rows = Number(params.rows) || 24; - const world = createGridWorld(cols, rows); - const gridId = String(params.grid_id || `grid-${Date.now()}`); - gridRegistry.set(gridId, world.grid); - return { gridId, cols: world.cols, rows: world.rows, cellCount: world.cellCount }; - } - case "edit_cell": { - const gridId = String(params.grid_id || "default"); - const grid = getOrCreateGrid(gridId); - return editCell( - grid, - Number(params.x), - Number(params.y), - Number(params.layer) || 0, - params.data || {} - ); - } - case "compose_layers": { - const gridId = String(params.grid_id || "default"); - const grid = getOrCreateGrid(gridId); - const layers = params.layers || [0, 1, 2, 3, 4, 5]; - const result = composeGridLayers(grid, layers); - return { cellCount: result.cellCount, layerCount: result.layers.length }; - } - case "export_uvox": { - const gridId = String(params.grid_id || "default"); - const grid = getOrCreateGrid(gridId); - const outputPath = String(params.output_path || "output.uvox"); - return exportUvox(grid, gridId, outputPath); - } - case "pathfind": { - const gridId = String(params.grid_id || "default"); - const grid = getOrCreateGrid(gridId); - return findPath( - grid, - Number(params.start_x), - Number(params.start_y), - Number(params.end_x), - Number(params.end_y), - Number(params.layer) || 0 - ); - } - case "import_basic_program": - return importBasicProgram( - String(params.program || ""), - String(params.world_name || "BASIC World") - ); - case "import_amos_program": - return importAmosProgram( - String(params.program || ""), - String(params.world_name || "AMOS World") - ); - case "latlon_to_ucode": - return { - coord: convertLatLonToUCode( - Number(params.lat), - Number(params.lon), - Number(params.level) || 340 - ) - }; - case "ucode_to_latlon": - return { - location: convertUCodeToLatLon(String(params.coord || "")) - }; - case "source_miner": { - const sourcePath = String(params.source_path || process.cwd()); - const langStr = String(params.language || "6502"); - const language = langStr.split(",").map((s) => s.trim()); - const targetStr = String(params.target_patterns || ""); - const excludeStr = String(params.exclude_patterns || ""); - const targetPatterns = targetStr ? targetStr.split(",").filter(Boolean) : []; - const excludePatterns = excludeStr ? excludeStr.split(",").filter(Boolean) : []; - return sourceMiner({ - source: { type: "local_path", url: sourcePath, language }, - options: { - scan_depth: "full", - target_patterns: targetPatterns.length > 0 ? targetPatterns : void 0, - exclude_patterns: excludePatterns.length > 0 ? excludePatterns : void 0 - } - }); - } - case "lens_craft": { - const minerJson = String(params.source_miner_json || "{}"); - const moduleName = String(params.module_name || "lens_extractor"); - const outputPath = String(params.output_path || ""); - const report = JSON.parse(minerJson); - return lensCraft({ - source_miner_report: report, - emulator: { type: "6502", endianness: "little" }, - output: { - language: "python", - module_name: moduleName, - path: outputPath || void 0 - } - }); - } - case "skin_weaver": { - const assetsJson = String(params.assets_json || "[]"); - const palette = String(params.palette || "bbc_mode7"); - const outputDir = String(params.output_dir || ""); - const assets = JSON.parse(assetsJson); - const result = skinWeaver({ - source_assets: assets, - target: { - locale: "teletext_grid", - resolution: { cols: 40, rows: 25 }, - palette - } - }); - if (outputDir) { - const writtenTo = writeSkinManifest(result, outputDir, "yaml"); - return { ...result, manifest_written_to: writtenTo }; - } - return result; - } - case "mcp_scribe": { - const minerJson = String(params.source_miner_json || "{}"); - const programName = String(params.program_name || "Unknown"); - const programType = params.program_type || "adapt-source"; - const report = JSON.parse(minerJson); - return mcpScribe({ - program_name: programName, - program_type: programType, - game_mechanics: { genre: [] }, - source_miner_report: report - }); - } - case "inspire_engine": { - const targetGame = String(params.target_game || ""); - const sourcesJson = String(params.sources_json || "[]"); - const runtime = String(params.runtime || "bbc_basic_sdl"); - const displayMode = String(params.display_mode || "teletext"); - if (!targetGame) throw new Error("Missing required parameter: target_game"); - const sources = JSON.parse(sourcesJson); - return inspireEngine({ - target_game: targetGame, - approach: "rewrite_inspired_by", - research_sources: sources, - design_constraints: { - target_runtime: runtime, - display_mode: displayMode - } - }); - } - case "ucode_weaver": { - const gddJson = String(params.gdd_json || "{}"); - const programName = String(params.program_name || "Weaver"); - const runtime = String(params.runtime || "bbc_basic_sdl"); - const displayMode = String(params.display_mode || "teletext"); - const gdd = JSON.parse(gddJson); - return ucodeWeaver({ - gdd: gdd.game_design_document || gdd, - program_name: programName, - runtime, - display_mode: displayMode - }); - } - default: - throw new Error(`Unknown tool: ${name}`); - } -} -async function handleRequest(body) { - let req; - try { - req = JSON.parse(body); - } catch { - return mcpError(null, -32700, "Parse error"); - } - if (req.jsonrpc !== "2.0") { - return mcpError(req.id, -32600, "Invalid Request"); - } - try { - switch (req.method) { - case "initialize": - return mcpResponse(req.id, { - protocolVersion: "2024-11-05", - capabilities: { tools: {} }, - serverInfo: { name: "gridsmith-mcp", version: "0.2.0" } - }); - case "tools/list": - return mcpResponse(req.id, { tools: GRIDSMITH_TOOLS }); - case "tools/call": { - const params = req.params; - if (!params?.name) { - return mcpError(req.id, -32602, "Missing tool name"); - } - const result = await invokeTool(params.name, params.arguments || {}); - return mcpResponse(req.id, { content: [{ type: "text", text: JSON.stringify(result) }] }); - } - case "ping": - return mcpResponse(req.id, {}); - default: - return mcpError(req.id, -32601, `Method not found: ${req.method}`); - } - } catch (err) { - return mcpError(req.id, -32e3, String(err)); - } -} -function main() { - const server = createServer(async (req, res) => { - if (req.method !== "POST") { - res.writeHead(405, { "Content-Type": "application/json" }); - res.end(JSON.stringify(mcpError(null, -32e3, "Method not allowed"))); - return; - } - const chunks = []; - for await (const chunk of req) { - chunks.push(chunk); - } - const body = Buffer.concat(chunks).toString("utf-8"); - const response = await handleRequest(body); - res.writeHead(200, { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": "*" - }); - res.end(JSON.stringify(response)); - }); - server.listen(Number(PORT), HOST, () => { - process.stderr.write(`GridSmith MCP server listening on ${HOST}:${PORT} -`); - }); -} -main(); diff --git a/agents/gridsmith/package.json b/agents/gridsmith/package.json index 6f9ffba..9419f45 100644 --- a/agents/gridsmith/package.json +++ b/agents/gridsmith/package.json @@ -8,8 +8,7 @@ "module": "./dist/index.js", "types": "./dist/index.d.ts", "bin": { - "gridsmith": "./dist/cli.js", - "gridsmith-mcp": "./dist/mcp/server.js" + "gridsmith": "./dist/cli.js" }, "exports": { ".": { @@ -22,7 +21,7 @@ "dist" ], "scripts": { - "build": "tsup src/index.ts src/cli.ts src/mcp/server.ts --format esm,cjs --dts", + "build": "tsup src/index.ts src/cli.ts --format esm,cjs --dts", "dev": "tsup src/index.ts src/cli.ts --format esm,cjs --dts --watch", "test": "vitest run" }, @@ -36,4 +35,4 @@ "typescript": "^5.7.3", "vitest": "^2.1.8" } -} \ No newline at end of file +} diff --git a/agents/gridsmith/src/mcp/server.ts b/agents/gridsmith/src/mcp/server.ts deleted file mode 100644 index 26bdff7..0000000 --- a/agents/gridsmith/src/mcp/server.ts +++ /dev/null @@ -1,339 +0,0 @@ -#!/usr/bin/env node - -import { createServer } from 'node:http' -import type { Grid } from '@udos/gridcore' -import { - GRIDSMITH_TOOLS, - createGridWorld, - convertLatLonToUCode, - convertUCodeToLatLon, - importBasicProgram, - importAmosProgram, - editCell, - composeGridLayers, - exportUvox, - findPath, - createWorld, - sourceMiner, - lensCraft, - skinWeaver, - writeSkinManifest, - mcpScribe, - inspireEngine, - ucodeWeaver, -} from '../index' - -const PORT = process.env.GRIDSMITH_MCP_PORT || '8670' -const HOST = process.env.GRIDSMITH_MCP_HOST || '127.0.0.1' - -// Stateful grid registry — maps grid_id -> Grid -const gridRegistry = new Map() - -interface McpRequest { - jsonrpc: '2.0' - id: number | string - method: string - params?: Record -} - -interface McpResponse { - jsonrpc: '2.0' - id: number | string | null - result?: unknown - error?: { code: number; message: string; data?: unknown } -} - -function mcpResponse(id: number | string | null, result: unknown): McpResponse { - return { jsonrpc: '2.0', id, result } -} - -function mcpError(id: number | string | null, code: number, message: string, data?: unknown): McpResponse { - return { jsonrpc: '2.0', id, error: { code, message, data } } -} - -function getOrCreateGrid(id: string, cols = 80, rows = 24): Grid { - let grid = gridRegistry.get(id) - if (!grid) { - const created = createGridWorld(cols, rows) - grid = created.grid - gridRegistry.set(id, grid) - } - return grid -} - -async function invokeTool(name: string, params: Record): Promise { - switch (name) { - case 'tools/list': - return { tools: GRIDSMITH_TOOLS } - - case 'create_world': - return createWorld({ - name: String(params.name || 'New World'), - type: (params.type as 'earth' | 'dungeon' | 'vault' | 'library') || 'earth', - cols: Number(params.cols) || 80, - rows: Number(params.rows) || 24, - seed: Number(params.seed) || undefined, - terrain: (params.terrain as Record) || undefined, - }) - - case 'create_grid': { - const cols = Number(params.cols) || 80 - const rows = Number(params.rows) || 24 - const world = createGridWorld(cols, rows) - const gridId = String(params.grid_id || `grid-${Date.now()}`) - gridRegistry.set(gridId, world.grid) - return { gridId, cols: world.cols, rows: world.rows, cellCount: world.cellCount } - } - - case 'edit_cell': { - const gridId = String(params.grid_id || 'default') - const grid = getOrCreateGrid(gridId) - return editCell( - grid, - Number(params.x), - Number(params.y), - Number(params.layer) || 0, - (params.data as Record) || {}, - ) - } - - case 'compose_layers': { - const gridId = String(params.grid_id || 'default') - const grid = getOrCreateGrid(gridId) - const layers = (params.layers as number[]) || [0, 1, 2, 3, 4, 5] - const result = composeGridLayers(grid, layers) - return { cellCount: result.cellCount, layerCount: result.layers.length } - } - - case 'export_uvox': { - const gridId = String(params.grid_id || 'default') - const grid = getOrCreateGrid(gridId) - const outputPath = String(params.output_path || 'output.uvox') - return exportUvox(grid, gridId, outputPath) - } - - case 'pathfind': { - const gridId = String(params.grid_id || 'default') - const grid = getOrCreateGrid(gridId) - return findPath( - grid, - Number(params.start_x), - Number(params.start_y), - Number(params.end_x), - Number(params.end_y), - Number(params.layer) || 0, - ) - } - - case 'import_basic_program': - return importBasicProgram( - String(params.program || ''), - String(params.world_name || 'BASIC World'), - ) - - case 'import_amos_program': - return importAmosProgram( - String(params.program || ''), - String(params.world_name || 'AMOS World'), - ) - - case 'latlon_to_ucode': - return { - coord: convertLatLonToUCode( - Number(params.lat), - Number(params.lon), - Number(params.level) || 340, - ), - } - - case 'ucode_to_latlon': - return { - location: convertUCodeToLatLon(String(params.coord || '')), - } - - case 'source_miner': { - const sourcePath = String(params.source_path || process.cwd()) - const langStr = String(params.language || '6502') - const language = langStr.split(',').map((s: string) => s.trim()) - const targetStr = String(params.target_patterns || '') - const excludeStr = String(params.exclude_patterns || '') - const targetPatterns = targetStr ? targetStr.split(',').filter(Boolean) : [] - const excludePatterns = excludeStr ? excludeStr.split(',').filter(Boolean) : [] - - return sourceMiner({ - source: { type: 'local_path', url: sourcePath, language }, - options: { - scan_depth: 'full', - target_patterns: targetPatterns.length > 0 ? targetPatterns : undefined, - exclude_patterns: excludePatterns.length > 0 ? excludePatterns : undefined, - }, - }) - } - - case 'lens_craft': { - const minerJson = String(params.source_miner_json || '{}') - const moduleName = String(params.module_name || 'lens_extractor') - const outputPath = String(params.output_path || '') - - const report = JSON.parse(minerJson) - return lensCraft({ - source_miner_report: report, - emulator: { type: '6502', endianness: 'little' }, - output: { - language: 'python', - module_name: moduleName, - path: outputPath || undefined, - }, - }) - } - - case 'skin_weaver': { - const assetsJson = String(params.assets_json || '[]') - const palette = String(params.palette || 'bbc_mode7') - const outputDir = String(params.output_dir || '') - - const assets = JSON.parse(assetsJson) - const result = skinWeaver({ - source_assets: assets, - target: { - locale: 'teletext_grid', - resolution: { cols: 40, rows: 25 }, - palette, - }, - }) - - if (outputDir) { - const writtenTo = writeSkinManifest(result, outputDir, 'yaml') - return { ...result, manifest_written_to: writtenTo } - } - return result - } - - case 'mcp_scribe': { - const minerJson = String(params.source_miner_json || '{}') - const programName = String(params.program_name || 'Unknown') - const programType = (params.program_type as string) || 'adapt-source' - - const report = JSON.parse(minerJson) - return mcpScribe({ - program_name: programName, - program_type: programType as 'adapt-source' | 'rewrite' | 'port-c-to-basic' | 'rewrite_inspired_by', - game_mechanics: { genre: [] }, - source_miner_report: report, - }) - } - - case 'inspire_engine': { - const targetGame = String(params.target_game || '') - const sourcesJson = String(params.sources_json || '[]') - const runtime = String(params.runtime || 'bbc_basic_sdl') - const displayMode = String(params.display_mode || 'teletext') - - if (!targetGame) throw new Error('Missing required parameter: target_game') - - const sources = JSON.parse(sourcesJson) - return inspireEngine({ - target_game: targetGame, - approach: 'rewrite_inspired_by', - research_sources: sources, - design_constraints: { - target_runtime: runtime, - display_mode: displayMode, - }, - }) - } - - case 'ucode_weaver': { - const gddJson = String(params.gdd_json || '{}') - const programName = String(params.program_name || 'Weaver') - const runtime = String(params.runtime || 'bbc_basic_sdl') - const displayMode = String(params.display_mode || 'teletext') - - const gdd = JSON.parse(gddJson) - return ucodeWeaver({ - gdd: gdd.game_design_document || gdd, - program_name: programName, - runtime, - display_mode: displayMode, - }) - } - - default: - throw new Error(`Unknown tool: ${name}`) - } -} - -async function handleRequest(body: string): Promise { - let req: McpRequest - - try { - req = JSON.parse(body) as McpRequest - } catch { - return mcpError(null, -32700, 'Parse error') - } - - if (req.jsonrpc !== '2.0') { - return mcpError(req.id, -32600, 'Invalid Request') - } - - try { - switch (req.method) { - case 'initialize': - return mcpResponse(req.id, { - protocolVersion: '2024-11-05', - capabilities: { tools: {} }, - serverInfo: { name: 'gridsmith-mcp', version: '0.2.0' }, - }) - - case 'tools/list': - return mcpResponse(req.id, { tools: GRIDSMITH_TOOLS }) - - case 'tools/call': { - const params = req.params as { name?: string; arguments?: Record } - if (!params?.name) { - return mcpError(req.id, -32602, 'Missing tool name') - } - const result = await invokeTool(params.name, params.arguments || {}) - return mcpResponse(req.id, { content: [{ type: 'text', text: JSON.stringify(result) }] }) - } - - case 'ping': - return mcpResponse(req.id, {}) - - default: - return mcpError(req.id, -32601, `Method not found: ${req.method}`) - } - } catch (err) { - return mcpError(req.id, -32000, String(err)) - } -} - -function main(): void { - const server = createServer(async (req, res) => { - if (req.method !== 'POST') { - res.writeHead(405, { 'Content-Type': 'application/json' }) - res.end(JSON.stringify(mcpError(null, -32000, 'Method not allowed'))) - return - } - - const chunks: Buffer[] = [] - for await (const chunk of req) { - chunks.push(chunk) - } - const body = Buffer.concat(chunks).toString('utf-8') - - const response = await handleRequest(body) - - res.writeHead(200, { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - }) - res.end(JSON.stringify(response)) - }) - - server.listen(Number(PORT), HOST, () => { - process.stderr.write(`GridSmith MCP server listening on ${HOST}:${PORT}\n`) - }) -} - -main() \ No newline at end of file diff --git a/config/mcp_config.json b/config/mcp_config.json deleted file mode 100644 index 0af31ce..0000000 --- a/config/mcp_config.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "mcpServers": { - "uCore": { - "command": "python3", - "args": ["-m", "app"], - "cwd": "/Users/fredbook/Code/uCore/backend", - "env": { - "PYTHONUNBUFFERED": "1", - "HIVE_DEFAULT_PROVIDER": "ollama", - "HIVE_DEFAULT_MODEL": "qwen2.5-coder:3b" - }, - "disabled": false - }, - "gridsmith-mcp": { - "command": "node", - "args": ["./agents/gridsmith/dist/mcp/server.js"], - "cwd": "/Users/fredbook/Code/uCode", - "env": { - "GRIDSMITH_MCP_PORT": "8670", - "GRIDSMITH_MCP_HOST": "127.0.0.1" - }, - "disabled": false - }, - "ollama": { - "command": "ollama", - "args": ["serve"], - "cwd": "/Users/fredbook/Code/uCode", - "env": {}, - "disabled": false - }, - "hivemind": { - "command": "python3", - "args": ["-m", "app.mcp.hivemind_server"], - "cwd": "/Users/fredbook/Code/uCore/backend", - "env": { - "HIVE_DEFAULT_PROVIDER": "ollama", - "HIVE_DEFAULT_MODEL": "qwen2.5-coder:3b" - }, - "disabled": false - }, - "feed": { - "command": "python3", - "args": ["-m", "app.mcp.feed.feed_server"], - "cwd": "/Users/fredbook/Code/uCore/backend", - "env": {}, - "disabled": false - } - } -} diff --git a/config/openrouter.yaml b/config/openrouter.yaml index bceda35..f93d89d 100644 --- a/config/openrouter.yaml +++ b/config/openrouter.yaml @@ -1,7 +1,6 @@ # OpenRouter Configuration for uCode (Local-First Mode) # Provider routing: Ollama local (free) → OpenRouter cloud (paid fallback) # See config/ollama-models.yaml for local-only config. -# See docs/AI_INTEGRATION.md for full setup guide. openrouter: endpoint: "https://openrouter.ai/api/v1" diff --git a/docs/AI_INTEGRATION.md b/docs/AI_INTEGRATION.md deleted file mode 100644 index c8c8a6a..0000000 --- a/docs/AI_INTEGRATION.md +++ /dev/null @@ -1,203 +0,0 @@ -# AI Integration — Ollama Local-First + Hivemind/OpenRouter - -uCode is configured for **local-first AI**: Ollama does the heavy lifting for -free, with OpenRouter as a paid fallback only when Ollama is unavailable or the -user explicitly requests a premium model. - -## Architecture - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ JETBRAINS PYCHARM │ -│ AI Assistant ───→ Ollama (http://localhost:11434/v1) ←── FREE │ -│ │ │ -│ └──→ OpenRouter (cloud fallback, paid) ←── only if needed │ -└─────────────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────────────┐ -│ UCODE CLI / MCP │ -│ GridSmith MCP ──→ uCore Snackbar (port 8484) ──→ LiteLLM │ -│ │ │ │ -│ │ ├── Ollama (local, free) ←── PRIMARY │ -│ │ └── OpenRouter (cloud) ←── FALLBACK │ -│ └── Ollama direct (bypass uCore) │ -└─────────────────────────────────────────────────────────────────┘ -``` - -## Provider Routing (Local-First) - -| Priority | Provider | Models | Cost | Use Case | -| -------- | -------------------- | ---------------------------- | --------- | ----------------------------------- | -| **1** | **Ollama (local)** | `qwen2.5-coder:3b` | **$0.00** | **Default — code gen, chat, edits** | -| **2** | **Ollama (local)** | `qwen2.5-coder:7b` | **$0.00** | **Complex reasoning, review** | -| **3** | **Ollama (local)** | `llama3.2:3b` | **$0.00** | **Lightweight fast completions** | -| 4 | OpenRouter (cloud) | `qwen/qwen3.6-27b` | $0.02/M | Budget cloud fallback | -| 5 | OpenRouter (cloud) | `deepseek/deepseek-v4-flash` | $0.05/M | Cloud code gen | -| opt-in | OpenRouter (premium) | `anthropic/claude-opus-4.7` | $15.00/M | Premium reasoning | - -**Key principle:** All local models are free. Cloud is only used when: - -- Ollama is not running -- Required model is not pulled -- User explicitly requests a cloud model - -## Setup - -### 1. Install Ollama (Local LLM) - -```bash -# Install -brew install ollama - -# Start server -ollama serve - -# Pull a model -ollama pull qwen2.5-coder:3b - -# Verify -curl http://localhost:11434/api/tags -``` - -### 2. JetBrains PyCharm AI Assistant Setup - -JetBrains AI Assistant auto-detects the project config in `.jetbrains/ai-assistant.xml` -and `.idea/aiSettings.xml`. These files point to Ollama as default provider. - -To verify in PyCharm: - -1. Open **Settings → Tools → AI Assistant** -2. Check **AI Providers**: should show `Ollama Local (Free)` as default -3. Check **Custom OpenAI-compatible** endpoint is set to `http://localhost:11434/v1` -4. Model should be `qwen2.5-coder:3b` - -To manually configure via UI: - -1. **Settings → Tools → AI Assistant → AI Providers** -2. Click **+** → **Custom OpenAI-compatible** -3. Endpoint: `http://localhost:11434/v1` -4. Model: `qwen2.5-coder:3b` -5. Set as default - -### 3. Configure OpenRouter (Optional — Cloud Fallback Only) - -```bash -./scripts/setup-ai-integration.sh -``` - -Or manually: - -```bash -mkdir -p "${UDOS_HOME:-$HOME/Code/.udos}/config" -cp config/hivemind.env "${UDOS_HOME:-$HOME/Code/.udos}/config/hivemind.env" -cp config/openrouter.yaml "${UDOS_HOME:-$HOME/Code/.udos}/config/openrouter.yaml" -``` - -### 4. Start uCore Snackbar - -```bash -cd ../uCore -python3 -m backend.app --host 127.0.0.1 --port 8484 -``` - -## Verification - -### Check Ollama is running - -```bash -curl -s http://localhost:11434/api/tags | python3 -m json.tool -``` - -### Check uCore snackbar health - -```bash -curl -s http://localhost:8484/api/health | python3 -m json.tool -``` - -### List available providers - -```bash -curl -s http://localhost:8484/api/mcp/providers | python3 -m json.tool -``` - -### Test chat completion - -```bash -curl -s http://localhost:8484/api/mcp/chat \ - -H "Content-Type: application/json" \ - -d '{"message": "Hello, what model are you?", "provider": "ollama"}' \ - | python3 -m json.tool -``` - -## Configuration Files - -| File | Purpose | -| ------------------------ | --------------------------------------------------- | -| `config/hivemind.env` | API keys and endpoint URLs | -| `config/openrouter.yaml` | Model routing tiers, costs, and budgets | -| `config/mcp_config.json` | MCP server definitions (GridSmith, uCore, Hivemind) | -| `config/vault.yaml` | Secret store paths and variables | - -## JetBrains Recommended Skills - -These skills optimise JetBrains AI for uCode development: - -| Skill | Model | Purpose | -| --------------- | --------------------------- | ------------------------------------ | -| Code generation | `qwen2.5-coder:3b` (Ollama) | Daily coding, refactoring, tests | -| Code review | `qwen2.5-coder:7b` (Ollama) | Deeper analysis, architecture review | -| Documentation | `qwen2.5-coder:3b` (Ollama) | Docstrings, README, devlogs | -| Commit messages | `qwen2.5-coder:3b` (Ollama) | Conventional commit generation | -| Test generation | `qwen2.5-coder:3b` (Ollama) | Unit/integration test writing | -| Debugging | `qwen2.5-coder:3b` (Ollama) | Error analysis, stack trace reading | - -Configure these in **Settings → Tools → AI Assistant → Skills**. - -To add a new skill: - -```bash -# Skills are configured per-task in PyCharm AI Assistant UI. -# Or use the project-level config: .jetbrains/ai-assistant.xml -``` - -## Cost Tracking (Zero-Cost by Default) - -With local-first mode, **all daily development costs $0.00**. - -``` -Ollama inference: $0.00/1K tokens ← everything runs locally -OpenRouter: $0.00 ← only used if Ollama unavailable -``` - -When cloud fallback is used, costs are tracked in: - -``` -~/.local/share/udos/costs/openrouter-usage.yaml -``` - -Budget limits in `config/openrouter.yaml`: - -- Alert threshold: $2.00 USD -- Hard limit: $10.00 USD -- Per-model cost tracking - -## PyCharm Run Configs - -| Config | Description | -| ------------------------- | -------------------------------------------------------------- | -| `uCode: self-check` | Full repo health check (npm install/build/test + Python check) | -| `uCode: inspect widgets` | Lists widget/viewport files in viewport-renderer | -| `uCode: inspect terminal` | Lists terminal/teletext/renderer files | - -## Troubleshooting - -| Symptom | Likely Cause | Fix | -| ---------------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------- | -| `Connection refused` on :11434 | Ollama not running | `ollama serve` | -| JetBrains AI says "Provider unavailable" | Ollama not running or wrong endpoint | Check `http://localhost:11434/v1` is reachable; verify in Settings → AI Providers | -| JetBrains AI uses wrong model | Config not picked up | Restart PyCharm; check `.jetbrains/ai-assistant.xml` is valid | -| `401 Unauthorized` from OpenRouter | Missing/expired API key | Update `config/hivemind.env` | -| `Model not found` errors | Model not pulled | `ollama pull qwen2.5-coder:3b` | -| uCore snackbar not responding | uCore not started | `python3 -m backend.app --port 8484` | -| High RAM usage | Too many Ollama models loaded | `ollama stop ` to unload; use 3B models on <16GB machines | -| Slow responses | Wrong model for task | Switch to `qwen2.5-coder:3b` for quick tasks, `7b` for complex ones | diff --git a/docs/DEV_PLAN.md b/docs/DEV_PLAN.md index 6ffd0da..ac41196 100644 --- a/docs/DEV_PLAN.md +++ b/docs/DEV_PLAN.md @@ -39,7 +39,7 @@ source .venv/bin/activate ```bash # Self-check before commit -./scripts/setup-ai-integration.sh config +npm run build && npm test # Or use PyCharm Run Config → "uCode: self-check" ``` @@ -75,11 +75,11 @@ Priority: basic runtime → amos runtime → shared ### Phase 3: Integration (COMPLETE) ``` -uCode packages → GridSmith MCP → uCore consumption +uCode packages → GridSmith CLI → uCore consumption ``` - GridSmith CLI works standalone (16 tools) -- GridSmith MCP tools registered with uCore (JSON-RPC on port 8670) +- GridSmith CLI tools consumed by uCore's owned API bridge - uCore can import `@udos/viewport-renderer` widgets - LENS program registry wired (Repton + Elite + NetHack + Eamon) @@ -216,7 +216,6 @@ ollama run qwen2.5-coder:3b --keep-alive -1 # keep forever | `config/ollama-models.yaml` | Ollama model tiers, routing, costs | | `config/openrouter.yaml` | OpenRouter model tiers, fallback chain | | `config/hivemind.env` | API keys, default provider/model | -| `config/mcp_config.json` | MCP server definitions | ### Env Variables @@ -273,10 +272,6 @@ ollama ps # Show running models ollama stop qwen2.5-coder:7b # Free RAM ollama rm qwen2.5-coder:14b # Delete model -# ── Config ───────────────────────────────────────────────────── -cp config/hivemind.env "${UDOS_HOME:-$HOME/Code/.udos}/config/" -./scripts/setup-ai-integration.sh # Full AI setup - # ── Health ────────────────────────────────────────────────────── curl http://localhost:11434/api/tags # Ollama health curl http://localhost:8484/api/health # uCore health diff --git a/docs/GRIDSMITH_DEV_PLAN.md b/docs/GRIDSMITH_DEV_PLAN.md index 3660612..bd90e6f 100644 --- a/docs/GRIDSMITH_DEV_PLAN.md +++ b/docs/GRIDSMITH_DEV_PLAN.md @@ -8,7 +8,7 @@ ## Architecture GridSmith is a **Node.js CLI toolkit** for deterministic world building with an -optional MCP adapter for compatible external clients. +an owned CLI contract for host applications. It complements the existing GridCore Vue library (display) and `gridsmith_api.py` (Python REST API). ## Component Map @@ -21,7 +21,6 @@ It complements the existing GridCore Vue library (display) and `gridsmith_api.py | GridSmith Node agent | `agents/gridsmith/` | Stub only | | CLI (`gridsmith`) | `agents/gridsmith/src/cli.ts` | Stub only | | Core GridSmith class | `agents/gridsmith/src/index.ts` | Needs build | -| MCP server | `agents/gridsmith/src/tools/` | Needs build | | BASIC importer | Needs creation | Missing | | AMOS importer | Needs creation | Missing | | World creation | Needs creation | Missing | @@ -35,15 +34,14 @@ It complements the existing GridCore Vue library (display) and `gridsmith_api.py 1. Implement `GridSmith` core class in `agents/gridsmith/src/index.ts` 2. Wire CLI commands via `commander` in `cli.ts` 3. Create `BASICParser` and `AMOSParser` importers -4. Build MCP tools in `src/tools/` -5. Write `scripts/register.js` for uCore agent registration -6. Test suite in `tests/` -7. npm link for global `gridsmith` command +4. Build deterministic operations in `src/tools/` +5. Test suite in `tests/` +6. npm link for the `gridsmith` command ## Integration Points - Python `gridsmith_api.py` endpoints call GridSmith via `gridsmith_bridge.py` -- MCP tools exposed by `agents/gridsmith/src/mcp/server.ts`; client configuration - remains external to uCode +- External MCP reads are registered only by uCore's `udos-mcp` adapter, which + delegates to the GridSmith CLI/API contract - Frontend display via GridCore Vue components (existing) - Workspace at `workspaces/gridcore/` (existing, empty) diff --git a/docs/UCODE_REPO_BOUNDARY.md b/docs/UCODE_REPO_BOUNDARY.md index cdbce84..79f2595 100644 --- a/docs/UCODE_REPO_BOUNDARY.md +++ b/docs/UCODE_REPO_BOUNDARY.md @@ -40,11 +40,11 @@ The system has **two separate lanes**, kept apart on purpose: | ---------------------------- | ------------------------------------------------------ | | `packages/gridcore` | Grid algebra, code addressing, cell/layer model | | `packages/viewport-renderer` | Browser render surfaces, Terminal/Teletext widgets | -| `agents/gridsmith` | CLI/MCP/import/export/build tooling | +| `agents/gridsmith` | CLI/import/export/build tooling | | `runtimes/basic` | BASIC runtime bridge/package | | `runtimes/amos` | AMOS runtime bridge/package | | `shared` | Cross-runtime support (not host-app-specific) | -| `config/` | AI model routing, MCP server definitions, vault config | +| `config/` | Runtime and development configuration | | `docs/` | Specs, boundary docs, workflows | | `tests/` | Integration tests | @@ -64,13 +64,13 @@ These belong to host applications (e.g., uCore). ``` uCore / host app | - | consumes (packages, CLI, MCP, artifacts) + | consumes (packages, CLI, artifacts) v uCode | | exposes v -Runtime + algebra + widgets + CLI/MCP +Runtime + algebra + widgets + CLI | | renders/inspects through v @@ -87,7 +87,7 @@ uCore owns: - user-facing developer view - orchestration UI - snackbar daemon - - Hivemind MCP server + - external MCP gateway and client configuration - Ollama lifecycle management uCode owns: @@ -97,7 +97,6 @@ uCode owns: - BASIC/AMOS bridges - GridSmith agent - CLI - - MCP tools - terminal/teletext render surfaces ``` @@ -139,26 +138,13 @@ No package should depend on uCore or any host application. > **uCode must be installable, buildable, testable, and packageable from this > repo alone. It must not depend on implementation code from uCore.** -## AI Integration Surface +## External Tool Surface -uCode provides MCP tooling (via GridSmith) that can be consumed by AI agents. -For full AI provider routing (Ollama, OpenRouter, Hivemind), uCode delegates to -uCore's snackbar daemon: - -``` -GridSmith MCP → uCore Snackbar (port 8484) → LiteLLM → Ollama/OpenRouter -``` - -Configuration for this lives in: - -- `config/hivemind.env` — API keys -- `config/openrouter.yaml` — Model routing tiers -- `config/mcp_config.json` — MCP server definitions -- `config/vault.yaml` — Vault/secret paths +uCode owns deterministic package, CLI, and runtime contracts. uCore may adapt a +bounded subset through its `udos-mcp` gateway. uCode does not implement or +configure an external MCP server. ## Related Documents - `docs/GRID_ALGEBRA_RELEASE_COLLATION.md` - `docs/UCODE_RUNTIME_SPEC.md` -- `config/openrouter.yaml` -- `config/mcp_config.json` diff --git a/docs/learning-pathway/README.md b/docs/learning-pathway/README.md index 560b869..00266cb 100644 --- a/docs/learning-pathway/README.md +++ b/docs/learning-pathway/README.md @@ -47,7 +47,8 @@ See [LEARNING-PATHS.md](LEARNING-PATHS.md) for complete details on each path. ## uCode Skills Framework -The uCode Skills Framework provides 6 automated tools for adapting classic programs to uCode. All 6 skills are built and wired into the GridSmith CLI and MCP server. +The uCode Skills Framework provides six automated tools for adapting classic +programs to uCode. All six are wired into the GridSmith CLI. | Skill | Purpose | Tests | |-------|---------|-------| diff --git a/docs/specs/SKILLS_FRAMEWORK.md b/docs/specs/SKILLS_FRAMEWORK.md index a79a69f..4c7d89f 100644 --- a/docs/specs/SKILLS_FRAMEWORK.md +++ b/docs/specs/SKILLS_FRAMEWORK.md @@ -608,6 +608,6 @@ class EliteLensExtractor: ### Phase 4: Integration (Week 7-8) -- Wire skills into GridSmith MCP tools +- Keep skills available through the GridSmith CLI tool registry - Create end-to-end pipeline: `Source-Miner → LENS-Craft → MCP-Scribe` -- Generate Program.yaml from skills pipeline output \ No newline at end of file +- Generate Program.yaml from skills pipeline output diff --git a/package-lock.json b/package-lock.json index fa6dc16..3f68ffe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ucode", - "version": "1.0.0", + "version": "0.1.0-dev", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ucode", - "version": "1.0.0", + "version": "0.1.0-dev", "workspaces": [ "packages/*", "agents/*" @@ -14,7 +14,7 @@ }, "agents/gridsmith": { "name": "@udos/gridsmith", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "@udos/gridcore": "^1.0.0", "@udos/viewport-renderer": "^1.0.0" diff --git a/scripts/setup-ai-integration.sh b/scripts/setup-ai-integration.sh deleted file mode 100755 index 96f770c..0000000 --- a/scripts/setup-ai-integration.sh +++ /dev/null @@ -1,328 +0,0 @@ -#!/usr/bin/env bash -# ============================================================================ -# uCode AI Integration Setup — Ollama Local-First Maximiser -# ============================================================================ -# This script configures uCode for local-first AI development with JetBrains. -# It sets up: -# 1. Ollama (local LLM inference) — primary provider, free -# 2. JetBrains AI Assistant config (.jetbrains/ + .idea/) -# 3. OpenRouter API key (cloud fallback only) -# 4. Hivemind agent config (local-first routing) -# 5. MCP server definitions -# 6. Shell env vars for local-first defaults -# -# Usage: -# chmod +x scripts/setup-ai-integration.sh -# ./scripts/setup-ai-integration.sh -# -# Or run individual steps: -# ./scripts/setup-ai-integration.sh ollama # Ollama only -# ./scripts/setup-ai-integration.sh jetbrains # JetBrains config only -# ./scripts/setup-ai-integration.sh openrouter # OpenRouter only -# ./scripts/setup-ai-integration.sh config # Copy config templates -# ./scripts/setup-ai-integration.sh env # Shell env setup -# ./scripts/setup-ai-integration.sh status # Full status check -# ============================================================================ - -set -euo pipefail - -UCODE_DIR="$(cd "$(dirname "$0")/.." && pwd)" -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -info() { echo -e "${GREEN}[✓]${NC} $1"; } -warn() { echo -e "${YELLOW}[!]${NC} $1"; } -error() { echo -e "${RED}[✗]${NC} $1"; } - -# ────────────────────────────────────────────────────────────────────────────── -# Step 1: Ollama Setup -# ────────────────────────────────────────────────────────────────────────────── -setup_ollama() { - echo "" - echo "━━━ Step 1: Ollama ━━━" - - if command -v ollama &>/dev/null; then - info "Ollama is already installed at $(which ollama)" - else - warn "Ollama is not installed." - warn "Install it via: curl -fsSL https://ollama.com/install.sh | sh" - warn "Or: brew install ollama" - warn "Then run: ollama serve" - warn "And pull a model: ollama pull qwen2.5-coder:3b" - return - fi - - # Check if ollama is running - if curl -sf http://localhost:11434/api/tags &>/dev/null; then - info "Ollama server is running on http://localhost:11434" - - # Check for recommended models - local models - models=$(curl -sf http://localhost:11434/api/tags | python3 -c "import sys,json; [print(m['name']) for m in json.load(sys.stdin).get('models',[])]" 2>/dev/null || echo "") - - if echo "$models" | grep -q "qwen2.5-coder"; then - info "Recommended model 'qwen2.5-coder' is already pulled" - else - warn "Pulling recommended model: qwen2.5-coder:3b (this may take a while)..." - ollama pull qwen2.5-coder:3b || warn "Pull failed. Check internet and disk space." - fi - else - warn "Ollama server is not running. Start it with: ollama serve" - fi -} - -# ────────────────────────────────────────────────────────────────────────────── -# Step 2: JetBrains AI Assistant Config -# ────────────────────────────────────────────────────────────────────────────── -setup_jetbrains() { - echo "" - echo "━━━ Step 2: JetBrains AI Assistant Config ━━━" - - local jetbrains_dir="$UCODE_DIR/.jetbrains" - local idea_dir="$UCODE_DIR/.idea" - - if [ -f "$jetbrains_dir/ai-assistant.xml" ]; then - info "JetBrains AI Assistant config found: $jetbrains_dir/ai-assistant.xml" - else - warn "JetBrains AI Assistant config not found (should be created by setup)" - fi - - if [ -f "$idea_dir/aiSettings.xml" ]; then - info "JetBrains shared AI settings found: $idea_dir/aiSettings.xml" - else - warn "JetBrains shared AI settings not found (should be created by setup)" - fi - - info "JetBrains AI is configured to use Ollama locally:" - info " Endpoint: http://localhost:11434/v1" - info " Model: qwen2.5-coder:3b" - info "" - info "To verify in PyCharm:" - info " Settings → Tools → AI Assistant → AI Providers" - info " Check 'Ollama Local (Free)' is default provider" -} - -# ────────────────────────────────────────────────────────────────────────────── -# Step 3: Shell Environment Setup -# ────────────────────────────────────────────────────────────────────────────── -setup_env() { - echo "" - echo "━━━ Step 3: Shell Environment ━━━" - - local shell_rc="$HOME/.zshrc" - local marker="# uCode AI local-first config" - local env_block="" - - read -r -d '' env_block << 'ENVEOF' || true -# uCode AI local-first config -export HIVE_DEFAULT_PROVIDER="ollama" -export HIVE_DEFAULT_MODEL="qwen2.5-coder:3b" -export OLLAMA_BASE_URL="http://localhost:11434" -export OLLAMA_DEFAULT_MODEL="qwen2.5-coder:3b" -ENVEOF - - if grep -q "$marker" "$shell_rc" 2>/dev/null; then - info "Shell env vars already configured in $shell_rc" - else - echo "" >> "$shell_rc" - echo "$env_block" >> "$shell_rc" - info "Added local-first AI env vars to $shell_rc" - info "Run: source $shell_rc" - fi -} - -# ────────────────────────────────────────────────────────────────────────────── -# Step 4: OpenRouter API Key -# ────────────────────────────────────────────────────────────────────────────── -setup_openrouter() { - echo "" - echo "━━━ Step 2: OpenRouter API Key ━━━" - - local env_file="$UCODE_DIR/config/hivemind.env" - - if [ ! -f "$env_file" ]; then - warn "Config file not found: $env_file" - warn "Re-creating from template..." - cat > "$env_file" << 'ENVEOF' -# Hivemind MCP — API Keys for uCode -# Copy your keys here and chmod 600 this file - -# OpenRouter (cloud models: glm-5.1, claude-opus-4.7, deepseek-v4-flash, qwen3.6-27b) -OPENROUTER_API_KEY="your-openrouter-api-key-here" - -# Local Ollama (fallback / dev agent) -OLLAMA_BASE_URL="http://localhost:11434" - -# Anthropic Direct (optional) -# ANTHROPIC_API_KEY="sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - -# OpenAI (optional) -# OPENAI_API_KEY="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -ENVEOF - warn "Template created at $env_file" - fi - - # Check if key is set - if grep -q "your-openrouter-api-key-here" "$env_file" 2>/dev/null; then - warn "OpenRouter API key is not configured." - warn "Edit $env_file and set OPENROUTER_API_KEY." - warn "Get a key at: https://openrouter.ai/keys" - else - info "OpenRouter API key appears to be configured." - fi - - chmod 600 "$env_file" 2>/dev/null || true - info "Config file permissions set to 600: $env_file" -} - -# ────────────────────────────────────────────────────────────────────────────── -# Step 5: Copy config templates to $UDOS_HOME/config (for uCore integration) -# ────────────────────────────────────────────────────────────────────────────── -setup_ucore_config() { - echo "" - echo "━━━ Step 3: uCore Config Sync ━━━" - - local udos_home="${UDOS_HOME:-$HOME/Code/.udos}" - local ucore_config_dir="$udos_home/config" - mkdir -p "$ucore_config_dir" - - # Copy openrouter.yaml if it doesn't exist in uCore config - if [ -f "$UCODE_DIR/config/openrouter.yaml" ]; then - if [ ! -f "$ucore_config_dir/openrouter.yaml" ]; then - cp "$UCODE_DIR/config/openrouter.yaml" "$ucore_config_dir/openrouter.yaml" - info "Copied openrouter.yaml → $ucore_config_dir/openrouter.yaml" - else - warn "openrouter.yaml already exists in uCore config (skipped)" - fi - fi - - # Copy hivemind.env - if [ -f "$UCODE_DIR/config/hivemind.env" ]; then - if [ ! -f "$ucore_config_dir/hivemind.env" ]; then - cp "$UCODE_DIR/config/hivemind.env" "$ucore_config_dir/hivemind.env" - info "Copied hivemind.env → $ucore_config_dir/hivemind.env" - else - warn "hivemind.env already exists in uCore config (skipped)" - fi - fi - - info "uCore config directory: $ucore_config_dir" -} - -# ────────────────────────────────────────────────────────────────────────────── -# Step 6: Verify MCP Configuration -# ────────────────────────────────────────────────────────────────────────────── -verify_mcp() { - echo "" - echo "━━━ Step 4: MCP Configuration ━━━" - - local mcp_file="$UCODE_DIR/config/mcp_config.json" - if [ -f "$mcp_file" ]; then - info "MCP config found: $mcp_file" - - # Validate JSON - if python3 -c "import json; json.load(open('$mcp_file'))" 2>/dev/null; then - info "MCP config is valid JSON" - local server_count - server_count=$(python3 -c "import json; print(len(json.load(open('$mcp_file')).get('mcpServers',{})))") - info "Defined MCP servers: $server_count" - else - error "MCP config has invalid JSON" - fi - else - warn "No MCP config found at $mcp_file" - fi -} - -# ────────────────────────────────────────────────────────────────────────────── -# Summary -# ────────────────────────────────────────────────────────────────────────────── -print_summary() { - echo "" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo " uCode AI Integration Setup — Summary" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "" - echo " AI Providers (local-first):" - echo " PRIMARY: Ollama (qwen2.5-coder:3b) — FREE" - echo " UPGRADE: Ollama (qwen2.5-coder:7b) — FREE" - echo " FALLBACK: OpenRouter (deepseek-v4-flash) — paid" - echo "" - echo " Config files:" - echo " $UCODE_DIR/.jetbrains/ai-assistant.xml" - echo " $UCODE_DIR/.idea/aiSettings.xml" - echo " $UCODE_DIR/config/hivemind.env" - echo " $UCODE_DIR/config/ollama-models.yaml" - echo " $UCODE_DIR/config/openrouter.yaml" - echo " $UCODE_DIR/config/mcp_config.json" - echo "" - echo " Shell env:" - echo " HIVE_DEFAULT_PROVIDER=ollama" - echo " HIVE_DEFAULT_MODEL=qwen2.5-coder:3b" - echo " OLLAMA_BASE_URL=http://localhost:11434" - echo "" - echo " Integration:" - echo " PyCharm AI: Ollama via http://localhost:11434/v1" - echo " uCore MCP: python3 -m app (port 8484)" - echo " GridSmith MCP: node agents/gridsmith/dist/cli.js mcp" - echo " Hivemind MCP: python3 -m app.mcp.hivemind_server (port 8490)" - echo "" - echo " Next steps:" - echo " 1. Start Ollama: ollama serve" - echo " 2. Pull a model: ollama pull qwen2.5-coder:3b" - echo " 3. Reload shell: source ~/.zshrc" - echo " 4. Open PyCharm: Verify AI Assistant uses Ollama" - echo " 5. (Optional) Set OPENROUTER_API_KEY for cloud fallback" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -} - -# ────────────────────────────────────────────────────────────────────────────── -# Main -# ────────────────────────────────────────────────────────────────────────────── -main() { - echo "╔══════════════════════════════════════════════════════╗" - echo "║ uCode AI Integration Setup ║" - echo "║ Ollama + OpenRouter + Hivemind ║" - echo "╚══════════════════════════════════════════════════════╝" - - case "${1:-all}" in - ollama) - setup_ollama - ;; - jetbrains) - setup_jetbrains - ;; - openrouter) - setup_openrouter - ;; - env) - setup_env - ;; - config) - setup_ucore_config - verify_mcp - ;; - status) - setup_ollama - setup_jetbrains - verify_mcp - ;; - all) - setup_ollama - setup_jetbrains - setup_env - setup_openrouter - setup_ucore_config - verify_mcp - print_summary - ;; - *) - echo "Usage: $0 [ollama|jetbrains|openrouter|env|config|status|all]" - exit 1 - ;; - esac -} - -main "$@"