aiden-runtime 4.0.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +11 -7
  2. package/config/hardware.json +2 -2
  3. package/dist/api/server.js +50 -52
  4. package/dist/cli/v4/aidenCLI.js +513 -14
  5. package/dist/cli/v4/aidenPrompt.js +317 -0
  6. package/dist/cli/v4/box.js +105 -39
  7. package/dist/cli/v4/callbacks.js +39 -6
  8. package/dist/cli/v4/chatSession.js +269 -52
  9. package/dist/cli/v4/citationFooter.js +97 -0
  10. package/dist/cli/v4/commands/channel.js +656 -0
  11. package/dist/cli/v4/commands/clear.js +1 -1
  12. package/dist/cli/v4/commands/compress.js +1 -1
  13. package/dist/cli/v4/commands/cron.js +44 -16
  14. package/dist/cli/v4/commands/fanout.js +236 -0
  15. package/dist/cli/v4/commands/help.js +15 -4
  16. package/dist/cli/v4/commands/history.js +84 -0
  17. package/dist/cli/v4/commands/index.js +19 -1
  18. package/dist/cli/v4/commands/mcp.js +358 -0
  19. package/dist/cli/v4/commands/setup.js +34 -0
  20. package/dist/cli/v4/commands/show.js +43 -0
  21. package/dist/cli/v4/commands/skills.js +169 -4
  22. package/dist/cli/v4/commands/status.js +84 -0
  23. package/dist/cli/v4/commands/subagent.js +78 -0
  24. package/dist/cli/v4/commands/verbose.js +1 -1
  25. package/dist/cli/v4/commands/voice.js +218 -0
  26. package/dist/cli/v4/cronCli.js +103 -0
  27. package/dist/cli/v4/display.js +300 -14
  28. package/dist/cli/v4/doctor.js +41 -0
  29. package/dist/cli/v4/envSources.js +105 -0
  30. package/dist/cli/v4/ghostMatch.js +74 -0
  31. package/dist/cli/v4/historyStore.js +163 -0
  32. package/dist/cli/v4/pasteCompression.js +124 -0
  33. package/dist/cli/v4/pasteIntercept.js +203 -0
  34. package/dist/cli/v4/replyRenderer.js +209 -0
  35. package/dist/cli/v4/resizeGuard.js +92 -0
  36. package/dist/cli/v4/setupWizard.js +466 -232
  37. package/dist/cli/v4/shellInterpolation.js +139 -0
  38. package/dist/cli/v4/skinEngine.js +21 -1
  39. package/dist/cli/v4/streamingPrefix.js +121 -0
  40. package/dist/cli/v4/syntaxHighlight.js +345 -0
  41. package/dist/cli/v4/table.js +216 -0
  42. package/dist/cli/v4/themeDetect.js +81 -0
  43. package/dist/cli/v4/uiBuild.js +74 -0
  44. package/dist/cli/v4/voiceCli.js +113 -0
  45. package/dist/cli/v4/voicePromptApi.js +196 -0
  46. package/dist/core/channels/discord.js +16 -10
  47. package/dist/core/channels/email.js +13 -9
  48. package/dist/core/channels/imessage.js +13 -9
  49. package/dist/core/channels/manager.js +25 -7
  50. package/dist/core/channels/pdf-extract.js +180 -0
  51. package/dist/core/channels/photo-vision.js +157 -0
  52. package/dist/core/channels/signal.js +11 -7
  53. package/dist/core/channels/slack.js +13 -10
  54. package/dist/core/channels/telegram-commands.js +154 -0
  55. package/dist/core/channels/telegram-groups.js +198 -0
  56. package/dist/core/channels/telegram-rate-limit.js +124 -0
  57. package/dist/core/channels/telegram.js +1980 -0
  58. package/dist/core/channels/twilio.js +11 -7
  59. package/dist/core/channels/webhook.js +9 -5
  60. package/dist/core/channels/whatsapp.js +15 -11
  61. package/dist/core/channels/whisper-transcribe.js +163 -0
  62. package/dist/core/cronManager.js +33 -294
  63. package/dist/core/gateway.js +29 -8
  64. package/dist/core/playwrightBridge.js +90 -0
  65. package/dist/core/v4/aidenAgent.js +35 -0
  66. package/dist/core/v4/auxiliaryClient.js +2 -2
  67. package/dist/core/v4/cron/atomicWrite.js +18 -4
  68. package/dist/core/v4/cron/cronExecute.js +300 -0
  69. package/dist/core/v4/cron/cronManager.js +502 -0
  70. package/dist/core/v4/cron/cronState.js +314 -0
  71. package/dist/core/v4/cron/cronTick.js +90 -0
  72. package/dist/core/v4/cron/diagnostics.js +104 -0
  73. package/dist/core/v4/cron/graceWindow.js +79 -0
  74. package/dist/core/v4/firstRun/providerDetection.js +287 -0
  75. package/dist/core/v4/logger/factory.js +110 -0
  76. package/dist/core/v4/logger/index.js +22 -0
  77. package/dist/core/v4/logger/logger.js +101 -0
  78. package/dist/core/v4/logger/sinks/fileSink.js +110 -0
  79. package/dist/core/v4/logger/sinks/multiSink.js +43 -0
  80. package/dist/core/v4/logger/sinks/nullSink.js +53 -0
  81. package/dist/core/v4/logger/sinks/stdSink.js +81 -0
  82. package/dist/core/v4/mcp/server/diagnostics.js +40 -0
  83. package/dist/core/v4/mcp/server/skillBridge.js +94 -0
  84. package/dist/core/v4/mcp/server/stdioServer.js +119 -0
  85. package/dist/core/v4/mcp/server/toolBridge.js +168 -0
  86. package/dist/core/v4/platformPaths.js +105 -0
  87. package/dist/core/v4/providerFallback.js +25 -0
  88. package/dist/core/v4/skillLoader.js +21 -5
  89. package/dist/core/v4/skillMining/candidateStore.js +164 -0
  90. package/dist/core/v4/skillMining/extractorPrompt.js +111 -0
  91. package/dist/core/v4/skillMining/proposalBuilder.js +139 -0
  92. package/dist/core/v4/skillMining/skillMiner.js +191 -0
  93. package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
  94. package/dist/core/v4/subagent/budget.js +76 -0
  95. package/dist/core/v4/subagent/diagnostics.js +22 -0
  96. package/dist/core/v4/subagent/fanout.js +216 -0
  97. package/dist/core/v4/subagent/merger.js +148 -0
  98. package/dist/core/v4/subagent/providerRotation.js +54 -0
  99. package/dist/core/v4/voice/audioStream.js +373 -0
  100. package/dist/core/v4/voice/cliVoice.js +393 -0
  101. package/dist/core/v4/voice/diagnostics.js +66 -0
  102. package/dist/core/v4/voice/ttsStream.js +193 -0
  103. package/dist/core/version.js +1 -1
  104. package/dist/core/visionAnalyze.js +291 -90
  105. package/dist/core/voice/audio.js +61 -5
  106. package/dist/core/voice/audioBackend.js +134 -0
  107. package/dist/core/voice/stt.js +61 -6
  108. package/dist/core/voice/tts.js +19 -3
  109. package/dist/providers/v4/nullAdapter.js +58 -0
  110. package/dist/tools/v4/index.js +32 -1
  111. package/dist/tools/v4/subagent/subagentFanout.js +166 -0
  112. package/package.json +11 -2
@@ -0,0 +1,358 @@
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
+ * cli/v4/commands/mcp.ts — Phase v4.1-mcp
10
+ *
11
+ * `aiden mcp <action>` subcommand. Three actions:
12
+ *
13
+ * serve — spawn the MCP stdio server. Blocks until parent closes
14
+ * stdio (this is the canonical Claude Desktop /
15
+ * Cursor / Claude Code lifecycle).
16
+ * status — print build fingerprint, exposed tool/skill counts, and
17
+ * current env config. Quick sanity check before pointing
18
+ * a client at the binary.
19
+ * tools — list every exposed tool name + category. Useful when the
20
+ * user wants to know what their allowlist currently maps
21
+ * to before they save the client config.
22
+ *
23
+ * `serve` runs in a deliberately stripped-down runtime: tools,
24
+ * skill loader, sessions, memory, processes — but NO provider /
25
+ * adapter / agent loop. The MCP protocol IS the agent loop here; the
26
+ * spawning client owns the model. This keeps `aiden mcp serve`
27
+ * startable on a freshly-installed Aiden with zero provider keys.
28
+ *
29
+ * Phase-9 approval engine is intentionally NOT wired. The bridge
30
+ * env-gate (`AIDEN_MCP_ALLOW_DESTRUCTIVE`) is the consent layer when
31
+ * there's no human at the REPL. The bridge filter blocks mutating
32
+ * tools by default; opting in means the user accepted server-side
33
+ * execution risk at config time.
34
+ */
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.AIDEN_MCP_BUILD = void 0;
37
+ exports.runMcpSubcommand = runMcpSubcommand;
38
+ /* eslint-disable @typescript-eslint/no-explicit-any */
39
+ const paths_1 = require("../../../core/v4/paths");
40
+ const toolRegistry_1 = require("../../../core/v4/toolRegistry");
41
+ const skillLoader_1 = require("../../../core/v4/skillLoader");
42
+ const sessionStore_1 = require("../../../core/v4/sessionStore");
43
+ const sessionManager_1 = require("../../../core/v4/sessionManager");
44
+ const memoryManager_1 = require("../../../core/v4/memoryManager");
45
+ const processRegistry_1 = require("../../../core/v4/processRegistry");
46
+ const config_1 = require("../../../core/v4/config");
47
+ const providerFallback_1 = require("../../../core/v4/providerFallback");
48
+ const chatCompletionsAdapter_1 = require("../../../providers/v4/chatCompletionsAdapter");
49
+ const aidenAgent_1 = require("../../../core/v4/aidenAgent");
50
+ const factory_1 = require("../../../core/v4/logger/factory");
51
+ const index_1 = require("../../../tools/v4/index");
52
+ const envSources_1 = require("../envSources");
53
+ const credentialResolver_1 = require("../../../providers/v4/credentialResolver");
54
+ const runtimeResolver_1 = require("../../../providers/v4/runtimeResolver");
55
+ const stdioServer_1 = require("../../../core/v4/mcp/server/stdioServer");
56
+ Object.defineProperty(exports, "AIDEN_MCP_BUILD", { enumerable: true, get: function () { return stdioServer_1.AIDEN_MCP_BUILD; } });
57
+ const diagnostics_1 = require("../../../core/v4/mcp/server/diagnostics");
58
+ const toolBridge_1 = require("../../../core/v4/mcp/server/toolBridge");
59
+ /** Build the slim runtime an MCP server needs. Tools + skills + the
60
+ * subsystems they consume — no provider, no agent. */
61
+ async function buildMcpRuntime(opts = {}) {
62
+ const paths = opts.pathsOverride ?? (0, paths_1.resolveAidenPaths)();
63
+ await (0, paths_1.ensureAidenDirsExist)(paths);
64
+ // ── Phase v4.1-mcp.2 — eager .env load ───────────────────────
65
+ // Stdio MCP clients (Claude Desktop, Cursor) spawn `aiden mcp
66
+ // serve` with an EMPTY env block by default. Without an explicit
67
+ // `env: {...}` per-server entry in their config, our spawned
68
+ // process has no GROQ_API_KEY etc., and any provider-using tool
69
+ // (subagent_fanout, web_search, fetch_url, …) fails. Load the
70
+ // well-known .env locations BEFORE the registry is built so tool
71
+ // factories that read env at registration time see live values.
72
+ // Fill-only — process.env wins, file values fill gaps.
73
+ const envReport = (0, envSources_1.loadMcpEnvSources)({
74
+ aidenHomeEnv: paths.envFile,
75
+ });
76
+ // mcp-stdio mode: file sink + stderr only. Crucial: this MUST happen
77
+ // before any module emits via console.* — but we don't use console in
78
+ // this module, and mcp-stdio mode + the no-stdout-sink invariant in
79
+ // factory.ts guarantee the protocol channel stays clean.
80
+ const { logger } = (0, factory_1.createBootLogger)({ mode: 'mcp-stdio', logsDir: paths.logsDir });
81
+ // Log the env-load report. NEVER log values — only paths + key
82
+ // NAMES. The mcp-stdio logger writes to file + stderr (zero stdout
83
+ // sinks per v4.1-mcp), so spawning clients see this in their MCP
84
+ // log stream and grep can confirm keys loaded.
85
+ for (const a of envReport.attempts) {
86
+ logger.info(`mcp env: ${a.exists ? 'loaded' : 'skipped (missing)'} ${a.path}`, {
87
+ scope: 'mcp',
88
+ path: a.path,
89
+ exists: a.exists,
90
+ applied: a.appliedKeys.length,
91
+ keyNames: a.appliedKeys,
92
+ });
93
+ }
94
+ const registry = new toolRegistry_1.ToolRegistry();
95
+ (0, index_1.registerAllTools)(registry);
96
+ const skillLoader = new skillLoader_1.SkillLoader(paths);
97
+ await skillLoader.loadAll().catch(() => undefined);
98
+ const store = new sessionStore_1.SessionStore(paths.sessionsDb);
99
+ const sessions = new sessionManager_1.SessionManager(store);
100
+ const memory = new memoryManager_1.MemoryManager(paths);
101
+ const processes = new processRegistry_1.ProcessRegistry();
102
+ const toolContext = {
103
+ cwd: process.cwd(),
104
+ paths,
105
+ sessions,
106
+ memory,
107
+ processes,
108
+ skillLoader,
109
+ // approvalEngine / ssrfProtection / tirithScanner / memoryGuard
110
+ // intentionally omitted — see header comment.
111
+ };
112
+ // ── Phase v4.1-subagent.2 — wire real subagent_fanout factory ────
113
+ //
114
+ // Without this, the stub registered by `registerAllTools` (from
115
+ // `registerReadOnlyTools`) returns "no providers configured" on
116
+ // every MCP-side call because its `resolveProviders` is `() => []`.
117
+ // The CLI path replaces the stub inside `buildAgentRuntime`; the
118
+ // MCP path is a different runtime build, so it needs its own
119
+ // replacement here.
120
+ //
121
+ // Provider resolution mirrors `buildAgentRuntime` but stripped:
122
+ // 1. Read config.yaml when present; fall back to groq /
123
+ // llama-3.3-70b-versatile (the same default the CLI uses).
124
+ // 2. RuntimeResolver constructs the active adapter.
125
+ // 3. If providerId is groq/together (chat_completions with
126
+ // multi-slot fallback), wrap in FallbackAdapter so subagent
127
+ // rotation gets a real list of provider options.
128
+ //
129
+ // When credentials are missing, leave the stub in place — the
130
+ // status command's "provider keys" block tells the user what to
131
+ // fix. We never throw out of buildMcpRuntime; an unwired stub is
132
+ // strictly better UX than a crashed MCP server.
133
+ await wireSubagentFanout({
134
+ registry,
135
+ paths,
136
+ sessionManager: sessions,
137
+ memoryManager: memory,
138
+ skillLoader,
139
+ logger: logger.child('subagent'),
140
+ });
141
+ return { paths, registry, skillLoader, toolContext, logger };
142
+ }
143
+ /** Mirror of `buildAgentFallbackSlots` (cli/v4/aidenCLI.ts) inlined
144
+ * here to avoid a load-time module cycle between aidenCLI and mcp.ts.
145
+ * Wraps the resolver-resolved primary adapter as slot 0 and appends
146
+ * the env-var-derived defaults so multi-key Groq fanouts and
147
+ * Together failover work without a config-yaml round-trip. */
148
+ function buildMcpFallbackSlots(primary, primaryProviderId, primaryModelId) {
149
+ const defaults = (0, providerFallback_1.buildDefaultSlots)({
150
+ adapterFactory: (cfg) => new chatCompletionsAdapter_1.ChatCompletionsAdapter({
151
+ baseUrl: cfg.baseUrl,
152
+ apiKey: cfg.apiKey,
153
+ model: cfg.model,
154
+ providerName: cfg.providerName,
155
+ }),
156
+ });
157
+ const primarySlot = {
158
+ id: 'primary',
159
+ providerId: primaryProviderId,
160
+ modelId: primaryModelId,
161
+ keyPresent: true,
162
+ keyTail: null,
163
+ build: () => primary,
164
+ };
165
+ return [primarySlot, ...defaults];
166
+ }
167
+ /** Resolve adapter + wire `subagent_fanout` into the MCP registry.
168
+ * Soft-fails (logs + leaves stub) when credentials are missing. */
169
+ async function wireSubagentFanout(opts) {
170
+ const config = new config_1.ConfigManager(opts.paths);
171
+ try {
172
+ await config.load();
173
+ }
174
+ catch {
175
+ // ENOENT → defaults. Other parse errors are logged but non-fatal.
176
+ opts.logger.warn('config.yaml load failed; using defaults', { scope: 'mcp' });
177
+ }
178
+ const providerId = config.getValue('model.provider', 'groq');
179
+ const modelId = config.getValue('model.modelId', 'llama-3.3-70b-versatile');
180
+ const credentialResolver = new credentialResolver_1.CredentialResolver(opts.paths.authJson);
181
+ const resolver = new runtimeResolver_1.RuntimeResolver(credentialResolver);
182
+ let adapter;
183
+ try {
184
+ adapter = await resolver.resolve({ providerId, modelId, config, paths: opts.paths });
185
+ }
186
+ catch (err) {
187
+ const msg = err instanceof Error ? err.message : String(err);
188
+ opts.logger.warn('subagent_fanout NOT wired — provider resolution failed (run `aiden setup` or set keys in .env)', { scope: 'mcp', providerId, modelId, error: msg });
189
+ return;
190
+ }
191
+ // Wrap in FallbackAdapter when the active provider is one of the
192
+ // chat-completions families with multi-slot fallback support.
193
+ // Same pattern aidenCLI.ts uses (line ~562); slot construction is
194
+ // inlined to avoid a module cycle between aidenCLI and mcp.ts.
195
+ let wrapped = adapter;
196
+ if (adapter.apiMode === 'chat_completions'
197
+ && (providerId === 'groq' || providerId === 'together')) {
198
+ const slots = buildMcpFallbackSlots(adapter, providerId, modelId);
199
+ const reachable = slots.filter((s) => s.keyPresent);
200
+ if (reachable.length >= 2) {
201
+ wrapped = new providerFallback_1.FallbackAdapter({
202
+ apiMode: 'chat_completions',
203
+ slots,
204
+ onRateLimit: (slotId) => opts.logger.info(`slot ${slotId} rate-limited`, { scope: 'mcp' }),
205
+ });
206
+ }
207
+ }
208
+ const finalAdapter = wrapped;
209
+ opts.registry.register((0, index_1.makeSubagentFanoutTool)({
210
+ logger: opts.logger,
211
+ resolveActiveModel: () => ({ providerId, modelId }),
212
+ aggregatorAdapter: finalAdapter,
213
+ resolveProviders: () => {
214
+ if (finalAdapter instanceof providerFallback_1.FallbackAdapter) {
215
+ const diag = finalAdapter.getDiagnostics();
216
+ const live = diag.slots.filter((s) => s.keyPresent);
217
+ if (live.length > 0) {
218
+ return live.map((s) => ({
219
+ providerId: s.providerId,
220
+ modelId: s.modelId,
221
+ label: s.id,
222
+ }));
223
+ }
224
+ }
225
+ return [{ providerId, modelId }];
226
+ },
227
+ runChild: async (childOpts) => {
228
+ const childCtx = {
229
+ cwd: process.cwd(),
230
+ paths: opts.paths,
231
+ sessions: opts.sessionManager,
232
+ memory: opts.memoryManager,
233
+ skillLoader: opts.skillLoader,
234
+ // approvalEngine intentionally undefined — N children
235
+ // contending for one stdin REPL would deadlock under MCP.
236
+ };
237
+ // Filter the child tool surface — read-only by default; opt-in
238
+ // via AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE=1 (mirrors MCP env from
239
+ // v4.1-mcp). Recursive fanout disallowed (depth=1).
240
+ const allowDestructive = process.env.AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE === '1' ||
241
+ process.env.AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE === 'true';
242
+ const childToolNames = [];
243
+ for (const name of opts.registry.list()) {
244
+ const h = opts.registry.get(name);
245
+ if (!h)
246
+ continue;
247
+ if (h.mutates && !allowDestructive)
248
+ continue;
249
+ if (name === 'subagent_fanout')
250
+ continue;
251
+ childToolNames.push(name);
252
+ }
253
+ const childExecutor = opts.registry.buildExecutor(childCtx);
254
+ const childTools = childToolNames
255
+ .map((n) => opts.registry.get(n)?.schema)
256
+ .filter((s) => !!s);
257
+ // Per-child cloned FallbackAdapter — own rate-limit state.
258
+ const childProvider = finalAdapter instanceof providerFallback_1.FallbackAdapter
259
+ ? finalAdapter.clone()
260
+ : finalAdapter;
261
+ const child = new aidenAgent_1.AidenAgent({
262
+ provider: childProvider,
263
+ tools: childTools,
264
+ toolExecutor: childExecutor,
265
+ maxTurns: childOpts.maxIterations,
266
+ providerId: childOpts.provider.providerId,
267
+ modelId: childOpts.provider.modelId,
268
+ // No promptBuilder — children get a brief system prompt
269
+ // (same lesson as v4.1-subagent.1: full SOUL.md makes
270
+ // trivial fanouts spend 30s+ on verbose self-introductions).
271
+ });
272
+ if (childOpts.signal.aborted) {
273
+ throw new Error('aborted before dispatch');
274
+ }
275
+ const roleLine = childOpts.role ? `Role: ${childOpts.role}. ` : '';
276
+ const childSystemPrompt = `You are one of N parallel subagents. ${roleLine}` +
277
+ `Answer the user's request concisely. Use available tools when ` +
278
+ `the answer requires real-world information you don't have memorized.`;
279
+ const history = [
280
+ { role: 'system', content: childSystemPrompt },
281
+ { role: 'user', content: childOpts.prompt },
282
+ ];
283
+ const result = await child.runConversation(history);
284
+ return result.finalContent;
285
+ },
286
+ }));
287
+ opts.logger.info('subagent_fanout: wired (replaces stub) [mcp serve]', {
288
+ providerId,
289
+ modelId,
290
+ fallback: finalAdapter instanceof providerFallback_1.FallbackAdapter ? 'FallbackAdapter' : 'direct',
291
+ });
292
+ }
293
+ async function runMcpSubcommand(action, opts = {}) {
294
+ const writeOut = opts.writeOut ?? ((t) => process.stdout.write(t));
295
+ const writeErr = opts.writeErr ?? ((t) => process.stderr.write(t));
296
+ switch (action) {
297
+ case 'serve': {
298
+ const { registry, skillLoader, toolContext, logger } = await buildMcpRuntime(opts);
299
+ await (0, stdioServer_1.startStdioMcpServer)({
300
+ registry,
301
+ skillLoader,
302
+ toolContext,
303
+ logger,
304
+ });
305
+ // Block forever — parent closes stdio when the client disconnects,
306
+ // which tears down the SDK transport and unwinds the process.
307
+ await new Promise(() => undefined);
308
+ return 0;
309
+ }
310
+ case 'status': {
311
+ const { registry, skillLoader } = await buildMcpRuntime(opts);
312
+ const diag = await (0, diagnostics_1.collectMcpDiagnostics)(registry, skillLoader);
313
+ writeOut(`Aiden MCP server\n`);
314
+ writeOut(` build: ${diag.build}\n`);
315
+ writeOut(` tools (total): ${diag.toolsTotal}\n`);
316
+ writeOut(` tools (exposed): ${diag.toolsExposed}\n`);
317
+ writeOut(` skills: ${diag.skillsTotal}\n`);
318
+ writeOut(` allowDestructive: ${diag.env.allowDestructive ? 'yes' : 'no'}\n`);
319
+ writeOut(` allowlist: ${diag.env.allowlist
320
+ ? diag.env.allowlist.join(', ') || '(empty)'
321
+ : '(unset — all)'}\n`);
322
+ // Phase v4.1-mcp.2 — provider key presence + source. NEVER log
323
+ // values; only the source tag (preset / aiden-env / unset).
324
+ writeOut(` provider keys:\n`);
325
+ const keys = (0, envSources_1.describeProviderKeys)();
326
+ const present = keys.filter((k) => k.present).length;
327
+ writeOut(` detected: ${present}/${keys.length}\n`);
328
+ for (const k of keys) {
329
+ const tag = k.present ? '✓' : '✗';
330
+ // Lower-case the key for display: "GROQ_API_KEY" → "groq".
331
+ const label = k.key.replace(/_API_KEY$/, '').toLowerCase();
332
+ const src = k.present
333
+ ? (k.source === 'aiden-env' ? '(.env)' : '(preset)')
334
+ : '(unset)';
335
+ writeOut(` ${tag} ${label.padEnd(12)} ${src}\n`);
336
+ }
337
+ return 0;
338
+ }
339
+ case 'tools': {
340
+ const { registry } = await buildMcpRuntime(opts);
341
+ const env = (0, toolBridge_1.readToolBridgeEnv)();
342
+ const list = (0, toolBridge_1.buildToolsList)(registry, env);
343
+ writeOut(`Aiden MCP — exposed tools (${list.length})\n`);
344
+ for (const tool of list) {
345
+ const handler = registry.get(tool.name);
346
+ const cat = handler?.category ?? '?';
347
+ const set = handler?.toolset ?? '-';
348
+ writeOut(` ${tool.name.padEnd(28)} ${cat.padEnd(8)} [${set}]\n`);
349
+ }
350
+ return 0;
351
+ }
352
+ default: {
353
+ writeErr(`Unknown 'aiden mcp' action: ${action}\n`);
354
+ writeErr(`Actions: serve | status | tools\n`);
355
+ return 1;
356
+ }
357
+ }
358
+ }
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setup = void 0;
4
+ const setupWizard_1 = require("../setupWizard");
5
+ exports.setup = {
6
+ name: 'setup',
7
+ description: 'Re-run the setup wizard (configure provider + API key).',
8
+ category: 'system',
9
+ icon: '⚙',
10
+ handler: async (ctx) => {
11
+ if (!ctx.paths) {
12
+ ctx.display.printError('Cannot run wizard from this context — no paths available.', 'This is a wiring bug; please report.');
13
+ return;
14
+ }
15
+ const result = await (0, setupWizard_1.runSetupWizard)({
16
+ paths: ctx.paths,
17
+ display: ctx.display,
18
+ force: true,
19
+ });
20
+ if (result.status === 'configured' && result.ran) {
21
+ ctx.display.write('\nProvider configured. ' +
22
+ 'Restart Aiden (`/quit` then re-run `aiden`) to pick up the new provider.\n\n');
23
+ }
24
+ else if (result.status === 'skipped') {
25
+ ctx.display.write('\nStill in explore mode. Run /setup again whenever you\'re ready.\n\n');
26
+ }
27
+ else if (result.status === 'exited') {
28
+ // Wizard explicitly chose to exit — but we're inside a REPL,
29
+ // so just report and stay in the session.
30
+ ctx.display.dim('Wizard exited; continuing existing session.');
31
+ }
32
+ return;
33
+ },
34
+ };
@@ -0,0 +1,43 @@
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
+ * cli/v4/commands/show.ts — Tier-3.1 (v4.1-tier3.1)
10
+ *
11
+ * `/show <id>` — print a previously compressed paste. Compressed
12
+ * pastes are echoed in the REPL as `[paste #<id>: <N> lines, <KB>]`
13
+ * and stored on disk; this command reverses that for the user
14
+ * (NOT the agent — the agent receives the full original text at
15
+ * paste time).
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.show = void 0;
19
+ const pasteCompression_1 = require("../pasteCompression");
20
+ exports.show = {
21
+ name: 'show',
22
+ description: 'Print the original content of a compressed paste (/show <id>).',
23
+ category: 'system',
24
+ icon: '>',
25
+ handler: async (ctx) => {
26
+ const id = (ctx.args[0] ?? '').trim();
27
+ if (!id) {
28
+ ctx.display.warn('Usage: /show <id> (id from the [paste #<id>: ...] echo)');
29
+ return {};
30
+ }
31
+ const original = await (0, pasteCompression_1.expandPaste)(id);
32
+ if (original == null) {
33
+ ctx.display.warn(`No paste with id ${id} found.`);
34
+ return {};
35
+ }
36
+ ctx.display.write('\n');
37
+ ctx.display.write(original);
38
+ if (!original.endsWith('\n'))
39
+ ctx.display.write('\n');
40
+ ctx.display.write('\n');
41
+ return {};
42
+ },
43
+ };
@@ -1,6 +1,27 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.skills = void 0;
7
+ /**
8
+ * Copyright (c) 2026 Shiva Deore (Taracod).
9
+ * Licensed under AGPL-3.0. See LICENSE for details.
10
+ *
11
+ * Aiden — local-first agent.
12
+ */
13
+ /**
14
+ * cli/v4/commands/skills.ts — Phase 14b
15
+ *
16
+ * `/skills [list|view <name>|install <id>]` — minimal CLI surface to
17
+ * Phase 10's SkillLoader + Phase 14a's SkillsHub. Default subcommand: list.
18
+ */
19
+ const node_fs_1 = require("node:fs");
20
+ const node_path_1 = __importDefault(require("node:path"));
21
+ const table_1 = require("../table");
22
+ const candidateStore_1 = require("../../../core/v4/skillMining/candidateStore");
23
+ const paths_1 = require("../../../core/v4/paths");
24
+ const skillSpec_1 = require("../../../core/v4/skillSpec");
4
25
  exports.skills = {
5
26
  name: 'skills',
6
27
  description: 'List, view, or install skills.',
@@ -19,9 +40,12 @@ exports.skills = {
19
40
  return {};
20
41
  }
21
42
  ctx.display.info(`Installed skills (${skills.length}):`);
22
- for (const s of skills) {
23
- ctx.display.write(` • ${s.name.padEnd(24)} ${s.description ?? ''}\n`);
24
- }
43
+ ctx.display.write((0, table_1.renderTable)(skills.map((s) => ({ name: s.name, description: s.description ?? '' })), [
44
+ { key: 'name', header: 'Name', align: 'left', minWidth: 16 },
45
+ // Tier-3.1b: drop the legacy `truncate: 60` cap so the
46
+ // description column flexes to fill available width.
47
+ { key: 'description', header: 'Description', align: 'left', flex: true },
48
+ ]));
25
49
  return {};
26
50
  }
27
51
  if (sub === 'view') {
@@ -69,7 +93,148 @@ exports.skills = {
69
93
  }
70
94
  return {};
71
95
  }
72
- ctx.display.printError(`Unknown subcommand: ${sub}`, 'Try: /skills list | view <name> | install <id>');
96
+ // ── Phase v4.1-skill-mining ──────────────────────────────────
97
+ // /skills review — list pending mined candidates
98
+ // /skills view-candidate <id> — preview a candidate's SKILL.md
99
+ // /skills accept <id> — promote candidate to live skill
100
+ // /skills reject <id> [reason]— record rejection (dedup-aware)
101
+ // /skills propose — explanation of the mining hook
102
+ const store = new candidateStore_1.CandidateStore();
103
+ if (sub === 'review') {
104
+ const candidates = await store.list();
105
+ if (candidates.length === 0) {
106
+ ctx.display.dim('(no pending candidates — mined skills appear here after a successful 3+ tool turn)');
107
+ return {};
108
+ }
109
+ ctx.display.info(`Pending mined candidates (${candidates.length}):`);
110
+ ctx.display.write((0, table_1.renderTable)(candidates.map((c) => {
111
+ // Pull the name + 1-line description from the candidate's
112
+ // own SKILL.md so the table reflects what the user will
113
+ // accept verbatim.
114
+ let name = '(unparsed)';
115
+ let description = '';
116
+ try {
117
+ const parsed = (0, skillSpec_1.parseSkillContent)(c.skillContent);
118
+ name = parsed.frontmatter.name ?? name;
119
+ description = parsed.frontmatter.description ?? '';
120
+ }
121
+ catch { /* fall through to defaults */ }
122
+ return {
123
+ id: c.id.slice(0, 8),
124
+ name,
125
+ confidence: c.candidateConfidence.toFixed(2),
126
+ session: c.sourceSessionId.slice(0, 8),
127
+ created: c.createdAt.slice(0, 19).replace('T', ' '),
128
+ description,
129
+ };
130
+ }), [
131
+ { key: 'id', header: 'ID', align: 'left' },
132
+ { key: 'name', header: 'Name', align: 'left' },
133
+ { key: 'confidence', header: 'Conf', align: 'right' },
134
+ { key: 'session', header: 'Session', align: 'left' },
135
+ { key: 'created', header: 'Created', align: 'left' },
136
+ { key: 'description', header: 'Description', align: 'left', flex: true },
137
+ ]));
138
+ ctx.display.dim('Use `/skills view-candidate <id-prefix>` to preview, `/skills accept <id>` to promote, `/skills reject <id> [reason]` to dismiss.');
139
+ return {};
140
+ }
141
+ if (sub === 'view-candidate') {
142
+ const idPrefix = ctx.args[1];
143
+ if (!idPrefix) {
144
+ ctx.display.printError('Usage: /skills view-candidate <id>');
145
+ return {};
146
+ }
147
+ const all = await store.list();
148
+ const match = all.find((c) => c.id.startsWith(idPrefix));
149
+ if (!match) {
150
+ ctx.display.printError(`No candidate matches id prefix '${idPrefix}'.`);
151
+ return {};
152
+ }
153
+ ctx.display.info(`Candidate ${match.id} (confidence ${match.candidateConfidence.toFixed(2)}):`);
154
+ ctx.display.write('\n');
155
+ ctx.display.write(match.skillContent);
156
+ ctx.display.write('\n');
157
+ return {};
158
+ }
159
+ if (sub === 'accept') {
160
+ const idPrefix = ctx.args[1];
161
+ if (!idPrefix) {
162
+ ctx.display.printError('Usage: /skills accept <id>');
163
+ return {};
164
+ }
165
+ const all = await store.list();
166
+ const match = all.find((c) => c.id.startsWith(idPrefix));
167
+ if (!match) {
168
+ ctx.display.printError(`No candidate matches id prefix '${idPrefix}'.`);
169
+ return {};
170
+ }
171
+ let parsed;
172
+ try {
173
+ parsed = (0, skillSpec_1.parseSkillContent)(match.skillContent);
174
+ }
175
+ catch (err) {
176
+ ctx.display.printError(`Candidate did not round-trip through the parser: ${err.message}`);
177
+ return {};
178
+ }
179
+ const skillName = parsed.frontmatter.name;
180
+ if (!skillName) {
181
+ ctx.display.printError('Candidate missing required `name` frontmatter field.');
182
+ return {};
183
+ }
184
+ const paths = (0, paths_1.resolveAidenPaths)();
185
+ const targetDir = node_path_1.default.join(paths.skillsDir, skillName);
186
+ if (!(0, node_fs_1.existsSync)(paths.skillsDir))
187
+ (0, node_fs_1.mkdirSync)(paths.skillsDir, { recursive: true });
188
+ if (!(0, node_fs_1.existsSync)(targetDir))
189
+ (0, node_fs_1.mkdirSync)(targetDir, { recursive: true });
190
+ const targetFile = node_path_1.default.join(targetDir, 'SKILL.md');
191
+ try {
192
+ await node_fs_1.promises.writeFile(targetFile, match.skillContent, 'utf8');
193
+ }
194
+ catch (err) {
195
+ ctx.display.printError(`Failed to write skill: ${err.message}`);
196
+ return {};
197
+ }
198
+ await store.remove(match.id);
199
+ // Invalidate the loader cache so the new skill is visible
200
+ // on the next /skills list call without a session restart.
201
+ try {
202
+ ctx.skillLoader?.invalidate?.();
203
+ }
204
+ catch { /* best-effort */ }
205
+ ctx.display.success(`Promoted '${skillName}' to ${targetFile}.`);
206
+ return {};
207
+ }
208
+ if (sub === 'reject') {
209
+ const idPrefix = ctx.args[1];
210
+ if (!idPrefix) {
211
+ ctx.display.printError('Usage: /skills reject <id> [reason]');
212
+ return {};
213
+ }
214
+ const reason = ctx.args.slice(2).join(' ').trim() || undefined;
215
+ const all = await store.list();
216
+ const match = all.find((c) => c.id.startsWith(idPrefix));
217
+ if (!match) {
218
+ ctx.display.printError(`No candidate matches id prefix '${idPrefix}'.`);
219
+ return {};
220
+ }
221
+ await store.recordRejection(match.fingerprint, reason);
222
+ await store.remove(match.id);
223
+ ctx.display.success(`Rejected candidate '${match.id.slice(0, 8)}'.` +
224
+ (reason ? ` Reason recorded: "${reason}"` : ''));
225
+ return {};
226
+ }
227
+ if (sub === 'propose') {
228
+ // Manual mining fires automatically post-turn via aidenAgent;
229
+ // this subcommand surfaces what's currently pending so the
230
+ // user understands the hook without needing to remember
231
+ // /skills review.
232
+ const candidates = await store.list();
233
+ ctx.display.info(`Skill mining is active — successful 3+ tool turns auto-stage candidates.`);
234
+ ctx.display.dim(`Pending: ${candidates.length}. Run /skills review to inspect.`);
235
+ return {};
236
+ }
237
+ ctx.display.printError(`Unknown subcommand: ${sub}`, 'Try: /skills list | view <name> | install <id> | review | view-candidate <id> | accept <id> | reject <id> [reason] | propose');
73
238
  return {};
74
239
  },
75
240
  };