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.
- package/README.md +11 -7
- package/config/hardware.json +2 -2
- package/dist/api/server.js +50 -52
- package/dist/cli/v4/aidenCLI.js +513 -14
- package/dist/cli/v4/aidenPrompt.js +317 -0
- package/dist/cli/v4/box.js +105 -39
- package/dist/cli/v4/callbacks.js +39 -6
- package/dist/cli/v4/chatSession.js +269 -52
- package/dist/cli/v4/citationFooter.js +97 -0
- package/dist/cli/v4/commands/channel.js +656 -0
- package/dist/cli/v4/commands/clear.js +1 -1
- package/dist/cli/v4/commands/compress.js +1 -1
- package/dist/cli/v4/commands/cron.js +44 -16
- package/dist/cli/v4/commands/fanout.js +236 -0
- package/dist/cli/v4/commands/help.js +15 -4
- package/dist/cli/v4/commands/history.js +84 -0
- package/dist/cli/v4/commands/index.js +19 -1
- package/dist/cli/v4/commands/mcp.js +358 -0
- package/dist/cli/v4/commands/setup.js +34 -0
- package/dist/cli/v4/commands/show.js +43 -0
- package/dist/cli/v4/commands/skills.js +169 -4
- package/dist/cli/v4/commands/status.js +84 -0
- package/dist/cli/v4/commands/subagent.js +78 -0
- package/dist/cli/v4/commands/verbose.js +1 -1
- package/dist/cli/v4/commands/voice.js +218 -0
- package/dist/cli/v4/cronCli.js +103 -0
- package/dist/cli/v4/display.js +300 -14
- package/dist/cli/v4/doctor.js +41 -0
- package/dist/cli/v4/envSources.js +105 -0
- package/dist/cli/v4/ghostMatch.js +74 -0
- package/dist/cli/v4/historyStore.js +163 -0
- package/dist/cli/v4/pasteCompression.js +124 -0
- package/dist/cli/v4/pasteIntercept.js +203 -0
- package/dist/cli/v4/replyRenderer.js +209 -0
- package/dist/cli/v4/resizeGuard.js +92 -0
- package/dist/cli/v4/setupWizard.js +466 -232
- package/dist/cli/v4/shellInterpolation.js +139 -0
- package/dist/cli/v4/skinEngine.js +21 -1
- package/dist/cli/v4/streamingPrefix.js +121 -0
- package/dist/cli/v4/syntaxHighlight.js +345 -0
- package/dist/cli/v4/table.js +216 -0
- package/dist/cli/v4/themeDetect.js +81 -0
- package/dist/cli/v4/uiBuild.js +74 -0
- package/dist/cli/v4/voiceCli.js +113 -0
- package/dist/cli/v4/voicePromptApi.js +196 -0
- package/dist/core/channels/discord.js +16 -10
- package/dist/core/channels/email.js +13 -9
- package/dist/core/channels/imessage.js +13 -9
- package/dist/core/channels/manager.js +25 -7
- package/dist/core/channels/pdf-extract.js +180 -0
- package/dist/core/channels/photo-vision.js +157 -0
- package/dist/core/channels/signal.js +11 -7
- package/dist/core/channels/slack.js +13 -10
- package/dist/core/channels/telegram-commands.js +154 -0
- package/dist/core/channels/telegram-groups.js +198 -0
- package/dist/core/channels/telegram-rate-limit.js +124 -0
- package/dist/core/channels/telegram.js +1980 -0
- package/dist/core/channels/twilio.js +11 -7
- package/dist/core/channels/webhook.js +9 -5
- package/dist/core/channels/whatsapp.js +15 -11
- package/dist/core/channels/whisper-transcribe.js +163 -0
- package/dist/core/cronManager.js +33 -294
- package/dist/core/gateway.js +29 -8
- package/dist/core/playwrightBridge.js +90 -0
- package/dist/core/v4/aidenAgent.js +35 -0
- package/dist/core/v4/auxiliaryClient.js +2 -2
- package/dist/core/v4/cron/atomicWrite.js +18 -4
- package/dist/core/v4/cron/cronExecute.js +300 -0
- package/dist/core/v4/cron/cronManager.js +502 -0
- package/dist/core/v4/cron/cronState.js +314 -0
- package/dist/core/v4/cron/cronTick.js +90 -0
- package/dist/core/v4/cron/diagnostics.js +104 -0
- package/dist/core/v4/cron/graceWindow.js +79 -0
- package/dist/core/v4/firstRun/providerDetection.js +287 -0
- package/dist/core/v4/logger/factory.js +110 -0
- package/dist/core/v4/logger/index.js +22 -0
- package/dist/core/v4/logger/logger.js +101 -0
- package/dist/core/v4/logger/sinks/fileSink.js +110 -0
- package/dist/core/v4/logger/sinks/multiSink.js +43 -0
- package/dist/core/v4/logger/sinks/nullSink.js +53 -0
- package/dist/core/v4/logger/sinks/stdSink.js +81 -0
- package/dist/core/v4/mcp/server/diagnostics.js +40 -0
- package/dist/core/v4/mcp/server/skillBridge.js +94 -0
- package/dist/core/v4/mcp/server/stdioServer.js +119 -0
- package/dist/core/v4/mcp/server/toolBridge.js +168 -0
- package/dist/core/v4/platformPaths.js +105 -0
- package/dist/core/v4/providerFallback.js +25 -0
- package/dist/core/v4/skillLoader.js +21 -5
- package/dist/core/v4/skillMining/candidateStore.js +164 -0
- package/dist/core/v4/skillMining/extractorPrompt.js +111 -0
- package/dist/core/v4/skillMining/proposalBuilder.js +139 -0
- package/dist/core/v4/skillMining/skillMiner.js +191 -0
- package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
- package/dist/core/v4/subagent/budget.js +76 -0
- package/dist/core/v4/subagent/diagnostics.js +22 -0
- package/dist/core/v4/subagent/fanout.js +216 -0
- package/dist/core/v4/subagent/merger.js +148 -0
- package/dist/core/v4/subagent/providerRotation.js +54 -0
- package/dist/core/v4/voice/audioStream.js +373 -0
- package/dist/core/v4/voice/cliVoice.js +393 -0
- package/dist/core/v4/voice/diagnostics.js +66 -0
- package/dist/core/v4/voice/ttsStream.js +193 -0
- package/dist/core/version.js +1 -1
- package/dist/core/visionAnalyze.js +291 -90
- package/dist/core/voice/audio.js +61 -5
- package/dist/core/voice/audioBackend.js +134 -0
- package/dist/core/voice/stt.js +61 -6
- package/dist/core/voice/tts.js +19 -3
- package/dist/providers/v4/nullAdapter.js +58 -0
- package/dist/tools/v4/index.js +32 -1
- package/dist/tools/v4/subagent/subagentFanout.js +166 -0
- 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
|
+
}
|