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.
Files changed (113) hide show
  1. package/README.md +19 -11
  2. package/config/hardware.json +2 -2
  3. package/dist/api/server.js +50 -52
  4. package/dist/cli/v4/aidenCLI.js +424 -7
  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 +102 -1
  28. package/dist/cli/v4/doctorLiveness.js +329 -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/shellInterpolation.js +139 -0
  37. package/dist/cli/v4/skinEngine.js +21 -1
  38. package/dist/cli/v4/streamingPrefix.js +121 -0
  39. package/dist/cli/v4/syntaxHighlight.js +345 -0
  40. package/dist/cli/v4/table.js +216 -0
  41. package/dist/cli/v4/themeDetect.js +81 -0
  42. package/dist/cli/v4/uiBuild.js +74 -0
  43. package/dist/cli/v4/voiceCli.js +113 -0
  44. package/dist/cli/v4/voicePromptApi.js +196 -0
  45. package/dist/core/channels/discord.js +16 -10
  46. package/dist/core/channels/email.js +13 -9
  47. package/dist/core/channels/imessage.js +13 -9
  48. package/dist/core/channels/manager.js +25 -7
  49. package/dist/core/channels/pdf-extract.js +180 -0
  50. package/dist/core/channels/photo-vision.js +157 -0
  51. package/dist/core/channels/signal.js +11 -7
  52. package/dist/core/channels/slack.js +13 -10
  53. package/dist/core/channels/telegram-commands.js +154 -0
  54. package/dist/core/channels/telegram-groups.js +198 -0
  55. package/dist/core/channels/telegram-rate-limit.js +124 -0
  56. package/dist/core/channels/telegram.js +1980 -0
  57. package/dist/core/channels/twilio.js +11 -7
  58. package/dist/core/channels/webhook.js +9 -5
  59. package/dist/core/channels/whatsapp.js +15 -11
  60. package/dist/core/channels/whisper-transcribe.js +163 -0
  61. package/dist/core/cronManager.js +33 -294
  62. package/dist/core/gateway.js +29 -8
  63. package/dist/core/playwrightBridge.js +90 -0
  64. package/dist/core/v4/aidenAgent.js +35 -0
  65. package/dist/core/v4/auxiliaryClient.js +2 -2
  66. package/dist/core/v4/cron/atomicWrite.js +18 -4
  67. package/dist/core/v4/cron/cronExecute.js +300 -0
  68. package/dist/core/v4/cron/cronManager.js +502 -0
  69. package/dist/core/v4/cron/cronState.js +314 -0
  70. package/dist/core/v4/cron/cronTick.js +90 -0
  71. package/dist/core/v4/cron/diagnostics.js +104 -0
  72. package/dist/core/v4/cron/graceWindow.js +79 -0
  73. package/dist/core/v4/logger/factory.js +110 -0
  74. package/dist/core/v4/logger/index.js +22 -0
  75. package/dist/core/v4/logger/logger.js +101 -0
  76. package/dist/core/v4/logger/sinks/fileSink.js +110 -0
  77. package/dist/core/v4/logger/sinks/multiSink.js +43 -0
  78. package/dist/core/v4/logger/sinks/nullSink.js +53 -0
  79. package/dist/core/v4/logger/sinks/stdSink.js +81 -0
  80. package/dist/core/v4/mcp/server/diagnostics.js +40 -0
  81. package/dist/core/v4/mcp/server/skillBridge.js +94 -0
  82. package/dist/core/v4/mcp/server/stdioServer.js +119 -0
  83. package/dist/core/v4/mcp/server/toolBridge.js +168 -0
  84. package/dist/core/v4/platformPaths.js +105 -0
  85. package/dist/core/v4/providerFallback.js +25 -0
  86. package/dist/core/v4/skillLoader.js +21 -5
  87. package/dist/core/v4/skillMining/candidateStore.js +164 -0
  88. package/dist/core/v4/skillMining/extractorPrompt.js +118 -0
  89. package/dist/core/v4/skillMining/proposalBuilder.js +140 -0
  90. package/dist/core/v4/skillMining/skillMiner.js +191 -0
  91. package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
  92. package/dist/core/v4/subagent/budget.js +76 -0
  93. package/dist/core/v4/subagent/diagnostics.js +22 -0
  94. package/dist/core/v4/subagent/fanout.js +216 -0
  95. package/dist/core/v4/subagent/merger.js +148 -0
  96. package/dist/core/v4/subagent/providerRotation.js +54 -0
  97. package/dist/core/v4/voice/audioStream.js +373 -0
  98. package/dist/core/v4/voice/cliVoice.js +393 -0
  99. package/dist/core/v4/voice/diagnostics.js +66 -0
  100. package/dist/core/v4/voice/ttsStream.js +193 -0
  101. package/dist/core/version.js +1 -1
  102. package/dist/core/visionAnalyze.js +291 -90
  103. package/dist/core/voice/audio.js +61 -5
  104. package/dist/core/voice/audioBackend.js +134 -0
  105. package/dist/core/voice/stt.js +61 -6
  106. package/dist/core/voice/tts.js +19 -3
  107. package/dist/moat/dangerousPatterns.js +1 -1
  108. package/dist/providers/v4/codexResponsesAdapter.js +7 -2
  109. package/dist/providers/v4/errors.js +51 -1
  110. package/dist/providers/v4/ollamaPromptToolsAdapter.js +9 -2
  111. package/dist/tools/v4/index.js +32 -1
  112. package/dist/tools/v4/subagent/subagentFanout.js +190 -0
  113. package/package.json +11 -2
@@ -0,0 +1,84 @@
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/status.ts — Tier-3.1 (v4.1-tier3.1)
10
+ *
11
+ * `/status` — full environment + capability table that the v4.0 boot
12
+ * card used to print inline. Slim boot now ships a 3-4 line summary;
13
+ * users who want the full picture (provider table, skill counts, MCP
14
+ * details, channel adapter list) call `/status` on demand.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.status = void 0;
18
+ const chatSession_1 = require("../chatSession");
19
+ const display_1 = require("../display");
20
+ exports.status = {
21
+ name: 'status',
22
+ description: 'Detailed runtime status (env, providers, skills, channels).',
23
+ category: 'system',
24
+ icon: 'i',
25
+ handler: async (ctx) => {
26
+ const display = ctx.display;
27
+ // Provider / model — drawn from the live session when available.
28
+ const provider = ctx.session?.getCurrentProvider() ?? '(unset)';
29
+ const model = ctx.session?.getCurrentModel() ?? '(unset)';
30
+ // Tools / skills counts.
31
+ const toolsCount = ctx.toolRegistry?.list().length ?? 0;
32
+ let skillsLoaded = 0;
33
+ try {
34
+ if (ctx.skillLoader)
35
+ skillsLoaded = (await ctx.skillLoader.list()).length;
36
+ }
37
+ catch {
38
+ skillsLoaded = 0;
39
+ }
40
+ // Channels — uses the same summariser the boot card used.
41
+ let channelSummary;
42
+ const cm = ctx.channelManager;
43
+ if (cm) {
44
+ const adapterStatuses = cm.getStatus().map((s) => {
45
+ const adapter = cm.get(s.name);
46
+ const botHandle = typeof adapter?.getBotUsername === 'function' ? adapter.getBotUsername() : null;
47
+ const state = typeof adapter?.getState === 'function' ? adapter.getState() : undefined;
48
+ return { id: s.name, healthy: s.healthy, botHandle, state };
49
+ });
50
+ channelSummary = (0, display_1.summarizeChannelState)({ adapters: adapterStatuses });
51
+ }
52
+ else {
53
+ channelSummary = (0, display_1.summarizeChannelState)(null);
54
+ }
55
+ display.write('\n');
56
+ display.write(display.twoColumnBlock({
57
+ title: 'Environment',
58
+ rows: [
59
+ { key: 'OS', value: (0, chatSession_1.detectOS)() },
60
+ { key: 'shell', value: (0, chatSession_1.detectShell)() },
61
+ { key: 'runtime', value: 'local-first' },
62
+ { key: 'provider', value: provider },
63
+ { key: 'model', value: model },
64
+ { key: 'tools', value: `${toolsCount} loaded` },
65
+ { key: 'skills', value: `${skillsLoaded} loaded` },
66
+ { key: 'channels', value: channelSummary },
67
+ ],
68
+ }, {
69
+ title: 'Capabilities',
70
+ rows: [
71
+ { key: 'web', value: 'research, extract' },
72
+ { key: 'browser', value: 'navigate, automate' },
73
+ { key: 'files', value: 'read, patch, organize' },
74
+ { key: 'execution', value: 'shell, code, workflows' },
75
+ { key: 'memory', value: 'persistent recall' },
76
+ ],
77
+ }) + '\n');
78
+ display.write('\n');
79
+ display.write(` ${display.rule()}\n`);
80
+ display.write(display.bottomPromptHint() + '\n');
81
+ display.write('\n');
82
+ return {};
83
+ },
84
+ };
@@ -0,0 +1,78 @@
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/subagent.ts — Phase v4.1-subagent
10
+ *
11
+ * `aiden subagent <action>` subcommand. Two actions:
12
+ *
13
+ * status — print build fingerprint + env config + provider count.
14
+ * tools — list the subagent_fanout schema (debug).
15
+ *
16
+ * Lightweight diagnostics surface — no provider resolution, no
17
+ * agent runtime build. The point is "what would my fanout look like
18
+ * before I run it?" — useful for triaging "no providers configured"
19
+ * errors and verifying env-var allowlists before pointing a client
20
+ * at the binary.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.AIDEN_SUBAGENT_BUILD = void 0;
24
+ exports.runSubagentSubcommand = runSubagentSubcommand;
25
+ /* eslint-disable @typescript-eslint/no-explicit-any */
26
+ const toolRegistry_1 = require("../../../core/v4/toolRegistry");
27
+ const index_1 = require("../../../tools/v4/index");
28
+ const diagnostics_1 = require("../../../core/v4/subagent/diagnostics");
29
+ Object.defineProperty(exports, "AIDEN_SUBAGENT_BUILD", { enumerable: true, get: function () { return diagnostics_1.AIDEN_SUBAGENT_BUILD; } });
30
+ const budget_1 = require("../../../core/v4/subagent/budget");
31
+ const merger_1 = require("../../../core/v4/subagent/merger");
32
+ async function runSubagentSubcommand(action, opts = {}) {
33
+ const writeOut = opts.writeOut ?? ((t) => process.stdout.write(t));
34
+ const writeErr = opts.writeErr ?? ((t) => process.stderr.write(t));
35
+ switch (action) {
36
+ case 'status': {
37
+ const budget = (0, budget_1.resolveBudget)();
38
+ const allowDestructive = process.env.AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE === '1' ||
39
+ process.env.AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE === 'true';
40
+ const aggOverride = (0, merger_1.resolveAggregatorOverride)();
41
+ writeOut(`Aiden subagent fanout\n`);
42
+ writeOut(` build: ${diagnostics_1.AIDEN_SUBAGENT_BUILD}\n`);
43
+ writeOut(` default n: ${budget_1.DEFAULT_FANOUT_N}\n`);
44
+ writeOut(` hard cap n: ${budget_1.MAX_FANOUT_N}\n`);
45
+ writeOut(` per-subagent timeout ms: ${budget.perSubagentTimeoutMs}\n`);
46
+ writeOut(` wall-clock cap ms: ${budget.wallClockCapMs}\n`);
47
+ writeOut(` max iterations: ${budget.maxIterations}\n`);
48
+ writeOut(` allowDestructive: ${allowDestructive ? 'yes' : 'no'}\n`);
49
+ writeOut(` aggregator override: ${aggOverride
50
+ ? `${aggOverride.providerId}:${aggOverride.modelId}`
51
+ : '(unset — use parent active model)'}\n`);
52
+ return 0;
53
+ }
54
+ case 'tools': {
55
+ const registry = new toolRegistry_1.ToolRegistry();
56
+ (0, index_1.registerAllTools)(registry);
57
+ const handler = registry.get('subagent_fanout');
58
+ if (!handler) {
59
+ writeErr('subagent_fanout not registered (this is a build bug)\n');
60
+ return 1;
61
+ }
62
+ writeOut(`Aiden subagent — tool schema\n`);
63
+ writeOut(` name: ${handler.schema.name}\n`);
64
+ writeOut(` category: ${handler.category}\n`);
65
+ writeOut(` mutates: ${handler.mutates}\n`);
66
+ writeOut(` toolset: ${handler.toolset}\n`);
67
+ writeOut(` description: ${handler.schema.description}\n`);
68
+ writeOut(`\n inputSchema (JSON):\n`);
69
+ writeOut(`${JSON.stringify(handler.schema.inputSchema, null, 2)}\n`);
70
+ return 0;
71
+ }
72
+ default: {
73
+ writeErr(`Unknown 'aiden subagent' action: ${action}\n`);
74
+ writeErr(`Actions: status | tools\n`);
75
+ return 1;
76
+ }
77
+ }
78
+ }
@@ -9,7 +9,7 @@ exports.verbose = {
9
9
  name: 'verbose',
10
10
  description: 'Set verbosity: compact | normal | verbose.',
11
11
  category: 'system',
12
- icon: '🔎',
12
+ icon: '~',
13
13
  handler: async (ctx) => {
14
14
  const cfg = ctx.config;
15
15
  if (!cfg) {
@@ -0,0 +1,218 @@
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/voice.ts — Phase v4.1-voice-cli
10
+ *
11
+ * `/voice` slash command. Subcommands:
12
+ *
13
+ * /voice on — enable voice mode for this session
14
+ * /voice off — disable
15
+ * /voice toggle — flip on/off
16
+ * /voice status — show fingerprint + backend + provider availability
17
+ * /voice mode push — switch to push-to-talk (default)
18
+ * /voice mode continuous — switch to continuous (VAD-driven)
19
+ * /voice provider <name> — pin TTS provider (edge | sapi | elevenlabs | voxcpm)
20
+ * /voice voice <id> — pin TTS voice (default: en-US-AriaNeural)
21
+ *
22
+ * Persistence: writes to the user's `.aiden/.env` via `upsertEnvVar`
23
+ * so settings survive REPL restarts. Mirrors the channel slash
24
+ * command's atomic .env mutation pattern.
25
+ *
26
+ * State surfaces via `process.env` keys:
27
+ * AIDEN_VOICE_ENABLED — "1" / "0"
28
+ * AIDEN_VOICE_MODE — "push-to-talk" / "continuous"
29
+ * AIDEN_VOICE_TTS_VOICE — voice id
30
+ * AIDEN_VOICE_PROVIDER — provider override
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.voice = void 0;
37
+ /* eslint-disable @typescript-eslint/no-explicit-any */
38
+ const node_fs_1 = require("node:fs");
39
+ const node_path_1 = __importDefault(require("node:path"));
40
+ const diagnostics_1 = require("../../../core/v4/voice/diagnostics");
41
+ // .env upsert (atomic) — mirrors the channel slash command's
42
+ // pattern. Local to this file so the import surface stays tight.
43
+ async function upsertEnvVar(envFile, key, value) {
44
+ const k = key.toUpperCase();
45
+ let body = '';
46
+ try {
47
+ body = await node_fs_1.promises.readFile(envFile, 'utf8');
48
+ }
49
+ catch { /* fresh file */ }
50
+ const lines = body.split(/\r?\n/);
51
+ let replaced = false;
52
+ for (let i = 0; i < lines.length; i += 1) {
53
+ if (lines[i].startsWith(`${k}=`)) {
54
+ lines[i] = `${k}=${value}`;
55
+ replaced = true;
56
+ }
57
+ }
58
+ if (!replaced)
59
+ lines.push(`${k}=${value}`);
60
+ while (lines.length > 0 && lines[lines.length - 1] === '')
61
+ lines.pop();
62
+ await node_fs_1.promises.mkdir(node_path_1.default.dirname(envFile), { recursive: true });
63
+ const tmp = `${envFile}.${process.pid}.tmp`;
64
+ await node_fs_1.promises.writeFile(tmp, `${lines.join('\n')}\n`, 'utf8');
65
+ await node_fs_1.promises.rename(tmp, envFile);
66
+ }
67
+ async function deleteEnvKey(envFile, key) {
68
+ const k = key.toUpperCase();
69
+ let body = '';
70
+ try {
71
+ body = await node_fs_1.promises.readFile(envFile, 'utf8');
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ const lines = body.split(/\r?\n/);
77
+ const filtered = lines.filter((l) => !l.startsWith(`${k}=`));
78
+ if (filtered.length === lines.length)
79
+ return false;
80
+ while (filtered.length > 0 && filtered[filtered.length - 1] === '')
81
+ filtered.pop();
82
+ const tmp = `${envFile}.${process.pid}.tmp`;
83
+ await node_fs_1.promises.writeFile(tmp, `${filtered.join('\n')}\n`, 'utf8');
84
+ await node_fs_1.promises.rename(tmp, envFile);
85
+ return true;
86
+ }
87
+ const USAGE = [
88
+ 'Usage:',
89
+ ' /voice on | off | toggle',
90
+ ' /voice status',
91
+ ' /voice mode push | continuous',
92
+ ' /voice provider edge | sapi | elevenlabs | voxcpm',
93
+ ' /voice voice <id> (e.g. en-US-AriaNeural)',
94
+ ].join('\n');
95
+ const handler = async (ctx) => {
96
+ const display = ctx.display;
97
+ const action = (ctx.args[0] ?? 'status').toLowerCase();
98
+ const paths = ctx.paths;
99
+ switch (action) {
100
+ case 'on':
101
+ case 'off':
102
+ case 'toggle': {
103
+ const current = process.env.AIDEN_VOICE_ENABLED === '1';
104
+ const next = action === 'on'
105
+ ? true
106
+ : action === 'off'
107
+ ? false
108
+ : !current;
109
+ process.env.AIDEN_VOICE_ENABLED = next ? '1' : '0';
110
+ if (paths) {
111
+ try {
112
+ if (next)
113
+ await upsertEnvVar(paths.envFile, 'AIDEN_VOICE_ENABLED', '1');
114
+ else
115
+ await deleteEnvKey(paths.envFile, 'AIDEN_VOICE_ENABLED');
116
+ }
117
+ catch (err) {
118
+ display.warn(`Could not persist /voice ${action} to .env: ${err.message}`);
119
+ }
120
+ }
121
+ const tty = !!process.stdin.isTTY && !!process.stdout.isTTY;
122
+ if (next && !tty) {
123
+ display.warn('Voice mode requested, but stdin is not a TTY. The next REPL session must run in an interactive terminal.');
124
+ }
125
+ display.info(`voice mode ${next ? 'on' : 'off'}`);
126
+ return;
127
+ }
128
+ case 'status': {
129
+ const diag = await (0, diagnostics_1.collectVoiceDiagnostics)();
130
+ display.info(`Aiden voice — ${diagnostics_1.AIDEN_VOICE_CLI_BUILD}`);
131
+ display.info(` enabled: ${process.env.AIDEN_VOICE_ENABLED === '1' ? 'yes' : 'no'}`);
132
+ display.info(` tty: ${diag.isTty ? 'yes' : 'no (voice refused — non-TTY stdin)'}`);
133
+ display.info(` mic backend: ${diag.audio.backend}`);
134
+ display.info(` mic active: ${diag.audio.active ? 'yes' : 'no'}`);
135
+ display.info(` sox on PATH: ${diag.audio.soxOnPath ? 'yes' : 'no'}`);
136
+ display.info(` mode: ${diag.config.mode}`);
137
+ display.info(` tts voice: ${diag.config.ttsVoice}`);
138
+ display.info(` beeps: ${diag.config.beepsEnabled ? 'on' : 'off'}`);
139
+ display.info(` tts providers:`);
140
+ for (const p of diag.ttsProviders) {
141
+ const tag = p.available ? '✓' : '✗';
142
+ display.info(` ${tag} ${p.name.padEnd(12)} ${p.note ?? ''}`);
143
+ }
144
+ return;
145
+ }
146
+ case 'mode': {
147
+ const sub = (ctx.args[1] ?? '').toLowerCase();
148
+ if (sub !== 'push' && sub !== 'push-to-talk' && sub !== 'continuous') {
149
+ display.warn('Mode must be "push" or "continuous"');
150
+ return;
151
+ }
152
+ const value = sub === 'continuous' ? 'continuous' : 'push-to-talk';
153
+ process.env.AIDEN_VOICE_MODE = value;
154
+ if (paths) {
155
+ try {
156
+ await upsertEnvVar(paths.envFile, 'AIDEN_VOICE_MODE', value);
157
+ }
158
+ catch (err) {
159
+ display.warn(`Could not persist mode: ${err.message}`);
160
+ }
161
+ }
162
+ display.info(`voice mode = ${value}`);
163
+ return;
164
+ }
165
+ case 'provider': {
166
+ const provider = (ctx.args[1] ?? '').toLowerCase();
167
+ const valid = ['edge', 'sapi', 'elevenlabs', 'voxcpm'];
168
+ if (!valid.includes(provider)) {
169
+ display.warn(`Provider must be one of: ${valid.join(', ')}`);
170
+ return;
171
+ }
172
+ process.env.AIDEN_VOICE_PROVIDER = provider;
173
+ if (paths) {
174
+ try {
175
+ await upsertEnvVar(paths.envFile, 'AIDEN_VOICE_PROVIDER', provider);
176
+ }
177
+ catch (err) {
178
+ display.warn(`Could not persist provider: ${err.message}`);
179
+ }
180
+ }
181
+ display.info(`tts provider = ${provider}`);
182
+ return;
183
+ }
184
+ case 'voice': {
185
+ const voiceId = (ctx.args[1] ?? '').trim();
186
+ if (!voiceId) {
187
+ display.warn('Voice id required (e.g. en-US-AriaNeural)');
188
+ return;
189
+ }
190
+ process.env.AIDEN_VOICE_TTS_VOICE = voiceId;
191
+ if (paths) {
192
+ try {
193
+ await upsertEnvVar(paths.envFile, 'AIDEN_VOICE_TTS_VOICE', voiceId);
194
+ }
195
+ catch (err) {
196
+ display.warn(`Could not persist voice: ${err.message}`);
197
+ }
198
+ }
199
+ display.info(`tts voice = ${voiceId}`);
200
+ return;
201
+ }
202
+ case 'help':
203
+ case '--help':
204
+ case '-h':
205
+ display.info(USAGE);
206
+ return;
207
+ default:
208
+ display.warn(`Unknown action: ${action}`);
209
+ display.info(USAGE);
210
+ return;
211
+ }
212
+ };
213
+ exports.voice = {
214
+ name: 'voice',
215
+ description: 'Voice mode (PTT + TTS). Subcommands: on/off/toggle/status/mode/provider/voice',
216
+ category: 'system',
217
+ handler,
218
+ };
@@ -0,0 +1,103 @@
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/cronCli.ts — Phase v4.1-cron
10
+ *
11
+ * `aiden cron <action>` top-level CLI subcommand. Three actions:
12
+ *
13
+ * status — print build fingerprint, tick + lock state,
14
+ * last 5 fires (boot-local). No mutation.
15
+ * list — table of jobs (id, schedule, enabled, next run).
16
+ * run <id> — fire a job immediately. Useful for scripted
17
+ * "trigger this scheduled task now" flows from
18
+ * a shell.
19
+ *
20
+ * Distinct from the `/cron` slash command (which mutates session
21
+ * state from inside the REPL). This subcommand is for scripting +
22
+ * sanity-checking from a non-interactive shell.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.AIDEN_CRON_BUILD = void 0;
26
+ exports.runCronSubcommand = runCronSubcommand;
27
+ /* eslint-disable @typescript-eslint/no-explicit-any */
28
+ const cronManager_1 = require("../../core/cronManager");
29
+ Object.defineProperty(exports, "AIDEN_CRON_BUILD", { enumerable: true, get: function () { return cronManager_1.AIDEN_CRON_BUILD; } });
30
+ async function runCronSubcommand(action, args, opts = {}) {
31
+ const writeOut = opts.writeOut ?? ((t) => process.stdout.write(t));
32
+ const writeErr = opts.writeErr ?? ((t) => process.stderr.write(t));
33
+ switch (action) {
34
+ case 'status': {
35
+ // Booting may armed timers we don't need for status — but it
36
+ // also populates the cache. Skip arming side-effect by NOT
37
+ // calling loadJobs; getDiagnostics works without the cache.
38
+ const diag = await (0, cronManager_1.getDiagnostics)();
39
+ writeOut(`Aiden cron — ${cronManager_1.AIDEN_CRON_BUILD}\n`);
40
+ writeOut(` schema version : ${diag.schemaVersion}\n`);
41
+ writeOut(` tick interval : ${diag.tickMs}ms\n`);
42
+ writeOut(` fire timeout : ${diag.timeoutMs}ms\n`);
43
+ writeOut(` heartbeat : ${diag.heartbeatActive ? 'active' : 'idle'}\n`);
44
+ writeOut(` last heartbeat : ${diag.lastHeartbeatAt ?? 'never'}\n`);
45
+ writeOut(` skipped ticks : ${diag.skippedTicks}\n`);
46
+ writeOut(` fires (boot) : ${diag.firesStarted}\n`);
47
+ writeOut(` lock : ${diag.lock.held ? 'held' : 'free'}\n`);
48
+ writeOut(` lock path : ${diag.lock.path}\n`);
49
+ if (diag.recentFires.length > 0) {
50
+ writeOut(` recent fires:\n`);
51
+ for (const r of diag.recentFires) {
52
+ const tag = r.status === 'ok' ? '✓'
53
+ : r.status === 'warn' ? '∼'
54
+ : r.status === 'timeout' ? 'T'
55
+ : '✗';
56
+ writeOut(` ${tag} [${r.jobId.slice(0, 8)}] ${r.startedAt} ${r.durationMs}ms` +
57
+ `${r.error ? ' ' + r.error.slice(0, 60) : ''}\n`);
58
+ }
59
+ }
60
+ return 0;
61
+ }
62
+ case 'list': {
63
+ const jobs = await (0, cronManager_1.listJobsAsync)();
64
+ if (jobs.length === 0) {
65
+ writeOut(`No cron jobs configured.\n`);
66
+ return 0;
67
+ }
68
+ writeOut(`Aiden cron — ${jobs.length} job(s)\n`);
69
+ writeOut(` ID NAME SCHEDULE STATE NEXT RUN\n`);
70
+ for (const j of jobs) {
71
+ const id = j.id.padEnd(5).slice(0, 5);
72
+ const name = (j.description || '(unnamed)').padEnd(31).slice(0, 31);
73
+ const sched = (j.schedule || '?').padEnd(33).slice(0, 33);
74
+ const stateLabel = j.state.padEnd(11).slice(0, 11);
75
+ const next = j.nextRun ?? 'never';
76
+ writeOut(` ${id} ${name} ${sched} ${stateLabel} ${next}\n`);
77
+ }
78
+ return 0;
79
+ }
80
+ case 'run': {
81
+ const id = args[0];
82
+ if (!id) {
83
+ writeErr(`Usage: aiden cron run <id>\n`);
84
+ return 1;
85
+ }
86
+ // loadJobs populates the cache + arms timers. The trigger is
87
+ // what we actually want; arming is side-effect.
88
+ await (0, cronManager_1.loadJobs)();
89
+ const ok = await (0, cronManager_1.triggerJob)(id);
90
+ if (!ok) {
91
+ writeErr(`Job not found: ${id}\n`);
92
+ return 1;
93
+ }
94
+ writeOut(`Triggered job ${id}.\n`);
95
+ return 0;
96
+ }
97
+ default: {
98
+ writeErr(`Unknown 'aiden cron' action: ${action}\n`);
99
+ writeErr(`Actions: status | list | run <id>\n`);
100
+ return 1;
101
+ }
102
+ }
103
+ }