aiden-runtime 4.0.2 → 4.1.1
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 +19 -11
- package/config/hardware.json +2 -2
- package/dist/api/server.js +50 -52
- package/dist/cli/v4/aidenCLI.js +424 -7
- 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 +256 -55
- 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 +16 -1
- package/dist/cli/v4/commands/mcp.js +358 -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 +297 -13
- package/dist/cli/v4/doctor.js +102 -1
- package/dist/cli/v4/doctorLiveness.js +329 -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/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/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 +118 -0
- package/dist/core/v4/skillMining/proposalBuilder.js +140 -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/moat/dangerousPatterns.js +1 -1
- package/dist/providers/v4/codexResponsesAdapter.js +7 -2
- package/dist/providers/v4/errors.js +51 -1
- package/dist/providers/v4/ollamaPromptToolsAdapter.js +9 -2
- package/dist/tools/v4/index.js +32 -1
- package/dist/tools/v4/subagent/subagentFanout.js +190 -0
- package/package.json +11 -2
|
@@ -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
|
+
}
|
|
@@ -412,6 +412,31 @@ class FallbackAdapter {
|
|
|
412
412
|
}
|
|
413
413
|
this.activeSlotId = slotId;
|
|
414
414
|
}
|
|
415
|
+
/**
|
|
416
|
+
* Phase v4.1-subagent — clone the adapter with FRESH mutable state
|
|
417
|
+
* but SHARED slot configs. Subagent fanout uses this so each
|
|
418
|
+
* subagent gets its own rate-limit bookkeeping (slotState,
|
|
419
|
+
* cooldownUntil, requestCount, activeSlotId all reset) without
|
|
420
|
+
* paying for slot-config rebuilds. The slot list and the cooldownMs
|
|
421
|
+
* / clock / observer callbacks are read-only and safely shared
|
|
422
|
+
* across clones.
|
|
423
|
+
*
|
|
424
|
+
* One subagent hitting a 429 marks ITS clone's slot as cooled-down;
|
|
425
|
+
* the parent and sibling clones still see the slot as available.
|
|
426
|
+
* That's a deliberate tradeoff: tighter contention on the same
|
|
427
|
+
* provider key, but isolation prevents one slow subagent from
|
|
428
|
+
* starving siblings via parent-side cooldown state.
|
|
429
|
+
*/
|
|
430
|
+
clone() {
|
|
431
|
+
return new FallbackAdapter({
|
|
432
|
+
apiMode: this.apiMode,
|
|
433
|
+
slots: this.slots,
|
|
434
|
+
cooldownMs: this.cooldownMs,
|
|
435
|
+
now: this.clock,
|
|
436
|
+
onRateLimit: this.onRateLimit,
|
|
437
|
+
onFallback: this.onFallback,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
415
440
|
/**
|
|
416
441
|
* Diagnostic snapshot for `/providers`. Per-slot cooldown is reported
|
|
417
442
|
* in seconds remaining (0 when the slot is fresh) so the slash command
|
|
@@ -74,15 +74,31 @@ class SkillLoader {
|
|
|
74
74
|
return { ...this.lastCounts, skippedPaths: [...this.lastCounts.skippedPaths] };
|
|
75
75
|
}
|
|
76
76
|
async load(name) {
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
77
|
+
// Phase v4.1-cross-platform: case-insensitive cache lookup so a
|
|
78
|
+
// skill registered as `WebSearch` resolves the same on Linux
|
|
79
|
+
// (case-sensitive FS) as on Windows (case-insensitive FS). The
|
|
80
|
+
// ON-DISK directory name still has to match in case for the
|
|
81
|
+
// disk-fallback path to work, so we ALSO loadAll first when the
|
|
82
|
+
// case-insensitive cache hit succeeds — that gives us the actual
|
|
83
|
+
// file-system path and the loader can serve it from cache.
|
|
84
|
+
const target = name.toLowerCase();
|
|
85
|
+
if (!this.cache) {
|
|
86
|
+
// Trigger a one-time scan so case-insensitive lookup has data.
|
|
87
|
+
try {
|
|
88
|
+
await this.loadAll();
|
|
89
|
+
}
|
|
90
|
+
catch { /* ignore — fall through to disk */ }
|
|
91
|
+
}
|
|
81
92
|
if (this.cache) {
|
|
82
|
-
const hit = this.cache.find((s) => s.frontmatter.name ===
|
|
93
|
+
const hit = this.cache.find((s) => (s.frontmatter.name ?? '').toLowerCase() === target);
|
|
83
94
|
if (hit)
|
|
84
95
|
return hit;
|
|
85
96
|
}
|
|
97
|
+
// Disk fallback: try the verbatim name, then a lowercase variant
|
|
98
|
+
// (covers case where the cache failed to populate but the on-disk
|
|
99
|
+
// dir uses lowercase). On case-sensitive filesystems neither will
|
|
100
|
+
// hit if the directory case doesn't match the requested name —
|
|
101
|
+
// that's fine, the cache lookup above is the authoritative path.
|
|
86
102
|
const dirSkill = node_path_1.default.join(this.paths.skillsDir, name, 'SKILL.md');
|
|
87
103
|
const fileSkill = node_path_1.default.join(this.paths.skillsDir, `${name}.md`);
|
|
88
104
|
return (await this.tryParse(dirSkill)) ?? (await this.tryParse(fileSkill));
|
|
@@ -0,0 +1,164 @@
|
|
|
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/skillMining/candidateStore.ts — Phase v4.1-skill-mining
|
|
10
|
+
*
|
|
11
|
+
* Atomic JSON queue for mined skill candidates and rejection
|
|
12
|
+
* fingerprints. Two files under `<aidenHome>/skills/learned/`:
|
|
13
|
+
*
|
|
14
|
+
* - `.candidates.json` — pending review queue, append-only via
|
|
15
|
+
* this module. `/skills review` reads it; `/skills accept`
|
|
16
|
+
* and `/skills reject` mutate it.
|
|
17
|
+
*
|
|
18
|
+
* - `.rejected.json` — fingerprint set the dedup gate consults
|
|
19
|
+
* so a user-rejected workflow doesn't get re-proposed on the
|
|
20
|
+
* next matching turn. Keyed by fingerprint, not id, so the
|
|
21
|
+
* dedup survives across the candidate's lifecycle.
|
|
22
|
+
*
|
|
23
|
+
* Concurrency: a per-process write queue serialises every mutation
|
|
24
|
+
* (mirrors the BundledManifest pattern at
|
|
25
|
+
* `core/v4/skillBundledManifest.ts:50-53`). Cross-process safety
|
|
26
|
+
* is non-goal — the agent is a single-user CLI.
|
|
27
|
+
*
|
|
28
|
+
* Atomicity: writes go to a sibling `.tmp` file then `rename` over
|
|
29
|
+
* the live path. Windows rename is atomic on the same volume so a
|
|
30
|
+
* crash mid-write never leaves a partial JSON file.
|
|
31
|
+
*/
|
|
32
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
33
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
34
|
+
};
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CandidateStore = void 0;
|
|
37
|
+
const node_fs_1 = require("node:fs");
|
|
38
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
39
|
+
const paths_1 = require("../paths");
|
|
40
|
+
const ENVELOPE_VERSION = 1;
|
|
41
|
+
class CandidateStore {
|
|
42
|
+
constructor() {
|
|
43
|
+
this.writeQueue = Promise.resolve();
|
|
44
|
+
}
|
|
45
|
+
/** `<aidenHome>/skills/learned/`. */
|
|
46
|
+
dir() {
|
|
47
|
+
return node_path_1.default.join((0, paths_1.resolveAidenPaths)().skillsDir, 'learned');
|
|
48
|
+
}
|
|
49
|
+
candidatesPath() {
|
|
50
|
+
return node_path_1.default.join(this.dir(), '.candidates.json');
|
|
51
|
+
}
|
|
52
|
+
rejectedPath() {
|
|
53
|
+
return node_path_1.default.join(this.dir(), '.rejected.json');
|
|
54
|
+
}
|
|
55
|
+
async ensureDir() {
|
|
56
|
+
await node_fs_1.promises.mkdir(this.dir(), { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
async readJson(p, fallback) {
|
|
59
|
+
try {
|
|
60
|
+
const raw = await node_fs_1.promises.readFile(p, 'utf8');
|
|
61
|
+
return JSON.parse(raw);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return fallback;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Atomic `tmp` + rename. */
|
|
68
|
+
async writeJsonAtomic(p, data) {
|
|
69
|
+
await this.ensureDir();
|
|
70
|
+
const tmp = `${p}.tmp`;
|
|
71
|
+
await node_fs_1.promises.writeFile(tmp, JSON.stringify(data, null, 2), 'utf8');
|
|
72
|
+
await node_fs_1.promises.rename(tmp, p);
|
|
73
|
+
}
|
|
74
|
+
/** Append a candidate to the pending queue. Returns the assigned id. */
|
|
75
|
+
async append(candidate) {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
this.writeQueue = this.writeQueue.then(async () => {
|
|
78
|
+
try {
|
|
79
|
+
const env = await this.readJson(this.candidatesPath(), {
|
|
80
|
+
version: ENVELOPE_VERSION,
|
|
81
|
+
candidates: [],
|
|
82
|
+
});
|
|
83
|
+
env.version = ENVELOPE_VERSION;
|
|
84
|
+
env.candidates.push(candidate);
|
|
85
|
+
await this.writeJsonAtomic(this.candidatesPath(), env);
|
|
86
|
+
resolve(candidate);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
reject(err);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/** Read the full pending queue, newest last. */
|
|
95
|
+
async list() {
|
|
96
|
+
const env = await this.readJson(this.candidatesPath(), {
|
|
97
|
+
version: ENVELOPE_VERSION,
|
|
98
|
+
candidates: [],
|
|
99
|
+
});
|
|
100
|
+
return env.candidates ?? [];
|
|
101
|
+
}
|
|
102
|
+
/** Fetch a single candidate by id, or undefined. */
|
|
103
|
+
async get(id) {
|
|
104
|
+
const all = await this.list();
|
|
105
|
+
return all.find((c) => c.id === id);
|
|
106
|
+
}
|
|
107
|
+
/** Remove a candidate by id. No-op if missing. */
|
|
108
|
+
async remove(id) {
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
this.writeQueue = this.writeQueue.then(async () => {
|
|
111
|
+
try {
|
|
112
|
+
const env = await this.readJson(this.candidatesPath(), {
|
|
113
|
+
version: ENVELOPE_VERSION,
|
|
114
|
+
candidates: [],
|
|
115
|
+
});
|
|
116
|
+
env.version = ENVELOPE_VERSION;
|
|
117
|
+
env.candidates = env.candidates.filter((c) => c.id !== id);
|
|
118
|
+
await this.writeJsonAtomic(this.candidatesPath(), env);
|
|
119
|
+
resolve();
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
reject(err);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/** Append a rejection fingerprint (with optional reason). */
|
|
128
|
+
async recordRejection(fingerprint, reason) {
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
this.writeQueue = this.writeQueue.then(async () => {
|
|
131
|
+
try {
|
|
132
|
+
const env = await this.readJson(this.rejectedPath(), {
|
|
133
|
+
version: ENVELOPE_VERSION,
|
|
134
|
+
rejected: [],
|
|
135
|
+
});
|
|
136
|
+
env.version = ENVELOPE_VERSION;
|
|
137
|
+
env.rejected.push({
|
|
138
|
+
fingerprint,
|
|
139
|
+
reason,
|
|
140
|
+
rejectedAt: new Date().toISOString(),
|
|
141
|
+
});
|
|
142
|
+
await this.writeJsonAtomic(this.rejectedPath(), env);
|
|
143
|
+
resolve();
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
reject(err);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/** Return the set of rejected fingerprints (for dedup). */
|
|
152
|
+
async loadRejected() {
|
|
153
|
+
const env = await this.readJson(this.rejectedPath(), {
|
|
154
|
+
version: ENVELOPE_VERSION,
|
|
155
|
+
rejected: [],
|
|
156
|
+
});
|
|
157
|
+
return new Set((env.rejected ?? []).map((r) => r.fingerprint));
|
|
158
|
+
}
|
|
159
|
+
/** Test/reset hook: drop the in-process write queue. Disk untouched. */
|
|
160
|
+
_resetForTests() {
|
|
161
|
+
this.writeQueue = Promise.resolve();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
exports.CandidateStore = CandidateStore;
|