diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c0f93d3..6ecdb05f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,55 @@ Product: https://apilium.com/us/products/mayros Download: https://mayros.apilium.com Docs: https://apilium.com/us/doc/mayros +## 0.2.0 (2026-03-13) + +Semantic DAG integration — full audit trail, time-travel, and verifiable history for the knowledge graph. + +### Semantic DAG + +- 12 new CortexClient methods: `dagTips`, `dagAction`, `dagHistory`, `dagChain`, `dagStats`, `dagPrune`, `dagAt`, `dagDiff`, `dagExport`, `dagSync`, `dagSyncPull`, `dagVerify` +- DAG DTOs: `DagActionDto` (with `signature` field), `DagTipsResponse`, `DagStatsResponse`, `DagTimeTravelResponse`, `DagDiffResponse`, `DagPruneRequest/Response`, `DagSyncRequest/Response`, `DagPullRequest/Response`, `DagVerifyResponse` +- `DagSyncResponse.actions` properly typed as `DagActionDto[]` + +### MCP Server + +- 10 new DAG tools: `mayros_dag_tips`, `mayros_dag_action`, `mayros_dag_chain`, `mayros_dag_history`, `mayros_dag_time_travel`, `mayros_dag_diff`, `mayros_dag_export`, `mayros_dag_stats`, `mayros_dag_verify`, `mayros_dag_prune` +- 2 new DAG resources: `mayros:///dag/tips`, `mayros:///dag/stats` +- 1 new prompt: `dag-audit` — guided audit workflow with history, verification, and diff +- Total MCP tools: 19 +- All MCP tools now have 30s request timeout (`AbortSignal.timeout`) +- `mayros_memory_stats` fetches 3 endpoints in parallel (`Promise.allSettled`) +- `mayros_remember` stores triples + Ineru entry in parallel +- `min_similarity` parameter now wired through to Cortex in `mayros_search` +- Fixed: `importance: 0` was impossible to set (changed `||` to `??`) +- Fixed: `listGraphSubjects` resource null guard for Cortex offline +- Fixed: agent ID regex now accepts uppercase, dots, and dashes +- Fixed: negative `depth` in `dag-audit` prompt now clamped to minimum 1 + +### CLI + +- `mayros dag` with 10 subcommands: `tips`, `action`, `history`, `chain`, `stats`, `export`, `diff`, `at`, `verify`, `prune` +- `mayros dag prune` now requires interactive confirmation (`[y/N]`) or `--yes` flag +- All 7 CLI modules now use `try/finally { client.destroy() }` (dashboard, session, mailbox, tasks, sync, workflow, teleport) + +### Infrastructure + +- Require AIngle Cortex >= 0.6.1 +- Version 0.1.16 → 0.2.0 +- Removed unused `parseWorktreeConfig` import from workflow-cli + +--- + +## 0.1.16 (2026-03-13) + +MCP server production hardening and Cortex version bump. + +- Deep hardening for MCP server production readiness (input validation, error boundaries) +- Resolved lint warnings in hardening code +- Require AIngle Cortex >= 0.5.0 + +--- + ## 0.1.15 (2026-03-12) MCP Server production-ready, Claude Desktop and Claude Code integration, documentation, and product page update. diff --git a/extensions/agent-mesh/package.json b/extensions/agent-mesh/package.json index 2d5b0716..3023ae0b 100644 --- a/extensions/agent-mesh/package.json +++ b/extensions/agent-mesh/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-agent-mesh", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros multi-agent coordination mesh with shared namespaces, delegation, and knowledge fusion", "type": "module", diff --git a/extensions/analytics/package.json b/extensions/analytics/package.json index 10302a4f..b1b301fd 100644 --- a/extensions/analytics/package.json +++ b/extensions/analytics/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-analytics", - "version": "0.1.16", + "version": "0.2.0", "private": true, "type": "module", "main": "index.ts", diff --git a/extensions/bash-sandbox/package.json b/extensions/bash-sandbox/package.json index de16ae47..6fd29204 100644 --- a/extensions/bash-sandbox/package.json +++ b/extensions/bash-sandbox/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-bash-sandbox", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Bash command sandbox with domain allowlist, command blocklist, and dangerous pattern detection", "type": "module", diff --git a/extensions/bluebubbles/package.json b/extensions/bluebubbles/package.json index 9de6434c..13bd2191 100644 --- a/extensions/bluebubbles/package.json +++ b/extensions/bluebubbles/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-bluebubbles", - "version": "0.1.16", + "version": "0.2.0", "description": "Mayros BlueBubbles channel plugin", "license": "MIT", "type": "module", diff --git a/extensions/ci-plugin/package.json b/extensions/ci-plugin/package.json index cde14ce0..23549e41 100644 --- a/extensions/ci-plugin/package.json +++ b/extensions/ci-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-ci-plugin", - "version": "0.1.16", + "version": "0.2.0", "description": "CI/CD pipeline integration for Mayros — GitHub Actions and GitLab CI providers", "type": "module", "dependencies": { diff --git a/extensions/code-indexer/package.json b/extensions/code-indexer/package.json index 6522de58..db07adba 100644 --- a/extensions/code-indexer/package.json +++ b/extensions/code-indexer/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-code-indexer", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros code indexer plugin — regex-based codebase scanning with RDF triple storage in Cortex", "type": "module", diff --git a/extensions/code-tools/package.json b/extensions/code-tools/package.json index eae05715..a5c3b769 100644 --- a/extensions/code-tools/package.json +++ b/extensions/code-tools/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-code-tools", - "version": "0.1.16", + "version": "0.2.0", "private": true, "type": "module", "dependencies": { diff --git a/extensions/copilot-proxy/package.json b/extensions/copilot-proxy/package.json index 58884fca..c2b0de48 100644 --- a/extensions/copilot-proxy/package.json +++ b/extensions/copilot-proxy/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-copilot-proxy", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros Copilot Proxy provider plugin", "type": "module", diff --git a/extensions/cortex-sync/package.json b/extensions/cortex-sync/package.json index 2bd2600a..e0d11038 100644 --- a/extensions/cortex-sync/package.json +++ b/extensions/cortex-sync/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-cortex-sync", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Cortex DAG synchronization — peer discovery, delta sync, and cross-device knowledge replication", "type": "module", diff --git a/extensions/diagnostics-otel/package.json b/extensions/diagnostics-otel/package.json index 651c4590..7ee87951 100644 --- a/extensions/diagnostics-otel/package.json +++ b/extensions/diagnostics-otel/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-diagnostics-otel", - "version": "0.1.16", + "version": "0.2.0", "description": "Mayros diagnostics OpenTelemetry exporter", "license": "MIT", "type": "module", diff --git a/extensions/discord/package.json b/extensions/discord/package.json index ca016aad..f93f8e46 100644 --- a/extensions/discord/package.json +++ b/extensions/discord/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-discord", - "version": "0.1.16", + "version": "0.2.0", "description": "Mayros Discord channel plugin", "license": "MIT", "type": "module", diff --git a/extensions/eruberu/package.json b/extensions/eruberu/package.json index 829f0282..f61dbfa8 100644 --- a/extensions/eruberu/package.json +++ b/extensions/eruberu/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-eruberu", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros intelligent model routing plugin — Q-Learning adaptive provider/model selection", "type": "module", diff --git a/extensions/feishu/package.json b/extensions/feishu/package.json index c2a184c9..970f57c8 100644 --- a/extensions/feishu/package.json +++ b/extensions/feishu/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-feishu", - "version": "0.1.16", + "version": "0.2.0", "description": "Mayros Feishu/Lark channel plugin (community maintained by @m1heng)", "license": "MIT", "type": "module", diff --git a/extensions/google-antigravity-auth/package.json b/extensions/google-antigravity-auth/package.json index 867ba9c6..6752b8b2 100644 --- a/extensions/google-antigravity-auth/package.json +++ b/extensions/google-antigravity-auth/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-google-antigravity-auth", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros Google Antigravity OAuth provider plugin", "type": "module", diff --git a/extensions/google-gemini-cli-auth/package.json b/extensions/google-gemini-cli-auth/package.json index 9b2b6e7b..765c7c19 100644 --- a/extensions/google-gemini-cli-auth/package.json +++ b/extensions/google-gemini-cli-auth/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-google-gemini-cli-auth", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros Gemini CLI OAuth provider plugin", "type": "module", diff --git a/extensions/googlechat/package.json b/extensions/googlechat/package.json index 58e94862..84da906b 100644 --- a/extensions/googlechat/package.json +++ b/extensions/googlechat/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-googlechat", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros Google Chat channel plugin", "type": "module", diff --git a/extensions/hayameru/package.json b/extensions/hayameru/package.json index bb579778..31f216bb 100644 --- a/extensions/hayameru/package.json +++ b/extensions/hayameru/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-hayameru", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros deterministic code transforms — bypass LLM for simple edits", "type": "module", diff --git a/extensions/imessage/package.json b/extensions/imessage/package.json index 55a4ba26..dc4b1310 100644 --- a/extensions/imessage/package.json +++ b/extensions/imessage/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-imessage", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros iMessage channel plugin", "type": "module", diff --git a/extensions/interactive-permissions/package.json b/extensions/interactive-permissions/package.json index 05ca5e32..623ffbc4 100644 --- a/extensions/interactive-permissions/package.json +++ b/extensions/interactive-permissions/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-interactive-permissions", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Runtime permission dialogs, bash intent classification, policy persistence, and audit trail", "type": "module", diff --git a/extensions/iot-bridge/package.json b/extensions/iot-bridge/package.json index 2fd3a2f6..908b1b88 100644 --- a/extensions/iot-bridge/package.json +++ b/extensions/iot-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-iot-bridge", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "IoT Bridge — connect MAYROS agents to aingle_minimal IoT nodes via REST", "type": "module", diff --git a/extensions/irc/package.json b/extensions/irc/package.json index a02311ad..c8e8dc85 100644 --- a/extensions/irc/package.json +++ b/extensions/irc/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-irc", - "version": "0.1.16", + "version": "0.2.0", "description": "Mayros IRC channel plugin", "license": "MIT", "type": "module", diff --git a/extensions/kakeru-bridge/package.json b/extensions/kakeru-bridge/package.json index f848e2ba..f4af60b4 100644 --- a/extensions/kakeru-bridge/package.json +++ b/extensions/kakeru-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-kakeru-bridge", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros dual-platform coordination bridge — Claude Code + Codex CLI", "type": "module", diff --git a/extensions/line/package.json b/extensions/line/package.json index c55cee72..90a5d62c 100644 --- a/extensions/line/package.json +++ b/extensions/line/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-line", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros LINE channel plugin", "type": "module", diff --git a/extensions/llm-hooks/package.json b/extensions/llm-hooks/package.json index 72ccc9d7..b87a13e6 100644 --- a/extensions/llm-hooks/package.json +++ b/extensions/llm-hooks/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-llm-hooks", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Markdown-defined hooks evaluated by LLM for policy enforcement", "type": "module", diff --git a/extensions/llm-task/package.json b/extensions/llm-task/package.json index 3d1b836e..fa30bfdf 100644 --- a/extensions/llm-task/package.json +++ b/extensions/llm-task/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-llm-task", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros JSON-only LLM task plugin", "type": "module", diff --git a/extensions/lobster/package.json b/extensions/lobster/package.json index cd58aead..9d087c83 100644 --- a/extensions/lobster/package.json +++ b/extensions/lobster/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-lobster", - "version": "0.1.16", + "version": "0.2.0", "description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)", "license": "MIT", "type": "module", diff --git a/extensions/lsp-bridge/package.json b/extensions/lsp-bridge/package.json index d22d91d0..f7496fa7 100644 --- a/extensions/lsp-bridge/package.json +++ b/extensions/lsp-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-lsp-bridge", - "version": "0.1.16", + "version": "0.2.0", "description": "Cortex-backed language server bridge for Mayros — hover, diagnostics, go-to-definition", "type": "module", "dependencies": { diff --git a/extensions/matrix/package.json b/extensions/matrix/package.json index 0006269a..ac52940e 100644 --- a/extensions/matrix/package.json +++ b/extensions/matrix/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-matrix", - "version": "0.1.16", + "version": "0.2.0", "description": "Mayros Matrix channel plugin", "license": "MIT", "type": "module", diff --git a/extensions/mattermost/package.json b/extensions/mattermost/package.json index 970b1bfe..5575182d 100644 --- a/extensions/mattermost/package.json +++ b/extensions/mattermost/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-mattermost", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros Mattermost channel plugin", "type": "module", diff --git a/extensions/mcp-client/package.json b/extensions/mcp-client/package.json index a1fb1cb8..1edcc042 100644 --- a/extensions/mcp-client/package.json +++ b/extensions/mcp-client/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-mcp-client", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "MCP server client with multi-transport support and Cortex tool registry", "type": "module", diff --git a/extensions/mcp-server/cortex-tools.ts b/extensions/mcp-server/cortex-tools.ts index 97480605..180703b7 100644 --- a/extensions/mcp-server/cortex-tools.ts +++ b/extensions/mcp-server/cortex-tools.ts @@ -5,14 +5,28 @@ import { Type } from "@sinclair/typebox"; import type { AdaptableTool } from "./tool-adapter.js"; +/** Default timeout for Cortex HTTP requests (30 s). */ +const REQUEST_TIMEOUT_MS = 30_000; + export type CortexToolDeps = { cortexBaseUrl: string; namespace: string; + authToken?: string; }; export function createCortexTools(deps: CortexToolDeps): AdaptableTool[] { const { cortexBaseUrl } = deps; + const defaultHeaders: Record = {}; + if (deps.authToken) { + defaultHeaders["Authorization"] = deps.authToken; + } + + const postHeaders: Record = { + ...defaultHeaders, + "Content-Type": "application/json", + }; + return [ { name: "mayros_cortex_query", @@ -39,7 +53,10 @@ export function createCortexTools(deps: CortexToolDeps): AdaptableTool[] { queryParams.set("limit", String(limit)); try { - const res = await fetch(`${cortexBaseUrl}/api/v1/triples?${queryParams}`); + const res = await fetch(`${cortexBaseUrl}/api/v1/triples?${queryParams}`, { + headers: defaultHeaders, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); if (!res.ok) { return { content: [{ type: "text" as const, text: `Query failed: ${res.statusText}` }], @@ -94,7 +111,8 @@ export function createCortexTools(deps: CortexToolDeps): AdaptableTool[] { try { const res = await fetch(`${cortexBaseUrl}/api/v1/triples`, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: postHeaders, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), body: JSON.stringify({ subject: params.subject, predicate: params.predicate, @@ -135,13 +153,23 @@ export function createCortexTools(deps: CortexToolDeps): AdaptableTool[] { "Get memory system statistics: STM entries, LTM entities, HNSW index size, graph triple count.", parameters: Type.Object({}), execute: async () => { + const fetchOpts = { + headers: defaultHeaders, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }; + + const [memResult, idxResult, graphResult] = await Promise.allSettled([ + fetch(`${cortexBaseUrl}/api/v1/memory/stats`, fetchOpts), + fetch(`${cortexBaseUrl}/api/v1/memory/index/stats`, fetchOpts), + fetch(`${cortexBaseUrl}/api/v1/stats`, fetchOpts), + ]); + const results: string[] = []; // Ineru stats - try { - const memRes = await fetch(`${cortexBaseUrl}/api/v1/memory/stats`); - if (memRes.ok) { - const stats = (await memRes.json()) as { + if (memResult.status === "fulfilled" && memResult.value.ok) { + try { + const stats = (await memResult.value.json()) as { stm_count: number; stm_capacity: number; ltm_entity_count: number; @@ -154,16 +182,15 @@ export function createCortexTools(deps: CortexToolDeps): AdaptableTool[] { ` LTM: ${stats.ltm_entity_count} entities, ${stats.ltm_link_count} links`, ` Size: ${(stats.total_memory_bytes / 1024).toFixed(1)} KB`, ); + } catch { + /* malformed JSON */ } - } catch { - /* Cortex unavailable */ } // HNSW stats - try { - const idxRes = await fetch(`${cortexBaseUrl}/api/v1/memory/index/stats`); - if (idxRes.ok) { - const idx = (await idxRes.json()) as { + if (idxResult.status === "fulfilled" && idxResult.value.ok) { + try { + const idx = (await idxResult.value.json()) as { point_count: number; dimensions: number; memory_bytes: number; @@ -174,16 +201,15 @@ export function createCortexTools(deps: CortexToolDeps): AdaptableTool[] { ` Dimensions: ${idx.dimensions}`, ` Size: ${(idx.memory_bytes / 1024).toFixed(1)} KB`, ); + } catch { + /* malformed JSON */ } - } catch { - /* */ } // Graph stats - try { - const graphRes = await fetch(`${cortexBaseUrl}/api/v1/stats`); - if (graphRes.ok) { - const stats = (await graphRes.json()) as { + if (graphResult.status === "fulfilled" && graphResult.value.ok) { + try { + const stats = (await graphResult.value.json()) as { graph: { triple_count: number; subject_count: number; @@ -196,9 +222,9 @@ export function createCortexTools(deps: CortexToolDeps): AdaptableTool[] { ` Subjects: ${stats.graph.subject_count}`, ` Predicates: ${stats.graph.predicate_count}`, ); + } catch { + /* malformed JSON */ } - } catch { - /* */ } return { diff --git a/extensions/mcp-server/dag-tools.test.ts b/extensions/mcp-server/dag-tools.test.ts new file mode 100644 index 00000000..83605354 --- /dev/null +++ b/extensions/mcp-server/dag-tools.test.ts @@ -0,0 +1,355 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { createDagTools } from "./dag-tools.js"; + +// ── Mock fetch ──────────────────────────────────────────────────────── + +const originalFetch = globalThis.fetch; + +function mockFetch(data: unknown, ok = true, text?: string) { + return vi.fn().mockResolvedValue({ + ok, + statusText: ok ? "OK" : "Internal Server Error", + json: () => Promise.resolve(data), + text: () => Promise.resolve(text ?? JSON.stringify(data)), + }); +} + +describe("DAG MCP Tools", () => { + const deps = { cortexBaseUrl: "http://127.0.0.1:19090", namespace: "test" }; + let tools: ReturnType; + + beforeEach(() => { + tools = createDagTools(deps); + }); + + afterEach(() => { + globalThis.fetch = originalFetch; + }); + + function findTool(name: string) { + const tool = tools.find((t) => t.name === name); + if (!tool) throw new Error(`Tool ${name} not found`); + return tool; + } + + // 1 + it("mayros_dag_tips happy path", async () => { + globalThis.fetch = mockFetch({ tips: ["aaa", "bbb"], count: 2 }); + const tool = findTool("mayros_dag_tips"); + const result = await tool.execute("id", {}); + expect(result.content[0]!.text).toContain("2 tip(s)"); + expect(result.content[0]!.text).toContain("aaa"); + }); + + // 2 + it("mayros_dag_tips Cortex down", async () => { + globalThis.fetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED")); + const tool = findTool("mayros_dag_tips"); + const result = await tool.execute("id", {}); + expect(result.content[0]!.text).toContain("Cortex unavailable"); + }); + + // 3 + it("mayros_dag_history happy path", async () => { + globalThis.fetch = mockFetch({ + actions: [ + { + hash: "abc123def456", + seq: 1, + timestamp: "2026-03-13T10:00:00Z", + payload_type: "CreateTriple", + payload_summary: "added fact", + }, + ], + }); + const tool = findTool("mayros_dag_history"); + const result = await tool.execute("id", { subject: "project:api", limit: 10 }); + expect(result.content[0]!.text).toContain("1 action(s)"); + expect(result.content[0]!.text).toContain("added fact"); + }); + + // 4 + it("mayros_dag_stats happy path", async () => { + globalThis.fetch = mockFetch({ action_count: 42, tip_count: 3 }); + const tool = findTool("mayros_dag_stats"); + const result = await tool.execute("id", {}); + expect(result.content[0]!.text).toContain("Actions: 42"); + expect(result.content[0]!.text).toContain("Tips: 3"); + }); + + // 5 + it("mayros_dag_export mermaid output", async () => { + const mermaid = "graph TD\n A-->B"; + globalThis.fetch = mockFetch(null, true, mermaid); + const tool = findTool("mayros_dag_export"); + const result = await tool.execute("id", { format: "mermaid" }); + expect(result.content[0]!.text).toContain("graph TD"); + }); + + // 6 + it("mayros_dag_diff happy path", async () => { + globalThis.fetch = mockFetch({ + from: "aaa111", + to: "bbb222", + action_count: 2, + actions: [ + { hash: "ccc333def456", payload_type: "CreateTriple", payload_summary: "add X" }, + { hash: "ddd444def456", payload_type: "DeleteTriple", payload_summary: "remove Y" }, + ], + }); + const tool = findTool("mayros_dag_diff"); + const result = await tool.execute("id", { from: "aaa111", to: "bbb222" }); + expect(result.content[0]!.text).toContain("2 action(s)"); + expect(result.content[0]!.text).toContain("add X"); + }); + + // 7 + it("mayros_dag_time_travel happy path", async () => { + globalThis.fetch = mockFetch({ + target_hash: "abc123", + target_timestamp: "2026-03-10T10:00:00Z", + actions_replayed: 15, + triple_count: 100, + }); + const tool = findTool("mayros_dag_time_travel"); + const result = await tool.execute("id", { hash: "abc123" }); + expect(result.content[0]!.text).toContain("Actions replayed: 15"); + expect(result.content[0]!.text).toContain("Triples at that point: 100"); + }); + + // 8 + it("mayros_dag_verify valid signature", async () => { + globalThis.fetch = mockFetch({ + valid: true, + action_hash: "abc123", + public_key: "ed25519_key", + detail: "Signature valid", + }); + const tool = findTool("mayros_dag_verify"); + const result = await tool.execute("id", { hash: "abc123", public_key: "ed25519_key" }); + expect(result.content[0]!.text).toContain("VALID"); + expect(result.content[0]!.text).toContain("Signature valid"); + }); + + // 9 + it("mayros_dag_prune happy path with confirm", async () => { + globalThis.fetch = mockFetch({ + pruned_count: 10, + retained_count: 32, + checkpoint_hash: "chk_abc", + }); + const tool = findTool("mayros_dag_prune"); + const result = await tool.execute("id", { + policy: "keep_last", + value: 32, + create_checkpoint: true, + confirm: true, + }); + expect(result.content[0]!.text).toContain("Pruned: 10"); + expect(result.content[0]!.text).toContain("Retained: 32"); + expect(result.content[0]!.text).toContain("chk_abc"); + }); + + // 10 + it("mayros_dag_prune rejects without confirm", async () => { + const tool = findTool("mayros_dag_prune"); + const result = await tool.execute("id", { + policy: "keep_last", + value: 32, + }); + expect(result.content[0]!.text).toContain("aborted"); + expect(result.content[0]!.text).toContain("confirm must be true"); + }); + + // 11 + it("mayros_dag_history Cortex down", async () => { + globalThis.fetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED")); + const tool = findTool("mayros_dag_history"); + const result = await tool.execute("id", { subject: "test" }); + expect(result.content[0]!.text).toContain("Cortex unavailable"); + }); + + // 12 + it("mayros_dag_stats Cortex down", async () => { + globalThis.fetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED")); + const tool = findTool("mayros_dag_stats"); + const result = await tool.execute("id", {}); + expect(result.content[0]!.text).toContain("Cortex unavailable"); + }); + + // 13 + it("mayros_dag_tips HTTP error", async () => { + globalThis.fetch = mockFetch(null, false); + const tool = findTool("mayros_dag_tips"); + const result = await tool.execute("id", {}); + expect(result.content[0]!.text).toContain("failed"); + }); + + // 14 + it("mayros_dag_verify Cortex down", async () => { + globalThis.fetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED")); + const tool = findTool("mayros_dag_verify"); + const result = await tool.execute("id", { hash: "abc", public_key: "key" }); + expect(result.content[0]!.text).toContain("Cortex unavailable"); + }); + + // 15 + it("mayros_dag_diff HTTP error", async () => { + globalThis.fetch = mockFetch(null, false); + const tool = findTool("mayros_dag_diff"); + const result = await tool.execute("id", { from: "a", to: "b" }); + expect(result.content[0]!.text).toContain("failed"); + }); + + // 16 + it("mayros_dag_history empty results", async () => { + globalThis.fetch = mockFetch({ actions: [] }); + const tool = findTool("mayros_dag_history"); + const result = await tool.execute("id", { subject: "test:none" }); + expect(result.content[0]!.text).toContain("No DAG history"); + }); + + // 17 + it("mayros_dag_history caps limit at 500", async () => { + globalThis.fetch = mockFetch({ actions: [] }); + const tool = findTool("mayros_dag_history"); + await tool.execute("id", { subject: "test", limit: 99999 }); + const url = (globalThis.fetch as ReturnType).mock.calls[0]![0] as string; + expect(url).toContain("limit=500"); + }); + + // 18 + it("mayros_dag_export truncates large output", async () => { + const hugeOutput = "x".repeat(300 * 1024); // 300 KB + globalThis.fetch = mockFetch(null, true, hugeOutput); + const tool = findTool("mayros_dag_export"); + const result = await tool.execute("id", { format: "mermaid" }); + expect(result.content[0]!.text).toContain("TRUNCATED"); + expect(result.content[0]!.text!.length).toBeLessThan(300 * 1024); + }); + + // 20 + it("mayros_dag_export Cortex down", async () => { + globalThis.fetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED")); + const tool = findTool("mayros_dag_export"); + const result = await tool.execute("id", {}); + expect(result.content[0]!.text).toContain("Cortex unavailable"); + }); + + // 21 + it("mayros_dag_prune Cortex down with confirm", async () => { + globalThis.fetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED")); + const tool = findTool("mayros_dag_prune"); + const result = await tool.execute("id", { policy: "keep_all", confirm: true }); + expect(result.content[0]!.text).toContain("Cortex unavailable"); + }); + + // 22 + it("mayros_dag_action happy path", async () => { + globalThis.fetch = mockFetch({ + hash: "abc123def456789000000000", + parents: ["parent1aaa", "parent2bbb"], + author: "node-1", + seq: 5, + timestamp: "2026-03-13T12:00:00Z", + payload_type: "TripleInsert", + payload_summary: "added 3 triples", + signed: true, + signature: "ed25519sig0123456789abcdef", + }); + const tool = findTool("mayros_dag_action"); + const result = await tool.execute("id", { hash: "abc123def456789000000000" }); + expect(result.content[0]!.text).toContain("Action abc123def456…"); + expect(result.content[0]!.text).toContain("Author: node-1"); + expect(result.content[0]!.text).toContain("Seq: 5"); + expect(result.content[0]!.text).toContain("TripleInsert"); + expect(result.content[0]!.text).toContain("Signed: true"); + }); + + // 23 + it("mayros_dag_action genesis (no parents)", async () => { + globalThis.fetch = mockFetch({ + hash: "genesis000000000000000000", + parents: [], + author: "node-1", + seq: 0, + timestamp: "2026-03-01T00:00:00Z", + payload_type: "Genesis", + payload_summary: "initial state", + signed: false, + signature: null, + }); + const tool = findTool("mayros_dag_action"); + const result = await tool.execute("id", { hash: "genesis000000000000000000" }); + expect(result.content[0]!.text).toContain("(genesis)"); + expect(result.content[0]!.text).toContain("Signed: false"); + }); + + // 24 + it("mayros_dag_action Cortex down", async () => { + globalThis.fetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED")); + const tool = findTool("mayros_dag_action"); + const result = await tool.execute("id", { hash: "abc" }); + expect(result.content[0]!.text).toContain("Cortex unavailable"); + }); + + // 25 + it("mayros_dag_action HTTP error", async () => { + globalThis.fetch = mockFetch(null, false); + const tool = findTool("mayros_dag_action"); + const result = await tool.execute("id", { hash: "abc" }); + expect(result.content[0]!.text).toContain("failed"); + }); + + // 26 + it("mayros_dag_chain happy path", async () => { + globalThis.fetch = mockFetch({ + actions: [ + { + hash: "aaa111def456", + seq: 1, + timestamp: "2026-03-13T10:00:00Z", + payload_type: "TripleInsert", + payload_summary: "added fact", + }, + { + hash: "bbb222def456", + seq: 2, + timestamp: "2026-03-13T11:00:00Z", + payload_type: "TripleDelete", + payload_summary: "removed old", + }, + ], + }); + const tool = findTool("mayros_dag_chain"); + const result = await tool.execute("id", { author: "node-1", limit: 10 }); + expect(result.content[0]!.text).toContain('2 action(s) by "node-1"'); + expect(result.content[0]!.text).toContain("added fact"); + expect(result.content[0]!.text).toContain("removed old"); + }); + + // 27 + it("mayros_dag_chain empty results", async () => { + globalThis.fetch = mockFetch({ actions: [] }); + const tool = findTool("mayros_dag_chain"); + const result = await tool.execute("id", { author: "unknown-node" }); + expect(result.content[0]!.text).toContain("No DAG actions"); + }); + + // 28 + it("mayros_dag_chain Cortex down", async () => { + globalThis.fetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED")); + const tool = findTool("mayros_dag_chain"); + const result = await tool.execute("id", { author: "node-1" }); + expect(result.content[0]!.text).toContain("Cortex unavailable"); + }); + + // 29 + it("mayros_dag_chain caps limit at 500", async () => { + globalThis.fetch = mockFetch({ actions: [] }); + const tool = findTool("mayros_dag_chain"); + await tool.execute("id", { author: "node-1", limit: 99999 }); + const url = (globalThis.fetch as ReturnType).mock.calls[0]![0] as string; + expect(url).toContain("limit=500"); + }); +}); diff --git a/extensions/mcp-server/dag-tools.ts b/extensions/mcp-server/dag-tools.ts new file mode 100644 index 00000000..cbb2cf05 --- /dev/null +++ b/extensions/mcp-server/dag-tools.ts @@ -0,0 +1,613 @@ +/** + * MCP-friendly Semantic DAG tools. + * + * Exposes 10 tools for DAG audit, time-travel, history, action lookup, + * chain inspection, export, diff, stats, verification, and pruning. + */ + +import { Type } from "@sinclair/typebox"; +import type { AdaptableTool } from "./tool-adapter.js"; + +export type DagToolDeps = { + cortexBaseUrl: string; + namespace?: string; + authToken?: string; +}; + +/** Safety limit: truncate export output to avoid blowing up LLM context. */ +const MAX_EXPORT_CHARS = 256 * 1024; + +/** Default timeout for Cortex HTTP requests (30 s). */ +const REQUEST_TIMEOUT_MS = 30_000; + +export function createDagTools(deps: DagToolDeps): AdaptableTool[] { + const { cortexBaseUrl } = deps; + + const defaultHeaders: Record = {}; + if (deps.authToken) { + defaultHeaders["Authorization"] = deps.authToken; + } + + const postHeaders: Record = { + ...defaultHeaders, + "Content-Type": "application/json", + }; + + return [ + { + name: "mayros_dag_tips", + description: + "Get the current DAG tip hashes. " + + "Tips are the latest actions with no children — the frontier of the DAG.", + parameters: Type.Object({}), + execute: async () => { + try { + const res = await fetch(`${cortexBaseUrl}/api/v1/dag/tips`, { + headers: defaultHeaders, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!res.ok) { + return { + content: [{ type: "text" as const, text: `DAG tips failed: ${res.statusText}` }], + }; + } + const data = (await res.json()) as { tips: string[]; count: number }; + return { + content: [ + { + type: "text" as const, + text: `DAG has ${data.count} tip(s):\n${data.tips.join("\n")}`, + }, + ], + }; + } catch { + return { + content: [ + { + type: "text" as const, + text: "Cortex unavailable. DAG tips cannot be retrieved.", + }, + ], + }; + } + }, + }, + + { + name: "mayros_dag_action", + description: + "Get details of a specific DAG action by its hash. " + + "Returns author, sequence number, timestamp, payload type, parents, and signature status.", + parameters: Type.Object({ + hash: Type.String({ description: "DAG action hash to look up" }), + }), + execute: async (_id: string, params: Record) => { + try { + const hash = encodeURIComponent(params.hash as string); + const res = await fetch(`${cortexBaseUrl}/api/v1/dag/action/${hash}`, { + headers: defaultHeaders, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!res.ok) { + return { + content: [ + { type: "text" as const, text: `DAG action lookup failed: ${res.statusText}` }, + ], + }; + } + + const a = (await res.json()) as { + hash: string; + parents: string[]; + author: string; + seq: number; + timestamp: string; + payload_type: string; + payload_summary: string; + signed: boolean; + signature: string | null; + }; + + const parentsList = + a.parents.length === 0 + ? "(genesis)" + : a.parents.map((p) => p.slice(0, 12) + "…").join(", "); + const sig = a.signed ? ` sig:${a.signature?.slice(0, 16) ?? "?"}…` : ""; + + return { + content: [ + { + type: "text" as const, + text: + `Action ${a.hash.slice(0, 12)}…\n` + + ` Author: ${a.author}\n` + + ` Seq: ${a.seq}\n` + + ` Timestamp: ${a.timestamp}\n` + + ` Type: ${a.payload_type}\n` + + ` Summary: ${a.payload_summary}\n` + + ` Parents: ${parentsList}\n` + + ` Signed: ${a.signed}${sig}`, + }, + ], + }; + } catch { + return { + content: [ + { type: "text" as const, text: "Cortex unavailable. DAG action lookup failed." }, + ], + }; + } + }, + }, + + { + name: "mayros_dag_chain", + description: + "Get the DAG action chain for a specific author/node. " + + "Shows all actions created by a given author in sequence order.", + parameters: Type.Object({ + author: Type.String({ description: "Author node ID to query chain for" }), + limit: Type.Optional( + Type.Number({ description: "Max actions to return (default 20, max 500)" }), + ), + }), + execute: async (_id: string, params: Record) => { + try { + const limit = Math.min((params.limit as number) ?? 20, 500); + const qs = new URLSearchParams(); + qs.set("author", params.author as string); + qs.set("limit", String(limit)); + + const res = await fetch(`${cortexBaseUrl}/api/v1/dag/chain?${qs}`, { + headers: defaultHeaders, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!res.ok) { + return { + content: [{ type: "text" as const, text: `DAG chain failed: ${res.statusText}` }], + }; + } + + const data = (await res.json()) as { + actions: Array<{ + hash: string; + seq: number; + timestamp: string; + payload_type: string; + payload_summary: string; + }>; + }; + + if (!data.actions || data.actions.length === 0) { + return { + content: [ + { + type: "text" as const, + text: `No DAG actions for author "${String(params.author)}".`, + }, + ], + }; + } + + const lines = data.actions.map( + (a) => ` #${a.seq} [${a.payload_type}] ${a.payload_summary} (${a.hash.slice(0, 12)}…)`, + ); + + return { + content: [ + { + type: "text" as const, + text: `${data.actions.length} action(s) by "${String(params.author)}":\n${lines.join("\n")}`, + }, + ], + }; + } catch { + return { + content: [ + { type: "text" as const, text: "Cortex unavailable. DAG chain cannot be retrieved." }, + ], + }; + } + }, + }, + + { + name: "mayros_dag_history", + description: + "Get the DAG action history for a specific subject. " + + "Shows all mutations that affected a given subject in the knowledge graph.", + parameters: Type.Object({ + subject: Type.String({ description: "Subject to query history for (e.g., 'project:api')" }), + limit: Type.Optional( + Type.Number({ description: "Max actions to return (default 20, max 500)" }), + ), + }), + execute: async (_id: string, params: Record) => { + try { + const limit = Math.min((params.limit as number) ?? 20, 500); + const qs = new URLSearchParams(); + qs.set("subject", params.subject as string); + qs.set("limit", String(limit)); + + const res = await fetch(`${cortexBaseUrl}/api/v1/dag/history?${qs}`, { + headers: defaultHeaders, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!res.ok) { + return { + content: [{ type: "text" as const, text: `DAG history failed: ${res.statusText}` }], + }; + } + + const data = (await res.json()) as { + actions: Array<{ + hash: string; + seq: number; + timestamp: string; + payload_type: string; + payload_summary: string; + }>; + }; + + if (!data.actions || data.actions.length === 0) { + return { + content: [ + { + type: "text" as const, + text: `No DAG history for subject "${String(params.subject)}".`, + }, + ], + }; + } + + const lines = data.actions.map( + (a) => ` #${a.seq} [${a.payload_type}] ${a.payload_summary} (${a.hash.slice(0, 12)}…)`, + ); + + return { + content: [ + { + type: "text" as const, + text: `${data.actions.length} action(s) for "${String(params.subject)}":\n${lines.join("\n")}`, + }, + ], + }; + } catch { + return { + content: [ + { + type: "text" as const, + text: "Cortex unavailable. DAG history cannot be retrieved.", + }, + ], + }; + } + }, + }, + + { + name: "mayros_dag_time_travel", + description: + "Time-travel to a specific DAG action hash. " + + "Reconstructs the knowledge graph state as it was at that point in time.", + parameters: Type.Object({ + hash: Type.String({ description: "DAG action hash to travel to" }), + }), + execute: async (_id: string, params: Record) => { + try { + const hash = encodeURIComponent(params.hash as string); + const res = await fetch(`${cortexBaseUrl}/api/v1/dag/at/${hash}`, { + headers: defaultHeaders, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!res.ok) { + return { + content: [ + { type: "text" as const, text: `DAG time-travel failed: ${res.statusText}` }, + ], + }; + } + + const data = (await res.json()) as { + target_hash: string; + target_timestamp: string; + actions_replayed: number; + triple_count: number; + }; + + return { + content: [ + { + type: "text" as const, + text: + `Time-travel to ${data.target_hash.slice(0, 12)}…\n` + + ` Timestamp: ${data.target_timestamp}\n` + + ` Actions replayed: ${data.actions_replayed}\n` + + ` Triples at that point: ${data.triple_count}`, + }, + ], + }; + } catch { + return { + content: [ + { type: "text" as const, text: "Cortex unavailable. DAG time-travel failed." }, + ], + }; + } + }, + }, + + { + name: "mayros_dag_diff", + description: + "Show the diff between two DAG action hashes. " + + "Lists all actions between the two points.", + parameters: Type.Object({ + from: Type.String({ description: "Starting action hash" }), + to: Type.String({ description: "Ending action hash" }), + }), + execute: async (_id: string, params: Record) => { + try { + const qs = new URLSearchParams(); + qs.set("from", params.from as string); + qs.set("to", params.to as string); + + const res = await fetch(`${cortexBaseUrl}/api/v1/dag/diff?${qs}`, { + headers: defaultHeaders, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!res.ok) { + return { + content: [{ type: "text" as const, text: `DAG diff failed: ${res.statusText}` }], + }; + } + + const data = (await res.json()) as { + from: string; + to: string; + action_count: number; + actions: Array<{ + hash: string; + payload_type: string; + payload_summary: string; + }>; + }; + + const lines = data.actions.map( + (a) => ` [${a.payload_type}] ${a.payload_summary} (${a.hash.slice(0, 12)}…)`, + ); + + return { + content: [ + { + type: "text" as const, + text: + `Diff: ${data.from.slice(0, 12)}… → ${data.to.slice(0, 12)}…\n` + + `${data.action_count} action(s):\n${lines.join("\n")}`, + }, + ], + }; + } catch { + return { + content: [{ type: "text" as const, text: "Cortex unavailable. DAG diff failed." }], + }; + } + }, + }, + + { + name: "mayros_dag_export", + description: + "Export the DAG as a visual graph. " + + "Supports Mermaid, DOT (Graphviz), and JSON formats. " + + "Output is truncated at 256 KB to protect LLM context.", + parameters: Type.Object({ + format: Type.Optional( + Type.Union([Type.Literal("mermaid"), Type.Literal("dot"), Type.Literal("json")], { + description: "Export format (default: mermaid)", + default: "mermaid", + }), + ), + }), + execute: async (_id: string, params: Record) => { + try { + const format = (params.format as string) ?? "mermaid"; + const res = await fetch( + `${cortexBaseUrl}/api/v1/dag/export?format=${encodeURIComponent(format)}`, + { headers: defaultHeaders, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) }, + ); + if (!res.ok) { + return { + content: [{ type: "text" as const, text: `DAG export failed: ${res.statusText}` }], + }; + } + + let text = await res.text(); + let truncated = false; + if (text.length > MAX_EXPORT_CHARS) { + text = text.slice(0, MAX_EXPORT_CHARS); + truncated = true; + } + + return { + content: [ + { + type: "text" as const, + text: truncated + ? `${text}\n\n[OUTPUT TRUNCATED — ${MAX_EXPORT_CHARS} char limit reached. Use the CLI \`mayros dag export\` for the full output.]` + : text, + }, + ], + }; + } catch { + return { + content: [{ type: "text" as const, text: "Cortex unavailable. DAG export failed." }], + }; + } + }, + }, + + { + name: "mayros_dag_stats", + description: "Get DAG statistics: total action count and tip count.", + parameters: Type.Object({}), + execute: async () => { + try { + const res = await fetch(`${cortexBaseUrl}/api/v1/dag/stats`, { + headers: defaultHeaders, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!res.ok) { + return { + content: [{ type: "text" as const, text: `DAG stats failed: ${res.statusText}` }], + }; + } + + const data = (await res.json()) as { action_count: number; tip_count: number }; + return { + content: [ + { + type: "text" as const, + text: `DAG Statistics:\n Actions: ${data.action_count}\n Tips: ${data.tip_count}`, + }, + ], + }; + } catch { + return { + content: [ + { type: "text" as const, text: "Cortex unavailable. DAG stats cannot be retrieved." }, + ], + }; + } + }, + }, + + { + name: "mayros_dag_verify", + description: + "Verify the Ed25519 signature of a DAG action. " + + "Checks cryptographic integrity of a specific action.", + parameters: Type.Object({ + hash: Type.String({ description: "DAG action hash to verify" }), + public_key: Type.String({ description: "Ed25519 public key (hex or base64)" }), + }), + execute: async (_id: string, params: Record) => { + try { + const hash = encodeURIComponent(params.hash as string); + const pubKey = encodeURIComponent(params.public_key as string); + const res = await fetch( + `${cortexBaseUrl}/api/v1/dag/verify/${hash}?public_key=${pubKey}`, + { headers: defaultHeaders, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) }, + ); + if (!res.ok) { + return { + content: [{ type: "text" as const, text: `DAG verify failed: ${res.statusText}` }], + }; + } + + const data = (await res.json()) as { + valid: boolean; + action_hash: string; + detail: string; + }; + + return { + content: [ + { + type: "text" as const, + text: `Verification: ${data.valid ? "VALID" : "INVALID"}\n Hash: ${data.action_hash}\n Detail: ${data.detail}`, + }, + ], + }; + } catch { + return { + content: [ + { type: "text" as const, text: "Cortex unavailable. DAG verification failed." }, + ], + }; + } + }, + }, + + { + name: "mayros_dag_prune", + description: + "DESTRUCTIVE: Prune old DAG actions. This permanently removes history. " + + "Always confirm with the user before calling. " + + "Policies: keep_all, keep_since, keep_last, keep_depth. " + + "Optionally creates a checkpoint before pruning.", + parameters: Type.Object({ + policy: Type.Union( + [ + Type.Literal("keep_all"), + Type.Literal("keep_since"), + Type.Literal("keep_last"), + Type.Literal("keep_depth"), + ], + { description: "Prune policy" }, + ), + value: Type.Optional( + Type.Number({ description: "Policy value (timestamp, count, or depth)" }), + ), + create_checkpoint: Type.Optional( + Type.Boolean({ description: "Create checkpoint before pruning (default: false)" }), + ), + confirm: Type.Boolean({ + description: "Must be true to execute. This is a destructive operation.", + }), + }), + execute: async (_id: string, params: Record) => { + if (params.confirm !== true) { + return { + content: [ + { + type: "text" as const, + text: "Prune aborted: confirm must be true. This is a destructive operation — ask the user to confirm before proceeding.", + }, + ], + }; + } + + try { + const res = await fetch(`${cortexBaseUrl}/api/v1/dag/prune`, { + method: "POST", + headers: postHeaders, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + body: JSON.stringify({ + policy: params.policy, + value: params.value, + create_checkpoint: params.create_checkpoint, + }), + }); + + if (!res.ok) { + return { + content: [{ type: "text" as const, text: `DAG prune failed: ${res.statusText}` }], + }; + } + + const data = (await res.json()) as { + pruned_count: number; + retained_count: number; + checkpoint_hash: string | null; + }; + + const checkpoint = data.checkpoint_hash ? `\n Checkpoint: ${data.checkpoint_hash}` : ""; + + return { + content: [ + { + type: "text" as const, + text: `Prune complete:\n Pruned: ${data.pruned_count}\n Retained: ${data.retained_count}${checkpoint}`, + }, + ], + }; + } catch { + return { + content: [{ type: "text" as const, text: "Cortex unavailable. DAG prune failed." }], + }; + } + }, + }, + ]; +} diff --git a/extensions/mcp-server/index.ts b/extensions/mcp-server/index.ts index f54030d3..b8cdfc09 100644 --- a/extensions/mcp-server/index.ts +++ b/extensions/mcp-server/index.ts @@ -92,6 +92,8 @@ const mcpServerPlugin = { getRule: async () => null, getGraphStats: async () => null, listGraphSubjects: async () => [], + getDagTips: async () => null, + getDagStats: async () => null, }; const promptSources: PromptDataSources = { @@ -195,13 +197,22 @@ const mcpServerPlugin = { const { createGovernanceTools } = await import("./governance-tools.js"); const { createCortexTools } = await import("./cortex-tools.js"); + const authToken = cfg.cortex?.authToken; const mcpTools: AdaptableTool[] = [ - ...createMemoryTools({ cortexBaseUrl: cortexBase, namespace: ns }), + ...createMemoryTools({ cortexBaseUrl: cortexBase, namespace: ns, authToken }), ...createBudgetTools(), ...createGovernanceTools(), - ...createCortexTools({ cortexBaseUrl: cortexBase, namespace: ns }), + ...createCortexTools({ cortexBaseUrl: cortexBase, namespace: ns, authToken }), ]; + // DAG tools — enabled by default, opt-out via cortex.dag.enabled = false + if (cfg.cortex?.dag?.enabled !== false) { + const { createDagTools } = await import("./dag-tools.js"); + mcpTools.push( + ...createDagTools({ cortexBaseUrl: cortexBase, namespace: ns, authToken }), + ); + } + // Combine: dedicated MCP tools first, then auto-discovered plugin tools const allTools = [...mcpTools, ...pluginTools]; @@ -433,6 +444,27 @@ const mcpServerPlugin = { return []; } }; + + // DAG resources — enabled by default + if (cfg.cortex?.dag?.enabled !== false) { + resourceSources.getDagTips = async () => { + try { + const data = await client.dagTips(); + return { tips: data.tips, count: data.count }; + } catch { + return null; + } + }; + + resourceSources.getDagStats = async () => { + try { + const data = await client.dagStats(); + return { actionCount: data.action_count, tipCount: data.tip_count }; + } catch { + return null; + } + }; + } } catch { // Cortex not available } diff --git a/extensions/mcp-server/memory-tools.ts b/extensions/mcp-server/memory-tools.ts index 00ece1af..12468673 100644 --- a/extensions/mcp-server/memory-tools.ts +++ b/extensions/mcp-server/memory-tools.ts @@ -12,11 +12,25 @@ import type { AdaptableTool } from "./tool-adapter.js"; export type MemoryToolDeps = { cortexBaseUrl: string; namespace: string; + authToken?: string; }; +/** Default timeout for Cortex HTTP requests (30 s). */ +const REQUEST_TIMEOUT_MS = 30_000; + export function createMemoryTools(deps: MemoryToolDeps): AdaptableTool[] { const { cortexBaseUrl, namespace } = deps; + const defaultHeaders: Record = {}; + if (deps.authToken) { + defaultHeaders["Authorization"] = deps.authToken; + } + + const postHeaders: Record = { + ...defaultHeaders, + "Content-Type": "application/json", + }; + return [ // ── mayros_remember ────────────────────────────────────────────── { @@ -50,7 +64,7 @@ export function createMemoryTools(deps: MemoryToolDeps): AdaptableTool[] { const content = params.content as string; const category = (params.category as string) ?? "general"; const tags = Array.isArray(params.tags) ? (params.tags as string[]) : []; - const importance = Number(params.importance) || 0.7; + const importance = Number(params.importance ?? 0.7); // Store as RDF triple in Cortex (timestamp + random suffix to avoid collisions) const subject = `${namespace}:memory:${Date.now()}-${randomBytes(4).toString("hex")}`; @@ -65,37 +79,43 @@ export function createMemoryTools(deps: MemoryToolDeps): AdaptableTool[] { })), ]; - // Store in Cortex graph + // Store in Cortex graph (parallel) + Ineru STM const errors: string[] = []; - for (const triple of triples) { - try { - const tripleRes = await fetch(`${cortexBaseUrl}/api/v1/triples`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(triple), - }); - if (!tripleRes.ok) errors.push(`triple store: ${tripleRes.statusText}`); - } catch (err) { - errors.push(`triple store: ${err instanceof Error ? err.message : String(err)}`); - } - } - // Also store in Ineru STM for vector search - try { - const memRes = await fetch(`${cortexBaseUrl}/api/v1/memory/remember`, { + const triplePromises = triples.map((triple) => + fetch(`${cortexBaseUrl}/api/v1/triples`, { method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - entry_type: category, - data: { content, tags }, - tags, - importance, + headers: postHeaders, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + body: JSON.stringify(triple), + }) + .then((res) => { + if (!res.ok) errors.push(`triple store: ${res.statusText}`); + }) + .catch((err) => { + errors.push(`triple store: ${err instanceof Error ? err.message : String(err)}`); }), + ); + + const ineruPromise = fetch(`${cortexBaseUrl}/api/v1/memory/remember`, { + method: "POST", + headers: postHeaders, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + body: JSON.stringify({ + entry_type: category, + data: { content, tags }, + tags, + importance, + }), + }) + .then((res) => { + if (!res.ok) errors.push(`ineru store: ${res.statusText}`); + }) + .catch((err) => { + errors.push(`ineru store: ${err instanceof Error ? err.message : String(err)}`); }); - if (!memRes.ok) errors.push(`ineru store: ${memRes.statusText}`); - } catch (err) { - errors.push(`ineru store: ${err instanceof Error ? err.message : String(err)}`); - } + + await Promise.allSettled([...triplePromises, ineruPromise]); const summary = `Remembered: "${content.slice(0, 80)}${content.length > 80 ? "..." : ""}" [${category}]${tags.length > 0 ? ` #${tags.join(" #")}` : ""}`; return { @@ -126,14 +146,15 @@ export function createMemoryTools(deps: MemoryToolDeps): AdaptableTool[] { const query = params.query as string | undefined; const tags = Array.isArray(params.tags) ? (params.tags as string[]) : undefined; const category = params.category as string | undefined; - const limit = Math.min(Number(params.limit) || 10, 100); + const limit = Math.min((params.limit as number) ?? 10, 100); // Query Ineru recall endpoint let recallRes: Response; try { recallRes = await fetch(`${cortexBaseUrl}/api/v1/memory/recall`, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: postHeaders, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), body: JSON.stringify({ text: query, tags: tags ?? [], @@ -157,6 +178,7 @@ export function createMemoryTools(deps: MemoryToolDeps): AdaptableTool[] { try { const graphRes = await fetch( `${cortexBaseUrl}/api/v1/triples?predicate=${encodeURIComponent(`${namespace}:memory:content`)}&limit=${limit}`, + { headers: defaultHeaders, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) }, ); if (!graphRes.ok) { return { @@ -256,14 +278,16 @@ export function createMemoryTools(deps: MemoryToolDeps): AdaptableTool[] { }), execute: async (_id: string, params: Record) => { const text = params.text as string; - const k = Math.min(Number(params.k) || 5, 100); + const k = Math.min((params.k as number) ?? 5, 100); + const minSim = Number(params.min_similarity ?? 0.3); let recallRes: Response; try { recallRes = await fetch(`${cortexBaseUrl}/api/v1/memory/recall`, { method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ text, limit: k }), + headers: postHeaders, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + body: JSON.stringify({ text, limit: k, min_similarity: minSim }), }); } catch { return { @@ -340,6 +364,8 @@ export function createMemoryTools(deps: MemoryToolDeps): AdaptableTool[] { `${cortexBaseUrl}/api/v1/memory/${encodeURIComponent(memoryId)}`, { method: "DELETE", + headers: defaultHeaders, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), }, ); return { diff --git a/extensions/mcp-server/package.json b/extensions/mcp-server/package.json index 79b9b4fd..f71af5e5 100644 --- a/extensions/mcp-server/package.json +++ b/extensions/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-mcp-server", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "MCP server exposing Mayros tools, Cortex resources, and workflow prompts via Model Context Protocol", "type": "module", diff --git a/extensions/mcp-server/prompt-provider.test.ts b/extensions/mcp-server/prompt-provider.test.ts index 553b4a1f..e06e96f6 100644 --- a/extensions/mcp-server/prompt-provider.test.ts +++ b/extensions/mcp-server/prompt-provider.test.ts @@ -148,4 +148,36 @@ describe("McpPromptProvider", () => { it("unknown prompt throws PROMPT_NOT_FOUND", async () => { await expect(provider.getPrompt("nonexistent", {})).rejects.toThrow(); }); + + // 17 + it("listPrompts includes dag-audit", () => { + const prompts = provider.listPrompts(); + const names = prompts.map((p) => p.name); + expect(names).toContain("dag-audit"); + }); + + // 18 + it("dag-audit returns audit workflow instructions", async () => { + const messages = await provider.getPrompt("dag-audit", { + subject: "project:api", + depth: "5", + }); + expect(messages).toHaveLength(1); + expect(messages[0]!.content.text).toContain("project:api"); + expect(messages[0]!.content.text).toContain("last 5 actions"); + expect(messages[0]!.content.text).toContain("mayros_dag_history"); + expect(messages[0]!.content.text).toContain("mayros_dag_verify"); + expect(messages[0]!.content.text).toContain("mayros_dag_diff"); + }); + + // 19 + it("dag-audit defaults depth to 10", async () => { + const messages = await provider.getPrompt("dag-audit", { subject: "test:sub" }); + expect(messages[0]!.content.text).toContain("last 10 actions"); + }); + + // 20 + it("dag-audit throws without subject", async () => { + await expect(provider.getPrompt("dag-audit", {})).rejects.toThrow("subject"); + }); }); diff --git a/extensions/mcp-server/prompt-provider.ts b/extensions/mcp-server/prompt-provider.ts index a35b3934..abf3e763 100644 --- a/extensions/mcp-server/prompt-provider.ts +++ b/extensions/mcp-server/prompt-provider.ts @@ -117,6 +117,23 @@ const PROMPT_DEFINITIONS: McpPromptDef[] = [ }, ], }, + { + name: "dag-audit", + description: + "Audit the semantic DAG for a subject: review history, verify signatures, and diff changes", + arguments: [ + { + name: "subject", + description: "Subject to audit (e.g., 'project:api')", + required: true, + }, + { + name: "depth", + description: "Number of recent actions to review (default: 10)", + required: false, + }, + ], + }, ]; // ============================================================================ @@ -161,6 +178,9 @@ export class McpPromptProvider { case "feature-development": return this.buildFeatureDev(args.feature, args.phase); + case "dag-audit": + return this.buildDagAudit(args.subject, args.depth); + default: throw new McpError(ErrorCodes.PROMPT_NOT_FOUND, `Unknown prompt: ${name}`); } @@ -405,4 +425,52 @@ export class McpPromptProvider { }, ]; } + + private buildDagAudit(subject?: string, depth?: string): McpPromptMessage[] { + if (!subject) { + throw new McpError(ErrorCodes.INVALID_PARAMS, "Missing required argument: subject"); + } + + const parsed = depth ? parseInt(depth, 10) : 10; + const limit = Number.isNaN(parsed) || parsed < 1 ? 10 : parsed; + + const instructions = [ + `# DAG Audit: ${subject}`, + ``, + `## Objective`, + `Perform a complete audit of the semantic DAG for subject "${subject}".`, + `Review the last ${limit} actions, verify signatures, and identify anomalies.`, + ``, + `## Step 1: Retrieve History`, + `Use the \`mayros_dag_history\` tool with:`, + ` - subject: "${subject}"`, + ` - limit: ${limit}`, + ``, + `Review each action's payload type, author, timestamp, and sequence number.`, + `Flag any gaps in sequence numbers or unexpected authors.`, + ``, + `## Step 2: Verify Signatures`, + `For each signed action in the history, use the \`mayros_dag_verify\` tool`, + `to confirm Ed25519 signature validity. Report any invalid signatures.`, + ``, + `## Step 3: Diff Analysis`, + `If there are at least 2 actions, use \`mayros_dag_diff\` between the`, + `oldest and newest action hashes to see the full change set.`, + `Summarize what changed and whether the mutations are consistent.`, + ``, + `## Output Format`, + `Provide a structured audit report with:`, + `- Timeline of actions`, + `- Signature verification results`, + `- Anomalies or concerns`, + `- Summary assessment`, + ]; + + return [ + { + role: "assistant", + content: { type: "text", text: instructions.join("\n") }, + }, + ]; + } } diff --git a/extensions/mcp-server/resource-provider.test.ts b/extensions/mcp-server/resource-provider.test.ts index 4454a667..d0cc1bbe 100644 --- a/extensions/mcp-server/resource-provider.test.ts +++ b/extensions/mcp-server/resource-provider.test.ts @@ -4,7 +4,6 @@ import { type ResourceDataSources, type AgentInfo, } from "./resource-provider.js"; -import { ErrorCodes } from "./protocol.js"; // ── Mock data ───────────────────────────────────────────────────────── @@ -80,6 +79,14 @@ function createMockSources(): ResourceDataSources { predicateCount: 45, }), listGraphSubjects: async () => ["mayros:project:convention:c1", "mayros:rule:global:r1"], + getDagTips: async () => ({ + tips: ["abc123", "def456"], + count: 2, + }), + getDagStats: async () => ({ + actionCount: 42, + tipCount: 2, + }), }; } @@ -184,6 +191,46 @@ describe("McpResourceProvider", () => { }); // 13 + it("listResources includes DAG resources", async () => { + const resources = await provider.listResources(); + const uris = resources.map((r) => r.uri); + expect(uris).toContain("mayros:///dag/tips"); + expect(uris).toContain("mayros:///dag/stats"); + }); + + // 14 + it("readResource dag tips", async () => { + const result = await provider.readResource("mayros:///dag/tips"); + const data = JSON.parse(result.text!); + expect(data.tips).toEqual(["abc123", "def456"]); + expect(data.count).toBe(2); + }); + + // 15 + it("readResource dag stats", async () => { + const result = await provider.readResource("mayros:///dag/stats"); + const data = JSON.parse(result.text!); + expect(data.actionCount).toBe(42); + expect(data.tipCount).toBe(2); + }); + + // 16 + it("readResource dag tips returns error when Cortex unavailable", async () => { + provider.updateSources({ getDagTips: async () => null }); + const result = await provider.readResource("mayros:///dag/tips"); + const data = JSON.parse(result.text!); + expect(data.error).toBe("Cortex unavailable"); + }); + + // 17 + it("readResource dag stats returns error when Cortex unavailable", async () => { + provider.updateSources({ getDagStats: async () => null }); + const result = await provider.readResource("mayros:///dag/stats"); + const data = JSON.parse(result.text!); + expect(data.error).toBe("Cortex unavailable"); + }); + + // 18 it("updateSources replaces data sources", async () => { provider.updateSources({ listAgents: () => [], diff --git a/extensions/mcp-server/resource-provider.ts b/extensions/mcp-server/resource-provider.ts index 0db892bc..153136c8 100644 --- a/extensions/mcp-server/resource-provider.ts +++ b/extensions/mcp-server/resource-provider.ts @@ -61,6 +61,18 @@ export type GraphStatsInfo = { predicateCount: number; }; +/** DAG tips info for resource exposure. */ +export type DagTipsInfo = { + tips: string[]; + count: number; +}; + +/** DAG statistics for resource exposure. */ +export type DagStatsInfo = { + actionCount: number; + tipCount: number; +}; + // ============================================================================ // Data Source Callbacks // ============================================================================ @@ -74,6 +86,8 @@ export type ResourceDataSources = { getRule: (id: string) => Promise; getGraphStats: () => Promise; listGraphSubjects: () => Promise; + getDagTips: () => Promise; + getDagStats: () => Promise; }; // ============================================================================ @@ -132,6 +146,20 @@ export class McpResourceProvider { mimeType: "application/json", }); + resources.push({ + uri: "mayros:///dag/tips", + name: "DAG Tips", + description: "Current DAG tip hashes (frontier of the semantic DAG)", + mimeType: "application/json", + }); + + resources.push({ + uri: "mayros:///dag/stats", + name: "DAG Statistics", + description: "Semantic DAG action count and tip count", + mimeType: "application/json", + }); + // Dynamic agent resources const agents = this.sources.listAgents(); for (const agent of agents) { @@ -166,7 +194,7 @@ export class McpResourceProvider { return { uri, mimeType: "application/json", text: JSON.stringify(summary, null, 2) }; } - const agentMatch = path.match(/^\/agents\/([a-z][a-z0-9_-]*)$/); + const agentMatch = path.match(/^\/agents\/([a-zA-Z][a-zA-Z0-9_.-]*)$/); if (agentMatch) { const agent = this.sources.getAgent(agentMatch[1]!); if (!agent) { @@ -226,9 +254,42 @@ export class McpResourceProvider { if (path === "/graph/subjects") { const subjects = await this.sources.listGraphSubjects(); + if (!subjects) { + return { + uri, + mimeType: "application/json", + text: JSON.stringify({ error: "Cortex unavailable" }), + }; + } return { uri, mimeType: "application/json", text: JSON.stringify(subjects, null, 2) }; } + // ── DAG ─────────────────────────────────────────────────────────── + + if (path === "/dag/tips") { + const tips = await this.sources.getDagTips(); + if (!tips) { + return { + uri, + mimeType: "application/json", + text: JSON.stringify({ error: "Cortex unavailable" }), + }; + } + return { uri, mimeType: "application/json", text: JSON.stringify(tips, null, 2) }; + } + + if (path === "/dag/stats") { + const stats = await this.sources.getDagStats(); + if (!stats) { + return { + uri, + mimeType: "application/json", + text: JSON.stringify({ error: "Cortex unavailable" }), + }; + } + return { uri, mimeType: "application/json", text: JSON.stringify(stats, null, 2) }; + } + throw new McpError(ErrorCodes.RESOURCE_NOT_FOUND, `Unknown resource: ${uri}`); } } diff --git a/extensions/mcp-server/server.test.ts b/extensions/mcp-server/server.test.ts index b9dd01d2..78cb2950 100644 --- a/extensions/mcp-server/server.test.ts +++ b/extensions/mcp-server/server.test.ts @@ -43,6 +43,8 @@ function createEmptyResourceSources(): ResourceDataSources { getRule: async () => null, getGraphStats: async () => null, listGraphSubjects: async () => [], + getDagTips: async () => null, + getDagStats: async () => null, }; } diff --git a/extensions/memory-core/package.json b/extensions/memory-core/package.json index b3b5a3e4..88a9cc09 100644 --- a/extensions/memory-core/package.json +++ b/extensions/memory-core/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-memory-core", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros core memory search plugin", "type": "module", diff --git a/extensions/memory-lancedb/package.json b/extensions/memory-lancedb/package.json index c80d4723..76f8e38e 100644 --- a/extensions/memory-lancedb/package.json +++ b/extensions/memory-lancedb/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-memory-lancedb", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros LanceDB-backed long-term memory plugin with auto-recall/capture", "type": "module", diff --git a/extensions/memory-semantic/package.json b/extensions/memory-semantic/package.json index 36b93da3..d86f4ff3 100644 --- a/extensions/memory-semantic/package.json +++ b/extensions/memory-semantic/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-memory-semantic", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros semantic memory plugin via AIngle Cortex sidecar (RDF triples, identity graph, Ineru STM/LTM)", "type": "module", diff --git a/extensions/minimax-portal-auth/package.json b/extensions/minimax-portal-auth/package.json index a36d45cd..9083e69a 100644 --- a/extensions/minimax-portal-auth/package.json +++ b/extensions/minimax-portal-auth/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-minimax-portal-auth", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros MiniMax Portal OAuth provider plugin", "type": "module", diff --git a/extensions/msteams/package.json b/extensions/msteams/package.json index aed118d9..a8de58ca 100644 --- a/extensions/msteams/package.json +++ b/extensions/msteams/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-msteams", - "version": "0.1.16", + "version": "0.2.0", "description": "Mayros Microsoft Teams channel plugin", "license": "MIT", "type": "module", diff --git a/extensions/nextcloud-talk/package.json b/extensions/nextcloud-talk/package.json index 3afed6f3..3424181e 100644 --- a/extensions/nextcloud-talk/package.json +++ b/extensions/nextcloud-talk/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-nextcloud-talk", - "version": "0.1.16", + "version": "0.2.0", "description": "Mayros Nextcloud Talk channel plugin", "license": "MIT", "type": "module", diff --git a/extensions/nostr/package.json b/extensions/nostr/package.json index b6fc436f..8c890d2c 100644 --- a/extensions/nostr/package.json +++ b/extensions/nostr/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-nostr", - "version": "0.1.16", + "version": "0.2.0", "description": "Mayros Nostr channel plugin for NIP-04 encrypted DMs", "license": "MIT", "type": "module", diff --git a/extensions/open-prose/package.json b/extensions/open-prose/package.json index d5074dcd..4b676495 100644 --- a/extensions/open-prose/package.json +++ b/extensions/open-prose/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-open-prose", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "OpenProse VM skill pack plugin (slash command + telemetry).", "type": "module", diff --git a/extensions/osameru-governance/package.json b/extensions/osameru-governance/package.json index 4a839f97..733992a2 100644 --- a/extensions/osameru-governance/package.json +++ b/extensions/osameru-governance/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-osameru-governance", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros governance control plane — policy enforcement, HMAC audit trail, trust tiers", "type": "module", diff --git a/extensions/semantic-observability/package.json b/extensions/semantic-observability/package.json index 63d473dc..4546a075 100644 --- a/extensions/semantic-observability/package.json +++ b/extensions/semantic-observability/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-semantic-observability", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros semantic observability plugin — structured tracing of agent decisions as RDF events", "type": "module", diff --git a/extensions/semantic-skills/package.json b/extensions/semantic-skills/package.json index 36873b9d..a13c2208 100644 --- a/extensions/semantic-skills/package.json +++ b/extensions/semantic-skills/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-semantic-skills", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros semantic skills plugin — graph-aware skills with PoL assertions, ZK proofs, and permission gating", "type": "module", diff --git a/extensions/shared/cortex-client.ts b/extensions/shared/cortex-client.ts index 30f38552..e985249f 100644 --- a/extensions/shared/cortex-client.ts +++ b/extensions/shared/cortex-client.ts @@ -273,6 +273,68 @@ export type P2pDisconnectResponse = { status: string; }; +// ============================================================================ +// DAG DTOs +// ============================================================================ + +export type DagActionDto = { + hash: string; + parents: string[]; + author: string; + seq: number; + timestamp: string; + payload_type: string; + payload_summary: string; + signed: boolean; + signature: string | null; +}; + +export type DagTipsResponse = { tips: string[]; count: number }; + +export type DagStatsResponse = { action_count: number; tip_count: number }; + +export type DagTimeTravelResponse = { + target_hash: string; + target_timestamp: string; + actions_replayed: number; + triple_count: number; + triples: Array<{ subject: string; predicate: string; object: unknown }>; +}; + +export type DagDiffResponse = { + from: string; + to: string; + action_count: number; + actions: DagActionDto[]; +}; + +export type DagPruneRequest = { + policy: "keep_all" | "keep_since" | "keep_last" | "keep_depth"; + value?: number; + create_checkpoint?: boolean; +}; + +export type DagPruneResponse = { + pruned_count: number; + retained_count: number; + checkpoint_hash: string | null; +}; + +export type DagSyncRequest = { local_tips: string[]; want?: string[] }; +export type DagSyncResponse = { + actions: DagActionDto[]; + remote_tips: string[]; + action_count: number; +}; +export type DagPullRequest = { peer_url: string }; +export type DagPullResponse = { ingested: number; already_had: number; remote_tips: string[] }; +export type DagVerifyResponse = { + valid: boolean; + public_key: string; + action_hash: string; + detail: string; +}; + // ============================================================================ // Error // ============================================================================ @@ -725,4 +787,77 @@ export class CortexClient implements CortexClientLike, CortexLike { async rebuildVectorIndex(): Promise { return this.request("POST", "/api/v1/memory/index/rebuild"); } + + // ---------- Semantic DAG ---------- + + async dagTips(): Promise { + return this.request("GET", "/api/v1/dag/tips"); + } + + async dagAction(hash: string): Promise { + return this.request("GET", `/api/v1/dag/action/${encodeURIComponent(hash)}`); + } + + async dagHistory(opts: { + subject: string; + limit?: number; + }): Promise<{ actions: DagActionDto[] }> { + const qs = this.queryString({ subject: opts.subject, limit: opts.limit }); + return this.request("GET", `/api/v1/dag/history${qs}`); + } + + async dagChain(author: string, limit?: number): Promise<{ actions: DagActionDto[] }> { + const qs = this.queryString({ author, limit }); + return this.request("GET", `/api/v1/dag/chain${qs}`); + } + + async dagStats(): Promise { + return this.request("GET", "/api/v1/dag/stats"); + } + + async dagPrune(req: DagPruneRequest): Promise { + return this.request("POST", "/api/v1/dag/prune", req); + } + + async dagAt(hash: string): Promise { + return this.request("GET", `/api/v1/dag/at/${encodeURIComponent(hash)}`); + } + + async dagDiff(from: string, to: string): Promise { + const qs = this.queryString({ from, to }); + return this.request("GET", `/api/v1/dag/diff${qs}`); + } + + async dagExport(format: string = "mermaid"): Promise { + if (this.destroyed) { + throw new CortexError("Client has been destroyed", 0, "CLIENT_DESTROYED"); + } + const url = `${this.baseUrl}/api/v1/dag/export?format=${encodeURIComponent(format)}`; + const res = await resilientFetch( + url, + { method: "GET", headers: this.headers }, + this.resilienceConfig, + this.breaker, + ); + if (!res.ok) { + throw new CortexError(`DAG export failed with ${res.status}`, res.status); + } + return res.text(); + } + + async dagSync(req: DagSyncRequest): Promise { + return this.request("POST", "/api/v1/dag/sync", req); + } + + async dagSyncPull(req: DagPullRequest): Promise { + return this.request("POST", "/api/v1/dag/sync/pull", req); + } + + async dagVerify(hash: string, publicKey: string): Promise { + const qs = this.queryString({ public_key: publicKey }); + return this.request( + "GET", + `/api/v1/dag/verify/${encodeURIComponent(hash)}${qs}`, + ); + } } diff --git a/extensions/shared/cortex-config.ts b/extensions/shared/cortex-config.ts index aab193ec..00604cf8 100644 --- a/extensions/shared/cortex-config.ts +++ b/extensions/shared/cortex-config.ts @@ -18,6 +18,11 @@ export type P2pConfig = { mdns: boolean; }; +export type DagConfig = { + /** Enable Semantic DAG (default: true). Set to false to disable DAG tools and audit trail. */ + enabled: boolean; +}; + export type CortexConfig = { host: string; port: number; @@ -28,6 +33,8 @@ export type CortexConfig = { requireAuth?: boolean; strictVersionCheck?: boolean; p2p?: P2pConfig; + /** Semantic DAG configuration. Enabled by default in Cortex >= 0.6.1. */ + dag?: DagConfig; /** Directory for Cortex persistent data (graph.sled, ineru.snapshot). */ dataDir?: string; }; @@ -80,6 +87,7 @@ export function parseCortexConfig(raw: unknown): CortexConfig { "requireAuth", "strictVersionCheck", "p2p", + "dag", "dataDir", ], "cortex config", @@ -100,6 +108,7 @@ export function parseCortexConfig(raw: unknown): CortexConfig { const requireAuth = cortex.requireAuth === true; const strictVersionCheck = cortex.strictVersionCheck === true; const p2p = parseP2pConfig(cortex.p2p); + const dag = parseDagConfig(cortex.dag); const dataDir = typeof cortex.dataDir === "string" ? cortex.dataDir : undefined; return { @@ -112,6 +121,7 @@ export function parseCortexConfig(raw: unknown): CortexConfig { requireAuth, strictVersionCheck, p2p, + dag, dataDir, }; } @@ -145,6 +155,15 @@ export function parseP2pConfig(raw: unknown): P2pConfig | undefined { return { enabled, port, seed, manualPeers, mdns }; } +export function parseDagConfig(raw: unknown): DagConfig { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) { + return { enabled: true }; + } + const d = raw as Record; + assertAllowedKeys(d, ["enabled"], "dag config"); + return { enabled: d.enabled !== false }; +} + function parseResilienceConfig(raw: unknown): ResilienceConfig | undefined { if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined; const r = raw as Record; diff --git a/extensions/shared/cortex-version.ts b/extensions/shared/cortex-version.ts index 84a74c92..aeb90eba 100644 --- a/extensions/shared/cortex-version.ts +++ b/extensions/shared/cortex-version.ts @@ -5,4 +5,4 @@ * features or API changes. `mayros update` and the sidecar startup * check will compare the installed binary against this value. */ -export const REQUIRED_CORTEX_VERSION = "0.5.0"; +export const REQUIRED_CORTEX_VERSION = "0.6.1"; diff --git a/extensions/signal/package.json b/extensions/signal/package.json index ce62fc95..3aaec235 100644 --- a/extensions/signal/package.json +++ b/extensions/signal/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-signal", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros Signal channel plugin", "type": "module", diff --git a/extensions/skill-hub/package.json b/extensions/skill-hub/package.json index 961957e6..dbeb0fea 100644 --- a/extensions/skill-hub/package.json +++ b/extensions/skill-hub/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-skill-hub", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Apilium Hub marketplace — publish, install, sign, and verify semantic skills", "type": "module", diff --git a/extensions/slack/package.json b/extensions/slack/package.json index 5b97e3dd..9783edf0 100644 --- a/extensions/slack/package.json +++ b/extensions/slack/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-slack", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros Slack channel plugin", "type": "module", diff --git a/extensions/telegram/package.json b/extensions/telegram/package.json index 269b7d05..0f80a978 100644 --- a/extensions/telegram/package.json +++ b/extensions/telegram/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-telegram", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros Telegram channel plugin", "type": "module", diff --git a/extensions/tlon/package.json b/extensions/tlon/package.json index d9c07f61..2625bd9f 100644 --- a/extensions/tlon/package.json +++ b/extensions/tlon/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-tlon", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros Tlon/Urbit channel plugin", "type": "module", diff --git a/extensions/token-economy/package.json b/extensions/token-economy/package.json index 5edad119..3fc756b0 100644 --- a/extensions/token-economy/package.json +++ b/extensions/token-economy/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-token-economy", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros token economy plugin — per-session cost tracking, configurable budgets with soft-stop, and prompt-level memoization", "type": "module", diff --git a/extensions/tomeru-guard/package.json b/extensions/tomeru-guard/package.json index 250045e2..f10e00ed 100644 --- a/extensions/tomeru-guard/package.json +++ b/extensions/tomeru-guard/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-tomeru-guard", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros rate limiting and loop breaking plugin — prevents runaway tool execution", "type": "module", diff --git a/extensions/twitch/package.json b/extensions/twitch/package.json index 94cab09c..12fc9d1a 100644 --- a/extensions/twitch/package.json +++ b/extensions/twitch/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-twitch", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros Twitch channel plugin", "type": "module", diff --git a/extensions/voice-call/package.json b/extensions/voice-call/package.json index a627ad74..fc8a7b28 100644 --- a/extensions/voice-call/package.json +++ b/extensions/voice-call/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-voice-call", - "version": "0.1.16", + "version": "0.2.0", "description": "Mayros voice-call plugin", "license": "MIT", "type": "module", diff --git a/extensions/whatsapp/package.json b/extensions/whatsapp/package.json index 6058acac..0413b6b2 100644 --- a/extensions/whatsapp/package.json +++ b/extensions/whatsapp/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-whatsapp", - "version": "0.1.16", + "version": "0.2.0", "private": true, "description": "Mayros WhatsApp channel plugin", "type": "module", diff --git a/extensions/zalo/package.json b/extensions/zalo/package.json index 68885e0a..767d0efc 100644 --- a/extensions/zalo/package.json +++ b/extensions/zalo/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-zalo", - "version": "0.1.16", + "version": "0.2.0", "description": "Mayros Zalo channel plugin", "license": "MIT", "type": "module", diff --git a/extensions/zalouser/package.json b/extensions/zalouser/package.json index 256da4ea..412097e8 100644 --- a/extensions/zalouser/package.json +++ b/extensions/zalouser/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-zalouser", - "version": "0.1.16", + "version": "0.2.0", "description": "Mayros Zalo Personal Account plugin via zca-cli", "license": "MIT", "type": "module", diff --git a/package.json b/package.json index f2b5ddca..5e6e01c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros", - "version": "0.1.16", + "version": "0.2.0", "description": "Multi-channel AI agent framework with knowledge graph, MCP support, and coding CLI", "keywords": [ "agent", diff --git a/scripts/postinstall.mjs b/scripts/postinstall.mjs index a32c1b27..4e8a7713 100644 --- a/scripts/postinstall.mjs +++ b/scripts/postinstall.mjs @@ -8,7 +8,14 @@ */ import { execFileSync } from "node:child_process"; -import { createWriteStream, existsSync, chmodSync, unlinkSync } from "node:fs"; +import { + createWriteStream, + existsSync, + chmodSync, + unlinkSync, + renameSync, + readdirSync, +} from "node:fs"; import { mkdir } from "node:fs/promises"; import { homedir, platform, arch } from "node:os"; import { join } from "node:path"; @@ -114,15 +121,24 @@ async function main() { execFileSync("tar", ["xzf", archivePath, "-C", INSTALL_DIR], { timeout: 30_000 }); } - // Verify + permissions + // Verify + rename platform-suffixed binary if needed if (!existsSync(binaryPath)) { - console.warn( - `[mayros] Cortex binary not found after extraction. Install later with: mayros update`, + const baseName = BINARY_NAME.replace(/\.exe$/, ""); + const candidates = readdirSync(INSTALL_DIR).filter( + (f) => f.startsWith(baseName + "-") && !f.endsWith(".tar.gz") && !f.endsWith(".zip"), ); - try { - unlinkSync(archivePath); - } catch {} - return; + if (candidates.length === 1) { + renameSync(join(INSTALL_DIR, candidates[0]), binaryPath); + console.log(`[mayros] Renamed ${candidates[0]} → ${BINARY_NAME}`); + } else { + console.warn( + `[mayros] Cortex binary not found after extraction. Install later with: mayros update`, + ); + try { + unlinkSync(archivePath); + } catch {} + return; + } } if (!IS_WIN) { chmodSync(binaryPath, 0o755); diff --git a/src/cli/cortex-cli.ts b/src/cli/cortex-cli.ts index 690fba9a..d1bd338f 100644 --- a/src/cli/cortex-cli.ts +++ b/src/cli/cortex-cli.ts @@ -7,51 +7,8 @@ */ import type { Command } from "commander"; -import { parseCortexConfig } from "../../extensions/shared/cortex-config.js"; -import { CortexClient } from "../../extensions/shared/cortex-client.js"; -import { loadConfig } from "../config/config.js"; import { addGatewayClientOptions, callGatewayFromCli, type GatewayRpcOpts } from "./gateway-rpc.js"; - -// ============================================================================ -// Cortex resolution -// ============================================================================ - -function resolveCortexClient(opts: { - cortexHost?: string; - cortexPort?: string; - cortexToken?: string; -}): CortexClient { - // 1. Try from user config file first (has correct defaults: 127.0.0.1:19090) - if ( - !opts.cortexHost && - !opts.cortexPort && - !process.env.CORTEX_HOST && - !process.env.CORTEX_PORT - ) { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["memory-semantic"]?.config as - | { cortex?: { host?: string; port?: number; authToken?: string } } - | undefined; - if (pluginCfg?.cortex) { - return new CortexClient(parseCortexConfig(pluginCfg.cortex)); - } - } catch { - // Config not available — fall through to env/cli/defaults - } - } - - // 2. Build from CLI flags → env vars → parseCortexConfig defaults (19090) - const raw: Record = {}; - const host = opts.cortexHost ?? process.env.CORTEX_HOST; - if (host) raw.host = host; - const portStr = opts.cortexPort ?? process.env.CORTEX_PORT; - if (portStr) raw.port = Number.parseInt(portStr, 10); - const authToken = opts.cortexToken ?? process.env.CORTEX_AUTH_TOKEN; - if (authToken) raw.authToken = authToken; - - return new CortexClient(parseCortexConfig(raw)); -} +import { resolveCortexClient } from "./shared/cortex-resolution.js"; // ============================================================================ // Registration @@ -105,7 +62,10 @@ export function registerCortexCli(program: Command) { } // Direct check - const client = resolveCortexClient(parent); + const client = resolveCortexClient( + { host: parent.cortexHost, port: parent.cortexPort, token: parent.cortexToken }, + { defaultPort: 19090 }, + ); try { console.log(`Endpoint: ${client.baseUrl}`); const healthy = await client.isHealthy(); @@ -157,7 +117,10 @@ export function registerCortexCli(program: Command) { } console.log("Gateway unavailable, checking Cortex directly..."); - const client = resolveCortexClient(parent); + const client = resolveCortexClient( + { host: parent.cortexHost, port: parent.cortexPort, token: parent.cortexToken }, + { defaultPort: 19090 }, + ); try { const healthy = await client.isHealthy(); if (healthy) { diff --git a/src/cli/dag-cli.ts b/src/cli/dag-cli.ts new file mode 100644 index 00000000..c7a384e4 --- /dev/null +++ b/src/cli/dag-cli.ts @@ -0,0 +1,404 @@ +/** + * `mayros dag` — Semantic DAG CLI. + * + * Provides access to the DAG audit trail, time-travel, history, + * diff, export, stats, verification, and pruning. + * + * Subcommands: + * tips — Show current DAG tips + * history — Action history for a subject + * stats — DAG statistics + * export — Export DAG as DOT, Mermaid, or JSON + * diff — Diff between two action hashes + * at — Time-travel to a specific action + * verify — Verify Ed25519 signature of an action + * prune — Prune old DAG actions + */ + +import { createInterface } from "node:readline"; +import type { Command } from "commander"; +import { CortexError } from "../../extensions/shared/cortex-client.js"; +import { resolveCortexClient } from "./shared/cortex-resolution.js"; + +/** Prompt the user for confirmation on destructive operations. */ +async function confirmAction(message: string): Promise { + const rl = createInterface({ input: process.stdin, output: process.stdout }); + return new Promise((resolve) => { + rl.question(`${message} [y/N] `, (answer) => { + rl.close(); + resolve(answer.trim().toLowerCase() === "y"); + }); + }); +} + +/** Print a user-friendly error and set exit code. */ +function handleError(err: unknown): void { + if (err instanceof CortexError) { + if (err.code === "CONNECTION_ERROR") { + console.error( + "Cortex is not running. Start it with `mayros cortex start` or check --cortex-host/--cortex-port.", + ); + } else { + console.error(`Cortex error (${err.status}): ${err.message}`); + } + } else { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + } + process.exitCode = 1; +} + +// ============================================================================ +// Registration +// ============================================================================ + +export function registerDagCli(program: Command) { + const dag = program + .command("dag") + .description("Semantic DAG — audit, time-travel, and history") + .option("--cortex-host ", "Cortex host (default: 127.0.0.1 or from config)") + .option("--cortex-port ", "Cortex port (default: 19090 or from config)") + .option("--cortex-token ", "Cortex auth token (or set CORTEX_AUTH_TOKEN)"); + + // ------------------------------------------------------------------ + // mayros dag tips + // ------------------------------------------------------------------ + dag + .command("tips") + .description("Show current DAG tip hashes (frontier of the DAG)") + .action(async () => { + const parent = dag.opts(); + const client = resolveCortexClient({ + host: parent.cortexHost, + port: parent.cortexPort, + token: parent.cortexToken, + }); + + try { + const data = await client.dagTips(); + console.log(`DAG Tips (${data.count}):\n`); + for (const tip of data.tips) { + console.log(` ${tip}`); + } + } catch (err) { + handleError(err); + } finally { + client.destroy(); + } + }); + + // ------------------------------------------------------------------ + // mayros dag action + // ------------------------------------------------------------------ + dag + .command("action") + .description("Show details of a specific DAG action by hash") + .argument("", "DAG action hash") + .action(async (hash: string) => { + const parent = dag.opts(); + const client = resolveCortexClient({ + host: parent.cortexHost, + port: parent.cortexPort, + token: parent.cortexToken, + }); + + try { + const a = await client.dagAction(hash); + console.log(`Action ${a.hash}:`); + console.log(` author: ${a.author}`); + console.log(` seq: ${a.seq}`); + console.log(` timestamp: ${a.timestamp}`); + console.log(` type: ${a.payload_type}`); + console.log(` summary: ${a.payload_summary}`); + console.log(` parents: ${a.parents.length === 0 ? "(genesis)" : a.parents.join(", ")}`); + console.log(` signed: ${a.signed}${a.signature ? ` (${a.signature.slice(0, 16)}…)` : ""}`); + } catch (err) { + handleError(err); + } finally { + client.destroy(); + } + }); + + // ------------------------------------------------------------------ + // mayros dag history + // ------------------------------------------------------------------ + dag + .command("history") + .description("Show DAG action history for a subject") + .argument("", "Subject to query history for") + .option("--limit ", "Max actions to return", "20") + .action(async (subject: string, opts: { limit?: string }) => { + const parent = dag.opts(); + const client = resolveCortexClient({ + host: parent.cortexHost, + port: parent.cortexPort, + token: parent.cortexToken, + }); + + try { + const limit = parseInt(opts.limit ?? "20", 10); + const data = await client.dagHistory({ subject, limit }); + + if (!data.actions || data.actions.length === 0) { + console.log(`No DAG history for subject "${subject}".`); + return; + } + + console.log(`History for "${subject}" (${data.actions.length} actions):\n`); + for (const a of data.actions) { + console.log(` #${a.seq} [${a.payload_type}] ${a.payload_summary}`); + console.log(` hash: ${a.hash} author: ${a.author} time: ${a.timestamp}`); + } + } catch (err) { + handleError(err); + } finally { + client.destroy(); + } + }); + + // ------------------------------------------------------------------ + // mayros dag chain + // ------------------------------------------------------------------ + dag + .command("chain") + .description("Show the DAG action chain for a specific author/node") + .argument("", "Author node ID") + .option("--limit ", "Max actions to return", "20") + .action(async (author: string, opts: { limit?: string }) => { + const parent = dag.opts(); + const client = resolveCortexClient({ + host: parent.cortexHost, + port: parent.cortexPort, + token: parent.cortexToken, + }); + + try { + const limit = parseInt(opts.limit ?? "20", 10); + const data = await client.dagChain(author, limit); + + if (!data.actions || data.actions.length === 0) { + console.log(`No DAG actions for author "${author}".`); + return; + } + + console.log(`Chain for "${author}" (${data.actions.length} actions):\n`); + for (const a of data.actions) { + console.log(` #${a.seq} [${a.payload_type}] ${a.payload_summary}`); + console.log(` hash: ${a.hash} time: ${a.timestamp}`); + } + } catch (err) { + handleError(err); + } finally { + client.destroy(); + } + }); + + // ------------------------------------------------------------------ + // mayros dag stats + // ------------------------------------------------------------------ + dag + .command("stats") + .description("Show DAG statistics") + .action(async () => { + const parent = dag.opts(); + const client = resolveCortexClient({ + host: parent.cortexHost, + port: parent.cortexPort, + token: parent.cortexToken, + }); + + try { + const data = await client.dagStats(); + console.log("DAG Statistics:"); + console.log(` Actions: ${data.action_count}`); + console.log(` Tips: ${data.tip_count}`); + } catch (err) { + handleError(err); + } finally { + client.destroy(); + } + }); + + // ------------------------------------------------------------------ + // mayros dag export [--format dot|mermaid|json] + // ------------------------------------------------------------------ + dag + .command("export") + .description("Export the DAG as a visual graph") + .option("--format ", "Export format: dot, mermaid, or json", "mermaid") + .action(async (opts: { format?: string }) => { + const parent = dag.opts(); + const client = resolveCortexClient({ + host: parent.cortexHost, + port: parent.cortexPort, + token: parent.cortexToken, + }); + + try { + const text = await client.dagExport(opts.format ?? "mermaid"); + console.log(text); + } catch (err) { + handleError(err); + } finally { + client.destroy(); + } + }); + + // ------------------------------------------------------------------ + // mayros dag diff + // ------------------------------------------------------------------ + dag + .command("diff") + .description("Show diff between two DAG action hashes") + .argument("", "Starting action hash") + .argument("", "Ending action hash") + .action(async (from: string, to: string) => { + const parent = dag.opts(); + const client = resolveCortexClient({ + host: parent.cortexHost, + port: parent.cortexPort, + token: parent.cortexToken, + }); + + try { + const data = await client.dagDiff(from, to); + console.log(`Diff: ${data.from} → ${data.to}`); + console.log(`${data.action_count} action(s):\n`); + for (const a of data.actions) { + console.log(` [${a.payload_type}] ${a.payload_summary} (${a.hash.slice(0, 12)}…)`); + } + } catch (err) { + handleError(err); + } finally { + client.destroy(); + } + }); + + // ------------------------------------------------------------------ + // mayros dag at + // ------------------------------------------------------------------ + dag + .command("at") + .description("Time-travel to a specific DAG action hash") + .argument("", "DAG action hash to travel to") + .action(async (hash: string) => { + const parent = dag.opts(); + const client = resolveCortexClient({ + host: parent.cortexHost, + port: parent.cortexPort, + token: parent.cortexToken, + }); + + try { + const data = await client.dagAt(hash); + console.log(`Time-travel to ${data.target_hash}`); + console.log(` Timestamp: ${data.target_timestamp}`); + console.log(` Actions replayed: ${data.actions_replayed}`); + console.log(` Triples at that point: ${data.triple_count}`); + + if (data.triples && data.triples.length > 0) { + console.log(`\nTriples (${data.triples.length}):`); + for (const t of data.triples.slice(0, 20)) { + console.log(` ${t.subject} -> ${t.predicate} -> ${JSON.stringify(t.object)}`); + } + if (data.triples.length > 20) { + console.log(` ... and ${data.triples.length - 20} more`); + } + } + } catch (err) { + handleError(err); + } finally { + client.destroy(); + } + }); + + // ------------------------------------------------------------------ + // mayros dag verify --public-key + // ------------------------------------------------------------------ + dag + .command("verify") + .description("Verify Ed25519 signature of a DAG action") + .argument("", "DAG action hash to verify") + .requiredOption("--public-key ", "Ed25519 public key (hex or base64)") + .action(async (hash: string, opts: { publicKey: string }) => { + const parent = dag.opts(); + const client = resolveCortexClient({ + host: parent.cortexHost, + port: parent.cortexPort, + token: parent.cortexToken, + }); + + try { + const data = await client.dagVerify(hash, opts.publicKey); + console.log(`Verification: ${data.valid ? "VALID ✓" : "INVALID ✗"}`); + console.log(` Hash: ${data.action_hash}`); + console.log(` Public key: ${data.public_key}`); + console.log(` Detail: ${data.detail}`); + } catch (err) { + handleError(err); + } finally { + client.destroy(); + } + }); + + // ------------------------------------------------------------------ + // mayros dag prune --policy [--value N] [--checkpoint] + // ------------------------------------------------------------------ + dag + .command("prune") + .description("Prune old DAG actions") + .requiredOption( + "--policy ", + "Prune policy: keep_all, keep_since, keep_last, or keep_depth", + ) + .option("--value ", "Policy value (timestamp, count, or depth)", parseInt) + .option("--checkpoint", "Create checkpoint before pruning") + .option("--yes", "Skip confirmation prompt") + .action( + async (opts: { policy: string; value?: number; checkpoint?: boolean; yes?: boolean }) => { + const validPolicies = ["keep_all", "keep_since", "keep_last", "keep_depth"] as const; + if (!validPolicies.includes(opts.policy as (typeof validPolicies)[number])) { + console.error( + `Invalid policy "${opts.policy}". Must be one of: ${validPolicies.join(", ")}`, + ); + process.exitCode = 1; + return; + } + + if (!opts.yes) { + const confirmed = await confirmAction( + `This will permanently prune DAG history (policy: ${opts.policy}). Continue?`, + ); + if (!confirmed) { + console.log("Prune cancelled."); + return; + } + } + + const parent = dag.opts(); + const client = resolveCortexClient({ + host: parent.cortexHost, + port: parent.cortexPort, + token: parent.cortexToken, + }); + + try { + const data = await client.dagPrune({ + policy: opts.policy as "keep_all" | "keep_since" | "keep_last" | "keep_depth", + value: opts.value, + create_checkpoint: opts.checkpoint, + }); + + console.log("Prune complete:"); + console.log(` Pruned: ${data.pruned_count}`); + console.log(` Retained: ${data.retained_count}`); + if (data.checkpoint_hash) { + console.log(` Checkpoint: ${data.checkpoint_hash}`); + } + } catch (err) { + handleError(err); + } finally { + client.destroy(); + } + }, + ); +} diff --git a/src/cli/dashboard-cli.ts b/src/cli/dashboard-cli.ts index 3ff9998e..7ba9c49d 100644 --- a/src/cli/dashboard-cli.ts +++ b/src/cli/dashboard-cli.ts @@ -10,55 +10,13 @@ */ import type { Command } from "commander"; -import { parseCortexConfig } from "../../extensions/shared/cortex-config.js"; import { CortexClient } from "../../extensions/shared/cortex-client.js"; import { AgentMailbox } from "../../extensions/agent-mesh/agent-mailbox.js"; import { TeamManager } from "../../extensions/agent-mesh/team-manager.js"; import { TeamDashboardService } from "../../extensions/agent-mesh/team-dashboard.js"; -import { loadConfig } from "../config/config.js"; +import { resolveCortexClient, resolveNamespace } from "./shared/cortex-resolution.js"; -// ============================================================================ -// Cortex resolution (reads from agent-mesh plugin config) -// ============================================================================ - -function resolveCortexClient(opts: { host?: string; port?: string; token?: string }): CortexClient { - const host = opts.host ?? process.env.CORTEX_HOST ?? "127.0.0.1"; - const port = opts.port - ? Number.parseInt(opts.port, 10) - : process.env.CORTEX_PORT - ? Number.parseInt(process.env.CORTEX_PORT, 10) - : 8080; - const authToken = opts.token ?? process.env.CORTEX_AUTH_TOKEN ?? undefined; - - if (!opts.host && !opts.port && !process.env.CORTEX_HOST && !process.env.CORTEX_PORT) { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["agent-mesh"]?.config as - | { cortex?: { host?: string; port?: number; authToken?: string } } - | undefined; - if (pluginCfg?.cortex) { - const cortex = parseCortexConfig(pluginCfg.cortex); - return new CortexClient(cortex); - } - } catch { - // Config not available — use defaults - } - } - - return new CortexClient(parseCortexConfig({ host, port, authToken })); -} - -function resolveNamespace(): string { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["agent-mesh"]?.config as - | { agentNamespace?: string } - | undefined; - return pluginCfg?.agentNamespace ?? "mayros"; - } catch { - return "mayros"; - } -} +const CORTEX_PLUGIN = { pluginName: "agent-mesh" } as const; function resolveDashboard(client: CortexClient, ns: string): TeamDashboardService { const mailbox = new AgentMailbox(client, ns); @@ -83,7 +41,7 @@ export function registerDashboardCli(program: Command) { .command("team-dashboard") .description("Team dashboard — real-time agent status and activity") .option("--cortex-host ", "Cortex host (default: 127.0.0.1 or from config)") - .option("--cortex-port ", "Cortex port (default: 8080 or from config)") + .option("--cortex-port ", "Cortex port (default: 19090 or from config)") .option("--cortex-token ", "Cortex auth token (or set CORTEX_AUTH_TOKEN)"); // ---- team ---- @@ -94,44 +52,54 @@ export function registerDashboardCli(program: Command) { .option("--format ", "Output format (terminal|json)", "terminal") .action(async (teamId, opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); - - const healthy = await client.isHealthy(); - if (!healthy) { - console.log("Cortex offline. Cannot load dashboard."); - return; - } + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + CORTEX_PLUGIN, + ); + const ns = resolveNamespace("agent-mesh"); + + try { + const healthy = await client.isHealthy(); + if (!healthy) { + console.log("Cortex offline. Cannot load dashboard."); + return; + } - const dashboard = resolveDashboard(client, ns); - const d = await dashboard.getTeamDashboard(teamId); + const dashboard = resolveDashboard(client, ns); + const d = await dashboard.getTeamDashboard(teamId); - if (!d) { - console.log(`Team ${teamId} not found.`); - return; - } + if (!d) { + console.log(`Team ${teamId} not found.`); + return; + } - if (opts.format === "json") { - console.log(JSON.stringify(d, null, 2)); - return; - } + if (opts.format === "json") { + console.log(JSON.stringify(d, null, 2)); + return; + } - console.log(`Dashboard: "${d.teamName}" (${d.teamId})`); - console.log(` status: ${d.teamStatus}`); - console.log(` strategy: ${d.strategy}`); - console.log(` created: ${d.createdAt}`); - console.log(` updated: ${d.updatedAt}`); - console.log(` mail: ${d.mailboxSummary.total} total, ${d.mailboxSummary.unread} unread`); - console.log(` members:`); - for (const m of d.members) { - const events = m.totalEvents > 0 ? ` events:${m.totalEvents}` : ""; - const errors = m.errors > 0 ? ` errors:${m.errors}` : ""; - const unread = m.unreadMessages > 0 ? ` unread:${m.unreadMessages}` : ""; - console.log(` - ${m.agentId} (${m.role}): ${m.status}${events}${errors}${unread}`); + console.log(`Dashboard: "${d.teamName}" (${d.teamId})`); + console.log(` status: ${d.teamStatus}`); + console.log(` strategy: ${d.strategy}`); + console.log(` created: ${d.createdAt}`); + console.log(` updated: ${d.updatedAt}`); + console.log(` mail: ${d.mailboxSummary.total} total, ${d.mailboxSummary.unread} unread`); + console.log(` members:`); + for (const m of d.members) { + const events = m.totalEvents > 0 ? ` events:${m.totalEvents}` : ""; + const errors = m.errors > 0 ? ` errors:${m.errors}` : ""; + const unread = m.unreadMessages > 0 ? ` unread:${m.unreadMessages}` : ""; + console.log(` - ${m.agentId} (${m.role}): ${m.status}${events}${errors}${unread}`); + } + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + client.destroy(); } }); @@ -142,42 +110,52 @@ export function registerDashboardCli(program: Command) { .option("--format ", "Output format (terminal|json)", "terminal") .action(async (opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); - - const healthy = await client.isHealthy(); - if (!healthy) { - console.log("Cortex offline. Cannot load dashboard."); - return; - } + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + CORTEX_PLUGIN, + ); + const ns = resolveNamespace("agent-mesh"); + + try { + const healthy = await client.isHealthy(); + if (!healthy) { + console.log("Cortex offline. Cannot load dashboard."); + return; + } - const dashboard = resolveDashboard(client, ns); - const s = await dashboard.getSummary(); + const dashboard = resolveDashboard(client, ns); + const s = await dashboard.getSummary(); - if (opts.format === "json") { - console.log(JSON.stringify(s, null, 2)); - return; - } + if (opts.format === "json") { + console.log(JSON.stringify(s, null, 2)); + return; + } - if (s.activeTeams === 0) { - console.log("No active teams."); - return; - } + if (s.activeTeams === 0) { + console.log("No active teams."); + return; + } - console.log(`Dashboard Summary:`); - console.log(` active teams: ${s.activeTeams}`); - console.log(` total agents: ${s.totalAgents}`); - console.log(` total unread: ${s.totalUnread}`); - console.log(` total errors: ${s.totalErrors}`); - console.log(); - for (const t of s.teams) { - console.log( - ` ${t.teamId}: "${t.teamName}" [${t.teamStatus}] — ${t.members.length} members`, - ); + console.log(`Dashboard Summary:`); + console.log(` active teams: ${s.activeTeams}`); + console.log(` total agents: ${s.totalAgents}`); + console.log(` total unread: ${s.totalUnread}`); + console.log(` total errors: ${s.totalErrors}`); + console.log(); + for (const t of s.teams) { + console.log( + ` ${t.teamId}: "${t.teamName}" [${t.teamStatus}] — ${t.members.length} members`, + ); + } + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + client.destroy(); } }); @@ -189,41 +167,53 @@ export function registerDashboardCli(program: Command) { .option("--format ", "Output format (terminal|json)", "terminal") .action(async (agentId, opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); - - const healthy = await client.isHealthy(); - if (!healthy) { - console.log("Cortex offline. Cannot load agent activity."); - return; - } + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + CORTEX_PLUGIN, + ); + const ns = resolveNamespace("agent-mesh"); + + try { + const healthy = await client.isHealthy(); + if (!healthy) { + console.log("Cortex offline. Cannot load agent activity."); + return; + } - const dashboard = resolveDashboard(client, ns); - const act = await dashboard.getAgentActivity(agentId); + const dashboard = resolveDashboard(client, ns); + const act = await dashboard.getAgentActivity(agentId); - if (opts.format === "json") { - console.log(JSON.stringify(act, null, 2)); - return; - } + if (opts.format === "json") { + console.log(JSON.stringify(act, null, 2)); + return; + } - console.log(`Agent Activity: ${act.agentId}`); - if (act.teams.length === 0) { - console.log(" Not a member of any team."); - } else { - console.log(` teams (${act.teams.length}):`); - for (const t of act.teams) { - console.log(` - ${t.teamId}: "${t.teamName}" role:${t.role} status:${t.status}`); + console.log(`Agent Activity: ${act.agentId}`); + if (act.teams.length === 0) { + console.log(" Not a member of any team."); + } else { + console.log(` teams (${act.teams.length}):`); + for (const t of act.teams) { + console.log(` - ${t.teamId}: "${t.teamName}" role:${t.role} status:${t.status}`); + } } - } - console.log(` mailbox: ${act.mailboxStats.total} total, ${act.mailboxStats.unread} unread`); - if (act.traceStats) { console.log( - ` trace: ${act.traceStats.totalEvents} events, ${act.traceStats.errors} errors`, + ` mailbox: ${act.mailboxStats.total} total, ${act.mailboxStats.unread} unread`, ); + if (act.traceStats) { + console.log( + ` trace: ${act.traceStats.totalEvents} events, ${act.traceStats.errors} errors`, + ); + } + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + client.destroy(); } }); } diff --git a/src/cli/doctor-cli.ts b/src/cli/doctor-cli.ts index db48f39b..10fa180e 100644 --- a/src/cli/doctor-cli.ts +++ b/src/cli/doctor-cli.ts @@ -14,9 +14,8 @@ import { execSync } from "node:child_process"; import type { Command } from "commander"; -import { parseCortexConfig } from "../../extensions/shared/cortex-config.js"; -import { CortexClient } from "../../extensions/shared/cortex-client.js"; import { REQUIRED_CORTEX_VERSION } from "../../extensions/shared/cortex-version.js"; +import { resolveCortexClient } from "./shared/cortex-resolution.js"; import { detectRuntime, runtimeSatisfies, parseSemver, isAtLeast } from "../infra/runtime-guard.js"; import { loadConfig } from "../config/config.js"; import { buildPluginStatusReport } from "../plugins/status.js"; @@ -132,7 +131,7 @@ async function checkCortex(opts: { }): Promise { const checks: DoctorCheck[] = []; - const client = resolveCortexClient(opts); + const client = resolveCortexClient(opts, { pluginName: ["memory-semantic", "agent-mesh"] }); const healthy = await client.isHealthy(); if (!healthy) { @@ -340,39 +339,6 @@ function checkConfig(): DoctorCheck[] { return checks; } -// ============================================================================ -// Cortex resolution (reads from config) -// ============================================================================ - -function resolveCortexClient(opts: { host?: string; port?: string; token?: string }): CortexClient { - const host = opts.host ?? process.env.CORTEX_HOST ?? "127.0.0.1"; - const port = opts.port - ? Number.parseInt(opts.port, 10) - : process.env.CORTEX_PORT - ? Number.parseInt(process.env.CORTEX_PORT, 10) - : 8080; - const authToken = opts.token ?? process.env.CORTEX_AUTH_TOKEN ?? undefined; - - if (!opts.host && !opts.port && !process.env.CORTEX_HOST && !process.env.CORTEX_PORT) { - try { - const cfg = loadConfig(); - // Try memory-semantic plugin config - const pluginCfg = (cfg.plugins?.entries?.["memory-semantic"]?.config ?? - cfg.plugins?.entries?.["agent-mesh"]?.config) as - | { cortex?: { host?: string; port?: number; authToken?: string } } - | undefined; - if (pluginCfg?.cortex) { - const cortex = parseCortexConfig(pluginCfg.cortex); - return new CortexClient(cortex); - } - } catch { - // Config not available — use defaults - } - } - - return new CortexClient(parseCortexConfig({ host, port, authToken })); -} - // ============================================================================ // Registration // ============================================================================ @@ -382,7 +348,7 @@ export function registerDoctorCli(program: Command) { .command("diagnose") .description("Diagnostic checks — runtime, Cortex, plugins, security, config") .option("--cortex-host ", "Cortex host (default: 127.0.0.1 or from config)") - .option("--cortex-port ", "Cortex port (default: 8080 or from config)") + .option("--cortex-port ", "Cortex port (default: 19090 or from config)") .option("--cortex-token ", "Cortex auth token (or set CORTEX_AUTH_TOKEN)") .option("--json", "Output as JSON"); diff --git a/src/cli/fork-cli.ts b/src/cli/fork-cli.ts index e822f2d4..4ffaa605 100644 --- a/src/cli/fork-cli.ts +++ b/src/cli/fork-cli.ts @@ -11,54 +11,9 @@ */ import type { Command } from "commander"; -import { parseCortexConfig } from "../../extensions/shared/cortex-config.js"; -import { CortexClient } from "../../extensions/shared/cortex-client.js"; import { TraceEmitter } from "../../extensions/semantic-observability/trace-emitter.js"; import { SessionForkManager } from "../../extensions/semantic-observability/session-fork.js"; -import { loadConfig } from "../config/config.js"; - -// ============================================================================ -// Cortex resolution (reads from semantic-observability plugin config) -// ============================================================================ - -function resolveCortexClient(opts: { host?: string; port?: string; token?: string }): CortexClient { - const host = opts.host ?? process.env.CORTEX_HOST ?? "127.0.0.1"; - const port = opts.port - ? Number.parseInt(opts.port, 10) - : process.env.CORTEX_PORT - ? Number.parseInt(process.env.CORTEX_PORT, 10) - : 8080; - const authToken = opts.token ?? process.env.CORTEX_AUTH_TOKEN ?? undefined; - - if (!opts.host && !opts.port && !process.env.CORTEX_HOST && !process.env.CORTEX_PORT) { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["semantic-observability"]?.config as - | { cortex?: { host?: string; port?: number; authToken?: string } } - | undefined; - if (pluginCfg?.cortex) { - const cortex = parseCortexConfig(pluginCfg.cortex); - return new CortexClient(cortex); - } - } catch { - // Config not available — use defaults - } - } - - return new CortexClient(parseCortexConfig({ host, port, authToken })); -} - -function resolveNamespace(): string { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["semantic-observability"]?.config as - | { agentNamespace?: string } - | undefined; - return pluginCfg?.agentNamespace ?? "mayros"; - } catch { - return "mayros"; - } -} +import { resolveCortexClient, resolveNamespace } from "./shared/cortex-resolution.js"; // ============================================================================ // Registration @@ -69,7 +24,7 @@ export function registerSessionCli(program: Command) { .command("session") .description("Session fork/rewind — checkpoint, fork, and rewind agent sessions") .option("--cortex-host ", "Cortex host (default: 127.0.0.1 or from config)") - .option("--cortex-port ", "Cortex port (default: 8080 or from config)") + .option("--cortex-port ", "Cortex port (default: 19090 or from config)") .option("--cortex-token ", "Cortex auth token (or set CORTEX_AUTH_TOKEN)"); // ---- checkpoint ---- @@ -81,35 +36,45 @@ export function registerSessionCli(program: Command) { .option("--format ", "Output format (terminal|json)", "terminal") .action(async (opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); - - const healthy = await client.isHealthy(); - if (!healthy) { - console.log("Cortex offline. Cannot create checkpoint."); - return; - } - - const emitter = new TraceEmitter(client, ns, 5000); - const mgr = new SessionForkManager(client, emitter, ns); - - const cp = await mgr.checkpoint(opts.session); - - if (opts.format === "json") { - console.log(JSON.stringify(cp, null, 2)); - return; - } - - console.log(`Checkpoint created:`); - console.log(` session: ${cp.sessionKey}`); - console.log(` timestamp: ${cp.timestamp}`); - console.log(` events: ${cp.eventCount}`); - if (cp.lastEventId) { - console.log(` lastEvent: ${cp.lastEventId}`); + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + { pluginName: "semantic-observability" }, + ); + const ns = resolveNamespace("semantic-observability"); + + try { + const healthy = await client.isHealthy(); + if (!healthy) { + console.log("Cortex offline. Cannot create checkpoint."); + return; + } + + const emitter = new TraceEmitter(client, ns, 5000); + const mgr = new SessionForkManager(client, emitter, ns); + + const cp = await mgr.checkpoint(opts.session); + + if (opts.format === "json") { + console.log(JSON.stringify(cp, null, 2)); + return; + } + + console.log(`Checkpoint created:`); + console.log(` session: ${cp.sessionKey}`); + console.log(` timestamp: ${cp.timestamp}`); + console.log(` events: ${cp.eventCount}`); + if (cp.lastEventId) { + console.log(` lastEvent: ${cp.lastEventId}`); + } + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + client.destroy(); } }); @@ -123,34 +88,44 @@ export function registerSessionCli(program: Command) { .option("--format ", "Output format (terminal|json)", "terminal") .action(async (opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); - - const healthy = await client.isHealthy(); - if (!healthy) { - console.log("Cortex offline. Cannot fork session."); - return; + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + { pluginName: "semantic-observability" }, + ); + const ns = resolveNamespace("semantic-observability"); + + try { + const healthy = await client.isHealthy(); + if (!healthy) { + console.log("Cortex offline. Cannot fork session."); + return; + } + + const emitter = new TraceEmitter(client, ns, 5000); + const mgr = new SessionForkManager(client, emitter, ns); + + const result = await mgr.fork(opts.session, opts.name); + + if (opts.format === "json") { + console.log(JSON.stringify(result, null, 2)); + return; + } + + console.log(`Session forked:`); + console.log(` original: ${result.originalSession}`); + console.log(` forked: ${result.forkedSession}`); + console.log(` forkedAt: ${result.forkedAt}`); + console.log(` events copied: ${result.eventsCopied}`); + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + client.destroy(); } - - const emitter = new TraceEmitter(client, ns, 5000); - const mgr = new SessionForkManager(client, emitter, ns); - - const result = await mgr.fork(opts.session, opts.name); - - if (opts.format === "json") { - console.log(JSON.stringify(result, null, 2)); - return; - } - - console.log(`Session forked:`); - console.log(` original: ${result.originalSession}`); - console.log(` forked: ${result.forkedSession}`); - console.log(` forkedAt: ${result.forkedAt}`); - console.log(` events copied: ${result.eventsCopied}`); }); // ---- rewind ---- @@ -163,34 +138,44 @@ export function registerSessionCli(program: Command) { .option("--format ", "Output format (terminal|json)", "terminal") .action(async (opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); - - const healthy = await client.isHealthy(); - if (!healthy) { - console.log("Cortex offline. Cannot rewind session."); - return; - } - - const emitter = new TraceEmitter(client, ns, 5000); - const mgr = new SessionForkManager(client, emitter, ns); - - const result = await mgr.rewind(opts.session, opts.to); - - if (opts.format === "json") { - console.log(JSON.stringify(result, null, 2)); - return; + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + { pluginName: "semantic-observability" }, + ); + const ns = resolveNamespace("semantic-observability"); + + try { + const healthy = await client.isHealthy(); + if (!healthy) { + console.log("Cortex offline. Cannot rewind session."); + return; + } + + const emitter = new TraceEmitter(client, ns, 5000); + const mgr = new SessionForkManager(client, emitter, ns); + + const result = await mgr.rewind(opts.session, opts.to); + + if (opts.format === "json") { + console.log(JSON.stringify(result, null, 2)); + return; + } + + console.log(`Session rewound:`); + console.log(` session: ${result.sessionKey}`); + console.log(` rewindPoint: ${result.rewindPoint}`); + console.log(` events removed: ${result.eventsRemoved}`); + console.log(` events retained: ${result.eventsRetained}`); + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + client.destroy(); } - - console.log(`Session rewound:`); - console.log(` session: ${result.sessionKey}`); - console.log(` rewindPoint: ${result.rewindPoint}`); - console.log(` events removed: ${result.eventsRemoved}`); - console.log(` events retained: ${result.eventsRetained}`); }); // ---- forks ---- @@ -202,40 +187,50 @@ export function registerSessionCli(program: Command) { .option("--format ", "Output format (terminal|json)", "terminal") .action(async (opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); - - const healthy = await client.isHealthy(); - if (!healthy) { - console.log("Cortex offline. Cannot list forks."); - return; - } - - const emitter = new TraceEmitter(client, ns, 5000); - const mgr = new SessionForkManager(client, emitter, ns); - - const forks = await mgr.listForks(opts.session); - - if (opts.format === "json") { - console.log(JSON.stringify(forks, null, 2)); - return; - } - - if (forks.length === 0) { - console.log("No fork/rewind history found."); - return; - } - - console.log(`Session history (${forks.length} entries):`); - for (const f of forks) { - const parent = f.parentSession ? ` (parent: ${f.parentSession})` : ""; - const forkTime = f.forkedAt ? ` forked: ${f.forkedAt}` : ""; - const cpCount = f.checkpoints.length > 0 ? ` checkpoints: ${f.checkpoints.length}` : ""; - console.log(` ${f.sessionKey} [${f.status}]${parent}${forkTime}${cpCount}`); + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + { pluginName: "semantic-observability" }, + ); + const ns = resolveNamespace("semantic-observability"); + + try { + const healthy = await client.isHealthy(); + if (!healthy) { + console.log("Cortex offline. Cannot list forks."); + return; + } + + const emitter = new TraceEmitter(client, ns, 5000); + const mgr = new SessionForkManager(client, emitter, ns); + + const forks = await mgr.listForks(opts.session); + + if (opts.format === "json") { + console.log(JSON.stringify(forks, null, 2)); + return; + } + + if (forks.length === 0) { + console.log("No fork/rewind history found."); + return; + } + + console.log(`Session history (${forks.length} entries):`); + for (const f of forks) { + const parent = f.parentSession ? ` (parent: ${f.parentSession})` : ""; + const forkTime = f.forkedAt ? ` forked: ${f.forkedAt}` : ""; + const cpCount = f.checkpoints.length > 0 ? ` checkpoints: ${f.checkpoints.length}` : ""; + console.log(` ${f.sessionKey} [${f.status}]${parent}${forkTime}${cpCount}`); + } + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + client.destroy(); } }); } diff --git a/src/cli/kg-cli.ts b/src/cli/kg-cli.ts index 0d0da8b6..3f999a6d 100644 --- a/src/cli/kg-cli.ts +++ b/src/cli/kg-cli.ts @@ -16,55 +16,10 @@ */ import type { Command } from "commander"; -import { parseCortexConfig } from "../../extensions/shared/cortex-config.js"; -import { CortexClient } from "../../extensions/shared/cortex-client.js"; import { ProjectMemory } from "../../extensions/memory-semantic/project-memory.js"; import { codePredicate } from "../../extensions/code-indexer/rdf-mapper.js"; import { getIndexStats } from "../../extensions/code-indexer/incremental.js"; -import { loadConfig } from "../config/config.js"; - -// ============================================================================ -// Cortex resolution -// ============================================================================ - -function resolveCortexClient(opts: { host?: string; port?: string; token?: string }): CortexClient { - const host = opts.host ?? process.env.CORTEX_HOST ?? "127.0.0.1"; - const port = opts.port - ? Number.parseInt(opts.port, 10) - : process.env.CORTEX_PORT - ? Number.parseInt(process.env.CORTEX_PORT, 10) - : 8080; - const authToken = opts.token ?? process.env.CORTEX_AUTH_TOKEN ?? undefined; - - if (!opts.host && !opts.port && !process.env.CORTEX_HOST && !process.env.CORTEX_PORT) { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["memory-semantic"]?.config as - | { cortex?: { host?: string; port?: number; authToken?: string } } - | undefined; - if (pluginCfg?.cortex) { - const cortex = parseCortexConfig(pluginCfg.cortex); - return new CortexClient(cortex); - } - } catch { - // Config not available — use defaults - } - } - - return new CortexClient(parseCortexConfig({ host, port, authToken })); -} - -function resolveNamespace(): string { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["memory-semantic"]?.config as - | { agentNamespace?: string } - | undefined; - return pluginCfg?.agentNamespace ?? "mayros"; - } catch { - return "mayros"; - } -} +import { resolveCortexClient, resolveNamespace } from "./shared/cortex-resolution.js"; // ============================================================================ // Registration @@ -75,7 +30,7 @@ export function registerKgCli(program: Command) { .command("kg") .description("Knowledge graph — search, explore, and query project memory") .option("--cortex-host ", "Cortex host (default: 127.0.0.1 or from config)") - .option("--cortex-port ", "Cortex port (default: 8080 or from config)") + .option("--cortex-port ", "Cortex port (default: 19090 or from config)") .option("--cortex-token ", "Cortex auth token (or set CORTEX_AUTH_TOKEN)"); // ------------------------------------------------------------------ diff --git a/src/cli/lsp-cli.ts b/src/cli/lsp-cli.ts index 217a99b0..c25a6ea9 100644 --- a/src/cli/lsp-cli.ts +++ b/src/cli/lsp-cli.ts @@ -13,54 +13,9 @@ */ import type { Command } from "commander"; -import { parseCortexConfig } from "../../extensions/shared/cortex-config.js"; -import { CortexClient } from "../../extensions/shared/cortex-client.js"; import { LspCortexBackend } from "../../extensions/lsp-bridge/lsp-cortex-backend.js"; import { severityLabel } from "../../extensions/lsp-bridge/lsp-protocol.js"; -import { loadConfig } from "../config/config.js"; - -// ============================================================================ -// Cortex resolution -// ============================================================================ - -function resolveCortexClient(opts: { host?: string; port?: string; token?: string }): CortexClient { - const host = opts.host ?? process.env.CORTEX_HOST ?? "127.0.0.1"; - const port = opts.port - ? Number.parseInt(opts.port, 10) - : process.env.CORTEX_PORT - ? Number.parseInt(process.env.CORTEX_PORT, 10) - : 8080; - const authToken = opts.token ?? process.env.CORTEX_AUTH_TOKEN ?? undefined; - - if (!opts.host && !opts.port && !process.env.CORTEX_HOST && !process.env.CORTEX_PORT) { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["lsp-bridge"]?.config as - | { cortex?: { host?: string; port?: number; authToken?: string } } - | undefined; - if (pluginCfg?.cortex) { - const cortex = parseCortexConfig(pluginCfg.cortex); - return new CortexClient(cortex); - } - } catch { - // Config not available — use defaults - } - } - - return new CortexClient(parseCortexConfig({ host, port, authToken })); -} - -function resolveNamespace(): string { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["lsp-bridge"]?.config as - | { namespace?: string } - | undefined; - return pluginCfg?.namespace ?? "mayros"; - } catch { - return "mayros"; - } -} +import { resolveCortexClient, resolveNamespace } from "./shared/cortex-resolution.js"; // ============================================================================ // Registration @@ -71,7 +26,7 @@ export function registerLspCli(program: Command) { .command("lsp") .description("LSP bridge — query Cortex-stored language diagnostics and definitions") .option("--cortex-host ", "Cortex host (default: 127.0.0.1 or from config)") - .option("--cortex-port ", "Cortex port (default: 8080 or from config)") + .option("--cortex-port ", "Cortex port (default: 19090 or from config)") .option("--cortex-token ", "Cortex auth token (or set CORTEX_AUTH_TOKEN)"); // ---- diagnostics ---- @@ -83,12 +38,15 @@ export function registerLspCli(program: Command) { .option("--format ", "Output format (terminal|json)", "terminal") .action(async (opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + { pluginName: "lsp-bridge" }, + ); + const ns = resolveNamespace("lsp-bridge"); const healthy = await client.isHealthy(); if (!healthy) { @@ -140,11 +98,14 @@ export function registerLspCli(program: Command) { .description("Show LSP bridge status (Cortex connectivity)") .action(async (_opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + { pluginName: "lsp-bridge" }, + ); const healthy = await client.isHealthy(); console.log(`Cortex: ${healthy ? "connected" : "offline"}`); diff --git a/src/cli/mailbox-cli.ts b/src/cli/mailbox-cli.ts index cb0d1356..6f5bbb0e 100644 --- a/src/cli/mailbox-cli.ts +++ b/src/cli/mailbox-cli.ts @@ -12,57 +12,12 @@ */ import type { Command } from "commander"; -import { parseCortexConfig } from "../../extensions/shared/cortex-config.js"; -import { CortexClient } from "../../extensions/shared/cortex-client.js"; import { AgentMailbox, isValidMailMessageType, isValidMailStatus, } from "../../extensions/agent-mesh/agent-mailbox.js"; -import { loadConfig } from "../config/config.js"; - -// ============================================================================ -// Cortex resolution (reads from agent-mesh plugin config) -// ============================================================================ - -function resolveCortexClient(opts: { host?: string; port?: string; token?: string }): CortexClient { - const host = opts.host ?? process.env.CORTEX_HOST ?? "127.0.0.1"; - const port = opts.port - ? Number.parseInt(opts.port, 10) - : process.env.CORTEX_PORT - ? Number.parseInt(process.env.CORTEX_PORT, 10) - : 8080; - const authToken = opts.token ?? process.env.CORTEX_AUTH_TOKEN ?? undefined; - - if (!opts.host && !opts.port && !process.env.CORTEX_HOST && !process.env.CORTEX_PORT) { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["agent-mesh"]?.config as - | { cortex?: { host?: string; port?: number; authToken?: string } } - | undefined; - if (pluginCfg?.cortex) { - const cortex = parseCortexConfig(pluginCfg.cortex); - return new CortexClient(cortex); - } - } catch { - // Config not available — use defaults - } - } - - return new CortexClient(parseCortexConfig({ host, port, authToken })); -} - -function resolveNamespace(): string { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["agent-mesh"]?.config as - | { agentNamespace?: string } - | undefined; - return pluginCfg?.agentNamespace ?? "mayros"; - } catch { - return "mayros"; - } -} +import { resolveCortexClient, resolveNamespace } from "./shared/cortex-resolution.js"; // ============================================================================ // Registration @@ -73,7 +28,7 @@ export function registerMailboxCli(program: Command) { .command("mailbox") .description("Agent mailbox — persistent messaging between agents") .option("--cortex-host ", "Cortex host (default: 127.0.0.1 or from config)") - .option("--cortex-port ", "Cortex port (default: 8080 or from config)") + .option("--cortex-port ", "Cortex port (default: 19090 or from config)") .option("--cortex-token ", "Cortex auth token (or set CORTEX_AUTH_TOKEN)"); // ---- list ---- @@ -88,45 +43,55 @@ export function registerMailboxCli(program: Command) { .option("--format ", "Output format (terminal|json)", "terminal") .action(async (opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); - - const healthy = await client.isHealthy(); - if (!healthy) { - console.log("Cortex offline. Cannot list mailbox."); - return; - } + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + { pluginName: "agent-mesh" }, + ); + const ns = resolveNamespace("agent-mesh"); - const mailbox = new AgentMailbox(client, ns); - const agent = opts.agent ?? "main"; + try { + const healthy = await client.isHealthy(); + if (!healthy) { + console.log("Cortex offline. Cannot list mailbox."); + return; + } - const messages = await mailbox.inbox({ - agent, - status: opts.status && isValidMailStatus(opts.status) ? opts.status : undefined, - type: opts.type && isValidMailMessageType(opts.type) ? opts.type : undefined, - from: opts.from, - limit: Number.parseInt(opts.limit, 10) || 20, - }); + const mailbox = new AgentMailbox(client, ns); + const agent = opts.agent ?? "main"; - if (opts.format === "json") { - console.log(JSON.stringify(messages, null, 2)); - return; - } + const messages = await mailbox.inbox({ + agent, + status: opts.status && isValidMailStatus(opts.status) ? opts.status : undefined, + type: opts.type && isValidMailMessageType(opts.type) ? opts.type : undefined, + from: opts.from, + limit: Number.parseInt(opts.limit, 10) || 20, + }); - if (messages.length === 0) { - console.log(`No messages for ${agent}.`); - return; - } + if (opts.format === "json") { + console.log(JSON.stringify(messages, null, 2)); + return; + } + + if (messages.length === 0) { + console.log(`No messages for ${agent}.`); + return; + } - console.log(`Inbox for ${agent} (${messages.length} messages):`); - for (const m of messages) { - const readMark = m.status === "unread" ? "*" : " "; - const preview = m.content.length > 60 ? m.content.slice(0, 60) + "…" : m.content; - console.log(` ${readMark} ${m.id} from:${m.from} [${m.type}] ${preview}`); + console.log(`Inbox for ${agent} (${messages.length} messages):`); + for (const m of messages) { + const readMark = m.status === "unread" ? "*" : " "; + const preview = m.content.length > 60 ? m.content.slice(0, 60) + "…" : m.content; + console.log(` ${readMark} ${m.id} from:${m.from} [${m.type}] ${preview}`); + } + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + client.destroy(); } }); @@ -139,46 +104,56 @@ export function registerMailboxCli(program: Command) { .option("--format ", "Output format (terminal|json)", "terminal") .action(async (messageId, opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); - - const healthy = await client.isHealthy(); - if (!healthy) { - console.log("Cortex offline. Cannot read message."); - return; - } + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + { pluginName: "agent-mesh" }, + ); + const ns = resolveNamespace("agent-mesh"); - const mailbox = new AgentMailbox(client, ns); - const agent = opts.agent ?? "main"; + try { + const healthy = await client.isHealthy(); + if (!healthy) { + console.log("Cortex offline. Cannot read message."); + return; + } - const msg = await mailbox.getMessage(agent, messageId); - if (!msg) { - console.log(`Message ${messageId} not found.`); - return; - } + const mailbox = new AgentMailbox(client, ns); + const agent = opts.agent ?? "main"; - // Mark as read - await mailbox.markRead(agent, messageId); + const msg = await mailbox.getMessage(agent, messageId); + if (!msg) { + console.log(`Message ${messageId} not found.`); + return; + } - if (opts.format === "json") { - console.log(JSON.stringify({ ...msg, status: "read" }, null, 2)); - return; - } + // Mark as read + await mailbox.markRead(agent, messageId); - console.log(`Message ${msg.id}:`); - console.log(` from: ${msg.from}`); - console.log(` to: ${msg.to}`); - console.log(` type: ${msg.type}`); - console.log(` sent: ${msg.sentAt}`); - if (msg.replyTo) { - console.log(` replyTo: ${msg.replyTo}`); + if (opts.format === "json") { + console.log(JSON.stringify({ ...msg, status: "read" }, null, 2)); + return; + } + + console.log(`Message ${msg.id}:`); + console.log(` from: ${msg.from}`); + console.log(` to: ${msg.to}`); + console.log(` type: ${msg.type}`); + console.log(` sent: ${msg.sentAt}`); + if (msg.replyTo) { + console.log(` replyTo: ${msg.replyTo}`); + } + console.log(` status: read`); + console.log(`\n${msg.content}`); + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + client.destroy(); } - console.log(` status: read`); - console.log(`\n${msg.content}`); }); // ---- send ---- @@ -197,27 +172,30 @@ export function registerMailboxCli(program: Command) { .option("--format ", "Output format (terminal|json)", "terminal") .action(async (opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); - - const healthy = await client.isHealthy(); - if (!healthy) { - console.log("Cortex offline. Cannot send message."); - return; - } + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + { pluginName: "agent-mesh" }, + ); + const ns = resolveNamespace("agent-mesh"); - if (!isValidMailMessageType(opts.type)) { - console.error(`Invalid message type: ${opts.type}`); - return; - } + try { + const healthy = await client.isHealthy(); + if (!healthy) { + console.log("Cortex offline. Cannot send message."); + return; + } - const mailbox = new AgentMailbox(client, ns); + if (!isValidMailMessageType(opts.type)) { + console.error(`Invalid message type: ${opts.type}`); + return; + } + + const mailbox = new AgentMailbox(client, ns); - try { const msg = await mailbox.send({ from: opts.from, to: opts.to, @@ -239,7 +217,10 @@ export function registerMailboxCli(program: Command) { console.log(` replyTo: ${msg.replyTo}`); } } catch (err) { - console.error(`Error: ${String(err)}`); + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + client.destroy(); } }); @@ -251,29 +232,39 @@ export function registerMailboxCli(program: Command) { .option("--agent ", "Recipient agent ID (defaults to main)") .action(async (messageId, opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); - - const healthy = await client.isHealthy(); - if (!healthy) { - console.log("Cortex offline. Cannot archive message."); - return; - } + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + { pluginName: "agent-mesh" }, + ); + const ns = resolveNamespace("agent-mesh"); - const mailbox = new AgentMailbox(client, ns); - const agent = opts.agent ?? "main"; + try { + const healthy = await client.isHealthy(); + if (!healthy) { + console.log("Cortex offline. Cannot archive message."); + return; + } - const ok = await mailbox.markArchived(agent, messageId); - if (!ok) { - console.log(`Message ${messageId} not found.`); - return; - } + const mailbox = new AgentMailbox(client, ns); + const agent = opts.agent ?? "main"; + + const ok = await mailbox.markArchived(agent, messageId); + if (!ok) { + console.log(`Message ${messageId} not found.`); + return; + } - console.log(`Message ${messageId} archived.`); + console.log(`Message ${messageId} archived.`); + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + client.destroy(); + } }); // ---- stats ---- @@ -284,39 +275,49 @@ export function registerMailboxCli(program: Command) { .option("--format ", "Output format (terminal|json)", "terminal") .action(async (opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); - - const healthy = await client.isHealthy(); - if (!healthy) { - console.log("Cortex offline. Cannot get mailbox stats."); - return; - } + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + { pluginName: "agent-mesh" }, + ); + const ns = resolveNamespace("agent-mesh"); - const mailbox = new AgentMailbox(client, ns); - const agent = opts.agent ?? "main"; + try { + const healthy = await client.isHealthy(); + if (!healthy) { + console.log("Cortex offline. Cannot get mailbox stats."); + return; + } - const stats = await mailbox.stats(agent); + const mailbox = new AgentMailbox(client, ns); + const agent = opts.agent ?? "main"; - if (opts.format === "json") { - console.log(JSON.stringify(stats, null, 2)); - return; - } + const stats = await mailbox.stats(agent); + + if (opts.format === "json") { + console.log(JSON.stringify(stats, null, 2)); + return; + } - console.log(`Mailbox stats for ${agent}:`); - console.log(` total: ${stats.total}`); - console.log(` unread: ${stats.unread}`); - console.log(` read: ${stats.read}`); - console.log(` archived: ${stats.archived}`); - if (Object.keys(stats.byType).length > 0) { - console.log(` by type:`); - for (const [type, count] of Object.entries(stats.byType)) { - console.log(` ${type}: ${count}`); + console.log(`Mailbox stats for ${agent}:`); + console.log(` total: ${stats.total}`); + console.log(` unread: ${stats.unread}`); + console.log(` read: ${stats.read}`); + console.log(` archived: ${stats.archived}`); + if (Object.keys(stats.byType).length > 0) { + console.log(` by type:`); + for (const [type, count] of Object.entries(stats.byType)) { + console.log(` ${type}: ${count}`); + } } + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + client.destroy(); } }); } diff --git a/src/cli/plan-cli.ts b/src/cli/plan-cli.ts index c14ef222..82238706 100644 --- a/src/cli/plan-cli.ts +++ b/src/cli/plan-cli.ts @@ -23,9 +23,8 @@ import { randomUUID } from "node:crypto"; import type { Command } from "commander"; -import { parseCortexConfig } from "../../extensions/shared/cortex-config.js"; import { CortexClient } from "../../extensions/shared/cortex-client.js"; -import { loadConfig } from "../config/config.js"; +import { resolveCortexClient, resolveNamespace } from "./shared/cortex-resolution.js"; // ============================================================================ // Types @@ -59,49 +58,6 @@ type AssertionEntry = { addedAt: string; }; -// ============================================================================ -// Cortex resolution (shared with trace-cli) -// ============================================================================ - -function resolveCortexClient(opts: { host?: string; port?: string; token?: string }): CortexClient { - const host = opts.host ?? process.env.CORTEX_HOST ?? "127.0.0.1"; - const port = opts.port - ? Number.parseInt(opts.port, 10) - : process.env.CORTEX_PORT - ? Number.parseInt(process.env.CORTEX_PORT, 10) - : 8080; - const authToken = opts.token ?? process.env.CORTEX_AUTH_TOKEN ?? undefined; - - if (!opts.host && !opts.port && !process.env.CORTEX_HOST && !process.env.CORTEX_PORT) { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["semantic-observability"]?.config as - | { cortex?: { host?: string; port?: number; authToken?: string } } - | undefined; - if (pluginCfg?.cortex) { - const cortex = parseCortexConfig(pluginCfg.cortex); - return new CortexClient(cortex); - } - } catch { - // Config not available - } - } - - return new CortexClient(parseCortexConfig({ host, port, authToken })); -} - -function resolveNamespace(): string { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["semantic-observability"]?.config as - | { agentNamespace?: string } - | undefined; - return pluginCfg?.agentNamespace ?? "mayros"; - } catch { - return "mayros"; - } -} - // ============================================================================ // Plan store (Cortex-backed) // ============================================================================ @@ -407,7 +363,7 @@ export function registerPlanCli(program: Command) { "Semantic plan mode — explore, assert, approve, execute with Cortex-backed decision graph", ) .option("--cortex-host ", "Cortex host (default: 127.0.0.1 or from config)") - .option("--cortex-port ", "Cortex port (default: 8080 or from config)") + .option("--cortex-port ", "Cortex port (default: 19090 or from config)") .option("--cortex-token ", "Cortex auth token (or set CORTEX_AUTH_TOKEN)"); function getStore(parentOpts: { @@ -415,12 +371,15 @@ export function registerPlanCli(program: Command) { cortexPort?: string; cortexToken?: string; }) { - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + { pluginName: "semantic-observability" }, + ); + const ns = resolveNamespace("semantic-observability"); return { store: new PlanStore(client, ns), client }; } diff --git a/src/cli/program/register.subclis.ts b/src/cli/program/register.subclis.ts index 56092e97..c5080830 100644 --- a/src/cli/program/register.subclis.ts +++ b/src/cli/program/register.subclis.ts @@ -314,6 +314,15 @@ const entries: SubCliEntry[] = [ mod.registerKgCli(program); }, }, + { + name: "dag", + description: "Semantic DAG — audit, time-travel, and history", + hasSubcommands: true, + register: async (program) => { + const mod = await import("../dag-cli.js"); + mod.registerDagCli(program); + }, + }, { name: "workflow", description: "Multi-agent workflows — run, list, and track workflow execution", diff --git a/src/cli/rules-cli.ts b/src/cli/rules-cli.ts index f1b949b9..e027eab8 100644 --- a/src/cli/rules-cli.ts +++ b/src/cli/rules-cli.ts @@ -13,53 +13,8 @@ */ import type { Command } from "commander"; -import { parseCortexConfig } from "../../extensions/shared/cortex-config.js"; -import { CortexClient } from "../../extensions/shared/cortex-client.js"; import { RulesEngine, type RuleScope } from "../../extensions/memory-semantic/rules-engine.js"; -import { loadConfig } from "../config/config.js"; - -// ============================================================================ -// Cortex resolution (same pattern as kg-cli.ts) -// ============================================================================ - -function resolveCortexClient(opts: { host?: string; port?: string; token?: string }): CortexClient { - const host = opts.host ?? process.env.CORTEX_HOST ?? "127.0.0.1"; - const port = opts.port - ? Number.parseInt(opts.port, 10) - : process.env.CORTEX_PORT - ? Number.parseInt(process.env.CORTEX_PORT, 10) - : 8080; - const authToken = opts.token ?? process.env.CORTEX_AUTH_TOKEN ?? undefined; - - if (!opts.host && !opts.port && !process.env.CORTEX_HOST && !process.env.CORTEX_PORT) { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["memory-semantic"]?.config as - | { cortex?: { host?: string; port?: number; authToken?: string } } - | undefined; - if (pluginCfg?.cortex) { - const cortex = parseCortexConfig(pluginCfg.cortex); - return new CortexClient(cortex); - } - } catch { - // Config not available — use defaults - } - } - - return new CortexClient(parseCortexConfig({ host, port, authToken })); -} - -function resolveNamespace(): string { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["memory-semantic"]?.config as - | { agentNamespace?: string } - | undefined; - return pluginCfg?.agentNamespace ?? "mayros"; - } catch { - return "mayros"; - } -} +import { resolveCortexClient, resolveNamespace } from "./shared/cortex-resolution.js"; const VALID_SCOPES = ["global", "project", "agent", "skill", "file"]; @@ -72,7 +27,7 @@ export function registerRulesCli(program: Command) { .command("rules") .description("Rules engine — manage Cortex-backed hierarchical rules") .option("--cortex-host ", "Cortex host (default: 127.0.0.1 or from config)") - .option("--cortex-port ", "Cortex port (default: 8080 or from config)") + .option("--cortex-port ", "Cortex port (default: 19090 or from config)") .option("--cortex-token ", "Cortex auth token (or set CORTEX_AUTH_TOKEN)"); // ------------------------------------------------------------------ diff --git a/src/cli/serve-cli.ts b/src/cli/serve-cli.ts index 1dee5c4a..ac5f8e9b 100644 --- a/src/cli/serve-cli.ts +++ b/src/cli/serve-cli.ts @@ -61,18 +61,36 @@ export function registerServeCli(program: Command): void { const cortexPort = config.cortex?.port ?? 19090; const cortexBase = `http://127.0.0.1:${cortexPort}`; const ns = config.agentNamespace || "mayros"; + const cortexHeaders: Record = { "Content-Type": "application/json" }; + if (config.cortex?.authToken) { + cortexHeaders["Authorization"] = config.cortex.authToken; + } const { createMemoryTools } = await import("../../extensions/mcp-server/memory-tools.js"); const { createBudgetTools } = await import("../../extensions/mcp-server/budget-tools.js"); const { createGovernanceTools } = await import("../../extensions/mcp-server/governance-tools.js"); const { createCortexTools } = await import("../../extensions/mcp-server/cortex-tools.js"); + const { createDagTools } = await import("../../extensions/mcp-server/dag-tools.js"); const tools = [ - ...createMemoryTools({ cortexBaseUrl: cortexBase, namespace: ns }), + ...createMemoryTools({ + cortexBaseUrl: cortexBase, + namespace: ns, + authToken: config.cortex?.authToken, + }), ...createBudgetTools(), ...createGovernanceTools(), - ...createCortexTools({ cortexBaseUrl: cortexBase, namespace: ns }), + ...createCortexTools({ + cortexBaseUrl: cortexBase, + namespace: ns, + authToken: config.cortex?.authToken, + }), + ...createDagTools({ + cortexBaseUrl: cortexBase, + namespace: ns, + authToken: config.cortex?.authToken, + }), ]; // Discover agents @@ -114,6 +132,30 @@ export function registerServeCli(program: Command): void { getRule: async () => null, getGraphStats: async () => null, listGraphSubjects: async () => [], + getDagTips: async () => { + try { + const res = await fetch(`${cortexBase}/api/v1/dag/tips`, { + headers: cortexHeaders, + }); + if (!res.ok) return null; + const data = (await res.json()) as { tips: string[]; count: number }; + return { tips: data.tips, count: data.count }; + } catch { + return null; + } + }, + getDagStats: async () => { + try { + const res = await fetch(`${cortexBase}/api/v1/dag/stats`, { + headers: cortexHeaders, + }); + if (!res.ok) return null; + const data = (await res.json()) as { action_count: number; tip_count: number }; + return { actionCount: data.action_count, tipCount: data.tip_count }; + } catch { + return null; + } + }, }, promptSources: { listConventions: async () => [], diff --git a/src/cli/shared/cortex-resolution.ts b/src/cli/shared/cortex-resolution.ts new file mode 100644 index 00000000..634923ee --- /dev/null +++ b/src/cli/shared/cortex-resolution.ts @@ -0,0 +1,89 @@ +/** + * Shared Cortex client resolution for CLI commands. + * + * Centralizes the host/port/token resolution logic that was previously + * duplicated across 15+ CLI files. Resolution order: + * 1. CLI flags (--cortex-host, --cortex-port, --cortex-token) + * 2. Environment variables (CORTEX_HOST, CORTEX_PORT, CORTEX_AUTH_TOKEN) + * 3. Plugin config from mayros.yaml + * 4. Defaults (127.0.0.1:19090) + */ + +import { parseCortexConfig } from "../../../extensions/shared/cortex-config.js"; +import { CortexClient } from "../../../extensions/shared/cortex-client.js"; +import { loadConfig } from "../../config/config.js"; + +export type CortexCliOpts = { + host?: string; + port?: string; + token?: string; +}; + +export type ResolveCortexOptions = { + /** Plugin name(s) to check in config. First match wins. */ + pluginName?: string | string[]; + /** Default port when no config/env/flag is set (default: 19090). */ + defaultPort?: number; +}; + +/** + * Resolve a CortexClient from CLI flags, env vars, or plugin config. + * + * @param opts CLI flags (host, port, token) + * @param options Resolution options (pluginName, defaultPort) + */ +export function resolveCortexClient( + opts: CortexCliOpts, + options?: ResolveCortexOptions, +): CortexClient { + const defaultPort = options?.defaultPort ?? 19090; + const pluginNames = options?.pluginName + ? Array.isArray(options.pluginName) + ? options.pluginName + : [options.pluginName] + : ["memory-semantic"]; + + const host = opts.host ?? process.env.CORTEX_HOST ?? "127.0.0.1"; + const port = opts.port + ? Number.parseInt(opts.port, 10) + : process.env.CORTEX_PORT + ? Number.parseInt(process.env.CORTEX_PORT, 10) + : defaultPort; + const authToken = opts.token ?? process.env.CORTEX_AUTH_TOKEN ?? undefined; + + if (!opts.host && !opts.port && !process.env.CORTEX_HOST && !process.env.CORTEX_PORT) { + try { + const cfg = loadConfig(); + for (const name of pluginNames) { + const pluginCfg = cfg.plugins?.entries?.[name]?.config as + | { cortex?: { host?: string; port?: number; authToken?: string } } + | undefined; + if (pluginCfg?.cortex) { + const cortex = parseCortexConfig(pluginCfg.cortex); + return new CortexClient(cortex); + } + } + } catch { + // Config not available — use defaults + } + } + + return new CortexClient(parseCortexConfig({ host, port, authToken })); +} + +/** + * Resolve the agent namespace from plugin config. + * + * @param pluginName Plugin name to read namespace from (default: "memory-semantic") + */ +export function resolveNamespace(pluginName?: string): string { + try { + const cfg = loadConfig(); + const pluginCfg = cfg.plugins?.entries?.[pluginName ?? "memory-semantic"]?.config as + | { agentNamespace?: string; namespace?: string } + | undefined; + return pluginCfg?.agentNamespace ?? pluginCfg?.namespace ?? "mayros"; + } catch { + return "mayros"; + } +} diff --git a/src/cli/sync-cli.test.ts b/src/cli/sync-cli.test.ts index 735c5819..36631c9a 100644 --- a/src/cli/sync-cli.test.ts +++ b/src/cli/sync-cli.test.ts @@ -64,6 +64,7 @@ vi.mock("../../extensions/shared/cortex-client.js", () => ({ async p2pAddPeer(addr: string) { return mockState.p2pAddPeerFn(addr); } + destroy() {} }, })); diff --git a/src/cli/sync-cli.ts b/src/cli/sync-cli.ts index 6f207c63..aa7ca6b2 100644 --- a/src/cli/sync-cli.ts +++ b/src/cli/sync-cli.ts @@ -11,57 +11,12 @@ */ import type { Command } from "commander"; -import { parseCortexConfig } from "../../extensions/shared/cortex-config.js"; -import { CortexClient, type P2pStatusResponse } from "../../extensions/shared/cortex-client.js"; +import { + type CortexClient, + type P2pStatusResponse, +} from "../../extensions/shared/cortex-client.js"; import { PeerManager } from "../../extensions/cortex-sync/peer-manager.js"; -import { loadConfig } from "../config/config.js"; - -// ============================================================================ -// Cortex resolution -// ============================================================================ - -function resolveCortexClient(opts: { host?: string; port?: string; token?: string }): CortexClient { - const host = opts.host ?? process.env.CORTEX_HOST ?? "127.0.0.1"; - const port = opts.port - ? Number.parseInt(opts.port, 10) - : process.env.CORTEX_PORT - ? Number.parseInt(process.env.CORTEX_PORT, 10) - : 8080; - const authToken = opts.token ?? process.env.CORTEX_AUTH_TOKEN ?? undefined; - - if (!opts.host && !opts.port && !process.env.CORTEX_HOST && !process.env.CORTEX_PORT) { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["cortex-sync"]?.config as - | { cortex?: { host?: string; port?: number; authToken?: string } } - | undefined; - if (pluginCfg?.cortex) { - const cortex = parseCortexConfig(pluginCfg.cortex); - return new CortexClient(cortex); - } - } catch { - // Config not available - } - } - - try { - return new CortexClient(parseCortexConfig({ host, port, authToken })); - } catch { - return new CortexClient(parseCortexConfig({})); - } -} - -function resolveNamespace(): string { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["cortex-sync"]?.config as - | { namespace?: string } - | undefined; - return pluginCfg?.namespace ?? "mayros"; - } catch { - return "mayros"; - } -} +import { resolveCortexClient, resolveNamespace } from "./shared/cortex-resolution.js"; async function probeP2pStatus(client: CortexClient): Promise { try { @@ -91,67 +46,77 @@ export function registerSyncCli(program: Command) { .option("--format ", "Output format (terminal|json)", "terminal") .action(async (opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); - - const healthy = await client.isHealthy(); - if (!healthy) { - console.log("Cortex offline. Cannot query sync status."); - return; - } + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + { pluginName: "cortex-sync" }, + ); + const ns = resolveNamespace("cortex-sync"); + + try { + const healthy = await client.isHealthy(); + if (!healthy) { + console.log("Cortex offline. Cannot query sync status."); + return; + } - const pm = new PeerManager(client, ns); - const status = await pm.status(); - const peers = await pm.listPeers(); + const pm = new PeerManager(client, ns); + const status = await pm.status(); + const peers = await pm.listPeers(); - if (opts.format === "json") { - console.log(JSON.stringify({ status, peers }, null, 2)); - return; - } + if (opts.format === "json") { + console.log(JSON.stringify({ status, peers }, null, 2)); + return; + } - console.log("Cortex Sync Status:"); - console.log(` Total peers: ${status.totalPeers}`); - console.log(` Active: ${status.activePeers}`); - console.log(` Unreachable: ${status.unreachablePeers}`); - console.log(` Total syncs: ${status.totalSyncs}`); - console.log(` Total triples synced: ${status.totalTriplesSynced}`); - - if (peers.length > 0) { - console.log("\nPeers:"); - for (const peer of peers) { - const lastSync = peer.lastSyncAt || "never"; - console.log(` ${peer.nodeId} [${peer.status}]`); - console.log(` endpoint: ${peer.endpoint}`); - console.log(` namespaces: ${peer.namespaces.join(", ")}`); - console.log(` last sync: ${lastSync}`); - console.log(` syncs: ${peer.totalSyncs}, triples: ${peer.totalTriplesSynced}`); + console.log("Cortex Sync Status:"); + console.log(` Total peers: ${status.totalPeers}`); + console.log(` Active: ${status.activePeers}`); + console.log(` Unreachable: ${status.unreachablePeers}`); + console.log(` Total syncs: ${status.totalSyncs}`); + console.log(` Total triples synced: ${status.totalTriplesSynced}`); + + if (peers.length > 0) { + console.log("\nPeers:"); + for (const peer of peers) { + const lastSync = peer.lastSyncAt || "never"; + console.log(` ${peer.nodeId} [${peer.status}]`); + console.log(` endpoint: ${peer.endpoint}`); + console.log(` namespaces: ${peer.namespaces.join(", ")}`); + console.log(` last sync: ${lastSync}`); + console.log(` syncs: ${peer.totalSyncs}, triples: ${peer.totalTriplesSynced}`); + } } - } - // B4: Show native P2P info if available - const p2p = await probeP2pStatus(client); - if (p2p?.enabled) { - console.log("\nNative P2P:"); - console.log(` Node ID: ${p2p.node_id.slice(0, 16)}...`); - console.log(` Port: ${p2p.port}`); - console.log(` Mode: native (QUIC gossip)`); - console.log(` Connected peers: ${p2p.peer_count}`); - if (p2p.connected_peers.length > 0) { - console.log(" P2P Peers:"); - for (const pp of p2p.connected_peers) { - console.log(` ${pp.addr} [${pp.connected ? "connected" : "disconnected"}]`); + // B4: Show native P2P info if available + const p2p = await probeP2pStatus(client); + if (p2p?.enabled) { + console.log("\nNative P2P:"); + console.log(` Node ID: ${p2p.node_id.slice(0, 16)}...`); + console.log(` Port: ${p2p.port}`); + console.log(` Mode: native (QUIC gossip)`); + console.log(` Connected peers: ${p2p.peer_count}`); + if (p2p.connected_peers.length > 0) { + console.log(" P2P Peers:"); + for (const pp of p2p.connected_peers) { + console.log(` ${pp.addr} [${pp.connected ? "connected" : "disconnected"}]`); + } } + console.log( + ` Gossip: round ${p2p.gossip_stats.round}, known ${p2p.gossip_stats.known_ids}`, + ); + console.log( + ` Sync: ${p2p.sync_stats.local_ids} local, ${p2p.sync_stats.total_successful_syncs} successful syncs`, + ); } - console.log( - ` Gossip: round ${p2p.gossip_stats.round}, known ${p2p.gossip_stats.known_ids}`, - ); - console.log( - ` Sync: ${p2p.sync_stats.local_ids} local, ${p2p.sync_stats.total_successful_syncs} successful syncs`, - ); + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + client.destroy(); } }); @@ -165,49 +130,59 @@ export function registerSyncCli(program: Command) { .option("--namespaces ", "Namespaces to sync") .action(async (nodeId, endpoint, opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); - - const healthy = await client.isHealthy(); - if (!healthy) { - console.log("Cortex offline. Cannot pair."); - return; - } + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + { pluginName: "cortex-sync" }, + ); + const ns = resolveNamespace("cortex-sync"); + + try { + const healthy = await client.isHealthy(); + if (!healthy) { + console.log("Cortex offline. Cannot pair."); + return; + } - const pm = new PeerManager(client, ns); - const existing = await pm.getPeer(nodeId); - if (existing && existing.status !== "removed") { - console.log(`Peer ${nodeId} already exists (status: ${existing.status}).`); - return; - } + const pm = new PeerManager(client, ns); + const existing = await pm.getPeer(nodeId); + if (existing && existing.status !== "removed") { + console.log(`Peer ${nodeId} already exists (status: ${existing.status}).`); + return; + } - const peer = await pm.addPeer({ - nodeId, - endpoint, - namespaces: opts.namespaces ?? [ns], - enabled: true, - }); - - console.log(`Paired with peer ${peer.nodeId}:`); - console.log(` Endpoint: ${peer.endpoint}`); - console.log(` Namespaces: ${peer.namespaces.join(", ")}`); - console.log(` Status: ${peer.status}`); - - // B4: Also connect via P2P API when native is active - const p2p = await probeP2pStatus(client); - if (p2p?.enabled) { - try { - const url = new URL(endpoint); - const p2pAddr = `${url.hostname}:${p2p.port}`; - const res = await client.p2pAddPeer(p2pAddr); - console.log(` P2P: ${res.status} (${res.addr})`); - } catch { - console.log(" P2P: connection failed (will retry via gossip)"); + const peer = await pm.addPeer({ + nodeId, + endpoint, + namespaces: opts.namespaces ?? [ns], + enabled: true, + }); + + console.log(`Paired with peer ${peer.nodeId}:`); + console.log(` Endpoint: ${peer.endpoint}`); + console.log(` Namespaces: ${peer.namespaces.join(", ")}`); + console.log(` Status: ${peer.status}`); + + // B4: Also connect via P2P API when native is active + const p2p = await probeP2pStatus(client); + if (p2p?.enabled) { + try { + const url = new URL(endpoint); + const p2pAddr = `${url.hostname}:${p2p.port}`; + const res = await client.p2pAddPeer(p2pAddr); + console.log(` P2P: ${res.status} (${res.addr})`); + } catch { + console.log(" P2P: connection failed (will retry via gossip)"); + } } + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + client.destroy(); } }); @@ -219,28 +194,38 @@ export function registerSyncCli(program: Command) { .argument("", "Peer node ID") .action(async (nodeId, _opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); - - const healthy = await client.isHealthy(); - if (!healthy) { - console.log("Cortex offline. Cannot remove peer."); - return; - } + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + { pluginName: "cortex-sync" }, + ); + const ns = resolveNamespace("cortex-sync"); + + try { + const healthy = await client.isHealthy(); + if (!healthy) { + console.log("Cortex offline. Cannot remove peer."); + return; + } - const pm = new PeerManager(client, ns); - const ok = await pm.removePeer(nodeId); + const pm = new PeerManager(client, ns); + const ok = await pm.removePeer(nodeId); - if (!ok) { - console.log(`Peer ${nodeId} not found.`); - return; - } + if (!ok) { + console.log(`Peer ${nodeId} not found.`); + return; + } - console.log(`Peer ${nodeId} removed.`); + console.log(`Peer ${nodeId} removed.`); + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + client.destroy(); + } }); // ---- now ---- @@ -251,53 +236,63 @@ export function registerSyncCli(program: Command) { .option("--peer ", "Sync with a specific peer (omit for all)") .action(async (opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); - - const healthy = await client.isHealthy(); - if (!healthy) { - console.log("Cortex offline. Cannot sync."); - return; - } + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + { pluginName: "cortex-sync" }, + ); + const ns = resolveNamespace("cortex-sync"); + + try { + const healthy = await client.isHealthy(); + if (!healthy) { + console.log("Cortex offline. Cannot sync."); + return; + } - const pm = new PeerManager(client, ns); - - // B4: Check for native P2P mode - const p2p = await probeP2pStatus(client); - - if (p2p?.enabled) { - console.log("Sync handled by native P2P gossip."); - console.log( - ` Gossip: round ${p2p.gossip_stats.round}, known ${p2p.gossip_stats.known_ids}`, - ); - console.log( - ` Sync: ${p2p.sync_stats.local_ids} local, ${p2p.sync_stats.total_successful_syncs} successful syncs`, - ); - console.log(` Connected P2P peers: ${p2p.peer_count}`); - return; - } + const pm = new PeerManager(client, ns); + + // B4: Check for native P2P mode + const p2p = await probeP2pStatus(client); - if (opts.peer) { - const peer = await pm.getPeer(opts.peer); - if (!peer) { - console.log(`Peer ${opts.peer} not found.`); + if (p2p?.enabled) { + console.log("Sync handled by native P2P gossip."); + console.log( + ` Gossip: round ${p2p.gossip_stats.round}, known ${p2p.gossip_stats.known_ids}`, + ); + console.log( + ` Sync: ${p2p.sync_stats.local_ids} local, ${p2p.sync_stats.total_successful_syncs} successful syncs`, + ); + console.log(` Connected P2P peers: ${p2p.peer_count}`); return; } - console.log(`Triggering sync with ${opts.peer}...`); - console.log("Note: Full sync requires the cortex-sync plugin running in the gateway."); - console.log(`Peer status: ${peer.status}`); - } else { - const peers = await pm.listPeers(); - const active = peers.filter((p) => p.status === "active"); - console.log(`Found ${active.length} active peer(s).`); - console.log("Note: Full sync requires the cortex-sync plugin running in the gateway."); - for (const p of active) { - console.log(` ${p.nodeId} → ${p.endpoint}`); + + if (opts.peer) { + const peer = await pm.getPeer(opts.peer); + if (!peer) { + console.log(`Peer ${opts.peer} not found.`); + return; + } + console.log(`Triggering sync with ${opts.peer}...`); + console.log("Note: Full sync requires the cortex-sync plugin running in the gateway."); + console.log(`Peer status: ${peer.status}`); + } else { + const peers = await pm.listPeers(); + const active = peers.filter((p) => p.status === "active"); + console.log(`Found ${active.length} active peer(s).`); + console.log("Note: Full sync requires the cortex-sync plugin running in the gateway."); + for (const p of active) { + console.log(` ${p.nodeId} → ${p.endpoint}`); + } } + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + client.destroy(); } }); } diff --git a/src/cli/tasks-cli.ts b/src/cli/tasks-cli.ts index 031a18a5..05c109ee 100644 --- a/src/cli/tasks-cli.ts +++ b/src/cli/tasks-cli.ts @@ -11,56 +11,11 @@ */ import type { Command } from "commander"; -import { parseCortexConfig } from "../../extensions/shared/cortex-config.js"; -import { CortexClient } from "../../extensions/shared/cortex-client.js"; import { BackgroundTracker, isValidBackgroundTaskStatus, } from "../../extensions/agent-mesh/background-tracker.js"; -import { loadConfig } from "../config/config.js"; - -// ============================================================================ -// Cortex resolution (reads from agent-mesh plugin config) -// ============================================================================ - -function resolveCortexClient(opts: { host?: string; port?: string; token?: string }): CortexClient { - const host = opts.host ?? process.env.CORTEX_HOST ?? "127.0.0.1"; - const port = opts.port - ? Number.parseInt(opts.port, 10) - : process.env.CORTEX_PORT - ? Number.parseInt(process.env.CORTEX_PORT, 10) - : 8080; - const authToken = opts.token ?? process.env.CORTEX_AUTH_TOKEN ?? undefined; - - if (!opts.host && !opts.port && !process.env.CORTEX_HOST && !process.env.CORTEX_PORT) { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["agent-mesh"]?.config as - | { cortex?: { host?: string; port?: number; authToken?: string } } - | undefined; - if (pluginCfg?.cortex) { - const cortex = parseCortexConfig(pluginCfg.cortex); - return new CortexClient(cortex); - } - } catch { - // Config not available — use defaults - } - } - - return new CortexClient(parseCortexConfig({ host, port, authToken })); -} - -function resolveNamespace(): string { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["agent-mesh"]?.config as - | { agentNamespace?: string } - | undefined; - return pluginCfg?.agentNamespace ?? "mayros"; - } catch { - return "mayros"; - } -} +import { resolveCortexClient, resolveNamespace } from "./shared/cortex-resolution.js"; // ============================================================================ // Registration @@ -71,7 +26,7 @@ export function registerTasksCli(program: Command) { .command("tasks") .description("Background tasks — list, inspect, and manage background agent tasks") .option("--cortex-host ", "Cortex host (default: 127.0.0.1 or from config)") - .option("--cortex-port ", "Cortex port (default: 8080 or from config)") + .option("--cortex-port ", "Cortex port (default: 19090 or from config)") .option("--cortex-token ", "Cortex auth token (or set CORTEX_AUTH_TOKEN)"); // ---- list ---- @@ -85,41 +40,51 @@ export function registerTasksCli(program: Command) { .option("--format ", "Output format (terminal|json)", "terminal") .action(async (opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); - - const healthy = await client.isHealthy(); - if (!healthy) { - console.log("Cortex offline. Cannot list tasks."); - return; - } - - const tracker = new BackgroundTracker(client, ns); - const taskList = await tracker.listTasks({ - status: opts.status && isValidBackgroundTaskStatus(opts.status) ? opts.status : undefined, - agentId: opts.agent, - limit: Number.parseInt(opts.limit, 10) || 20, - }); - - if (opts.format === "json") { - console.log(JSON.stringify(taskList, null, 2)); - return; - } - - if (taskList.length === 0) { - console.log("No background tasks found."); - return; - } - - console.log(`Background tasks (${taskList.length}):`); - for (const t of taskList) { - const progress = t.progress !== undefined ? ` ${t.progress}%` : ""; - const desc = t.description.length > 50 ? t.description.slice(0, 50) + "…" : t.description; - console.log(` ${t.id} [${t.status}]${progress} ${t.agentId} ${desc}`); + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + { pluginName: "agent-mesh" }, + ); + const ns = resolveNamespace("agent-mesh"); + + try { + const healthy = await client.isHealthy(); + if (!healthy) { + console.log("Cortex offline. Cannot list tasks."); + return; + } + + const tracker = new BackgroundTracker(client, ns); + const taskList = await tracker.listTasks({ + status: opts.status && isValidBackgroundTaskStatus(opts.status) ? opts.status : undefined, + agentId: opts.agent, + limit: Number.parseInt(opts.limit, 10) || 20, + }); + + if (opts.format === "json") { + console.log(JSON.stringify(taskList, null, 2)); + return; + } + + if (taskList.length === 0) { + console.log("No background tasks found."); + return; + } + + console.log(`Background tasks (${taskList.length}):`); + for (const t of taskList) { + const progress = t.progress !== undefined ? ` ${t.progress}%` : ""; + const desc = t.description.length > 50 ? t.description.slice(0, 50) + "…" : t.description; + console.log(` ${t.id} [${t.status}]${progress} ${t.agentId} ${desc}`); + } + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + client.destroy(); } }); @@ -132,48 +97,58 @@ export function registerTasksCli(program: Command) { .option("--format ", "Output format (terminal|json)", "terminal") .action(async (taskId, opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); - - const healthy = await client.isHealthy(); - if (!healthy) { - console.log("Cortex offline. Cannot get task status."); - return; - } - - const tracker = new BackgroundTracker(client, ns); - const task = await tracker.getTask(taskId); - - if (!task) { - console.log(`Task ${taskId} not found.`); - return; - } - - if (opts.format === "json") { - console.log(JSON.stringify(task, null, 2)); - return; - } - - console.log(`Task ${task.id}:`); - console.log(` agent: ${task.agentId}`); - console.log(` description: ${task.description}`); - console.log(` status: ${task.status}`); - console.log(` started: ${task.startedAt}`); - if (task.completedAt) { - console.log(` completed: ${task.completedAt}`); - } - if (task.progress !== undefined) { - console.log(` progress: ${task.progress}%`); - } - if (task.result) { - console.log(` result: ${task.result}`); - } - if (task.error) { - console.log(` error: ${task.error}`); + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + { pluginName: "agent-mesh" }, + ); + const ns = resolveNamespace("agent-mesh"); + + try { + const healthy = await client.isHealthy(); + if (!healthy) { + console.log("Cortex offline. Cannot get task status."); + return; + } + + const tracker = new BackgroundTracker(client, ns); + const task = await tracker.getTask(taskId); + + if (!task) { + console.log(`Task ${taskId} not found.`); + return; + } + + if (opts.format === "json") { + console.log(JSON.stringify(task, null, 2)); + return; + } + + console.log(`Task ${task.id}:`); + console.log(` agent: ${task.agentId}`); + console.log(` description: ${task.description}`); + console.log(` status: ${task.status}`); + console.log(` started: ${task.startedAt}`); + if (task.completedAt) { + console.log(` completed: ${task.completedAt}`); + } + if (task.progress !== undefined) { + console.log(` progress: ${task.progress}%`); + } + if (task.result) { + console.log(` result: ${task.result}`); + } + if (task.error) { + console.log(` error: ${task.error}`); + } + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + client.destroy(); } }); @@ -185,28 +160,38 @@ export function registerTasksCli(program: Command) { .argument("", "Task ID") .action(async (taskId, _opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); - - const healthy = await client.isHealthy(); - if (!healthy) { - console.log("Cortex offline. Cannot cancel task."); - return; + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + { pluginName: "agent-mesh" }, + ); + const ns = resolveNamespace("agent-mesh"); + + try { + const healthy = await client.isHealthy(); + if (!healthy) { + console.log("Cortex offline. Cannot cancel task."); + return; + } + + const tracker = new BackgroundTracker(client, ns); + const ok = await tracker.cancel(taskId); + + if (!ok) { + console.log(`Task ${taskId} not found.`); + return; + } + + console.log(`Task ${taskId} cancelled.`); + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + client.destroy(); } - - const tracker = new BackgroundTracker(client, ns); - const ok = await tracker.cancel(taskId); - - if (!ok) { - console.log(`Task ${taskId} not found.`); - return; - } - - console.log(`Task ${taskId} cancelled.`); }); // ---- summary ---- @@ -217,33 +202,43 @@ export function registerTasksCli(program: Command) { .option("--format ", "Output format (terminal|json)", "terminal") .action(async (opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); - - const healthy = await client.isHealthy(); - if (!healthy) { - console.log("Cortex offline. Cannot get task summary."); - return; + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + { pluginName: "agent-mesh" }, + ); + const ns = resolveNamespace("agent-mesh"); + + try { + const healthy = await client.isHealthy(); + if (!healthy) { + console.log("Cortex offline. Cannot get task summary."); + return; + } + + const tracker = new BackgroundTracker(client, ns); + const s = await tracker.summary(); + + if (opts.format === "json") { + console.log(JSON.stringify(s, null, 2)); + return; + } + + console.log(`Background task summary:`); + console.log(` total: ${s.total}`); + console.log(` running: ${s.running}`); + console.log(` completed: ${s.completed}`); + console.log(` failed: ${s.failed}`); + console.log(` cancelled: ${s.cancelled}`); + console.log(` pending: ${s.pending}`); + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + client.destroy(); } - - const tracker = new BackgroundTracker(client, ns); - const s = await tracker.summary(); - - if (opts.format === "json") { - console.log(JSON.stringify(s, null, 2)); - return; - } - - console.log(`Background task summary:`); - console.log(` total: ${s.total}`); - console.log(` running: ${s.running}`); - console.log(` completed: ${s.completed}`); - console.log(` failed: ${s.failed}`); - console.log(` cancelled: ${s.cancelled}`); - console.log(` pending: ${s.pending}`); }); } diff --git a/src/cli/teleport-cli.ts b/src/cli/teleport-cli.ts index 2f9cc5f4..e0cb37de 100644 --- a/src/cli/teleport-cli.ts +++ b/src/cli/teleport-cli.ts @@ -14,9 +14,11 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { basename, resolve } from "node:path"; import process from "node:process"; -import { parseCortexConfig } from "../../extensions/shared/cortex-config.js"; -import { CortexClient } from "../../extensions/shared/cortex-client.js"; -import { loadConfig } from "../config/config.js"; +import { + resolveCortexClient, + resolveNamespace, + type CortexCliOpts, +} from "./shared/cortex-resolution.js"; import { exportSession, importSession, @@ -28,54 +30,14 @@ import { // Cortex resolution // ============================================================================ -function resolveCortexClient(opts: { - host?: string; - port?: string; - token?: string; -}): CortexClient | undefined { - const host = opts.host ?? process.env.CORTEX_HOST ?? "127.0.0.1"; - const rawPort = opts.port - ? Number.parseInt(opts.port, 10) - : process.env.CORTEX_PORT - ? Number.parseInt(process.env.CORTEX_PORT, 10) - : 8080; - const port = Number.isFinite(rawPort) ? rawPort : 8080; - const authToken = opts.token ?? process.env.CORTEX_AUTH_TOKEN ?? undefined; - - if (!opts.host && !opts.port && !process.env.CORTEX_HOST && !process.env.CORTEX_PORT) { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["memory-semantic"]?.config as - | { cortex?: { host?: string; port?: number; authToken?: string } } - | undefined; - if (pluginCfg?.cortex) { - const cortex = parseCortexConfig(pluginCfg.cortex); - return new CortexClient(cortex); - } - } catch { - // Config not available - } - } - +function resolveCortexClientSafe(opts: CortexCliOpts) { try { - return new CortexClient(parseCortexConfig({ host, port, authToken })); + return resolveCortexClient(opts); } catch { return undefined; } } -function resolveNamespace(): string { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["memory-semantic"]?.config as - | { namespace?: string } - | undefined; - return pluginCfg?.namespace ?? "mayros"; - } catch { - return "mayros"; - } -} - function resolveSessionPaths(sessionKey: string): { transcriptPath: string; storePath: string; @@ -124,32 +86,39 @@ export function registerTeleportCli(program: Command) { } const paths = resolveSessionPaths(sessionKey); - const cortexClient = resolveCortexClient({ + const cortexClient = resolveCortexClientSafe({ host: parentOpts.cortexHost, port: parentOpts.cortexPort, token: parentOpts.cortexToken, }); const ns = resolveNamespace(); - console.log(`Exporting session: ${sessionKey}`); - - const result = await exportSession({ - sessionKey, - transcriptPath: paths.transcriptPath, - storePath: paths.storePath, - cortexClient, - namespace: ns, - includeProjectMemory: opts.projectMemory, - }); - - const safeKey = basename(sessionKey).replace(/[^a-zA-Z0-9_-]/g, "_"); - const outputFile = opts.output ?? `teleport-${safeKey}.json`; - writeFileSync(outputFile, JSON.stringify(result.bundle, null, 2), "utf-8"); - - console.log(`Exported to: ${outputFile}`); - console.log(` transcript: ${result.transcriptSize} bytes`); - console.log(` cortex triples: ${result.tripleCount}`); - console.log(` device: ${result.bundle.sourceDeviceId}`); + try { + console.log(`Exporting session: ${sessionKey}`); + + const result = await exportSession({ + sessionKey, + transcriptPath: paths.transcriptPath, + storePath: paths.storePath, + cortexClient, + namespace: ns, + includeProjectMemory: opts.projectMemory, + }); + + const safeKey = basename(sessionKey).replace(/[^a-zA-Z0-9_-]/g, "_"); + const outputFile = opts.output ?? `teleport-${safeKey}.json`; + writeFileSync(outputFile, JSON.stringify(result.bundle, null, 2), "utf-8"); + + console.log(`Exported to: ${outputFile}`); + console.log(` transcript: ${result.transcriptSize} bytes`); + console.log(` cortex triples: ${result.tripleCount}`); + console.log(` device: ${result.bundle.sourceDeviceId}`); + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + cortexClient?.destroy(); + } }); // ---- import ---- @@ -185,32 +154,39 @@ export function registerTeleportCli(program: Command) { const bundle = data as TeleportBundle; const paths = resolveSessionPaths(opts.remap ?? bundle.sessionKey); - const cortexClient = resolveCortexClient({ + const cortexClient = resolveCortexClientSafe({ host: parentOpts.cortexHost, port: parentOpts.cortexPort, token: parentOpts.cortexToken, }); const ns = resolveNamespace(); - console.log(`Importing session from: ${file}`); - console.log(` source device: ${bundle.sourceDeviceId}`); - console.log(` exported at: ${bundle.exportedAt}`); - - const result = await importSession({ - bundle, - targetTranscriptDir: paths.sessionsDir, - targetStorePath: paths.storePath, - cortexClient, - namespace: ns, - remapSessionKey: opts.remap, - }); - - console.log(`\nImported successfully:`); - console.log(` session key: ${result.sessionKey}`); - console.log(` transcript: ${result.transcriptPath}`); - console.log(` cortex triples: ${result.triplesImported}`); - if (result.remapped) { - console.log(` remapped from: ${bundle.sessionKey}`); + try { + console.log(`Importing session from: ${file}`); + console.log(` source device: ${bundle.sourceDeviceId}`); + console.log(` exported at: ${bundle.exportedAt}`); + + const result = await importSession({ + bundle, + targetTranscriptDir: paths.sessionsDir, + targetStorePath: paths.storePath, + cortexClient, + namespace: ns, + remapSessionKey: opts.remap, + }); + + console.log(`\nImported successfully:`); + console.log(` session key: ${result.sessionKey}`); + console.log(` transcript: ${result.transcriptPath}`); + console.log(` cortex triples: ${result.triplesImported}`); + if (result.remapped) { + console.log(` remapped from: ${bundle.sessionKey}`); + } + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + cortexClient?.destroy(); } }); diff --git a/src/cli/trace-cli.ts b/src/cli/trace-cli.ts index 73d86981..30fa7717 100644 --- a/src/cli/trace-cli.ts +++ b/src/cli/trace-cli.ts @@ -13,56 +13,10 @@ */ import type { Command } from "commander"; -import { parseCortexConfig } from "../../extensions/shared/cortex-config.js"; -import { CortexClient } from "../../extensions/shared/cortex-client.js"; import { DecisionGraph } from "../../extensions/semantic-observability/decision-graph.js"; import { ObservabilityQueryEngine } from "../../extensions/semantic-observability/query-engine.js"; import { ObservabilityFormatter } from "../../extensions/semantic-observability/formatters.js"; -import { loadConfig } from "../config/config.js"; - -// ============================================================================ -// Cortex resolution -// ============================================================================ - -function resolveCortexClient(opts: { host?: string; port?: string; token?: string }): CortexClient { - const host = opts.host ?? process.env.CORTEX_HOST ?? "127.0.0.1"; - const port = opts.port - ? Number.parseInt(opts.port, 10) - : process.env.CORTEX_PORT - ? Number.parseInt(process.env.CORTEX_PORT, 10) - : 8080; - const authToken = opts.token ?? process.env.CORTEX_AUTH_TOKEN ?? undefined; - - // Try to read from mayros config plugin entries as fallback - if (!opts.host && !opts.port && !process.env.CORTEX_HOST && !process.env.CORTEX_PORT) { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["semantic-observability"]?.config as - | { cortex?: { host?: string; port?: number; authToken?: string } } - | undefined; - if (pluginCfg?.cortex) { - const cortex = parseCortexConfig(pluginCfg.cortex); - return new CortexClient(cortex); - } - } catch { - // Config not available — use defaults - } - } - - return new CortexClient(parseCortexConfig({ host, port, authToken })); -} - -function resolveNamespace(): string { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["semantic-observability"]?.config as - | { agentNamespace?: string } - | undefined; - return pluginCfg?.agentNamespace ?? "mayros"; - } catch { - return "mayros"; - } -} +import { resolveCortexClient, resolveNamespace } from "./shared/cortex-resolution.js"; // ============================================================================ // Registration @@ -73,7 +27,7 @@ export function registerTraceCli(program: Command) { .command("trace") .description("Inspect agent trace events — query, explain, stats, session trees") .option("--cortex-host ", "Cortex host (default: 127.0.0.1 or from config)") - .option("--cortex-port ", "Cortex port (default: 8080 or from config)") + .option("--cortex-port ", "Cortex port (default: 19090 or from config)") .option("--cortex-token ", "Cortex auth token (or set CORTEX_AUTH_TOKEN)"); // ------------------------------------------------------------------ @@ -92,12 +46,11 @@ export function registerTraceCli(program: Command) { .option("--format ", "Output format: terminal, json, markdown", "terminal") .action(async (opts) => { const parent = trace.opts(); - const client = resolveCortexClient({ - host: parent.cortexHost, - port: parent.cortexPort, - token: parent.cortexToken, - }); - const ns = resolveNamespace(); + const client = resolveCortexClient( + { host: parent.cortexHost, port: parent.cortexPort, token: parent.cortexToken }, + { pluginName: "semantic-observability" }, + ); + const ns = resolveNamespace("semantic-observability"); const graph = new DecisionGraph(client, ns); try { @@ -129,12 +82,11 @@ export function registerTraceCli(program: Command) { .argument("", "Event ID to explain") .action(async (eventId: string) => { const parent = trace.opts(); - const client = resolveCortexClient({ - host: parent.cortexHost, - port: parent.cortexPort, - token: parent.cortexToken, - }); - const ns = resolveNamespace(); + const client = resolveCortexClient( + { host: parent.cortexHost, port: parent.cortexPort, token: parent.cortexToken }, + { pluginName: "semantic-observability" }, + ); + const ns = resolveNamespace("semantic-observability"); const graph = new DecisionGraph(client, ns); try { @@ -157,12 +109,11 @@ export function registerTraceCli(program: Command) { .option("--format ", "Output format: terminal, json", "terminal") .action(async (opts) => { const parent = trace.opts(); - const client = resolveCortexClient({ - host: parent.cortexHost, - port: parent.cortexPort, - token: parent.cortexToken, - }); - const ns = resolveNamespace(); + const client = resolveCortexClient( + { host: parent.cortexHost, port: parent.cortexPort, token: parent.cortexToken }, + { pluginName: "semantic-observability" }, + ); + const ns = resolveNamespace("semantic-observability"); const queryEngine = new ObservabilityQueryEngine(client, ns); try { @@ -193,12 +144,11 @@ export function registerTraceCli(program: Command) { .option("--format ", "Output format: terminal, json", "terminal") .action(async (sessionKey: string, opts: { format?: string }) => { const parent = trace.opts(); - const client = resolveCortexClient({ - host: parent.cortexHost, - port: parent.cortexPort, - token: parent.cortexToken, - }); - const ns = resolveNamespace(); + const client = resolveCortexClient( + { host: parent.cortexHost, port: parent.cortexPort, token: parent.cortexToken }, + { pluginName: "semantic-observability" }, + ); + const ns = resolveNamespace("semantic-observability"); const graph = new DecisionGraph(client, ns); try { @@ -229,12 +179,11 @@ export function registerTraceCli(program: Command) { .description("Check Cortex connectivity and configuration") .action(async () => { const parent = trace.opts(); - const client = resolveCortexClient({ - host: parent.cortexHost, - port: parent.cortexPort, - token: parent.cortexToken, - }); - const ns = resolveNamespace(); + const client = resolveCortexClient( + { host: parent.cortexHost, port: parent.cortexPort, token: parent.cortexToken }, + { pluginName: "semantic-observability" }, + ); + const ns = resolveNamespace("semantic-observability"); try { console.log(`Cortex endpoint: ${client.baseUrl}`); diff --git a/src/cli/workflow-cli.ts b/src/cli/workflow-cli.ts index 386d315f..57f919d0 100644 --- a/src/cli/workflow-cli.ts +++ b/src/cli/workflow-cli.ts @@ -12,61 +12,15 @@ */ import type { Command } from "commander"; -import { parseCortexConfig } from "../../extensions/shared/cortex-config.js"; import { CortexClient } from "../../extensions/shared/cortex-client.js"; import { KnowledgeFusion } from "../../extensions/agent-mesh/knowledge-fusion.js"; import { NamespaceManager } from "../../extensions/agent-mesh/namespace-manager.js"; import { TeamManager } from "../../extensions/agent-mesh/team-manager.js"; import { WorkflowOrchestrator } from "../../extensions/agent-mesh/workflow-orchestrator.js"; -import { - listWorkflows as listWorkflowDefs, - getWorkflow as getWorkflowDef, -} from "../../extensions/agent-mesh/workflows/registry.js"; -import { parseTeamsConfig, parseWorktreeConfig } from "../../extensions/agent-mesh/config.js"; +import { listWorkflows as listWorkflowDefs } from "../../extensions/agent-mesh/workflows/registry.js"; +import { parseTeamsConfig } from "../../extensions/agent-mesh/config.js"; import { loadConfig } from "../config/config.js"; - -// ============================================================================ -// Cortex resolution (reads from agent-mesh plugin config) -// ============================================================================ - -function resolveCortexClient(opts: { host?: string; port?: string; token?: string }): CortexClient { - const host = opts.host ?? process.env.CORTEX_HOST ?? "127.0.0.1"; - const port = opts.port - ? Number.parseInt(opts.port, 10) - : process.env.CORTEX_PORT - ? Number.parseInt(process.env.CORTEX_PORT, 10) - : 8080; - const authToken = opts.token ?? process.env.CORTEX_AUTH_TOKEN ?? undefined; - - if (!opts.host && !opts.port && !process.env.CORTEX_HOST && !process.env.CORTEX_PORT) { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["agent-mesh"]?.config as - | { cortex?: { host?: string; port?: number; authToken?: string } } - | undefined; - if (pluginCfg?.cortex) { - const cortex = parseCortexConfig(pluginCfg.cortex); - return new CortexClient(cortex); - } - } catch { - // Config not available — use defaults - } - } - - return new CortexClient(parseCortexConfig({ host, port, authToken })); -} - -function resolveNamespace(): string { - try { - const cfg = loadConfig(); - const pluginCfg = cfg.plugins?.entries?.["agent-mesh"]?.config as - | { agentNamespace?: string } - | undefined; - return pluginCfg?.agentNamespace ?? "mayros"; - } catch { - return "mayros"; - } -} +import { resolveCortexClient, resolveNamespace } from "./shared/cortex-resolution.js"; function resolveTeamsConfig(): { maxTeamSize: number; @@ -105,7 +59,7 @@ export function registerWorkflowCli(program: Command) { .command("workflow") .description("Multi-agent workflows — run, list, and track workflow execution") .option("--cortex-host ", "Cortex host (default: 127.0.0.1 or from config)") - .option("--cortex-port ", "Cortex port (default: 8080 or from config)") + .option("--cortex-port ", "Cortex port (default: 19090 or from config)") .option("--cortex-token ", "Cortex auth token (or set CORTEX_AUTH_TOKEN)"); // ---- run ---- @@ -122,22 +76,25 @@ export function registerWorkflowCli(program: Command) { .option("--format ", "Output format (terminal|json)", "terminal") .action(async (name, opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); - - const healthy = await client.isHealthy(); - if (!healthy) { - console.log("Cortex offline. Cannot run workflow."); - return; - } - - const orchestrator = createOrchestrator(client, ns); + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + { pluginName: "agent-mesh" }, + ); + const ns = resolveNamespace("agent-mesh"); try { + const healthy = await client.isHealthy(); + if (!healthy) { + console.log("Cortex offline. Cannot run workflow."); + return; + } + + const orchestrator = createOrchestrator(client, ns); + const entry = await orchestrator.startWorkflow({ workflowName: name, path: opts.path, @@ -176,7 +133,10 @@ export function registerWorkflowCli(program: Command) { const result = await orchestrator.completeWorkflow(entry.id); console.log(`\nResult: ${result.summary}`); } catch (err) { - console.error(`Error: ${String(err)}`); + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + client.destroy(); } }); @@ -215,72 +175,82 @@ export function registerWorkflowCli(program: Command) { .option("--format ", "Output format (terminal|json)", "terminal") .action(async (id, opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); - - const healthy = await client.isHealthy(); - if (!healthy) { - console.log("Cortex offline. Cannot get workflow status."); - return; - } - - const orchestrator = createOrchestrator(client, ns); + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + { pluginName: "agent-mesh" }, + ); + const ns = resolveNamespace("agent-mesh"); - if (!id) { - // List recent runs - const runs = await orchestrator.listWorkflowRuns(); - if (runs.length === 0) { - console.log("No workflow runs found."); + try { + const healthy = await client.isHealthy(); + if (!healthy) { + console.log("Cortex offline. Cannot get workflow status."); return; } - if (opts.format === "json") { - console.log(JSON.stringify(runs, null, 2)); + const orchestrator = createOrchestrator(client, ns); + + if (!id) { + // List recent runs + const runs = await orchestrator.listWorkflowRuns(); + if (runs.length === 0) { + console.log("No workflow runs found."); + return; + } + + if (opts.format === "json") { + console.log(JSON.stringify(runs, null, 2)); + return; + } + + console.log(`Recent workflow runs (${runs.length}):`); + for (const r of runs) { + console.log(` - ${r.id}: ${r.name} [${r.state}] (updated: ${r.updatedAt})`); + } return; } - console.log(`Recent workflow runs (${runs.length}):`); - for (const r of runs) { - console.log(` - ${r.id}: ${r.name} [${r.state}] (updated: ${r.updatedAt})`); + const entry = await orchestrator.getWorkflow(id); + if (!entry) { + console.log(`Workflow ${id} not found.`); + return; } - return; - } - const entry = await orchestrator.getWorkflow(id); - if (!entry) { - console.log(`Workflow ${id} not found.`); - return; - } - - if (opts.format === "json") { - console.log(JSON.stringify(entry, null, 2)); - return; - } + if (opts.format === "json") { + console.log(JSON.stringify(entry, null, 2)); + return; + } - console.log(`Workflow "${entry.name}" (${entry.id}):`); - console.log(` state: ${entry.state}`); - console.log(` path: ${entry.path}`); - console.log(` current phase: ${entry.currentPhase}`); - console.log(` team: ${entry.teamId}`); - console.log(` created: ${entry.createdAt}`); - console.log(` updated: ${entry.updatedAt}`); - - const phaseResults = Object.values(entry.phaseResults); - if (phaseResults.length > 0) { - console.log(` phase results:`); - for (const pr of phaseResults) { - console.log( - ` ${pr.phase}: ${pr.status} (${pr.agentResults.length} agents, ${pr.conflicts} conflicts)`, - ); + console.log(`Workflow "${entry.name}" (${entry.id}):`); + console.log(` state: ${entry.state}`); + console.log(` path: ${entry.path}`); + console.log(` current phase: ${entry.currentPhase}`); + console.log(` team: ${entry.teamId}`); + console.log(` created: ${entry.createdAt}`); + console.log(` updated: ${entry.updatedAt}`); + + const phaseResults = Object.values(entry.phaseResults); + if (phaseResults.length > 0) { + console.log(` phase results:`); + for (const pr of phaseResults) { + console.log( + ` ${pr.phase}: ${pr.status} (${pr.agentResults.length} agents, ${pr.conflicts} conflicts)`, + ); + } } - } - if (entry.result) { - console.log(` result: ${entry.result.summary}`); + if (entry.result) { + console.log(` result: ${entry.result.summary}`); + } + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + client.destroy(); } }); @@ -293,39 +263,49 @@ export function registerWorkflowCli(program: Command) { .option("--format ", "Output format (terminal|json)", "terminal") .action(async (opts, cmd) => { const parentOpts = cmd.parent.opts(); - const client = resolveCortexClient({ - host: parentOpts.cortexHost, - port: parentOpts.cortexPort, - token: parentOpts.cortexToken, - }); - const ns = resolveNamespace(); - - const healthy = await client.isHealthy(); - if (!healthy) { - console.log("Cortex offline. Cannot list workflow history."); - return; - } + const client = resolveCortexClient( + { + host: parentOpts.cortexHost, + port: parentOpts.cortexPort, + token: parentOpts.cortexToken, + }, + { pluginName: "agent-mesh" }, + ); + const ns = resolveNamespace("agent-mesh"); - const orchestrator = createOrchestrator(client, ns); - const runs = await orchestrator.listWorkflowRuns(); - const limit = Number.parseInt(opts.limit, 10) || 20; - const limited = runs.slice(0, limit); + try { + const healthy = await client.isHealthy(); + if (!healthy) { + console.log("Cortex offline. Cannot list workflow history."); + return; + } - if (opts.format === "json") { - console.log(JSON.stringify(limited, null, 2)); - return; - } + const orchestrator = createOrchestrator(client, ns); + const runs = await orchestrator.listWorkflowRuns(); + const limit = Number.parseInt(opts.limit, 10) || 20; + const limited = runs.slice(0, limit); - if (limited.length === 0) { - console.log("No workflow runs found."); - return; - } + if (opts.format === "json") { + console.log(JSON.stringify(limited, null, 2)); + return; + } - console.log( - `Workflow history (${limited.length}${runs.length > limit ? ` of ${runs.length}` : ""}):`, - ); - for (const r of limited) { - console.log(` - ${r.id}: ${r.name} [${r.state}] (updated: ${r.updatedAt})`); + if (limited.length === 0) { + console.log("No workflow runs found."); + return; + } + + console.log( + `Workflow history (${limited.length}${runs.length > limit ? ` of ${runs.length}` : ""}):`, + ); + for (const r of limited) { + console.log(` - ${r.id}: ${r.name} [${r.state}] (updated: ${r.updatedAt})`); + } + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } finally { + client.destroy(); } }); }