aiden-runtime 4.0.2 → 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 (108) 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 +421 -5
  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 +256 -55
  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 +16 -1
  18. package/dist/cli/v4/commands/mcp.js +358 -0
  19. package/dist/cli/v4/commands/show.js +43 -0
  20. package/dist/cli/v4/commands/skills.js +169 -4
  21. package/dist/cli/v4/commands/status.js +84 -0
  22. package/dist/cli/v4/commands/subagent.js +78 -0
  23. package/dist/cli/v4/commands/verbose.js +1 -1
  24. package/dist/cli/v4/commands/voice.js +218 -0
  25. package/dist/cli/v4/cronCli.js +103 -0
  26. package/dist/cli/v4/display.js +297 -13
  27. package/dist/cli/v4/doctor.js +41 -0
  28. package/dist/cli/v4/envSources.js +105 -0
  29. package/dist/cli/v4/ghostMatch.js +74 -0
  30. package/dist/cli/v4/historyStore.js +163 -0
  31. package/dist/cli/v4/pasteCompression.js +124 -0
  32. package/dist/cli/v4/pasteIntercept.js +203 -0
  33. package/dist/cli/v4/replyRenderer.js +209 -0
  34. package/dist/cli/v4/resizeGuard.js +92 -0
  35. package/dist/cli/v4/shellInterpolation.js +139 -0
  36. package/dist/cli/v4/skinEngine.js +21 -1
  37. package/dist/cli/v4/streamingPrefix.js +121 -0
  38. package/dist/cli/v4/syntaxHighlight.js +345 -0
  39. package/dist/cli/v4/table.js +216 -0
  40. package/dist/cli/v4/themeDetect.js +81 -0
  41. package/dist/cli/v4/uiBuild.js +74 -0
  42. package/dist/cli/v4/voiceCli.js +113 -0
  43. package/dist/cli/v4/voicePromptApi.js +196 -0
  44. package/dist/core/channels/discord.js +16 -10
  45. package/dist/core/channels/email.js +13 -9
  46. package/dist/core/channels/imessage.js +13 -9
  47. package/dist/core/channels/manager.js +25 -7
  48. package/dist/core/channels/pdf-extract.js +180 -0
  49. package/dist/core/channels/photo-vision.js +157 -0
  50. package/dist/core/channels/signal.js +11 -7
  51. package/dist/core/channels/slack.js +13 -10
  52. package/dist/core/channels/telegram-commands.js +154 -0
  53. package/dist/core/channels/telegram-groups.js +198 -0
  54. package/dist/core/channels/telegram-rate-limit.js +124 -0
  55. package/dist/core/channels/telegram.js +1980 -0
  56. package/dist/core/channels/twilio.js +11 -7
  57. package/dist/core/channels/webhook.js +9 -5
  58. package/dist/core/channels/whatsapp.js +15 -11
  59. package/dist/core/channels/whisper-transcribe.js +163 -0
  60. package/dist/core/cronManager.js +33 -294
  61. package/dist/core/gateway.js +29 -8
  62. package/dist/core/playwrightBridge.js +90 -0
  63. package/dist/core/v4/aidenAgent.js +35 -0
  64. package/dist/core/v4/auxiliaryClient.js +2 -2
  65. package/dist/core/v4/cron/atomicWrite.js +18 -4
  66. package/dist/core/v4/cron/cronExecute.js +300 -0
  67. package/dist/core/v4/cron/cronManager.js +502 -0
  68. package/dist/core/v4/cron/cronState.js +314 -0
  69. package/dist/core/v4/cron/cronTick.js +90 -0
  70. package/dist/core/v4/cron/diagnostics.js +104 -0
  71. package/dist/core/v4/cron/graceWindow.js +79 -0
  72. package/dist/core/v4/logger/factory.js +110 -0
  73. package/dist/core/v4/logger/index.js +22 -0
  74. package/dist/core/v4/logger/logger.js +101 -0
  75. package/dist/core/v4/logger/sinks/fileSink.js +110 -0
  76. package/dist/core/v4/logger/sinks/multiSink.js +43 -0
  77. package/dist/core/v4/logger/sinks/nullSink.js +53 -0
  78. package/dist/core/v4/logger/sinks/stdSink.js +81 -0
  79. package/dist/core/v4/mcp/server/diagnostics.js +40 -0
  80. package/dist/core/v4/mcp/server/skillBridge.js +94 -0
  81. package/dist/core/v4/mcp/server/stdioServer.js +119 -0
  82. package/dist/core/v4/mcp/server/toolBridge.js +168 -0
  83. package/dist/core/v4/platformPaths.js +105 -0
  84. package/dist/core/v4/providerFallback.js +25 -0
  85. package/dist/core/v4/skillLoader.js +21 -5
  86. package/dist/core/v4/skillMining/candidateStore.js +164 -0
  87. package/dist/core/v4/skillMining/extractorPrompt.js +111 -0
  88. package/dist/core/v4/skillMining/proposalBuilder.js +139 -0
  89. package/dist/core/v4/skillMining/skillMiner.js +191 -0
  90. package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
  91. package/dist/core/v4/subagent/budget.js +76 -0
  92. package/dist/core/v4/subagent/diagnostics.js +22 -0
  93. package/dist/core/v4/subagent/fanout.js +216 -0
  94. package/dist/core/v4/subagent/merger.js +148 -0
  95. package/dist/core/v4/subagent/providerRotation.js +54 -0
  96. package/dist/core/v4/voice/audioStream.js +373 -0
  97. package/dist/core/v4/voice/cliVoice.js +393 -0
  98. package/dist/core/v4/voice/diagnostics.js +66 -0
  99. package/dist/core/v4/voice/ttsStream.js +193 -0
  100. package/dist/core/version.js +1 -1
  101. package/dist/core/visionAnalyze.js +291 -90
  102. package/dist/core/voice/audio.js +61 -5
  103. package/dist/core/voice/audioBackend.js +134 -0
  104. package/dist/core/voice/stt.js +61 -6
  105. package/dist/core/voice/tts.js +19 -3
  106. package/dist/tools/v4/index.js +32 -1
  107. package/dist/tools/v4/subagent/subagentFanout.js +166 -0
  108. 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
- // Honour the cache when available so we don't re-walk for a
78
- // single-skill lookup. Falls through to disk on a miss so newly
79
- // dropped skills still resolve in long-running processes (the cache
80
- // never sees them otherwise that's the whole point of `invalidate`).
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 === 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;