From d2b6df3fb8a886c4e1ce2d12303b6b55e5cfa5b8 Mon Sep 17 00:00:00 2001 From: It Apilium Date: Sun, 8 Mar 2026 19:01:02 +0100 Subject: [PATCH 1/4] feat: add P2P config, CortexClient P2P methods, and sidecar flag forwarding - Add P2pConfig type and parseP2pConfig() with port/seed/peers validation - Add p2p field to CortexConfig and parseCortexConfig - Add P2P DTOs (P2pStatusResponse, P2pGossipStats, P2pSyncStats, etc.) - Add 5 CortexClient methods: p2pStatus, p2pListPeers, p2pAddPeer, p2pRemovePeer, p2pProbe (null-safe for graceful degradation) - Forward --p2p, --p2p-port, --p2p-seed, --p2p-mdns, --p2p-peer flags from sidecar config to spawned Cortex process - Add 21 P2P tests to cortex-client and 4 to cortex-sidecar --- .../memory-semantic/cortex-sidecar.test.ts | 126 ++++++++++ extensions/memory-semantic/cortex-sidecar.ts | 12 + extensions/shared/cortex-client.test.ts | 237 ++++++++++++++++++ extensions/shared/cortex-client.ts | 83 +++++- extensions/shared/cortex-config.ts | 41 +++ 5 files changed, 497 insertions(+), 2 deletions(-) diff --git a/extensions/memory-semantic/cortex-sidecar.test.ts b/extensions/memory-semantic/cortex-sidecar.test.ts index f019862c..e3488133 100644 --- a/extensions/memory-semantic/cortex-sidecar.test.ts +++ b/extensions/memory-semantic/cortex-sidecar.test.ts @@ -337,3 +337,129 @@ describe("ensureCortexSecrets", () => { expect(writeCall?.[2]).toEqual(expect.objectContaining({ mode: 0o600 })); }); }); + +// ============================================================================ +// P2P flag forwarding (B1) +// ============================================================================ + +describe("CortexSidecar P2P flags", () => { + beforeEach(() => { + mockState.healthCallCount = 0; + mockState.healthReturnValues = []; + mockState.fakeProc = null; + vi.clearAllMocks(); + mockState.spawnFn.mockImplementation(() => { + const proc = new FakeChildProcess(); + mockState.fakeProc = proc; + return proc; + }); + mockState.existsSyncFn.mockReturnValue(true); + mockState.locateCortexBinaryFn.mockResolvedValue("/usr/bin/fake-cortex"); + mockState.getCortexBinaryVersionFn.mockReturnValue("0.3.7"); + }); + + it("adds P2P flags when p2p is enabled", async () => { + mockState.healthReturnValues = [false, true]; + + const sidecar = new CortexSidecar({ + host: "127.0.0.1", + port: 9999, + autoStart: true, + binaryPath: "/usr/bin/fake-cortex", + p2p: { + enabled: true, + port: 19091, + seed: "test-seed", + manualPeers: ["192.168.1.5:19091"], + mdns: true, + }, + }); + + await sidecar.start(); + + const spawnCall = mockState.spawnFn.mock.calls[0]; + const args = spawnCall?.[1] as string[]; + expect(args).toContain("--p2p"); + expect(args).toContain("--p2p-port"); + expect(args).toContain("19091"); + expect(args).toContain("--p2p-seed"); + expect(args).toContain("test-seed"); + expect(args).toContain("--p2p-mdns"); + expect(args).toContain("--p2p-peer"); + expect(args).toContain("192.168.1.5:19091"); + + await sidecar.stop(); + }); + + it("does not add P2P flags when p2p is not enabled", async () => { + mockState.healthReturnValues = [false, true]; + + const sidecar = new CortexSidecar({ + host: "127.0.0.1", + port: 9999, + autoStart: true, + binaryPath: "/usr/bin/fake-cortex", + }); + + await sidecar.start(); + + const spawnCall = mockState.spawnFn.mock.calls[0]; + const args = spawnCall?.[1] as string[]; + expect(args).not.toContain("--p2p"); + expect(args).not.toContain("--p2p-port"); + + await sidecar.stop(); + }); + + it("does not add P2P flags when p2p.enabled is false", async () => { + mockState.healthReturnValues = [false, true]; + + const sidecar = new CortexSidecar({ + host: "127.0.0.1", + port: 9999, + autoStart: true, + binaryPath: "/usr/bin/fake-cortex", + p2p: { + enabled: false, + port: 19091, + manualPeers: [], + mdns: false, + }, + }); + + await sidecar.start(); + + const spawnCall = mockState.spawnFn.mock.calls[0]; + const args = spawnCall?.[1] as string[]; + expect(args).not.toContain("--p2p"); + + await sidecar.stop(); + }); + + it("adds multiple --p2p-peer flags", async () => { + mockState.healthReturnValues = [false, true]; + + const sidecar = new CortexSidecar({ + host: "127.0.0.1", + port: 9999, + autoStart: true, + binaryPath: "/usr/bin/fake-cortex", + p2p: { + enabled: true, + port: 19091, + manualPeers: ["10.0.0.1:19091", "10.0.0.2:19093"], + mdns: false, + }, + }); + + await sidecar.start(); + + const spawnCall = mockState.spawnFn.mock.calls[0]; + const args = spawnCall?.[1] as string[]; + const peerFlags = args.filter((_a: string, i: number) => args[i - 1] === "--p2p-peer"); + expect(peerFlags).toContain("10.0.0.1:19091"); + expect(peerFlags).toContain("10.0.0.2:19093"); + + await sidecar.stop(); + }); +}); diff --git a/extensions/memory-semantic/cortex-sidecar.ts b/extensions/memory-semantic/cortex-sidecar.ts index 4b204397..353faffc 100644 --- a/extensions/memory-semantic/cortex-sidecar.ts +++ b/extensions/memory-semantic/cortex-sidecar.ts @@ -144,6 +144,18 @@ export class CortexSidecar { this._status = "starting"; const args = ["--host", this.config.host, "--port", String(this.config.port)]; + + // P2P flag forwarding (B1): map CortexConfig.p2p to CLI flags + if (this.config.p2p?.enabled) { + args.push("--p2p"); + args.push("--p2p-port", String(this.config.p2p.port ?? 19091)); + if (this.config.p2p.seed) args.push("--p2p-seed", this.config.p2p.seed); + if (this.config.p2p.mdns) args.push("--p2p-mdns"); + for (const peer of this.config.p2p.manualPeers ?? []) { + args.push("--p2p-peer", peer); + } + } + const secrets = ensureCortexSecrets(); try { diff --git a/extensions/shared/cortex-client.test.ts b/extensions/shared/cortex-client.test.ts index b8511d54..dbef03ac 100644 --- a/extensions/shared/cortex-client.test.ts +++ b/extensions/shared/cortex-client.test.ts @@ -384,3 +384,240 @@ describe("type compatibility", () => { expect(match.subject).toBe("s"); }); }); + +// ============================================================================ +// P2P Config (B1) +// ============================================================================ + +import { parseP2pConfig } from "./cortex-config.js"; + +describe("parseP2pConfig", () => { + it("returns undefined for null/undefined", () => { + expect(parseP2pConfig(null)).toBeUndefined(); + expect(parseP2pConfig(undefined)).toBeUndefined(); + }); + + it("parses valid P2P config", () => { + const result = parseP2pConfig({ + enabled: true, + port: 19091, + seed: "test-seed", + manualPeers: ["127.0.0.1:19091", "192.168.1.5:19093"], + mdns: true, + }); + expect(result).toBeDefined(); + expect(result!.enabled).toBe(true); + expect(result!.port).toBe(19091); + expect(result!.seed).toBe("test-seed"); + expect(result!.manualPeers).toHaveLength(2); + expect(result!.mdns).toBe(true); + }); + + it("uses default port when not specified", () => { + const result = parseP2pConfig({ enabled: true }); + expect(result!.port).toBe(19091); + }); + + it("rejects invalid port", () => { + expect(() => parseP2pConfig({ port: 80 })).toThrow("between 1024 and 65535"); + expect(() => parseP2pConfig({ port: 70000 })).toThrow("between 1024 and 65535"); + }); + + it("rejects invalid seed characters", () => { + expect(() => parseP2pConfig({ seed: "has spaces" })).toThrow("alphanumeric"); + expect(() => parseP2pConfig({ seed: "has@special" })).toThrow("alphanumeric"); + }); + + it("allows valid seed characters", () => { + const result = parseP2pConfig({ seed: "abc-123_DEF" }); + expect(result!.seed).toBe("abc-123_DEF"); + }); + + it("filters invalid manualPeers format", () => { + const result = parseP2pConfig({ + manualPeers: ["127.0.0.1:19091", "badformat", "host:port", 42], + }); + expect(result!.manualPeers).toEqual(["127.0.0.1:19091"]); + }); + + it("returns empty manualPeers when not provided", () => { + const result = parseP2pConfig({}); + expect(result!.manualPeers).toEqual([]); + }); + + it("defaults mdns to false", () => { + const result = parseP2pConfig({}); + expect(result!.mdns).toBe(false); + }); +}); + +describe("parseCortexConfig with P2P", () => { + it("includes P2P config when provided", () => { + const cfg = parseCortexConfig({ + p2p: { enabled: true, port: 19091, mdns: true }, + }); + expect(cfg.p2p).toBeDefined(); + expect(cfg.p2p!.enabled).toBe(true); + expect(cfg.p2p!.mdns).toBe(true); + }); + + it("P2P is undefined when not provided (backward compat)", () => { + const cfg = parseCortexConfig({}); + expect(cfg.p2p).toBeUndefined(); + }); + + it("rejects unknown P2P keys", () => { + expect(() => parseCortexConfig({ p2p: { enabled: true, bogus: true } })).toThrow( + "unknown keys", + ); + }); +}); + +// ============================================================================ +// CortexClient P2P methods (B2) +// ============================================================================ + +describe("CortexClient P2P", () => { + let client: CortexClient; + + beforeEach(() => { + client = new CortexClient({ host: "127.0.0.1", port: 19090 }); + vi.stubGlobal("fetch", vi.fn()); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + function mockFetch(status: number, body: unknown = {}) { + (fetch as ReturnType).mockResolvedValueOnce({ + ok: status >= 200 && status < 300, + status, + json: () => Promise.resolve(body), + text: () => Promise.resolve(JSON.stringify(body)), + } as unknown as Response); + } + + it("p2pStatus sends GET to /api/v1/p2p/status", async () => { + const status = { + node_id: "abc123", + enabled: true, + port: 19091, + peer_count: 2, + connected_peers: [{ addr: "127.0.0.1:19091", connected: true }], + gossip_stats: { + round: 5, + pending_announcements: 0, + known_ids: 100, + bloom_filter_items: 100, + bloom_filter_fpr: 0.01, + }, + sync_stats: { + peer_count: 1, + local_ids: 100, + total_successful_syncs: 10, + total_failed_syncs: 0, + }, + }; + mockFetch(200, status); + + const result = await client.p2pStatus(); + expect(result.node_id).toBe("abc123"); + expect(result.peer_count).toBe(2); + + const call = (fetch as ReturnType).mock.calls[0]; + expect(call[0]).toBe("http://127.0.0.1:19090/api/v1/p2p/status"); + expect(call[1].method).toBe("GET"); + }); + + it("p2pListPeers returns connected_peers from status", async () => { + const peers = [ + { addr: "127.0.0.1:19091", connected: true }, + { addr: "192.168.1.5:19091", connected: true }, + ]; + mockFetch(200, { + node_id: "abc", + enabled: true, + port: 19091, + peer_count: 2, + connected_peers: peers, + gossip_stats: { + round: 0, + pending_announcements: 0, + known_ids: 0, + bloom_filter_items: 0, + bloom_filter_fpr: 0, + }, + sync_stats: { peer_count: 0, local_ids: 0, total_successful_syncs: 0, total_failed_syncs: 0 }, + }); + + const result = await client.p2pListPeers(); + expect(result).toHaveLength(2); + expect(result[0].addr).toBe("127.0.0.1:19091"); + }); + + it("p2pAddPeer sends POST with addr", async () => { + mockFetch(200, { status: "connected", addr: "192.168.1.5:19091" }); + + const result = await client.p2pAddPeer("192.168.1.5:19091"); + expect(result.status).toBe("connected"); + expect(result.addr).toBe("192.168.1.5:19091"); + + const call = (fetch as ReturnType).mock.calls[0]; + expect(call[0]).toBe("http://127.0.0.1:19090/api/v1/p2p/peers"); + expect(call[1].method).toBe("POST"); + }); + + it("p2pRemovePeer sends DELETE", async () => { + mockFetch(200, { status: "disconnected" }); + + const result = await client.p2pRemovePeer("192.168.1.5:19091"); + expect(result.status).toBe("disconnected"); + + const call = (fetch as ReturnType).mock.calls[0]; + expect(call[0]).toContain("/api/v1/p2p/peers/"); + expect(call[1].method).toBe("DELETE"); + }); + + it("p2pProbe returns status when available", async () => { + const status = { + node_id: "abc", + enabled: true, + port: 19091, + peer_count: 0, + connected_peers: [], + gossip_stats: { + round: 0, + pending_announcements: 0, + known_ids: 0, + bloom_filter_items: 0, + bloom_filter_fpr: 0, + }, + sync_stats: { peer_count: 0, local_ids: 0, total_successful_syncs: 0, total_failed_syncs: 0 }, + }; + mockFetch(200, status); + + const result = await client.p2pProbe(); + expect(result).not.toBeNull(); + expect(result!.enabled).toBe(true); + }); + + it("p2pProbe returns null on 404", async () => { + mockFetch(404, { error: "not found" }); + + const result = await client.p2pProbe(); + expect(result).toBeNull(); + }); + + it("p2pProbe returns null on connection error", async () => { + (fetch as ReturnType).mockRejectedValueOnce(new Error("ECONNREFUSED")); + + const result = await client.p2pProbe(); + expect(result).toBeNull(); + }); + + it("p2p methods throw on destroyed client", async () => { + client.destroy(); + await expect(client.p2pStatus()).rejects.toThrow("Client has been destroyed"); + }); +}); diff --git a/extensions/shared/cortex-client.ts b/extensions/shared/cortex-client.ts index 0194eaf2..4c20fa20 100644 --- a/extensions/shared/cortex-client.ts +++ b/extensions/shared/cortex-client.ts @@ -10,8 +10,13 @@ import type { CortexConfig } from "./cortex-config.js"; import { CircuitBreaker, resilientFetch, type ResilienceConfig } from "./cortex-resilience.js"; // Re-export config for convenience -export type { CortexConfig } from "./cortex-config.js"; -export { parseCortexConfig, assertAllowedKeys, resolveEnvVars } from "./cortex-config.js"; +export type { CortexConfig, P2pConfig } from "./cortex-config.js"; +export { + parseCortexConfig, + parseP2pConfig, + assertAllowedKeys, + resolveEnvVars, +} from "./cortex-config.js"; // ============================================================================ // DTOs — mirror Rust types from aingle_cortex/src/rest/*.rs @@ -225,6 +230,49 @@ export type TraceEventDto = { fields: Record; }; +// ============================================================================ +// P2P DTOs — mirror Rust types from aingle_cortex/src/p2p/manager.rs +// ============================================================================ + +export type P2pStatusResponse = { + node_id: string; + enabled: boolean; + port: number; + peer_count: number; + connected_peers: P2pPeerDto[]; + gossip_stats: P2pGossipStats; + sync_stats: P2pSyncStats; +}; + +export type P2pPeerDto = { + addr: string; + connected: boolean; +}; + +export type P2pGossipStats = { + round: number; + pending_announcements: number; + known_ids: number; + bloom_filter_items: number; + bloom_filter_fpr: number; +}; + +export type P2pSyncStats = { + peer_count: number; + local_ids: number; + total_successful_syncs: number; + total_failed_syncs: number; +}; + +export type P2pAddPeerResponse = { + status: string; + addr: string; +}; + +export type P2pDisconnectResponse = { + status: string; +}; + // ============================================================================ // Error // ============================================================================ @@ -621,4 +669,35 @@ export class CortexClient implements CortexClientLike, CortexLike { return false; } } + + // ---------- P2P (native QUIC gossip) ---------- + + async p2pStatus(): Promise { + return this.request("GET", "/api/v1/p2p/status"); + } + + async p2pListPeers(): Promise { + const status = await this.p2pStatus(); + return status.connected_peers; + } + + async p2pAddPeer(addr: string): Promise { + return this.request("POST", "/api/v1/p2p/peers", { addr }); + } + + async p2pRemovePeer(addr: string): Promise { + return this.request( + "DELETE", + `/api/v1/p2p/peers/${encodeURIComponent(addr)}`, + ); + } + + /** Probe P2P availability. Returns status if enabled, null otherwise. */ + async p2pProbe(): Promise { + try { + return await this.p2pStatus(); + } catch { + return null; + } + } } diff --git a/extensions/shared/cortex-config.ts b/extensions/shared/cortex-config.ts index e7d0a29c..6593f09e 100644 --- a/extensions/shared/cortex-config.ts +++ b/extensions/shared/cortex-config.ts @@ -10,6 +10,14 @@ import type { ResilienceConfig } from "./cortex-resilience.js"; // Types // ============================================================================ +export type P2pConfig = { + enabled: boolean; + port: number; + seed?: string; + manualPeers: string[]; + mdns: boolean; +}; + export type CortexConfig = { host: string; port: number; @@ -19,6 +27,7 @@ export type CortexConfig = { resilience?: ResilienceConfig; requireAuth?: boolean; strictVersionCheck?: boolean; + p2p?: P2pConfig; }; // ============================================================================ @@ -68,6 +77,7 @@ export function parseCortexConfig(raw: unknown): CortexConfig { "resilience", "requireAuth", "strictVersionCheck", + "p2p", ], "cortex config", ); @@ -86,6 +96,7 @@ export function parseCortexConfig(raw: unknown): CortexConfig { const resilience = parseResilienceConfig(cortex.resilience); const requireAuth = cortex.requireAuth === true; const strictVersionCheck = cortex.strictVersionCheck === true; + const p2p = parseP2pConfig(cortex.p2p); return { host, @@ -96,9 +107,39 @@ export function parseCortexConfig(raw: unknown): CortexConfig { resilience, requireAuth, strictVersionCheck, + p2p, }; } +export function parseP2pConfig(raw: unknown): P2pConfig | undefined { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined; + const p = raw as Record; + assertAllowedKeys(p, ["enabled", "port", "seed", "manualPeers", "mdns"], "p2p config"); + + const enabled = p.enabled === true; + const port = typeof p.port === "number" ? Math.floor(p.port) : 19091; + if (port < 1024 || port > 65535) { + throw new Error("p2p.port must be between 1024 and 65535"); + } + + const seed = typeof p.seed === "string" ? p.seed : undefined; + if (seed !== undefined) { + if (!/^[a-zA-Z0-9_-]+$/.test(seed)) { + throw new Error("p2p.seed must be alphanumeric (plus _ and -)"); + } + } + + const manualPeers = Array.isArray(p.manualPeers) + ? p.manualPeers + .filter((v): v is string => typeof v === "string") + .filter((v) => /^[^:]+:\d+$/.test(v)) + : []; + + const mdns = p.mdns === true; + + return { enabled, port, seed, manualPeers, mdns }; +} + function parseResilienceConfig(raw: unknown): ResilienceConfig | undefined { if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined; const r = raw as Record; From 43572c7e8559bb32c0642caee27b2bf382a08ffc Mon Sep 17 00:00:00 2001 From: It Apilium Date: Sun, 8 Mar 2026 19:01:14 +0100 Subject: [PATCH 2/4] feat: add dual sync mode bridge for native P2P and polled fallback - Add nativeP2pPreferred config field (default true) to cortex-sync - Probe native P2P at startup via p2pProbe(), switch to native mode when Cortex reports P2P enabled - Skip REST polling in native mode (gossip handles sync internally) - Include P2P status (node ID, peers, gossip/sync stats) in cortex_sync_status tool output when in native mode - Route cortex_sync_pair through p2pAddPeer API in native mode - Skip polled sync in agent_end/config_change hooks when native - Graceful degradation: stays in polled mode if P2P unavailable - Add 11 config tests and 12 bridge integration tests --- extensions/cortex-sync/config.test.ts | 105 ++++++ extensions/cortex-sync/config.ts | 27 +- extensions/cortex-sync/index.test.ts | 448 ++++++++++++++++++++++++++ extensions/cortex-sync/index.ts | 106 +++++- 4 files changed, 667 insertions(+), 19 deletions(-) create mode 100644 extensions/cortex-sync/config.test.ts create mode 100644 extensions/cortex-sync/index.test.ts diff --git a/extensions/cortex-sync/config.test.ts b/extensions/cortex-sync/config.test.ts new file mode 100644 index 00000000..90e2aae7 --- /dev/null +++ b/extensions/cortex-sync/config.test.ts @@ -0,0 +1,105 @@ +/** + * Tests for cortex-sync configuration parsing, including P2P bridge (B3). + */ + +import { describe, expect, it } from "vitest"; +import { parseCortexSyncConfig, type CortexSyncConfig } from "./config.js"; + +describe("parseCortexSyncConfig", () => { + it("returns defaults for empty input", () => { + const cfg = parseCortexSyncConfig({}); + expect(cfg.namespace).toBe("mayros"); + expect(cfg.sync.intervalSeconds).toBe(300); + expect(cfg.sync.autoSync).toBe(false); + expect(cfg.sync.conflictStrategy).toBe("last-writer-wins"); + expect(cfg.sync.maxTriplesPerSync).toBe(5000); + expect(cfg.sync.syncTimeoutMs).toBe(30000); + expect(cfg.sync.nativeP2pPreferred).toBe(true); + expect(cfg.discovery.bonjourEnabled).toBe(false); + expect(cfg.discovery.manualPeers).toEqual([]); + }); + + it("nativeP2pPreferred defaults to true", () => { + const cfg = parseCortexSyncConfig({}); + expect(cfg.sync.nativeP2pPreferred).toBe(true); + }); + + it("nativeP2pPreferred can be set to false", () => { + const cfg = parseCortexSyncConfig({ + sync: { nativeP2pPreferred: false }, + }); + expect(cfg.sync.nativeP2pPreferred).toBe(false); + }); + + it("nativeP2pPreferred true is respected", () => { + const cfg = parseCortexSyncConfig({ + sync: { nativeP2pPreferred: true }, + }); + expect(cfg.sync.nativeP2pPreferred).toBe(true); + }); + + it("clamps intervalSeconds to range", () => { + const cfg1 = parseCortexSyncConfig({ sync: { intervalSeconds: 1 } }); + expect(cfg1.sync.intervalSeconds).toBe(10); + + const cfg2 = parseCortexSyncConfig({ sync: { intervalSeconds: 100000 } }); + expect(cfg2.sync.intervalSeconds).toBe(86400); + }); + + it("clamps maxTriplesPerSync to range", () => { + const cfg1 = parseCortexSyncConfig({ sync: { maxTriplesPerSync: 1 } }); + expect(cfg1.sync.maxTriplesPerSync).toBe(100); + + const cfg2 = parseCortexSyncConfig({ sync: { maxTriplesPerSync: 100000 } }); + expect(cfg2.sync.maxTriplesPerSync).toBe(50000); + }); + + it("parses valid conflictStrategy", () => { + const cfg = parseCortexSyncConfig({ + sync: { conflictStrategy: "keep-both" }, + }); + expect(cfg.sync.conflictStrategy).toBe("keep-both"); + }); + + it("falls back to default for invalid conflictStrategy", () => { + const cfg = parseCortexSyncConfig({ + sync: { conflictStrategy: "invalid" }, + }); + expect(cfg.sync.conflictStrategy).toBe("last-writer-wins"); + }); + + it("parses manual peers", () => { + const cfg = parseCortexSyncConfig({ + discovery: { + manualPeers: [ + { + nodeId: "node1", + endpoint: "http://localhost:8080", + namespaces: ["ns1"], + enabled: true, + }, + ], + }, + }); + expect(cfg.discovery.manualPeers).toHaveLength(1); + expect(cfg.discovery.manualPeers[0].nodeId).toBe("node1"); + }); + + it("filters invalid manual peers (missing nodeId or endpoint)", () => { + const cfg = parseCortexSyncConfig({ + discovery: { + manualPeers: [ + { nodeId: "", endpoint: "http://localhost:8080" }, + { nodeId: "valid", endpoint: "" }, + { nodeId: "ok", endpoint: "http://valid" }, + ], + }, + }); + expect(cfg.discovery.manualPeers).toHaveLength(1); + expect(cfg.discovery.manualPeers[0].nodeId).toBe("ok"); + }); + + it("rejects unknown sync keys", () => { + expect(() => parseCortexSyncConfig({ sync: { bogus: true } })).toThrow("unknown keys"); + }); +}); diff --git a/extensions/cortex-sync/config.ts b/extensions/cortex-sync/config.ts index 558d99c1..cecc03bb 100644 --- a/extensions/cortex-sync/config.ts +++ b/extensions/cortex-sync/config.ts @@ -36,6 +36,7 @@ export type SyncConfig = { conflictStrategy: ConflictStrategy; maxTriplesPerSync: number; syncTimeoutMs: number; + nativeP2pPreferred: boolean; }; export type DiscoveryConfig = { @@ -80,7 +81,14 @@ function parseSyncConfig(raw: unknown): SyncConfig { if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) { assertAllowedKeys( sync, - ["intervalSeconds", "autoSync", "conflictStrategy", "maxTriplesPerSync", "syncTimeoutMs"], + [ + "intervalSeconds", + "autoSync", + "conflictStrategy", + "maxTriplesPerSync", + "syncTimeoutMs", + "nativeP2pPreferred", + ], "sync config", ); } @@ -110,7 +118,17 @@ function parseSyncConfig(raw: unknown): SyncConfig { ? Math.max(5000, Math.min(120000, Math.floor(sync.syncTimeoutMs))) : DEFAULT_SYNC_TIMEOUT_MS; - return { intervalSeconds, autoSync, conflictStrategy, maxTriplesPerSync, syncTimeoutMs }; + const nativeP2pPreferred = + typeof sync.nativeP2pPreferred === "boolean" ? sync.nativeP2pPreferred : true; + + return { + intervalSeconds, + autoSync, + conflictStrategy, + maxTriplesPerSync, + syncTimeoutMs, + nativeP2pPreferred, + }; } function parsePeerConfig(raw: unknown): SyncPeerConfig { @@ -195,5 +213,10 @@ export const cortexSyncConfigUiHints = { default: DEFAULT_BONJOUR_ENABLED, description: "Enable local network peer discovery", }, + "sync.nativeP2pPreferred": { + type: "boolean", + default: true, + description: "Prefer native P2P gossip over REST polling when available", + }, "discovery.manualPeers": { type: "array", description: "Manually configured peers" }, } as const; diff --git a/extensions/cortex-sync/index.test.ts b/extensions/cortex-sync/index.test.ts new file mode 100644 index 00000000..14457094 --- /dev/null +++ b/extensions/cortex-sync/index.test.ts @@ -0,0 +1,448 @@ +/** + * Tests for cortex-sync plugin — dual sync mode (native P2P vs polled), + * tool behavior in each mode, and hook skip logic. + */ + +import { describe, expect, it, vi, beforeEach } from "vitest"; +import type { P2pStatusResponse } from "../shared/cortex-client.js"; + +// ---------- Mock state ---------- + +const mockState = vi.hoisted(() => ({ + healthyFn: vi.fn(async () => true), + p2pProbeFn: vi.fn(async (): Promise => null), + p2pStatusFn: vi.fn( + async (): Promise => ({ + node_id: "abcdef1234567890abcdef1234567890", + enabled: true, + port: 19091, + peer_count: 2, + connected_peers: [ + { addr: "10.0.0.1:19091", connected: true }, + { addr: "10.0.0.2:19091", connected: false }, + ], + gossip_stats: { + round: 42, + pending_announcements: 0, + known_ids: 1500, + bloom_filter_items: 1200, + bloom_filter_fpr: 0.01, + }, + sync_stats: { + peer_count: 2, + local_ids: 800, + total_successful_syncs: 15, + total_failed_syncs: 1, + }, + }), + ), + p2pAddPeerFn: vi.fn(async (addr: string) => ({ status: "connected", addr })), + listTriplesFn: vi.fn(async () => []), + createTripleFn: vi.fn(async () => ({})), + registeredTools: [] as Array<{ name: string; execute: (...args: unknown[]) => Promise }>, + registeredEvents: {} as Record Promise>>, +})); + +// Mock CortexClient +vi.mock("../shared/cortex-client.js", () => ({ + CortexClient: class MockCortexClient { + async isHealthy() { + return mockState.healthyFn(); + } + async p2pProbe() { + return mockState.p2pProbeFn(); + } + async p2pStatus() { + return mockState.p2pStatusFn(); + } + async p2pAddPeer(addr: string) { + return mockState.p2pAddPeerFn(addr); + } + async listTriples() { + return mockState.listTriplesFn(); + } + async createTriple() { + return mockState.createTripleFn(); + } + }, +})); + +// Mock PeerManager +vi.mock("./peer-manager.js", () => ({ + PeerManager: class MockPeerManager { + async initFromConfig() { + return 0; + } + async status() { + return { + totalPeers: 1, + activePeers: 1, + unreachablePeers: 0, + totalSyncs: 5, + totalTriplesSynced: 100, + }; + } + async listPeers() { + return [ + { + nodeId: "peer1", + endpoint: "http://10.0.0.1:8080", + namespaces: ["mayros"], + status: "active", + lastSyncAt: "2026-01-01T00:00:00Z", + totalSyncs: 5, + totalTriplesSynced: 100, + }, + ]; + } + async getPeer(nodeId: string) { + if (nodeId === "peer1") { + return { + nodeId: "peer1", + endpoint: "http://10.0.0.1:8080", + namespaces: ["mayros"], + status: "active", + }; + } + return null; + } + async addPeer(opts: { nodeId: string; endpoint: string; namespaces: string[] }) { + return { ...opts, status: "active" }; + } + toSyncPeer(peer: { nodeId: string; endpoint: string; namespaces: string[] }) { + return peer; + } + async recordSyncResult() {} + async markUnreachable() {} + }, +})); + +// Mock sync-protocol +vi.mock("./sync-protocol.js", () => ({ + syncWithPeer: vi.fn(async () => ({ + peerId: "peer1", + triplesReceived: 10, + triplesApplied: 8, + conflicts: [], + durationMs: 200, + })), +})); + +// Mock config +vi.mock("./config.js", () => ({ + parseCortexSyncConfig: vi.fn((input: Record) => { + const nativeP2pPreferred = + (input as { _nativeP2pPreferred?: boolean })._nativeP2pPreferred ?? true; + const autoSync = (input as { _autoSync?: boolean })._autoSync ?? false; + return { + namespace: "mayros", + cortex: { host: "127.0.0.1", port: 8080 }, + sync: { + intervalSeconds: 300, + autoSync, + conflictStrategy: "last-writer-wins", + maxTriplesPerSync: 5000, + syncTimeoutMs: 30000, + nativeP2pPreferred, + }, + discovery: { bonjourEnabled: false, manualPeers: [] }, + }; + }), +})); + +// ---------- Fake MayrosPluginApi ---------- + +function createFakeApi(pluginConfig: Record = {}) { + const tools: typeof mockState.registeredTools = []; + const events: typeof mockState.registeredEvents = {}; + const logs: string[] = []; + + const api = { + pluginConfig, + logger: { + info: vi.fn((msg: string) => logs.push(msg)), + warn: vi.fn((msg: string) => logs.push(msg)), + debug: vi.fn((msg: string) => logs.push(msg)), + error: vi.fn((msg: string) => logs.push(msg)), + }, + registerTool(def: { name: string; execute: (...args: unknown[]) => Promise }) { + tools.push(def); + }, + on(event: string, handler: (...args: unknown[]) => Promise) { + if (!events[event]) events[event] = []; + events[event].push(handler); + }, + }; + + return { api, tools, events, logs }; +} + +async function registerPlugin(pluginConfig: Record = {}) { + const { api, tools, events, logs } = createFakeApi(pluginConfig); + const mod = await import("./index.js"); + const plugin = mod.default; + await plugin.register(api as unknown as Parameters[0]); + // Allow the startup IIFE to resolve + await new Promise((r) => setTimeout(r, 10)); + return { tools, events, logs, api }; +} + +// ---------- Tests ---------- + +describe("cortex-sync plugin P2P bridge (B3)", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockState.healthyFn.mockResolvedValue(true); + mockState.p2pProbeFn.mockResolvedValue(null); + }); + + describe("probeP2p detection", () => { + it("stays in polled mode when p2pProbe returns null", async () => { + mockState.p2pProbeFn.mockResolvedValue(null); + + const { tools } = await registerPlugin(); + const statusTool = tools.find((t) => t.name === "cortex_sync_status"); + const result = (await statusTool!.execute("tc1", {})) as { + content: Array<{ text: string }>; + details: Record; + }; + + expect(result.details.syncMode).toBe("polled"); + expect(result.content[0].text).toContain("Mode: polled"); + }); + + it("switches to native mode when p2pProbe returns enabled status", async () => { + mockState.p2pProbeFn.mockResolvedValue({ + node_id: "abcdef1234567890abcdef1234567890", + enabled: true, + port: 19091, + peer_count: 0, + connected_peers: [], + gossip_stats: { + round: 0, + pending_announcements: 0, + known_ids: 0, + bloom_filter_items: 0, + bloom_filter_fpr: 0, + }, + sync_stats: { + peer_count: 0, + local_ids: 0, + total_successful_syncs: 0, + total_failed_syncs: 0, + }, + }); + + const { tools, logs } = await registerPlugin(); + const statusTool = tools.find((t) => t.name === "cortex_sync_status"); + const result = (await statusTool!.execute("tc1", {})) as { + content: Array<{ text: string }>; + details: Record; + }; + + expect(result.details.syncMode).toBe("native"); + expect(logs.some((l) => l.includes("native P2P detected"))).toBe(true); + }); + + it("respects nativeP2pPreferred=false (does not probe)", async () => { + // The mock parseCortexSyncConfig reads _nativeP2pPreferred + const probeSpy = mockState.p2pProbeFn; + + await registerPlugin({ _nativeP2pPreferred: false }); + + expect(probeSpy).not.toHaveBeenCalled(); + }); + }); + + describe("cortex_sync_status tool", () => { + it("includes P2P info in native mode", async () => { + mockState.p2pProbeFn.mockResolvedValue({ + node_id: "abcdef1234567890abcdef1234567890", + enabled: true, + port: 19091, + peer_count: 2, + connected_peers: [{ addr: "10.0.0.1:19091", connected: true }], + gossip_stats: { + round: 42, + pending_announcements: 0, + known_ids: 1500, + bloom_filter_items: 1200, + bloom_filter_fpr: 0.01, + }, + sync_stats: { + peer_count: 2, + local_ids: 800, + total_successful_syncs: 15, + total_failed_syncs: 1, + }, + }); + + const { tools } = await registerPlugin(); + const statusTool = tools.find((t) => t.name === "cortex_sync_status"); + const result = (await statusTool!.execute("tc1", {})) as { content: Array<{ text: string }> }; + const text = result.content[0].text; + + expect(text).toContain("Native P2P:"); + expect(text).toContain("Node ID: abcdef1234567890..."); + expect(text).toContain("Port: 19091"); + expect(text).toContain("native (QUIC gossip)"); + expect(text).toContain("Connected peers: 2"); + expect(text).toContain("10.0.0.1:19091 [connected]"); + }); + + it("does not include P2P info in polled mode", async () => { + mockState.p2pProbeFn.mockResolvedValue(null); + + const { tools } = await registerPlugin(); + const statusTool = tools.find((t) => t.name === "cortex_sync_status"); + const result = (await statusTool!.execute("tc1", {})) as { content: Array<{ text: string }> }; + const text = result.content[0].text; + + expect(text).toContain("Mode: polled"); + expect(text).not.toContain("Native P2P:"); + }); + }); + + describe("cortex_sync_pair tool", () => { + it("routes through P2P API in native mode", async () => { + mockState.p2pProbeFn.mockResolvedValue({ + node_id: "abcdef1234567890abcdef1234567890", + enabled: true, + port: 19091, + peer_count: 0, + connected_peers: [], + gossip_stats: { + round: 0, + pending_announcements: 0, + known_ids: 0, + bloom_filter_items: 0, + bloom_filter_fpr: 0, + }, + sync_stats: { + peer_count: 0, + local_ids: 0, + total_successful_syncs: 0, + total_failed_syncs: 0, + }, + }); + + const { tools } = await registerPlugin(); + const pairTool = tools.find((t) => t.name === "cortex_sync_pair"); + const result = (await pairTool!.execute("tc1", { + nodeId: "new-peer", + endpoint: "http://10.0.0.5:8080", + })) as { content: Array<{ text: string }>; details: Record }; + + expect(mockState.p2pAddPeerFn).toHaveBeenCalledWith("10.0.0.5:19091"); + expect(result.content[0].text).toContain("P2P: connected"); + expect(result.details.syncMode).toBe("native"); + }); + + it("does not call P2P API in polled mode", async () => { + mockState.p2pProbeFn.mockResolvedValue(null); + + const { tools } = await registerPlugin(); + const pairTool = tools.find((t) => t.name === "cortex_sync_pair"); + await pairTool!.execute("tc1", { + nodeId: "new-peer", + endpoint: "http://10.0.0.5:8080", + }); + + expect(mockState.p2pAddPeerFn).not.toHaveBeenCalled(); + }); + + it("handles P2P connection failure gracefully", async () => { + mockState.p2pProbeFn.mockResolvedValue({ + node_id: "abcdef1234567890abcdef1234567890", + enabled: true, + port: 19091, + peer_count: 0, + connected_peers: [], + gossip_stats: { + round: 0, + pending_announcements: 0, + known_ids: 0, + bloom_filter_items: 0, + bloom_filter_fpr: 0, + }, + sync_stats: { + peer_count: 0, + local_ids: 0, + total_successful_syncs: 0, + total_failed_syncs: 0, + }, + }); + mockState.p2pAddPeerFn.mockRejectedValue(new Error("connection refused")); + + const { tools } = await registerPlugin(); + const pairTool = tools.find((t) => t.name === "cortex_sync_pair"); + const result = (await pairTool!.execute("tc1", { + nodeId: "new-peer", + endpoint: "http://10.0.0.5:8080", + })) as { content: Array<{ text: string }> }; + + expect(result.content[0].text).toContain("P2P: connection failed"); + }); + }); + + describe("executePeerSync in native mode", () => { + it("returns null (no-op) for sync in native mode", async () => { + mockState.p2pProbeFn.mockResolvedValue({ + node_id: "abcdef1234567890abcdef1234567890", + enabled: true, + port: 19091, + peer_count: 0, + connected_peers: [], + gossip_stats: { + round: 0, + pending_announcements: 0, + known_ids: 0, + bloom_filter_items: 0, + bloom_filter_fpr: 0, + }, + sync_stats: { + peer_count: 0, + local_ids: 0, + total_successful_syncs: 0, + total_failed_syncs: 0, + }, + }); + + const { tools } = await registerPlugin(); + const syncNowTool = tools.find((t) => t.name === "cortex_sync_now"); + const result = (await syncNowTool!.execute("tc1", { peerId: "peer1" })) as { + content: Array<{ text: string }>; + }; + + // In native mode, executePeerSync returns null → "not found or unreachable" + expect(result.content[0].text).toContain("not found or unreachable"); + }); + }); + + describe("plugin structure", () => { + it("registers all 3 tools", async () => { + const { tools } = await registerPlugin(); + const toolNames = tools.map((t) => t.name); + + expect(toolNames).toContain("cortex_sync_status"); + expect(toolNames).toContain("cortex_sync_now"); + expect(toolNames).toContain("cortex_sync_pair"); + }); + + it("registers hooks when autoSync is enabled", async () => { + const { events } = await registerPlugin({ _autoSync: true }); + + expect(events.agent_end).toBeDefined(); + expect(events.agent_end.length).toBe(1); + expect(events.config_change).toBeDefined(); + expect(events.config_change.length).toBe(1); + }); + + it("does not register hooks when autoSync is disabled", async () => { + const { events } = await registerPlugin({ _autoSync: false }); + + expect(events.agent_end).toBeUndefined(); + expect(events.config_change).toBeUndefined(); + }); + }); +}); diff --git a/extensions/cortex-sync/index.ts b/extensions/cortex-sync/index.ts index f3438bbf..343156b0 100644 --- a/extensions/cortex-sync/index.ts +++ b/extensions/cortex-sync/index.ts @@ -16,7 +16,7 @@ import { Type } from "@sinclair/typebox"; import type { MayrosPluginApi } from "mayros/plugin-sdk"; -import { CortexClient } from "../shared/cortex-client.js"; +import { CortexClient, type P2pStatusResponse } from "../shared/cortex-client.js"; import { parseCortexSyncConfig, type CortexSyncConfig } from "./config.js"; import { PeerManager } from "./peer-manager.js"; import { syncWithPeer, type SyncPeer, type SyncDelta, type SyncResult } from "./sync-protocol.js"; @@ -74,6 +74,8 @@ const cortexSyncPlugin = { const peerManager = new PeerManager(client, ns); let cortexAvailable = false; + let syncMode: "native" | "polled" = "polled"; + let cachedP2pStatus: P2pStatusResponse | null = null; api.logger.info(`cortex-sync: plugin registered (ns: ${ns})`); @@ -86,7 +88,24 @@ const cortexSyncPlugin = { return cortexAvailable; } - // Initialize peers from config + /** B3: Probe native P2P availability. */ + async function probeP2p(): Promise { + if (!cfg.sync.nativeP2pPreferred) return; + try { + const status = await client.p2pProbe(); + if (status?.enabled) { + syncMode = "native"; + cachedP2pStatus = status; + api.logger.info( + `cortex-sync: native P2P detected (node: ${status.node_id.slice(0, 16)}...)`, + ); + } + } catch { + // P2P not available — stay in polled mode + } + } + + // Initialize peers from config + probe P2P void (async () => { try { const healthy = await checkCortex(); @@ -95,6 +114,7 @@ const cortexSyncPlugin = { if (added > 0) { api.logger.info(`cortex-sync: initialized ${added} peer(s) from config`); } + await probeP2p(); } } catch { api.logger.warn("cortex-sync: Cortex unavailable at startup"); @@ -108,6 +128,12 @@ const cortexSyncPlugin = { async function executePeerSync(nodeId: string): Promise { if (!cortexAvailable && !(await checkCortex())) return null; + // B3: Native P2P handles sync internally — skip REST polling + if (syncMode === "native") { + api.logger.info(`cortex-sync: sync handled by native P2P gossip (peer ${nodeId})`); + return null; + } + const peer = await peerManager.getPeer(nodeId); if (!peer || peer.status === "removed") return null; @@ -170,6 +196,7 @@ const cortexSyncPlugin = { const lines = [ `Cortex Sync Status:`, + ` Mode: ${syncMode}`, ` Total peers: ${status.totalPeers}`, ` Active: ${status.activePeers}`, ` Unreachable: ${status.unreachablePeers}`, @@ -191,9 +218,36 @@ const cortexSyncPlugin = { } } + // B3: Include P2P native info when available + if (syncMode === "native") { + try { + const p2p = await client.p2pStatus(); + cachedP2pStatus = p2p; + lines.push( + "Native P2P:", + ` Node ID: ${p2p.node_id.slice(0, 16)}...`, + ` Port: ${p2p.port}`, + ` Mode: native (QUIC gossip)`, + ` Connected peers: ${p2p.peer_count}`, + ); + if (p2p.connected_peers.length > 0) { + lines.push(" P2P Peers:"); + for (const pp of p2p.connected_peers) { + lines.push(` ${pp.addr} [${pp.connected ? "connected" : "disconnected"}]`); + } + } + lines.push( + ` Gossip: round ${p2p.gossip_stats.round}, known ${p2p.gossip_stats.known_ids}`, + ` Sync: ${p2p.sync_stats.local_ids} local, ${p2p.sync_stats.total_successful_syncs} successful syncs`, + ); + } catch { + lines.push("Native P2P: status unavailable"); + } + } + return { content: [{ type: "text" as const, text: lines.join("\n") }], - details: { totalPeers: status.totalPeers }, + details: { totalPeers: status.totalPeers, syncMode }, }; }, }, @@ -346,21 +400,35 @@ const cortexSyncPlugin = { enabled: true, }); + // B3: Also add via P2P API when native mode is active + let p2pResult: string | undefined; + if (syncMode === "native") { + try { + // Extract host:port from endpoint URL for P2P connection + const url = new URL(endpoint); + const p2pAddr = `${url.hostname}:${cachedP2pStatus?.port ?? 19091}`; + const res = await client.p2pAddPeer(p2pAddr); + p2pResult = `P2P: ${res.status} (${res.addr})`; + } catch { + p2pResult = "P2P: connection failed (will retry via gossip)"; + } + } + + const resultLines = [ + `Paired with peer ${peer.nodeId}:`, + ` Endpoint: ${peer.endpoint}`, + ` Namespaces: ${peer.namespaces.join(", ")}`, + ` Status: ${peer.status}`, + ]; + if (p2pResult) resultLines.push(` ${p2pResult}`); + resultLines.push( + "", + `Run 'mayros sync now' or use cortex_sync_now to trigger first sync.`, + ); + return { - content: [ - { - type: "text" as const, - text: [ - `Paired with peer ${peer.nodeId}:`, - ` Endpoint: ${peer.endpoint}`, - ` Namespaces: ${peer.namespaces.join(", ")}`, - ` Status: ${peer.status}`, - "", - `Run 'mayros sync now' or use cortex_sync_now to trigger first sync.`, - ].join("\n"), - }, - ], - details: { action: "paired", nodeId: peer.nodeId }, + content: [{ type: "text" as const, text: resultLines.join("\n") }], + details: { action: "paired", nodeId: peer.nodeId, syncMode }, }; }, }, @@ -374,6 +442,8 @@ const cortexSyncPlugin = { if (cfg.sync.autoSync) { api.on("agent_end", async () => { if (!cortexAvailable) return; + // B3: Skip polled sync in native mode — gossip handles it + if (syncMode === "native") return; try { const results = await syncAllPeers(); if (results.length > 0) { @@ -387,6 +457,8 @@ const cortexSyncPlugin = { api.on("config_change", async () => { if (!cortexAvailable) return; + // B3: Skip polled sync in native mode — gossip handles it + if (syncMode === "native") return; try { await syncAllPeers(); } catch { From f2749d7ac76f268d8cb5317bd533cd078773de30 Mon Sep 17 00:00:00 2001 From: It Apilium Date: Sun, 8 Mar 2026 19:01:23 +0100 Subject: [PATCH 3/4] feat: enhance sync CLI with native P2P status, pairing, and gossip info - Show Native P2P section in `mayros sync status` when P2P is active (node ID, port, connected peers, gossip round/known IDs, sync stats) - Route `mayros sync pair` through p2pAddPeer API when native P2P active - Show gossip stats in `mayros sync now` when native mode detected - Graceful fallback to polled mode when P2P probe returns null - Add 7 CLI integration tests --- src/cli/sync-cli.test.ts | 271 +++++++++++++++++++++++++++++++++++++++ src/cli/sync-cli.ts | 60 ++++++++- 2 files changed, 330 insertions(+), 1 deletion(-) create mode 100644 src/cli/sync-cli.test.ts diff --git a/src/cli/sync-cli.test.ts b/src/cli/sync-cli.test.ts new file mode 100644 index 00000000..735c5819 --- /dev/null +++ b/src/cli/sync-cli.test.ts @@ -0,0 +1,271 @@ +/** + * Tests for sync-cli P2P enhancements (B4). + * + * Validates that the CLI commands display P2P info when available + * and fall back gracefully when not. + */ + +import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; +import type { P2pStatusResponse } from "../../extensions/shared/cortex-client.js"; + +// ---------- Mock state ---------- + +const mockState = vi.hoisted(() => ({ + healthyFn: vi.fn(async () => true), + p2pProbeFn: vi.fn(async (): Promise => null), + p2pAddPeerFn: vi.fn(async (addr: string) => ({ status: "connected", addr })), + peerManagerStatusFn: vi.fn(async () => ({ + totalPeers: 1, + activePeers: 1, + unreachablePeers: 0, + totalSyncs: 5, + totalTriplesSynced: 100, + })), + peerManagerListPeersFn: vi.fn(async () => [ + { + nodeId: "peer1", + endpoint: "http://10.0.0.1:8080", + namespaces: ["mayros"], + status: "active", + lastSyncAt: "2026-01-01", + totalSyncs: 5, + totalTriplesSynced: 100, + }, + ]), + peerManagerGetPeerFn: vi.fn(async (nodeId: string) => { + if (nodeId === "peer1") { + return { + nodeId: "peer1", + endpoint: "http://10.0.0.1:8080", + namespaces: ["mayros"], + status: "active", + }; + } + return null; + }), + peerManagerAddPeerFn: vi.fn( + async (opts: { nodeId: string; endpoint: string; namespaces: string[] }) => ({ + ...opts, + status: "active", + }), + ), + consoleLogs: [] as string[], +})); + +// Mock CortexClient +vi.mock("../../extensions/shared/cortex-client.js", () => ({ + CortexClient: class MockCortexClient { + async isHealthy() { + return mockState.healthyFn(); + } + async p2pProbe() { + return mockState.p2pProbeFn(); + } + async p2pAddPeer(addr: string) { + return mockState.p2pAddPeerFn(addr); + } + }, +})); + +// Mock cortex-config +vi.mock("../../extensions/shared/cortex-config.js", () => ({ + parseCortexConfig: vi.fn((input: Record) => ({ + host: input?.host ?? "127.0.0.1", + port: input?.port ?? 8080, + })), +})); + +// Mock PeerManager +vi.mock("../../extensions/cortex-sync/peer-manager.js", () => ({ + PeerManager: class MockPeerManager { + async status() { + return mockState.peerManagerStatusFn(); + } + async listPeers() { + return mockState.peerManagerListPeersFn(); + } + async getPeer(nodeId: string) { + return mockState.peerManagerGetPeerFn(nodeId); + } + async addPeer(opts: { nodeId: string; endpoint: string; namespaces: string[] }) { + return mockState.peerManagerAddPeerFn(opts); + } + }, +})); + +// Mock config +vi.mock("../config/config.js", () => ({ + loadConfig: vi.fn(() => ({ + plugins: { entries: {} }, + })), +})); + +import { Command } from "commander"; +import { registerSyncCli } from "./sync-cli.js"; + +// ---------- Helpers ---------- + +function createProgram(): Command { + const program = new Command(); + program.exitOverride(); // Prevent process.exit calls + registerSyncCli(program); + return program; +} + +function captureConsole() { + mockState.consoleLogs = []; + const spy = vi.spyOn(console, "log").mockImplementation((...args: unknown[]) => { + mockState.consoleLogs.push(args.map(String).join(" ")); + }); + return spy; +} + +function makeP2pStatus(overrides: Partial = {}): P2pStatusResponse { + return { + node_id: "abcdef1234567890abcdef1234567890", + enabled: true, + port: 19091, + peer_count: 2, + connected_peers: [ + { addr: "10.0.0.1:19091", connected: true }, + { addr: "10.0.0.2:19091", connected: false }, + ], + gossip_stats: { + round: 42, + pending_announcements: 0, + known_ids: 1500, + bloom_filter_items: 1200, + bloom_filter_fpr: 0.01, + }, + sync_stats: { + peer_count: 2, + local_ids: 800, + total_successful_syncs: 15, + total_failed_syncs: 1, + }, + ...overrides, + }; +} + +// ---------- Tests ---------- + +describe("sync-cli P2P enhancements (B4)", () => { + let consoleSpy: ReturnType; + + beforeEach(() => { + vi.clearAllMocks(); + mockState.healthyFn.mockResolvedValue(true); + mockState.p2pProbeFn.mockResolvedValue(null); + mockState.consoleLogs = []; + consoleSpy = captureConsole(); + }); + + afterEach(() => { + consoleSpy.mockRestore(); + }); + + describe("sync status", () => { + it("shows Native P2P section when P2P is available", async () => { + mockState.p2pProbeFn.mockResolvedValue(makeP2pStatus()); + + const program = createProgram(); + await program.parseAsync(["node", "test", "sync", "status"]); + + const output = mockState.consoleLogs.join("\n"); + expect(output).toContain("Native P2P:"); + expect(output).toContain("Node ID: abcdef1234567890..."); + expect(output).toContain("Port: 19091"); + expect(output).toContain("native (QUIC gossip)"); + expect(output).toContain("Connected peers: 2"); + expect(output).toContain("10.0.0.1:19091 [connected]"); + expect(output).toContain("10.0.0.2:19091 [disconnected]"); + expect(output).toContain("Gossip: round 42, known 1500"); + expect(output).toContain("Sync: 800 local, 15 successful syncs"); + }); + + it("does not show P2P section when P2P probe returns null", async () => { + mockState.p2pProbeFn.mockResolvedValue(null); + + const program = createProgram(); + await program.parseAsync(["node", "test", "sync", "status"]); + + const output = mockState.consoleLogs.join("\n"); + expect(output).toContain("Cortex Sync Status:"); + expect(output).not.toContain("Native P2P:"); + }); + + it("does not show P2P section when enabled=false", async () => { + mockState.p2pProbeFn.mockResolvedValue(makeP2pStatus({ enabled: false })); + + const program = createProgram(); + await program.parseAsync(["node", "test", "sync", "status"]); + + const output = mockState.consoleLogs.join("\n"); + expect(output).not.toContain("Native P2P:"); + }); + }); + + describe("sync pair", () => { + it("calls p2pAddPeer when native P2P is active", async () => { + mockState.p2pProbeFn.mockResolvedValue(makeP2pStatus()); + mockState.peerManagerGetPeerFn.mockResolvedValue(null); + + const program = createProgram(); + await program.parseAsync([ + "node", + "test", + "sync", + "pair", + "new-peer", + "http://10.0.0.5:8080", + ]); + + expect(mockState.p2pAddPeerFn).toHaveBeenCalledWith("10.0.0.5:19091"); + const output = mockState.consoleLogs.join("\n"); + expect(output).toContain("P2P:"); + }); + + it("does not call p2pAddPeer when P2P is not active", async () => { + mockState.p2pProbeFn.mockResolvedValue(null); + mockState.peerManagerGetPeerFn.mockResolvedValue(null); + + const program = createProgram(); + await program.parseAsync([ + "node", + "test", + "sync", + "pair", + "new-peer", + "http://10.0.0.5:8080", + ]); + + expect(mockState.p2pAddPeerFn).not.toHaveBeenCalled(); + }); + }); + + describe("sync now", () => { + it("shows gossip info when native P2P is active", async () => { + mockState.p2pProbeFn.mockResolvedValue(makeP2pStatus()); + + const program = createProgram(); + await program.parseAsync(["node", "test", "sync", "now"]); + + const output = mockState.consoleLogs.join("\n"); + expect(output).toContain("Sync handled by native P2P gossip"); + expect(output).toContain("Gossip: round 42, known 1500"); + expect(output).toContain("Sync: 800 local, 15 successful syncs"); + expect(output).toContain("Connected P2P peers: 2"); + }); + + it("falls back to polled sync when P2P is not available", async () => { + mockState.p2pProbeFn.mockResolvedValue(null); + + const program = createProgram(); + await program.parseAsync(["node", "test", "sync", "now"]); + + const output = mockState.consoleLogs.join("\n"); + expect(output).not.toContain("Sync handled by native P2P gossip"); + expect(output).toContain("active peer"); + }); + }); +}); diff --git a/src/cli/sync-cli.ts b/src/cli/sync-cli.ts index 33053e6e..6f207c63 100644 --- a/src/cli/sync-cli.ts +++ b/src/cli/sync-cli.ts @@ -12,7 +12,7 @@ import type { Command } from "commander"; import { parseCortexConfig } from "../../extensions/shared/cortex-config.js"; -import { CortexClient } from "../../extensions/shared/cortex-client.js"; +import { CortexClient, type P2pStatusResponse } from "../../extensions/shared/cortex-client.js"; import { PeerManager } from "../../extensions/cortex-sync/peer-manager.js"; import { loadConfig } from "../config/config.js"; @@ -63,6 +63,14 @@ function resolveNamespace(): string { } } +async function probeP2pStatus(client: CortexClient): Promise { + try { + return await client.p2pProbe(); + } catch { + return null; + } +} + // ============================================================================ // Registration // ============================================================================ @@ -123,6 +131,28 @@ export function registerSyncCli(program: Command) { 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"}]`); + } + } + 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`, + ); + } }); // ---- pair ---- @@ -166,6 +196,19 @@ export function registerSyncCli(program: Command) { 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)"); + } + } }); // ---- remove ---- @@ -223,6 +266,21 @@ export function registerSyncCli(program: Command) { 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; + } + if (opts.peer) { const peer = await pm.getPeer(opts.peer); if (!peer) { From 1bbf02d03efe10ab89f4f9b228442ed611d3863f Mon Sep 17 00:00:00 2001 From: It Apilium Date: Sun, 8 Mar 2026 20:10:08 +0100 Subject: [PATCH 4/4] chore: bump to v0.1.8 and require Cortex >= 0.3.8 --- extensions/agent-mesh/package.json | 2 +- extensions/analytics/package.json | 2 +- extensions/bash-sandbox/package.json | 2 +- extensions/bluebubbles/package.json | 2 +- extensions/ci-plugin/package.json | 2 +- extensions/code-indexer/package.json | 2 +- extensions/code-tools/package.json | 2 +- extensions/copilot-proxy/package.json | 2 +- extensions/cortex-sync/package.json | 2 +- extensions/diagnostics-otel/package.json | 2 +- extensions/discord/package.json | 2 +- extensions/feishu/package.json | 2 +- extensions/google-antigravity-auth/package.json | 2 +- extensions/google-gemini-cli-auth/package.json | 2 +- extensions/googlechat/package.json | 2 +- extensions/imessage/package.json | 2 +- extensions/interactive-permissions/package.json | 2 +- extensions/iot-bridge/package.json | 2 +- extensions/irc/package.json | 2 +- extensions/line/package.json | 2 +- extensions/llm-hooks/package.json | 2 +- extensions/llm-task/package.json | 2 +- extensions/lobster/package.json | 2 +- extensions/lsp-bridge/package.json | 2 +- extensions/matrix/CHANGELOG.md | 6 ++++++ extensions/matrix/package.json | 2 +- extensions/mattermost/package.json | 2 +- extensions/mcp-client/package.json | 2 +- extensions/mcp-server/package.json | 2 +- extensions/memory-core/package.json | 2 +- extensions/memory-lancedb/package.json | 2 +- extensions/memory-semantic/package.json | 2 +- extensions/minimax-portal-auth/package.json | 2 +- extensions/msteams/CHANGELOG.md | 6 ++++++ extensions/msteams/package.json | 2 +- extensions/nextcloud-talk/package.json | 2 +- extensions/nostr/CHANGELOG.md | 6 ++++++ extensions/nostr/package.json | 2 +- extensions/open-prose/package.json | 2 +- extensions/semantic-observability/package.json | 2 +- extensions/semantic-skills/package.json | 2 +- extensions/shared/cortex-version.ts | 2 +- extensions/signal/package.json | 2 +- extensions/skill-hub/package.json | 2 +- extensions/slack/package.json | 2 +- extensions/telegram/package.json | 2 +- extensions/tlon/package.json | 2 +- extensions/token-economy/package.json | 2 +- extensions/twitch/CHANGELOG.md | 6 ++++++ extensions/twitch/package.json | 2 +- extensions/voice-call/CHANGELOG.md | 6 ++++++ extensions/voice-call/package.json | 2 +- extensions/whatsapp/package.json | 2 +- extensions/zalo/CHANGELOG.md | 6 ++++++ extensions/zalo/package.json | 2 +- extensions/zalouser/CHANGELOG.md | 6 ++++++ extensions/zalouser/package.json | 2 +- package.json | 2 +- 58 files changed, 93 insertions(+), 51 deletions(-) diff --git a/extensions/agent-mesh/package.json b/extensions/agent-mesh/package.json index 19473e4a..875fe0ac 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.7", + "version": "0.1.8", "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 a0c251a4..985dc56c 100644 --- a/extensions/analytics/package.json +++ b/extensions/analytics/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-analytics", - "version": "0.1.7", + "version": "0.1.8", "private": true, "type": "module", "main": "index.ts", diff --git a/extensions/bash-sandbox/package.json b/extensions/bash-sandbox/package.json index 2b3c7231..659377a2 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.7", + "version": "0.1.8", "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 c94e56ac..6449708d 100644 --- a/extensions/bluebubbles/package.json +++ b/extensions/bluebubbles/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-bluebubbles", - "version": "0.1.7", + "version": "0.1.8", "description": "Mayros BlueBubbles channel plugin", "license": "MIT", "type": "module", diff --git a/extensions/ci-plugin/package.json b/extensions/ci-plugin/package.json index d6f6837f..0b39f5a1 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.7", + "version": "0.1.8", "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 2b0be4c3..091c7ff8 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.7", + "version": "0.1.8", "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 3af322c3..dad61025 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.7", + "version": "0.1.8", "private": true, "type": "module", "dependencies": { diff --git a/extensions/copilot-proxy/package.json b/extensions/copilot-proxy/package.json index 14962c47..5d833f8c 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.7", + "version": "0.1.8", "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 ca69ba10..244cac4c 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.7", + "version": "0.1.8", "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 17ecfdca..b62214e5 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.7", + "version": "0.1.8", "description": "Mayros diagnostics OpenTelemetry exporter", "license": "MIT", "type": "module", diff --git a/extensions/discord/package.json b/extensions/discord/package.json index 4a6b7e7d..80786058 100644 --- a/extensions/discord/package.json +++ b/extensions/discord/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-discord", - "version": "0.1.7", + "version": "0.1.8", "description": "Mayros Discord channel plugin", "license": "MIT", "type": "module", diff --git a/extensions/feishu/package.json b/extensions/feishu/package.json index 8eee4b90..c9998700 100644 --- a/extensions/feishu/package.json +++ b/extensions/feishu/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-feishu", - "version": "0.1.7", + "version": "0.1.8", "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 20c67d7c..950b5831 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.7", + "version": "0.1.8", "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 851820ad..624e6d8f 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.7", + "version": "0.1.8", "private": true, "description": "Mayros Gemini CLI OAuth provider plugin", "type": "module", diff --git a/extensions/googlechat/package.json b/extensions/googlechat/package.json index 58c56312..e486acd0 100644 --- a/extensions/googlechat/package.json +++ b/extensions/googlechat/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-googlechat", - "version": "0.1.7", + "version": "0.1.8", "private": true, "description": "Mayros Google Chat channel plugin", "type": "module", diff --git a/extensions/imessage/package.json b/extensions/imessage/package.json index 78ad447a..85e58585 100644 --- a/extensions/imessage/package.json +++ b/extensions/imessage/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-imessage", - "version": "0.1.7", + "version": "0.1.8", "private": true, "description": "Mayros iMessage channel plugin", "type": "module", diff --git a/extensions/interactive-permissions/package.json b/extensions/interactive-permissions/package.json index 055881de..e593cc93 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.7", + "version": "0.1.8", "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 77b0d2c7..41cafdcf 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.7", + "version": "0.1.8", "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 6e1e397c..caa4638e 100644 --- a/extensions/irc/package.json +++ b/extensions/irc/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-irc", - "version": "0.1.7", + "version": "0.1.8", "description": "Mayros IRC channel plugin", "license": "MIT", "type": "module", diff --git a/extensions/line/package.json b/extensions/line/package.json index e0d114fc..f9294759 100644 --- a/extensions/line/package.json +++ b/extensions/line/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-line", - "version": "0.1.7", + "version": "0.1.8", "private": true, "description": "Mayros LINE channel plugin", "type": "module", diff --git a/extensions/llm-hooks/package.json b/extensions/llm-hooks/package.json index 8fd52f9e..50099ba7 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.7", + "version": "0.1.8", "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 3a5d2e7e..cfddeb23 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.7", + "version": "0.1.8", "private": true, "description": "Mayros JSON-only LLM task plugin", "type": "module", diff --git a/extensions/lobster/package.json b/extensions/lobster/package.json index 5663c391..e0ddd209 100644 --- a/extensions/lobster/package.json +++ b/extensions/lobster/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-lobster", - "version": "0.1.7", + "version": "0.1.8", "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 6e1276ca..7edcd72a 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.7", + "version": "0.1.8", "description": "Cortex-backed language server bridge for Mayros — hover, diagnostics, go-to-definition", "type": "module", "dependencies": { diff --git a/extensions/matrix/CHANGELOG.md b/extensions/matrix/CHANGELOG.md index 9105a106..6d2850d3 100644 --- a/extensions/matrix/CHANGELOG.md +++ b/extensions/matrix/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.1.8 + +### Changes + +- Version alignment with core Mayros release numbers. + ## 0.1.7 ### Changes diff --git a/extensions/matrix/package.json b/extensions/matrix/package.json index 3f454cf2..c7644687 100644 --- a/extensions/matrix/package.json +++ b/extensions/matrix/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-matrix", - "version": "0.1.7", + "version": "0.1.8", "description": "Mayros Matrix channel plugin", "license": "MIT", "type": "module", diff --git a/extensions/mattermost/package.json b/extensions/mattermost/package.json index 751416c2..6b9a0a33 100644 --- a/extensions/mattermost/package.json +++ b/extensions/mattermost/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-mattermost", - "version": "0.1.7", + "version": "0.1.8", "private": true, "description": "Mayros Mattermost channel plugin", "type": "module", diff --git a/extensions/mcp-client/package.json b/extensions/mcp-client/package.json index 38ea7ac1..98bbe3bd 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.7", + "version": "0.1.8", "private": true, "description": "MCP server client with multi-transport support and Cortex tool registry", "type": "module", diff --git a/extensions/mcp-server/package.json b/extensions/mcp-server/package.json index 644b5fe1..f16d24a8 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.7", + "version": "0.1.8", "private": true, "description": "MCP server exposing Mayros tools, Cortex resources, and workflow prompts via Model Context Protocol", "type": "module", diff --git a/extensions/memory-core/package.json b/extensions/memory-core/package.json index f170a25d..aa5dcce5 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.7", + "version": "0.1.8", "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 0d3c5d16..5ab3e2b8 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.7", + "version": "0.1.8", "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 1b5f7196..a4152d44 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.7", + "version": "0.1.8", "private": true, "description": "Mayros semantic memory plugin via AIngle Cortex sidecar (RDF triples, identity graph, Titans STM/LTM)", "type": "module", diff --git a/extensions/minimax-portal-auth/package.json b/extensions/minimax-portal-auth/package.json index 35d76d2c..40fc040b 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.7", + "version": "0.1.8", "private": true, "description": "Mayros MiniMax Portal OAuth provider plugin", "type": "module", diff --git a/extensions/msteams/CHANGELOG.md b/extensions/msteams/CHANGELOG.md index a7a7efc4..8e430ace 100644 --- a/extensions/msteams/CHANGELOG.md +++ b/extensions/msteams/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.1.8 + +### Changes + +- Version alignment with core Mayros release numbers. + ## 0.1.7 ### Changes diff --git a/extensions/msteams/package.json b/extensions/msteams/package.json index 2642fa46..91b6f1a1 100644 --- a/extensions/msteams/package.json +++ b/extensions/msteams/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-msteams", - "version": "0.1.7", + "version": "0.1.8", "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 556c5476..63396d89 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.7", + "version": "0.1.8", "description": "Mayros Nextcloud Talk channel plugin", "license": "MIT", "type": "module", diff --git a/extensions/nostr/CHANGELOG.md b/extensions/nostr/CHANGELOG.md index daacebb8..9fcbbd55 100644 --- a/extensions/nostr/CHANGELOG.md +++ b/extensions/nostr/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.1.8 + +### Changes + +- Version alignment with core Mayros release numbers. + ## 0.1.7 ### Changes diff --git a/extensions/nostr/package.json b/extensions/nostr/package.json index 3a8385ca..5bde3b09 100644 --- a/extensions/nostr/package.json +++ b/extensions/nostr/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-nostr", - "version": "0.1.7", + "version": "0.1.8", "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 730b5c48..0f611f1d 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.7", + "version": "0.1.8", "private": true, "description": "OpenProse VM skill pack plugin (slash command + telemetry).", "type": "module", diff --git a/extensions/semantic-observability/package.json b/extensions/semantic-observability/package.json index 641e16e7..30d4be4d 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.7", + "version": "0.1.8", "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 3b049e2f..e91a84d4 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.7", + "version": "0.1.8", "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-version.ts b/extensions/shared/cortex-version.ts index b1f733bc..8f698771 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.3.7"; +export const REQUIRED_CORTEX_VERSION = "0.3.8"; diff --git a/extensions/signal/package.json b/extensions/signal/package.json index 3874bd65..d74c9d83 100644 --- a/extensions/signal/package.json +++ b/extensions/signal/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-signal", - "version": "0.1.7", + "version": "0.1.8", "private": true, "description": "Mayros Signal channel plugin", "type": "module", diff --git a/extensions/skill-hub/package.json b/extensions/skill-hub/package.json index 6c805a9e..ea9b2983 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.7", + "version": "0.1.8", "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 1dae3322..799f2442 100644 --- a/extensions/slack/package.json +++ b/extensions/slack/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-slack", - "version": "0.1.7", + "version": "0.1.8", "private": true, "description": "Mayros Slack channel plugin", "type": "module", diff --git a/extensions/telegram/package.json b/extensions/telegram/package.json index ce49e639..58ca5890 100644 --- a/extensions/telegram/package.json +++ b/extensions/telegram/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-telegram", - "version": "0.1.7", + "version": "0.1.8", "private": true, "description": "Mayros Telegram channel plugin", "type": "module", diff --git a/extensions/tlon/package.json b/extensions/tlon/package.json index cdc32e7b..1247188b 100644 --- a/extensions/tlon/package.json +++ b/extensions/tlon/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-tlon", - "version": "0.1.7", + "version": "0.1.8", "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 54e81464..2f816147 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.7", + "version": "0.1.8", "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/twitch/CHANGELOG.md b/extensions/twitch/CHANGELOG.md index 31dbfad6..6d07061f 100644 --- a/extensions/twitch/CHANGELOG.md +++ b/extensions/twitch/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.1.8 + +### Changes + +- Version alignment with core Mayros release numbers. + ## 0.1.7 ### Changes diff --git a/extensions/twitch/package.json b/extensions/twitch/package.json index 883192a6..cb37c044 100644 --- a/extensions/twitch/package.json +++ b/extensions/twitch/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-twitch", - "version": "0.1.7", + "version": "0.1.8", "private": true, "description": "Mayros Twitch channel plugin", "type": "module", diff --git a/extensions/voice-call/CHANGELOG.md b/extensions/voice-call/CHANGELOG.md index 18d341a7..81e6876b 100644 --- a/extensions/voice-call/CHANGELOG.md +++ b/extensions/voice-call/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.1.8 + +### Changes + +- Version alignment with core Mayros release numbers. + ## 0.1.7 ### Changes diff --git a/extensions/voice-call/package.json b/extensions/voice-call/package.json index a46b93b1..0766f394 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.7", + "version": "0.1.8", "description": "Mayros voice-call plugin", "license": "MIT", "type": "module", diff --git a/extensions/whatsapp/package.json b/extensions/whatsapp/package.json index 73480625..53ded6d6 100644 --- a/extensions/whatsapp/package.json +++ b/extensions/whatsapp/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-whatsapp", - "version": "0.1.7", + "version": "0.1.8", "private": true, "description": "Mayros WhatsApp channel plugin", "type": "module", diff --git a/extensions/zalo/CHANGELOG.md b/extensions/zalo/CHANGELOG.md index 143786da..8b11e812 100644 --- a/extensions/zalo/CHANGELOG.md +++ b/extensions/zalo/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.1.8 + +### Changes + +- Version alignment with core Mayros release numbers. + ## 0.1.7 ### Changes diff --git a/extensions/zalo/package.json b/extensions/zalo/package.json index 6b0b7e74..d86889dd 100644 --- a/extensions/zalo/package.json +++ b/extensions/zalo/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-zalo", - "version": "0.1.7", + "version": "0.1.8", "description": "Mayros Zalo channel plugin", "license": "MIT", "type": "module", diff --git a/extensions/zalouser/CHANGELOG.md b/extensions/zalouser/CHANGELOG.md index 37f45250..ce9c1a0b 100644 --- a/extensions/zalouser/CHANGELOG.md +++ b/extensions/zalouser/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.1.8 + +### Changes + +- Version alignment with core Mayros release numbers. + ## 0.1.7 ### Changes diff --git a/extensions/zalouser/package.json b/extensions/zalouser/package.json index 5918b186..c136c28f 100644 --- a/extensions/zalouser/package.json +++ b/extensions/zalouser/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-zalouser", - "version": "0.1.7", + "version": "0.1.8", "description": "Mayros Zalo Personal Account plugin via zca-cli", "license": "MIT", "type": "module", diff --git a/package.json b/package.json index e7390aac..8f3841c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros", - "version": "0.1.7", + "version": "0.1.8", "description": "Multi-channel AI agent framework with knowledge graph, MCP support, and coding CLI", "keywords": [ "agent",