aiden-runtime 4.0.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +11 -7
  2. package/config/hardware.json +2 -2
  3. package/dist/api/server.js +50 -52
  4. package/dist/cli/v4/aidenCLI.js +513 -14
  5. package/dist/cli/v4/aidenPrompt.js +317 -0
  6. package/dist/cli/v4/box.js +105 -39
  7. package/dist/cli/v4/callbacks.js +39 -6
  8. package/dist/cli/v4/chatSession.js +269 -52
  9. package/dist/cli/v4/citationFooter.js +97 -0
  10. package/dist/cli/v4/commands/channel.js +656 -0
  11. package/dist/cli/v4/commands/clear.js +1 -1
  12. package/dist/cli/v4/commands/compress.js +1 -1
  13. package/dist/cli/v4/commands/cron.js +44 -16
  14. package/dist/cli/v4/commands/fanout.js +236 -0
  15. package/dist/cli/v4/commands/help.js +15 -4
  16. package/dist/cli/v4/commands/history.js +84 -0
  17. package/dist/cli/v4/commands/index.js +19 -1
  18. package/dist/cli/v4/commands/mcp.js +358 -0
  19. package/dist/cli/v4/commands/setup.js +34 -0
  20. package/dist/cli/v4/commands/show.js +43 -0
  21. package/dist/cli/v4/commands/skills.js +169 -4
  22. package/dist/cli/v4/commands/status.js +84 -0
  23. package/dist/cli/v4/commands/subagent.js +78 -0
  24. package/dist/cli/v4/commands/verbose.js +1 -1
  25. package/dist/cli/v4/commands/voice.js +218 -0
  26. package/dist/cli/v4/cronCli.js +103 -0
  27. package/dist/cli/v4/display.js +300 -14
  28. package/dist/cli/v4/doctor.js +41 -0
  29. package/dist/cli/v4/envSources.js +105 -0
  30. package/dist/cli/v4/ghostMatch.js +74 -0
  31. package/dist/cli/v4/historyStore.js +163 -0
  32. package/dist/cli/v4/pasteCompression.js +124 -0
  33. package/dist/cli/v4/pasteIntercept.js +203 -0
  34. package/dist/cli/v4/replyRenderer.js +209 -0
  35. package/dist/cli/v4/resizeGuard.js +92 -0
  36. package/dist/cli/v4/setupWizard.js +466 -232
  37. package/dist/cli/v4/shellInterpolation.js +139 -0
  38. package/dist/cli/v4/skinEngine.js +21 -1
  39. package/dist/cli/v4/streamingPrefix.js +121 -0
  40. package/dist/cli/v4/syntaxHighlight.js +345 -0
  41. package/dist/cli/v4/table.js +216 -0
  42. package/dist/cli/v4/themeDetect.js +81 -0
  43. package/dist/cli/v4/uiBuild.js +74 -0
  44. package/dist/cli/v4/voiceCli.js +113 -0
  45. package/dist/cli/v4/voicePromptApi.js +196 -0
  46. package/dist/core/channels/discord.js +16 -10
  47. package/dist/core/channels/email.js +13 -9
  48. package/dist/core/channels/imessage.js +13 -9
  49. package/dist/core/channels/manager.js +25 -7
  50. package/dist/core/channels/pdf-extract.js +180 -0
  51. package/dist/core/channels/photo-vision.js +157 -0
  52. package/dist/core/channels/signal.js +11 -7
  53. package/dist/core/channels/slack.js +13 -10
  54. package/dist/core/channels/telegram-commands.js +154 -0
  55. package/dist/core/channels/telegram-groups.js +198 -0
  56. package/dist/core/channels/telegram-rate-limit.js +124 -0
  57. package/dist/core/channels/telegram.js +1980 -0
  58. package/dist/core/channels/twilio.js +11 -7
  59. package/dist/core/channels/webhook.js +9 -5
  60. package/dist/core/channels/whatsapp.js +15 -11
  61. package/dist/core/channels/whisper-transcribe.js +163 -0
  62. package/dist/core/cronManager.js +33 -294
  63. package/dist/core/gateway.js +29 -8
  64. package/dist/core/playwrightBridge.js +90 -0
  65. package/dist/core/v4/aidenAgent.js +35 -0
  66. package/dist/core/v4/auxiliaryClient.js +2 -2
  67. package/dist/core/v4/cron/atomicWrite.js +18 -4
  68. package/dist/core/v4/cron/cronExecute.js +300 -0
  69. package/dist/core/v4/cron/cronManager.js +502 -0
  70. package/dist/core/v4/cron/cronState.js +314 -0
  71. package/dist/core/v4/cron/cronTick.js +90 -0
  72. package/dist/core/v4/cron/diagnostics.js +104 -0
  73. package/dist/core/v4/cron/graceWindow.js +79 -0
  74. package/dist/core/v4/firstRun/providerDetection.js +287 -0
  75. package/dist/core/v4/logger/factory.js +110 -0
  76. package/dist/core/v4/logger/index.js +22 -0
  77. package/dist/core/v4/logger/logger.js +101 -0
  78. package/dist/core/v4/logger/sinks/fileSink.js +110 -0
  79. package/dist/core/v4/logger/sinks/multiSink.js +43 -0
  80. package/dist/core/v4/logger/sinks/nullSink.js +53 -0
  81. package/dist/core/v4/logger/sinks/stdSink.js +81 -0
  82. package/dist/core/v4/mcp/server/diagnostics.js +40 -0
  83. package/dist/core/v4/mcp/server/skillBridge.js +94 -0
  84. package/dist/core/v4/mcp/server/stdioServer.js +119 -0
  85. package/dist/core/v4/mcp/server/toolBridge.js +168 -0
  86. package/dist/core/v4/platformPaths.js +105 -0
  87. package/dist/core/v4/providerFallback.js +25 -0
  88. package/dist/core/v4/skillLoader.js +21 -5
  89. package/dist/core/v4/skillMining/candidateStore.js +164 -0
  90. package/dist/core/v4/skillMining/extractorPrompt.js +111 -0
  91. package/dist/core/v4/skillMining/proposalBuilder.js +139 -0
  92. package/dist/core/v4/skillMining/skillMiner.js +191 -0
  93. package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
  94. package/dist/core/v4/subagent/budget.js +76 -0
  95. package/dist/core/v4/subagent/diagnostics.js +22 -0
  96. package/dist/core/v4/subagent/fanout.js +216 -0
  97. package/dist/core/v4/subagent/merger.js +148 -0
  98. package/dist/core/v4/subagent/providerRotation.js +54 -0
  99. package/dist/core/v4/voice/audioStream.js +373 -0
  100. package/dist/core/v4/voice/cliVoice.js +393 -0
  101. package/dist/core/v4/voice/diagnostics.js +66 -0
  102. package/dist/core/v4/voice/ttsStream.js +193 -0
  103. package/dist/core/version.js +1 -1
  104. package/dist/core/visionAnalyze.js +291 -90
  105. package/dist/core/voice/audio.js +61 -5
  106. package/dist/core/voice/audioBackend.js +134 -0
  107. package/dist/core/voice/stt.js +61 -6
  108. package/dist/core/voice/tts.js +19 -3
  109. package/dist/providers/v4/nullAdapter.js +58 -0
  110. package/dist/tools/v4/index.js +32 -1
  111. package/dist/tools/v4/subagent/subagentFanout.js +166 -0
  112. package/package.json +11 -2
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/logger/sinks/nullSink.ts — Phase v4.1-1.3a
10
+ *
11
+ * Discard every record. The default for `'test'` mode so vitest output
12
+ * stays clean even when the code under test emits warnings — and the
13
+ * fallback when a module is constructed without a logger (the
14
+ * `noopLogger` factory in `../factory.ts` uses this).
15
+ *
16
+ * `MemorySink` is also here: it captures records into an in-process
17
+ * array so tests can assert on log output without any I/O. Two surfaces
18
+ * in one file because they share the "nothing leaves this process"
19
+ * property.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.MemorySink = exports.NullSink = void 0;
23
+ /** Drops every record. */
24
+ class NullSink {
25
+ write(_record) { }
26
+ }
27
+ exports.NullSink = NullSink;
28
+ /**
29
+ * Captures every record into `records`. Tests use this:
30
+ *
31
+ * const sink = new MemorySink();
32
+ * const log = new CoreLogger({ sinks: [sink] });
33
+ * log.info('hello');
34
+ * expect(sink.records[0].msg).toBe('hello');
35
+ *
36
+ * `clear()` resets between assertions; `findScope()` is a small
37
+ * convenience for "did the channels.telegram scope log anything?".
38
+ */
39
+ class MemorySink {
40
+ constructor() {
41
+ this.records = [];
42
+ }
43
+ write(r) {
44
+ this.records.push(r);
45
+ }
46
+ clear() {
47
+ this.records.length = 0;
48
+ }
49
+ findScope(scope) {
50
+ return this.records.filter((r) => r.scope === scope || r.scope.startsWith(`${scope}.`));
51
+ }
52
+ }
53
+ exports.MemorySink = MemorySink;
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/logger/sinks/stdSink.ts — Phase v4.1-1.3a
10
+ *
11
+ * Stdout / stderr sinks. Two flavours:
12
+ *
13
+ * - StderrSink — pretty single-line format; used in cli-headless
14
+ * mode for warnings + errors so the user sees them
15
+ * without polluting stdout (which scripts may pipe).
16
+ * - StdoutJsonSink — NDJSON one-record-per-line; used in `serve` mode
17
+ * so log aggregators (systemd-journald, docker logs,
18
+ * Loki, etc.) get structured fields.
19
+ *
20
+ * No StdoutPrettySink in 3a — `cli-interactive` deliberately wires no
21
+ * stdout sink at all (REPL is sacred), and `cli-headless` uses the
22
+ * stderr flavour so stdout stays free for tool output piping. If a
23
+ * future mode needs colourful stdout (e.g. `aiden status` one-shot),
24
+ * add it then.
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.StdoutJsonSink = exports.StderrSink = void 0;
28
+ const LEVEL_THRESHOLD = {
29
+ debug: 10, info: 20, warn: 30, error: 40,
30
+ };
31
+ class StderrSink {
32
+ constructor(opts = {}) {
33
+ this.minLevel = LEVEL_THRESHOLD[opts.minLevel ?? 'warn'];
34
+ }
35
+ write(r) {
36
+ if (LEVEL_THRESHOLD[r.level] < this.minLevel)
37
+ return;
38
+ const scope = r.scope ? ` [${r.scope}]` : '';
39
+ try {
40
+ process.stderr.write(`${r.ts.toISOString()} [${r.level}]${scope} ${r.msg}\n`);
41
+ }
42
+ catch { /* dropped */ }
43
+ }
44
+ }
45
+ exports.StderrSink = StderrSink;
46
+ /**
47
+ * NDJSON stdout for `serve` mode. One record per line, structured
48
+ * fields preserved. Aggregators love this; humans don't. Headless
49
+ * scripts that want pretty output use the file sink instead and tail
50
+ * the log file.
51
+ */
52
+ class StdoutJsonSink {
53
+ constructor(opts = {}) {
54
+ this.minLevel = LEVEL_THRESHOLD[opts.minLevel ?? 'debug'];
55
+ }
56
+ write(r) {
57
+ if (LEVEL_THRESHOLD[r.level] < this.minLevel)
58
+ return;
59
+ const payload = {
60
+ ts: r.ts.toISOString(),
61
+ level: r.level,
62
+ scope: r.scope || undefined,
63
+ msg: r.msg,
64
+ };
65
+ if (r.ctx)
66
+ Object.assign(payload, r.ctx);
67
+ try {
68
+ process.stdout.write(safeJson(payload) + '\n');
69
+ }
70
+ catch { /* dropped */ }
71
+ }
72
+ }
73
+ exports.StdoutJsonSink = StdoutJsonSink;
74
+ function safeJson(obj) {
75
+ try {
76
+ return JSON.stringify(obj);
77
+ }
78
+ catch {
79
+ return '{"err":"unserializable"}';
80
+ }
81
+ }
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/mcp/server/diagnostics.ts — Phase v4.1-mcp
10
+ *
11
+ * Build fingerprint + counts surfaced by the `aiden mcp status` CLI
12
+ * subcommand and the launch log line. The fingerprint follows the same
13
+ * convention the Telegram adapter set in v4.1-3.2 — a constant string
14
+ * the user can grep for to verify the build that's actually running
15
+ * matches the phase they expected.
16
+ *
17
+ * Bump on every shipped phase. Format: `v4.1-mcp[+suffix]`.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.AIDEN_MCP_BUILD = void 0;
21
+ exports.collectMcpDiagnostics = collectMcpDiagnostics;
22
+ const toolBridge_1 = require("./toolBridge");
23
+ /** Build fingerprint — bump per phase. Surfaced in `aiden mcp status`
24
+ * and the stderr launch line so it's grep-able from the spawning
25
+ * client's log stream. */
26
+ exports.AIDEN_MCP_BUILD = 'v4.1-mcp.2';
27
+ async function collectMcpDiagnostics(registry, loader, env = (0, toolBridge_1.readToolBridgeEnv)()) {
28
+ const exposed = (0, toolBridge_1.exposedToolNames)(registry, env);
29
+ const skills = await loader.list();
30
+ return {
31
+ build: exports.AIDEN_MCP_BUILD,
32
+ toolsTotal: registry.list().length,
33
+ toolsExposed: exposed.length,
34
+ skillsTotal: skills.length,
35
+ env: {
36
+ allowDestructive: env.allowDestructive,
37
+ allowlist: env.allowlist ? [...env.allowlist].sort() : null,
38
+ },
39
+ };
40
+ }
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/mcp/server/skillBridge.ts — Phase v4.1-mcp
10
+ *
11
+ * Expose Aiden's loaded skills as MCP resources. Skills are inert
12
+ * markdown playbooks (`SKILL.md` + frontmatter), perfect candidates for
13
+ * MCP's read-only resource surface.
14
+ *
15
+ * URI scheme — `aiden-skill://<name>`. We deliberately namespaced this
16
+ * (rather than the bare `skill://`) so when a client connects to multiple
17
+ * agent MCP servers, resource URIs do not collide. The `name` segment is
18
+ * the SKILL.md frontmatter `name` field, which is already the lookup key
19
+ * used by the rest of the runtime (`SkillLoader.load(name)`).
20
+ *
21
+ * The bridge is intentionally read-only:
22
+ * - `resources/list` enumerates every loaded skill
23
+ * - `resources/read` returns the raw markdown (frontmatter + body)
24
+ *
25
+ * Mutations live behind the `skill_manage` tool; an MCP client that
26
+ * needs to install or modify a skill must go through that tool path so
27
+ * the same trust-level checks the REPL uses also apply remotely.
28
+ */
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.skillUri = skillUri;
31
+ exports.parseSkillUri = parseSkillUri;
32
+ exports.summaryToResource = summaryToResource;
33
+ exports.buildResourcesList = buildResourcesList;
34
+ exports.readSkillResource = readSkillResource;
35
+ const node_fs_1 = require("node:fs");
36
+ const URI_SCHEME = 'aiden-skill';
37
+ const URI_PREFIX = `${URI_SCHEME}://`;
38
+ function skillUri(name) {
39
+ return `${URI_PREFIX}${encodeURIComponent(name)}`;
40
+ }
41
+ /** Pull the `<name>` segment back out of an `aiden-skill://<name>` URI.
42
+ * Returns null on malformed input — the read handler reports `isError`
43
+ * in that case rather than crashing the protocol. */
44
+ function parseSkillUri(uri) {
45
+ if (!uri.startsWith(URI_PREFIX))
46
+ return null;
47
+ const raw = uri.slice(URI_PREFIX.length);
48
+ if (!raw)
49
+ return null;
50
+ try {
51
+ return decodeURIComponent(raw);
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ function summaryToResource(s) {
58
+ return {
59
+ uri: skillUri(s.name),
60
+ name: s.name,
61
+ description: s.description,
62
+ mimeType: 'text/markdown',
63
+ };
64
+ }
65
+ /**
66
+ * Build the resources array advertised on `resources/list`. Walks the
67
+ * SkillLoader cache (already warmed at boot in the runtime) so this is
68
+ * just an in-memory map.
69
+ */
70
+ async function buildResourcesList(loader) {
71
+ const list = await loader.list();
72
+ return list.map(summaryToResource);
73
+ }
74
+ /**
75
+ * `resources/read` handler. Returns the raw SKILL.md content for the
76
+ * named skill. Throws when the URI is malformed or the skill is unknown
77
+ * — the stdio-server layer maps those to JSON-RPC errors.
78
+ */
79
+ async function readSkillResource(loader, uri) {
80
+ const name = parseSkillUri(uri);
81
+ if (name === null) {
82
+ throw new Error(`Malformed resource URI (expected ${URI_PREFIX}<name>): ${uri}`);
83
+ }
84
+ const skill = await loader.load(name);
85
+ if (!skill) {
86
+ throw new Error(`Skill not found: ${name}`);
87
+ }
88
+ // SKILL.md content lives on disk. The loader exposes `filePath`; read
89
+ // the raw bytes so the client gets frontmatter + body verbatim. (We
90
+ // could reconstruct from `frontmatter` + `content`, but raw avoids
91
+ // round-trip drift if the loader ever rewrites YAML on parse.)
92
+ const text = await node_fs_1.promises.readFile(skill.filePath, 'utf-8');
93
+ return { uri, mimeType: 'text/markdown', text };
94
+ }
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/mcp/server/stdioServer.ts — Phase v4.1-mcp
10
+ *
11
+ * Stand up a Model Context Protocol server over stdio. Three protocol
12
+ * surfaces are wired:
13
+ *
14
+ * - tools/list → toolBridge.buildToolsList()
15
+ * - tools/call → toolBridge.buildToolCallHandler()
16
+ * - resources/list → skillBridge.buildResourcesList()
17
+ * - resources/read → skillBridge.readSkillResource()
18
+ *
19
+ * stdio invariants:
20
+ * - stdout is the JSON-RPC channel — NEVER write to it from this
21
+ * process outside the SDK transport. The logger is built in
22
+ * `'mcp-stdio'` mode (file + stderr only) and tools should only
23
+ * emit through that logger.
24
+ * - the launch banner goes to stderr on purpose; spawning clients
25
+ * (Claude Desktop, Cursor) capture stderr in their MCP log so the
26
+ * user can grep the build fingerprint to verify what's running.
27
+ *
28
+ * The server function is a long-running call: it returns a `stop()`
29
+ * handle but ordinarily blocks until the parent closes the stdio pair.
30
+ * The CLI's `serve` action awaits a never-resolving promise so the
31
+ * Node process stays alive.
32
+ */
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.AIDEN_MCP_BUILD = void 0;
35
+ exports.startStdioMcpServer = startStdioMcpServer;
36
+ // SDK 1.29 ships its public surface via the package `exports` map.
37
+ // Use the SDK's documented import paths verbatim — the wildcard
38
+ // `paths` mapping in tsconfig.json lets the legacy
39
+ // `moduleResolution: "node"` resolver find the type declarations,
40
+ // while at runtime Node's exports-map resolver picks the same files.
41
+ // (An earlier shape — `.../sdk/server/index` — typechecked but failed
42
+ // at runtime: Node's wildcard fallback yielded a path missing the
43
+ // `.js` extension. Phase v4.1-mcp.1 fix.)
44
+ const server_1 = require("@modelcontextprotocol/sdk/server");
45
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
46
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
47
+ const factory_1 = require("../../logger/factory");
48
+ const toolBridge_1 = require("./toolBridge");
49
+ const skillBridge_1 = require("./skillBridge");
50
+ const diagnostics_1 = require("./diagnostics");
51
+ var diagnostics_2 = require("./diagnostics");
52
+ Object.defineProperty(exports, "AIDEN_MCP_BUILD", { enumerable: true, get: function () { return diagnostics_2.AIDEN_MCP_BUILD; } });
53
+ /**
54
+ * Wire the MCP server up over stdio. Returns once the transport is
55
+ * connected; the caller is responsible for keeping the process alive
56
+ * (the `aiden mcp serve` CLI does this with a never-resolving promise).
57
+ */
58
+ async function startStdioMcpServer(opts) {
59
+ const logger = opts.logger ?? (0, factory_1.noopLogger)();
60
+ const env = opts.env ?? (0, toolBridge_1.readToolBridgeEnv)();
61
+ const server = new server_1.Server({ name: 'aiden', version: diagnostics_1.AIDEN_MCP_BUILD }, { capabilities: { tools: {}, resources: {} } });
62
+ const callTool = (0, toolBridge_1.buildToolCallHandler)(opts.registry, opts.toolContext, env, logger);
63
+ // ── tools/list ──────────────────────────────────────────────
64
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
65
+ tools: (0, toolBridge_1.buildToolsList)(opts.registry, env),
66
+ }));
67
+ // ── tools/call ──────────────────────────────────────────────
68
+ // SDK 1.29 typed the response as a discriminated union including a
69
+ // task-style alternative. Aiden returns the non-task `CallToolResult`
70
+ // shape; the cast widens the return type to the union.
71
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
72
+ const name = request.params.name;
73
+ const args = (request.params.arguments ?? {});
74
+ const result = await callTool(name, args);
75
+ return result;
76
+ });
77
+ // ── resources/list ──────────────────────────────────────────
78
+ server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => ({
79
+ resources: await (0, skillBridge_1.buildResourcesList)(opts.skillLoader),
80
+ }));
81
+ // ── resources/read ──────────────────────────────────────────
82
+ server.setRequestHandler(types_js_1.ReadResourceRequestSchema, async (request) => {
83
+ const uri = request.params.uri;
84
+ try {
85
+ const content = await (0, skillBridge_1.readSkillResource)(opts.skillLoader, uri);
86
+ return { contents: [content] };
87
+ }
88
+ catch (err) {
89
+ const message = err instanceof Error ? err.message : String(err);
90
+ logger.warn('mcp resources/read failed', { scope: 'mcp', uri, error: message });
91
+ throw err;
92
+ }
93
+ });
94
+ // ── connect transport ──────────────────────────────────────
95
+ const transport = new stdio_js_1.StdioServerTransport();
96
+ await server.connect(transport);
97
+ // Diagnostics — emitted to stderr (logger in mcp-stdio mode), grep-able
98
+ // from the spawning client's MCP log. Build fingerprint included so the
99
+ // user can verify the running version matches the phase they expected.
100
+ const diag = await (0, diagnostics_1.collectMcpDiagnostics)(opts.registry, opts.skillLoader, env);
101
+ logger.warn(`mcp launched build=${diag.build}`, {
102
+ scope: 'mcp',
103
+ build: diag.build,
104
+ toolsTotal: diag.toolsTotal,
105
+ toolsExposed: diag.toolsExposed,
106
+ skillsTotal: diag.skillsTotal,
107
+ allowDestructive: diag.env.allowDestructive,
108
+ allowlist: diag.env.allowlist,
109
+ });
110
+ return {
111
+ server,
112
+ stop: async () => {
113
+ try {
114
+ await server.close();
115
+ }
116
+ catch { /* already closed */ }
117
+ },
118
+ };
119
+ }
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/mcp/server/toolBridge.ts — Phase v4.1-mcp
10
+ *
11
+ * Bridge between Aiden's `ToolRegistry` and the MCP wire format. The
12
+ * registry already stores schemas in the exact shape MCP wants
13
+ * (`{ name, description, inputSchema: { type: 'object', properties, required? } }`),
14
+ * so this layer is a thin pass-through plus three filters:
15
+ *
16
+ * 1. `mutates` filter — read-only tools by default. Set
17
+ * `AIDEN_MCP_ALLOW_DESTRUCTIVE=1` to expose write/execute tools too.
18
+ * Phase-9 approval engine still gates them at execution time
19
+ * (defense in depth).
20
+ * 2. Allowlist filter — `AIDEN_MCP_TOOL_ALLOWLIST=tool_a,tool_b`
21
+ * restricts the surface to a CSV-named subset. Applied AFTER the
22
+ * mutates filter, so a user cannot allowlist `shell_exec` past the
23
+ * destructive gate without also setting `ALLOW_DESTRUCTIVE=1`.
24
+ * 3. The handler wrapper coerces every dispatch outcome into MCP's
25
+ * `CallToolResult` shape `{ content, isError }`. The agent loop's
26
+ * executor returns `ToolCallResult` whose `.error` field is set when
27
+ * the underlying handler threw or the moat layers refused the call;
28
+ * that becomes `isError: true` here. We NEVER throw out of the
29
+ * handler — protocol-level errors bypass the model's recovery path.
30
+ *
31
+ * The bridge is dependency-injected with the ToolRegistry + ToolContext
32
+ * the runtime already built. It does not own a logger; the stdio server
33
+ * passes one through for the env-config diagnostic line.
34
+ */
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.readToolBridgeEnv = readToolBridgeEnv;
37
+ exports.exposedToolNames = exposedToolNames;
38
+ exports.aidenToolToMCP = aidenToolToMCP;
39
+ exports.buildToolsList = buildToolsList;
40
+ exports.buildToolCallHandler = buildToolCallHandler;
41
+ const factory_1 = require("../../logger/factory");
42
+ function readToolBridgeEnv(env = process.env) {
43
+ const allowDestructive = env.AIDEN_MCP_ALLOW_DESTRUCTIVE === '1' ||
44
+ env.AIDEN_MCP_ALLOW_DESTRUCTIVE === 'true';
45
+ const raw = (env.AIDEN_MCP_TOOL_ALLOWLIST ?? '').trim();
46
+ const allowlist = raw
47
+ ? new Set(raw.split(',').map((s) => s.trim()).filter(Boolean))
48
+ : null;
49
+ return { allowDestructive, allowlist };
50
+ }
51
+ /**
52
+ * Compute the set of tool names exposed under the current env. Pure
53
+ * function over the registry snapshot; safe to call on every
54
+ * `tools/list` request (the registry is built once at boot).
55
+ */
56
+ function exposedToolNames(registry, env) {
57
+ const out = [];
58
+ for (const name of registry.list()) {
59
+ const handler = registry.get(name);
60
+ if (!handler)
61
+ continue;
62
+ if (handler.mutates && !env.allowDestructive)
63
+ continue;
64
+ if (env.allowlist && !env.allowlist.has(name))
65
+ continue;
66
+ out.push(name);
67
+ }
68
+ return out;
69
+ }
70
+ /** Pass-through schema convert. The shapes are already aligned. */
71
+ function aidenToolToMCP(handler) {
72
+ const s = handler.schema;
73
+ return {
74
+ name: s.name,
75
+ description: s.description,
76
+ inputSchema: {
77
+ type: 'object',
78
+ properties: s.inputSchema.properties,
79
+ required: s.inputSchema.required,
80
+ },
81
+ };
82
+ }
83
+ /** Build the tools array advertised on `tools/list`. */
84
+ function buildToolsList(registry, env) {
85
+ const out = [];
86
+ for (const name of exposedToolNames(registry, env)) {
87
+ const handler = registry.get(name);
88
+ if (handler)
89
+ out.push(aidenToolToMCP(handler));
90
+ }
91
+ return out;
92
+ }
93
+ /**
94
+ * Build a `tools/call` handler closed over the registry's executor and
95
+ * the per-process env. Each call:
96
+ *
97
+ * 1. Re-checks exposure (env may have shifted is irrelevant — we read
98
+ * it at server start — but this guards against allowlist drift if
99
+ * a future caller passes a fresh env snapshot).
100
+ * 2. Synthesises a `ToolCallRequest` for the executor; uses a stable
101
+ * id so logs cross-correlate.
102
+ * 3. Maps the executor's `ToolCallResult` to MCP's `{content,isError}`.
103
+ *
104
+ * Failures the executor reports via `result.error` become `isError: true`
105
+ * with the error message in the text payload — the model on the client
106
+ * side reads it and recovers. We never throw protocol-level.
107
+ */
108
+ function buildToolCallHandler(registry, context, env, logger = (0, factory_1.noopLogger)()) {
109
+ const exposed = new Set(exposedToolNames(registry, env));
110
+ const execute = registry.buildExecutor(context);
111
+ return async (name, args) => {
112
+ if (!exposed.has(name)) {
113
+ logger.warn('mcp tools/call refused — tool not exposed', {
114
+ scope: 'mcp',
115
+ tool: name,
116
+ allowDestructive: env.allowDestructive,
117
+ });
118
+ return {
119
+ content: [{
120
+ type: 'text',
121
+ text: JSON.stringify({
122
+ error: `Tool "${name}" is not exposed via MCP.`,
123
+ hint: env.allowDestructive
124
+ ? 'Tool may not be in AIDEN_MCP_TOOL_ALLOWLIST.'
125
+ : 'Set AIDEN_MCP_ALLOW_DESTRUCTIVE=1 to include mutating tools.',
126
+ }),
127
+ }],
128
+ isError: true,
129
+ };
130
+ }
131
+ const id = `mcp-${name}-${Date.now().toString(36)}`;
132
+ const call = { id, name, arguments: args ?? {} };
133
+ let result;
134
+ try {
135
+ result = await execute(call);
136
+ }
137
+ catch (err) {
138
+ // The executor itself swallows handler exceptions, but a bug in
139
+ // the executor (or a moat layer hard-throw) would land here.
140
+ const message = err instanceof Error ? err.message : String(err);
141
+ logger.error('mcp tools/call executor crashed', {
142
+ scope: 'mcp',
143
+ tool: name,
144
+ error: message,
145
+ });
146
+ return {
147
+ content: [{ type: 'text', text: `Internal error: ${message}` }],
148
+ isError: true,
149
+ };
150
+ }
151
+ if (result.error) {
152
+ return {
153
+ content: [{
154
+ type: 'text',
155
+ text: JSON.stringify({ error: result.error }, null, 2),
156
+ }],
157
+ isError: true,
158
+ };
159
+ }
160
+ const text = typeof result.result === 'string'
161
+ ? result.result
162
+ : JSON.stringify(result.result ?? null, null, 2);
163
+ return {
164
+ content: [{ type: 'text', text }],
165
+ isError: false,
166
+ };
167
+ };
168
+ }
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod). Licensed under AGPL-3.0.
4
+ *
5
+ * Aiden — local-first agent.
6
+ */
7
+ /**
8
+ * core/v4/platformPaths.ts — Phase v4.1-cross-platform
9
+ *
10
+ * Cross-platform helpers for path normalisation, home expansion,
11
+ * shell selection, and writability checks. Centralising these so
12
+ * every other module can import a single canonical surface — and
13
+ * so the path audit has one place to scan for OS-specific bugs.
14
+ *
15
+ * Most of the work delegates to Node's built-in `path` module; the
16
+ * value-add is the small bit of glue (`expandHome`, `platformShell`,
17
+ * `isWritable`) that's easy to get wrong if redone ad-hoc.
18
+ */
19
+ var __importDefault = (this && this.__importDefault) || function (mod) {
20
+ return (mod && mod.__esModule) ? mod : { "default": mod };
21
+ };
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.normalizePath = normalizePath;
24
+ exports.joinPaths = joinPaths;
25
+ exports.expandHome = expandHome;
26
+ exports.platformShell = platformShell;
27
+ exports.isWritable = isWritable;
28
+ exports.isReadable = isReadable;
29
+ exports.classifyPlatform = classifyPlatform;
30
+ const node_path_1 = __importDefault(require("node:path"));
31
+ const node_os_1 = __importDefault(require("node:os"));
32
+ const node_fs_1 = __importDefault(require("node:fs"));
33
+ /** Idiomatic platform-aware path normalisation. */
34
+ function normalizePath(p) {
35
+ if (typeof p !== 'string' || p.length === 0)
36
+ return p;
37
+ return node_path_1.default.normalize(p);
38
+ }
39
+ /** Re-export `path.join` under a stable name so callers don't have to import path directly. */
40
+ function joinPaths(...parts) {
41
+ return node_path_1.default.join(...parts);
42
+ }
43
+ /**
44
+ * Expand `~/` and `~` to the current user's home directory. Pass
45
+ * paths through unchanged if they don't start with the tilde token.
46
+ *
47
+ * expandHome('~/foo') → `${os.homedir()}/foo`
48
+ * expandHome('~') → `${os.homedir()}`
49
+ * expandHome('/abs/p') → '/abs/p'
50
+ * expandHome('./rel') → './rel'
51
+ */
52
+ function expandHome(p) {
53
+ if (typeof p !== 'string' || p.length === 0)
54
+ return p;
55
+ if (p === '~')
56
+ return node_os_1.default.homedir();
57
+ if (p.startsWith('~/') || p.startsWith('~\\')) {
58
+ return node_path_1.default.join(node_os_1.default.homedir(), p.slice(2));
59
+ }
60
+ return p;
61
+ }
62
+ function platformShell() {
63
+ if (process.platform === 'win32')
64
+ return 'powershell';
65
+ // POSIX: prefer bash when present, otherwise sh. We don't probe at
66
+ // runtime — bash is ubiquitous on macOS/Linux and `sh` is the
67
+ // POSIX-mandated fallback. Callers that need certainty can call
68
+ // `which bash` themselves.
69
+ return 'bash';
70
+ }
71
+ /**
72
+ * Cross-platform writability check. Returns true if the path exists
73
+ * AND the current process can write to it, false otherwise. Catches
74
+ * EACCES/EPERM/ENOENT silently — never throws.
75
+ */
76
+ function isWritable(p) {
77
+ try {
78
+ node_fs_1.default.accessSync(p, node_fs_1.default.constants.W_OK);
79
+ return true;
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ }
85
+ /**
86
+ * Cross-platform readability check — paired with isWritable for
87
+ * doctor's filesystem audit.
88
+ */
89
+ function isReadable(p) {
90
+ try {
91
+ node_fs_1.default.accessSync(p, node_fs_1.default.constants.R_OK);
92
+ return true;
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ }
98
+ function classifyPlatform() {
99
+ switch (process.platform) {
100
+ case 'win32': return 'win32';
101
+ case 'darwin': return 'darwin';
102
+ case 'linux': return 'linux';
103
+ default: return 'other';
104
+ }
105
+ }