aiden-runtime 4.1.0 → 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 CHANGED
@@ -1,3 +1,8 @@
1
+ <img width="1672" height="941" alt="AIDEN BOOTUP LOGO" src="https://github.com/user-attachments/assets/c0809009-73e2-4d58-9292-12fbd0324952" />
2
+
3
+
4
+
5
+
1
6
  ```
2
7
  █████╗ ██╗██████╗ ███████╗███╗ ██╗
3
8
  ██╔══██╗██║██╔══██╗██╔════╝████╗ ██║
@@ -8,7 +13,7 @@
8
13
 
9
14
  Autonomous AI Engine
10
15
 
11
- 19 providers · 68 skills · 42 tools · 9 channels · AGPL-3.0
16
+ 72 skills · 42 tools · 19 providers · 9 channels · AGPL-3.0
12
17
 
13
18
  Windows · Linux · WSL · macOS (API Mode)
14
19
 
@@ -16,12 +21,13 @@ Local-first · Self-healing routing · Browser & terminal control · Persistent
16
21
  ```
17
22
 
18
23
  <p align="center">
19
- <a href="https://github.com/taracodlabs/aiden-releases/releases/latest"><img src="https://img.shields.io/github/v/release/taracodlabs/aiden-releases?color=f97316&label=version&style=for-the-badge" alt="Latest version" /></a>
20
- <a href="https://github.com/taracodlabs/aiden-releases/releases"><img src="https://img.shields.io/github/downloads/taracodlabs/aiden-releases/total?color=f97316&label=downloads&style=for-the-badge" alt="Downloads" /></a>
24
+ <a href="https://github.com/taracodlabs/aiden/releases/latest"><img src="https://img.shields.io/github/v/release/taracodlabs/aiden?color=f97316&label=version&style=for-the-badge" alt="Latest version" /></a>
25
+ <a href="https://github.com/taracodlabs/aiden/releases"><img src="https://img.shields.io/github/downloads/taracodlabs/aiden/total?color=f97316&label=downloads&style=for-the-badge" alt="Downloads" /></a>
21
26
  <a href="https://discord.gg/gMZ3hUnQTm"><img src="https://img.shields.io/badge/chat-discord-7289da?logo=discord&logoColor=white&style=for-the-badge" alt="Discord" /></a>
22
27
  <a href="./LICENSE"><img src="https://img.shields.io/badge/license-AGPL--3.0-orange?style=for-the-badge" alt="License: AGPL-3.0" /></a>
23
28
  <a href="https://github.com/taracodlabs/aiden/stargazers"><img src="https://img.shields.io/github/stars/taracodlabs/aiden?style=for-the-badge&color=f9d71c" alt="Stars" /></a>
24
29
  <a href="https://www.npmjs.com/package/aiden-runtime"><img src="https://img.shields.io/npm/v/aiden-runtime?color=f97316&label=npm&style=for-the-badge" alt="npm" /></a>
30
+ <a href="https://amzn.to/4tpiXwM"><img src="https://img.shields.io/badge/book-Omega-ff9900?logo=amazon&logoColor=white&style=for-the-badge" alt="Book: Omega" /></a>
25
31
  </p>
26
32
 
27
33
  <p align="center">
@@ -103,14 +109,14 @@ Local-first · Self-healing routing · Browser & terminal control · Persistent
103
109
  <a href="https://aiden.taracod.com"><b>Website</b></a> &nbsp;·&nbsp;
104
110
  <a href="https://aiden.taracod.com/contact"><b>Contact</b></a> &nbsp;·&nbsp;
105
111
  <a href="https://discord.gg/gMZ3hUnQTm"><b>Discord</b></a> &nbsp;·&nbsp;
106
- <a href="https://github.com/taracodlabs/aiden-releases/releases/latest"><b>Download</b></a> &nbsp;·&nbsp;
107
- <a href="https://www.amazon.in/Omega-Shiva-Deore-ebook/dp/B0GX33VWZC/"><b>Book</b></a>
112
+ <a href="https://github.com/taracodlabs/aiden/releases/latest"><b>Download</b></a> &nbsp;·&nbsp;
113
+ <a href="https://amzn.to/4tpiXwM"><b>Book</b></a>
108
114
  </p>
109
115
 
110
116
  ---
111
117
 
112
- > **v4.1.0 — multi-channel autonomous engine · Telegram + MCP server + subagent fanout · voice CLI · hardened cron · skill mining · structured markdown rendering · cross-platform CI**
113
- > v4.1 turns Aiden into a multi-surface agent: a `ChannelAdapter`-shaped Telegram bot (text / voice / photo / PDF / groups / admin), an MCP server exposing 24 tools + the full skill catalog to Claude Desktop, parallel subagent fanout across `groq`/`together`, REPL voice mode (PTT + continuous), an auto-mining pipeline that proposes new skills from successful workflows, and a deep REPL polish layer (custom `@inquirer/core` prompt, autosuggest, sectioned boot card, sharp ASCII corners, theme detection). Linux/macOS/Windows × Node 20/22 CI matrix. See [changelog](#changelog) below.
118
+ > **v4.1.0 — Multi-channel autonomous AI engine**
119
+ > Telegram + MCP server + subagent fanout + voice CLI + skill mining. Hardened cron, structured markdown, cross-platform CI. See [changelog](#changelog) below.
114
120
 
115
121
  ---
116
122
 
@@ -326,7 +332,6 @@ We're shipping honest. Things that work, things that don't:
326
332
 
327
333
  **Not in v4.0:**
328
334
 
329
- - Subagent fanout / parallel agent swarm — single-loop only; deferred to v4.x
330
335
  - OCR — not bundled (vision-loop screen capture works, but no Tesseract)
331
336
  - Full agentskills.io ecosystem install — held pending license review
332
337
  - Docker sandbox backend — dropped in v4 rewrite
@@ -727,7 +732,6 @@ aiden # CLI
727
732
 
728
733
  - **npm package renamed** — `aiden-os` → `aiden-runtime`. Run `npm uninstall -g aiden-os && npm install -g aiden-runtime`.
729
734
  - **Slash commands consolidated** — v3's `/switch`, `/budget`, `/memory`, `/profile`, `/permissions`, `/sandbox`, `/retry`, `/failed`, `/publish` are gone. Use `/model`, `/usage`, `/identity`, `/yolo` for equivalent functionality. See `/help` for the v4 list.
730
- - **Subagent fanout removed** — v4 is single-loop only; subagent support deferred to v4.x.
731
735
  - **Docker sandbox dropped** — `AIDEN_SANDBOX_MODE` no longer applies. Tools run on the host. The `tirithScanner` secret/PII guard, `ssrfProtection`, and tiered approval engine remain as the safety layer.
732
736
  - **Skill registry install changed** — auto-fetch from external repos held pending license review. Skills install via `/skills install <local-path-or-url>` only at v4.0.
733
737
  - **Config compatible** — most environment variables (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GROQ_API_KEY`, etc.) are recognised as-is. Copy your existing `.env` and Aiden picks them up.
@@ -282,12 +282,13 @@ async function main(argv, opts = {}) {
282
282
  program
283
283
  .command('doctor')
284
284
  .description('Run diagnostic checks')
285
- .action(async () => {
285
+ .option('--providers', 'Also ping each configured / authed provider and report live status (deep check). Slower; useful before shipping or when a provider regression is suspected.')
286
+ .action(async (cmdOpts) => {
286
287
  if (opts.runDoctorHook) {
287
288
  await opts.runDoctorHook();
288
289
  return;
289
290
  }
290
- await (0, doctor_1.runDoctorCli)();
291
+ await (0, doctor_1.runDoctorCli)({ liveness: cmdOpts.providers === true });
291
292
  });
292
293
  program
293
294
  .command('sessions <action> [arg]')
@@ -18,6 +18,39 @@
18
18
  * will normally be sub-second.
19
19
  *
20
20
  */
21
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ var desc = Object.getOwnPropertyDescriptor(m, k);
24
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
25
+ desc = { enumerable: true, get: function() { return m[k]; } };
26
+ }
27
+ Object.defineProperty(o, k2, desc);
28
+ }) : (function(o, m, k, k2) {
29
+ if (k2 === undefined) k2 = k;
30
+ o[k2] = m[k];
31
+ }));
32
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
33
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
34
+ }) : function(o, v) {
35
+ o["default"] = v;
36
+ });
37
+ var __importStar = (this && this.__importStar) || (function () {
38
+ var ownKeys = function(o) {
39
+ ownKeys = Object.getOwnPropertyNames || function (o) {
40
+ var ar = [];
41
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
42
+ return ar;
43
+ };
44
+ return ownKeys(o);
45
+ };
46
+ return function (mod) {
47
+ if (mod && mod.__esModule) return mod;
48
+ var result = {};
49
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
50
+ __setModuleDefault(result, mod);
51
+ return result;
52
+ };
53
+ })();
21
54
  var __importDefault = (this && this.__importDefault) || function (mod) {
22
55
  return (mod && mod.__esModule) ? mod : { "default": mod };
23
56
  };
@@ -760,6 +793,33 @@ async function runDoctorCli(opts) {
760
793
  }
761
794
  }
762
795
  process.stdout.write(`\n${report.passed ? 'all checks passed' : 'some checks failed'} in ${report.totalMs} ms\n`);
763
- process.exitCode = report.passed ? 0 : 1;
796
+ // Phase v4.1.1-oauth-fix Phase 5: discoverability hint for the deep
797
+ // mode. Only emitted in standard mode — if the user already passed
798
+ // `--providers`, the section right below this is the answer to the hint.
799
+ if (!opts?.liveness) {
800
+ process.stdout.write(' hint: Run `aiden doctor --providers` for live provider checks\n');
801
+ }
802
+ // Phase v4.1.1-oauth-fix Phase 4: opt-in provider liveness.
803
+ // Runs after the standard report so a failed config check is visible
804
+ // before the network probes start. Section header + summary line are
805
+ // rendered by doctorLiveness.renderProviderLivenessSection so the
806
+ // formatting stays alongside its consumer.
807
+ let livenessFailed = false;
808
+ if (opts?.liveness) {
809
+ process.stdout.write('\n Running provider liveness checks...\n');
810
+ const { runProviderLiveness, renderProviderLivenessSection } = await Promise.resolve().then(() => __importStar(require('./doctorLiveness')));
811
+ const paths = opts.paths ?? (0, paths_1.resolveAidenPaths)();
812
+ const { results, summary } = await runProviderLiveness({
813
+ paths,
814
+ env: opts.env,
815
+ fetchImpl: opts.fetchImpl,
816
+ timeoutMs: opts.livenessTimeoutMs,
817
+ });
818
+ process.stdout.write(renderProviderLivenessSection(results, summary));
819
+ livenessFailed = summary.red > 0;
820
+ }
821
+ // Liveness reds count toward the overall exit code so CI / scripts
822
+ // can `aiden doctor --providers && deploy`.
823
+ process.exitCode = (report.passed && !livenessFailed) ? 0 : 1;
764
824
  return report;
765
825
  }
@@ -0,0 +1,329 @@
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/doctorLiveness.ts — Phase v4.1.1-oauth-fix Phase 4.
10
+ *
11
+ * `aiden doctor --providers` deep-mode helper. Pings every configured /
12
+ * authed provider with a minimal request, reports green / red / skipped
13
+ * + per-provider latency + verbatim upstream error message.
14
+ *
15
+ * Why a separate file:
16
+ * - Default doctor (`aiden doctor` with no flag) stays unchanged and
17
+ * fast — config-shape checks only.
18
+ * - `--providers` is opt-in. When the user types it we extend the
19
+ * report with one liveness row per probe, then render a summary
20
+ * line at the bottom.
21
+ * - Tool-catalog validation is deliberately OUT of scope. Liveness
22
+ * probes ship `tools: []` (see comment in checkProviderLiveness)
23
+ * so one bad tool schema doesn't false-red every provider that
24
+ * validates strictly. The eval-harness / registration-time schema
25
+ * validator (v4.1.1 main) is the right home for that concern.
26
+ *
27
+ * Trust artifact:
28
+ * - On failure we surface `err.message` VERBATIM (truncated to 200
29
+ * chars). Phase 3 (`providers/v4/errors.ts`) already composes the
30
+ * upstream response body into ProviderError.message — so a 400
31
+ * prints the actual OpenAI reason, not a generic "provider failed."
32
+ */
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.enumerateConfiguredProviders = enumerateConfiguredProviders;
35
+ exports.checkProviderLiveness = checkProviderLiveness;
36
+ exports.runProviderLiveness = runProviderLiveness;
37
+ exports.renderProviderLivenessSection = renderProviderLivenessSection;
38
+ const registry_1 = require("../../providers/v4/registry");
39
+ const runtimeResolver_1 = require("../../providers/v4/runtimeResolver");
40
+ const credentialResolver_1 = require("../../providers/v4/credentialResolver");
41
+ const tokenStore_1 = require("../../core/v4/auth/tokenStore");
42
+ const DEFAULT_LIVENESS_TIMEOUT_MS = 8000;
43
+ const PROBE_MAX_TOKENS = 4;
44
+ const ERROR_TRUNCATE_CHARS = 200;
45
+ const OLLAMA_PROBE_TIMEOUT_MS = 1500;
46
+ const OLLAMA_HEALTH_URL = 'http://127.0.0.1:11434/api/tags';
47
+ // ── Helpers ─────────────────────────────────────────────────────────────
48
+ /**
49
+ * Truncate a long error string for display. The full error remains on
50
+ * the in-memory `LivenessResult.error` for programmatic consumers /
51
+ * test assertions.
52
+ */
53
+ function truncate(s, max = ERROR_TRUNCATE_CHARS) {
54
+ if (s.length <= max)
55
+ return s;
56
+ return `${s.slice(0, max - 1)}…`;
57
+ }
58
+ /**
59
+ * Wrap a promise with a hard timeout. Resolves to the inner result on
60
+ * success, throws a clearly-labelled `Error` on timeout. Cleans up the
61
+ * timer either way.
62
+ */
63
+ async function withTimeout(p, ms, label) {
64
+ let timer;
65
+ try {
66
+ return await Promise.race([
67
+ p,
68
+ new Promise((_, reject) => {
69
+ timer = setTimeout(() => reject(new Error(`${label}: timeout after ${ms}ms`)), ms);
70
+ }),
71
+ ]);
72
+ }
73
+ finally {
74
+ if (timer)
75
+ clearTimeout(timer);
76
+ }
77
+ }
78
+ /**
79
+ * Decide whether a provider counts as "configured" for liveness purposes.
80
+ * Three paths:
81
+ * 1. API key in env (`apiKeyEnvVar` set and the env var is non-empty).
82
+ * 2. OAuth (entry.oauth present and a non-expired token sits in the
83
+ * tokenStore at <paths.root>/auth/<providerId>.json).
84
+ * 3. Local providers (ollama) — probed via a quick HTTP health check.
85
+ *
86
+ * Anything else is `configured: false` and gets a `skip_reason`.
87
+ */
88
+ async function enumerateConfiguredProviders(opts) {
89
+ const env = opts.env ?? process.env;
90
+ const fetchImpl = opts.fetchImpl ?? fetch;
91
+ const out = [];
92
+ for (const entry of Object.values(registry_1.PROVIDER_REGISTRY)) {
93
+ // Every provider needs at least one model to probe against.
94
+ const model = entry.modelIds[0];
95
+ if (!model) {
96
+ out.push({
97
+ entry,
98
+ model: '',
99
+ configured: false,
100
+ reason: 'no models declared in registry',
101
+ });
102
+ continue;
103
+ }
104
+ // 1. API-key providers.
105
+ if (entry.apiKeyEnvVar) {
106
+ const value = env[entry.apiKeyEnvVar];
107
+ if (value && value.length > 0) {
108
+ out.push({ entry, model, configured: true });
109
+ }
110
+ else {
111
+ out.push({
112
+ entry,
113
+ model,
114
+ configured: false,
115
+ reason: `env ${entry.apiKeyEnvVar} not set`,
116
+ });
117
+ }
118
+ continue;
119
+ }
120
+ // 2. OAuth providers — check tokenStore.
121
+ if (entry.oauth) {
122
+ try {
123
+ const tokens = await (0, tokenStore_1.loadTokens)(opts.paths, entry.oauth.providerId);
124
+ if (tokens && tokens.accessToken) {
125
+ if ((0, tokenStore_1.isExpired)(tokens, tokenStore_1.PREFLIGHT_REFRESH_WINDOW_MS)) {
126
+ out.push({
127
+ entry,
128
+ model,
129
+ configured: false,
130
+ reason: 'OAuth token expired — run `/auth refresh` or `/auth login`',
131
+ });
132
+ }
133
+ else {
134
+ out.push({ entry, model, configured: true });
135
+ }
136
+ }
137
+ else {
138
+ out.push({
139
+ entry,
140
+ model,
141
+ configured: false,
142
+ reason: 'no OAuth token — run `/auth login`',
143
+ });
144
+ }
145
+ }
146
+ catch (err) {
147
+ out.push({
148
+ entry,
149
+ model,
150
+ configured: false,
151
+ reason: `tokenStore read failed: ${err.message}`,
152
+ });
153
+ }
154
+ continue;
155
+ }
156
+ // 3. Local / no-credential providers (ollama). Configured = the
157
+ // local daemon answers a health probe.
158
+ const controller = new AbortController();
159
+ const timer = setTimeout(() => controller.abort(), OLLAMA_PROBE_TIMEOUT_MS);
160
+ try {
161
+ const res = await fetchImpl(OLLAMA_HEALTH_URL, { signal: controller.signal });
162
+ if (res.ok) {
163
+ out.push({ entry, model, configured: true });
164
+ }
165
+ else {
166
+ out.push({
167
+ entry,
168
+ model,
169
+ configured: false,
170
+ reason: `local daemon HTTP ${res.status}`,
171
+ });
172
+ }
173
+ }
174
+ catch (err) {
175
+ out.push({
176
+ entry,
177
+ model,
178
+ configured: false,
179
+ reason: `not running on ${OLLAMA_HEALTH_URL}`,
180
+ });
181
+ }
182
+ finally {
183
+ clearTimeout(timer);
184
+ }
185
+ }
186
+ return out;
187
+ }
188
+ /**
189
+ * Probe one provider/model pair via a minimal `adapter.call()`. Returns
190
+ * a structured result; never throws.
191
+ *
192
+ * On failure, `result.error` is `err.message` verbatim truncated to
193
+ * 200 chars. Phase v4.1.1-oauth-fix Phase 3 made `ProviderError.message`
194
+ * carry the upstream response body, so a 400 surfaces the actual
195
+ * reason (e.g. "Invalid schema for function 'subagent_fanout': …").
196
+ */
197
+ async function checkProviderLiveness(provider, model, adapter, opts) {
198
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_LIVENESS_TIMEOUT_MS;
199
+ const start = Date.now();
200
+ // Liveness probes "is this provider reachable + authenticated?".
201
+ // Tool-catalog validation is a separate concern (eval harness,
202
+ // v4.1.1 main). Sending tools: [] ensures one bad tool schema
203
+ // doesn't false-red every provider that validates strictly.
204
+ const input = {
205
+ messages: [{ role: 'user', content: 'ping' }],
206
+ tools: [],
207
+ maxTokens: PROBE_MAX_TOKENS,
208
+ };
209
+ try {
210
+ await withTimeout(adapter.call(input), timeoutMs, `liveness ${provider}`);
211
+ return {
212
+ provider,
213
+ model,
214
+ status: 'green',
215
+ latency_ms: Date.now() - start,
216
+ };
217
+ }
218
+ catch (err) {
219
+ const raw = err instanceof Error ? err.message : String(err);
220
+ return {
221
+ provider,
222
+ model,
223
+ status: 'red',
224
+ latency_ms: Date.now() - start,
225
+ error: truncate(raw),
226
+ };
227
+ }
228
+ }
229
+ /**
230
+ * Run liveness probes against every provider returned by
231
+ * `enumerateConfiguredProviders`. Unconfigured providers come back as
232
+ * `status: 'skipped'` without any network traffic.
233
+ *
234
+ * Returns `{ results, summary }`. Summary tallies + a wall-clock total
235
+ * so the UI can print "X green · Y red · Z skipped · NNNms total".
236
+ */
237
+ async function runProviderLiveness(opts) {
238
+ const start = Date.now();
239
+ const parallel = opts.parallel ?? true;
240
+ const resolver = opts.resolverImpl ?? new runtimeResolver_1.RuntimeResolver(new credentialResolver_1.CredentialResolver(opts.paths.authJson));
241
+ const configured = await enumerateConfiguredProviders({
242
+ paths: opts.paths,
243
+ env: opts.env,
244
+ fetchImpl: opts.fetchImpl,
245
+ });
246
+ const probe = async (c) => {
247
+ if (!c.configured) {
248
+ return {
249
+ provider: c.entry.id,
250
+ status: 'skipped',
251
+ latency_ms: 0,
252
+ skip_reason: c.reason ?? 'not configured',
253
+ };
254
+ }
255
+ try {
256
+ const adapter = await resolver.resolve({
257
+ providerId: c.entry.id,
258
+ modelId: c.model,
259
+ paths: opts.paths,
260
+ });
261
+ return await checkProviderLiveness(c.entry.id, c.model, adapter, { timeoutMs: opts.timeoutMs });
262
+ }
263
+ catch (err) {
264
+ // Resolve failure (missing credential, unknown model, etc.).
265
+ // Same surface treatment as a probe failure.
266
+ const raw = err instanceof Error ? err.message : String(err);
267
+ return {
268
+ provider: c.entry.id,
269
+ model: c.model,
270
+ status: 'red',
271
+ latency_ms: 0,
272
+ error: truncate(raw),
273
+ };
274
+ }
275
+ };
276
+ const results = parallel
277
+ ? await Promise.all(configured.map(probe))
278
+ : await runSequential(configured.map((c) => () => probe(c)));
279
+ const summary = {
280
+ green: results.filter((r) => r.status === 'green').length,
281
+ red: results.filter((r) => r.status === 'red').length,
282
+ skipped: results.filter((r) => r.status === 'skipped').length,
283
+ total_ms: Date.now() - start,
284
+ };
285
+ return { results, summary };
286
+ }
287
+ async function runSequential(thunks) {
288
+ const out = [];
289
+ for (const t of thunks)
290
+ out.push(await t());
291
+ return out;
292
+ }
293
+ /**
294
+ * Render the liveness section as plain-text rows. The doctor command
295
+ * prints this BELOW the standard health box so the default `aiden doctor`
296
+ * output stays byte-identical.
297
+ *
298
+ * Visual style matches the existing doctor rows (✓ / ✗ / -) but lays
299
+ * out as a tabular block rather than a box — the rows can be long
300
+ * (upstream error bodies) and forcing them into the 100-col box would
301
+ * truncate the diagnostic that's the whole point of the feature.
302
+ */
303
+ function renderProviderLivenessSection(results, summary) {
304
+ const nameWidth = Math.max(8, ...results.map((r) => r.provider.length));
305
+ const lines = [];
306
+ lines.push('');
307
+ lines.push(' Provider liveness (deep check)');
308
+ lines.push(` ${'─'.repeat(60)}`);
309
+ for (const r of results) {
310
+ const icon = r.status === 'green' ? '✓'
311
+ : r.status === 'red' ? '✗'
312
+ : '-';
313
+ const name = r.provider.padEnd(nameWidth);
314
+ const status = r.status === 'green' ? 'green'.padEnd(8)
315
+ : r.status === 'red' ? 'red '.padEnd(8)
316
+ : 'skip '.padEnd(8);
317
+ const latency = r.latency_ms > 0
318
+ ? `${r.latency_ms}ms`.padEnd(8)
319
+ : ''.padEnd(8);
320
+ const tail = r.status === 'green' ? (r.model ?? '')
321
+ : r.status === 'red' ? (r.error ?? 'unknown error')
322
+ : (r.skip_reason ?? 'not configured');
323
+ lines.push(` ${icon} ${name} ${status}${latency}${tail}`);
324
+ }
325
+ lines.push(` ${'─'.repeat(60)}`);
326
+ lines.push(` ${summary.green} green · ${summary.red} red · ${summary.skipped} skipped · ${summary.total_ms}ms total`);
327
+ lines.push('');
328
+ return lines.join('\n');
329
+ }
@@ -16,10 +16,10 @@
16
16
  * metadata.aiden.* must round-trip unchanged. We re-parse the
17
17
  * refined output and discard it if any required field drifts.
18
18
  *
19
- * 2. Never write attribution tokens (hermes / nous / "portions
20
- * adapted from" / "original copyright"). The permanent
21
- * attribution sweep validates this; if a refined output
22
- * contains any forbidden token, we fall back to the skeleton.
19
+ * 2. Never write attribution tokens or "portions adapted from..." /
20
+ * "original copyright" strings. The permanent attribution
21
+ * sweep validates this; if a refined output contains any
22
+ * forbidden token, we fall back to the skeleton.
23
23
  *
24
24
  * If the auxiliary client is unavailable, the call times out, or
25
25
  * the refined output fails validation, the function returns the
@@ -29,23 +29,30 @@
29
29
  Object.defineProperty(exports, "__esModule", { value: true });
30
30
  exports.refine = refine;
31
31
  const skillSpec_1 = require("../skillSpec");
32
- const FORBIDDEN_TOKENS_RE = /\b(hermes|nous|portions adapted from|original copyright)\b/i;
33
- const REFINER_SYSTEM_PROMPT = `
34
- You polish auto-generated skill markdown for a local-first AI agent.
35
-
36
- Your job is to improve the WORDING ONLY of an already-valid SKILL.md
37
- file. The frontmatter (everything between the leading "---" markers)
38
- must round-trip BYTE-FOR-BYTE unchanged. The "# <name>" heading must
39
- stay first in the body. The numbered "## Steps" list must remain
40
- numbered and in the same order; you may rephrase step descriptions
41
- but must NOT add or remove steps.
42
-
43
- Hard rules:
44
- - Output the COMPLETE SKILL.md, not a diff.
45
- - Do not add boilerplate, citations, or attribution to any other
46
- agent or codebase. The skill is 100% the user's own.
47
- - Do not introduce mock/fake values into commands.
48
- - Keep total length under 6000 characters.
32
+ const BANNED_TOKENS = [
33
+ 'portions adapted from',
34
+ 'original copyright',
35
+ 'derived from',
36
+ 'based on the',
37
+ 'adapted from',
38
+ ];
39
+ const FORBIDDEN_TOKENS_RE = new RegExp(`\\b(${BANNED_TOKENS.join('|')})\\b`, 'i');
40
+ const REFINER_SYSTEM_PROMPT = `
41
+ You polish auto-generated skill markdown for a local-first AI agent.
42
+
43
+ Your job is to improve the WORDING ONLY of an already-valid SKILL.md
44
+ file. The frontmatter (everything between the leading "---" markers)
45
+ must round-trip BYTE-FOR-BYTE unchanged. The "# <name>" heading must
46
+ stay first in the body. The numbered "## Steps" list must remain
47
+ numbered and in the same order; you may rephrase step descriptions
48
+ but must NOT add or remove steps.
49
+
50
+ Hard rules:
51
+ - Output the COMPLETE SKILL.md, not a diff.
52
+ - Do not add boilerplate, citations, or attribution to any other
53
+ agent or codebase. The skill is 100% the user's own.
54
+ - Do not introduce mock/fake values into commands.
55
+ - Keep total length under 6000 characters.
49
56
  `.trim();
50
57
  /**
51
58
  * Refine `skeleton` via the auxiliary client. Always returns a
@@ -18,8 +18,9 @@
18
18
  * - emits required fields `name`, `description`, `version`
19
19
  * - emits `metadata.aiden` with the mining provenance fields
20
20
  * - body is numbered tool-call steps in markdown
21
- * - never writes hermes/nous/copyright attribution strings — the
22
- * permanent attribution sweep validates this
21
+ * - never writes attribution strings — the banned-token regex
22
+ * strips them at extraction time and the permanent attribution
23
+ * sweep validates the result
23
24
  */
24
25
  Object.defineProperty(exports, "__esModule", { value: true });
25
26
  exports.deriveName = deriveName;
@@ -2,4 +2,4 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VERSION = void 0;
4
4
  // AUTO-GENERATED by scripts/inject-version.js — do not edit by hand
5
- exports.VERSION = '4.1.0';
5
+ exports.VERSION = '4.1.1';
@@ -52,7 +52,7 @@ exports.DANGEROUS_PATTERNS = [
52
52
  { name: 'kill_all', regex: /\bkill\s+-9\s+-1\b/, tier: 'dangerous', description: 'kill all processes' },
53
53
  { name: 'pkill_force', regex: /\bpkill\s+-9\b/, tier: 'caution', description: 'force kill processes' },
54
54
  { name: 'systemctl_disable', regex: /\bsystemctl\s+(-[^\s]+\s+)*(stop|restart|disable|mask)\b/i, tier: 'caution', description: 'stop/restart system service' },
55
- { name: 'pkill_aiden', regex: /\b(pkill|killall)\b.*\b(aiden|gateway|hermes)\b/i, tier: 'dangerous', description: 'kill aiden/gateway process (self-termination)' },
55
+ { name: 'pkill_aiden', regex: /\b(pkill|killall)\b.*\b(aiden|gateway)\b/i, tier: 'dangerous', description: 'kill aiden/gateway process (self-termination)' },
56
56
  // ── Sensitive write targets ───────────────────────────────────
57
57
  { name: 'write_etc', regex: />\s*\/etc\//, tier: 'dangerous', description: 'overwrite system config' },
58
58
  { name: 'tee_etc', regex: /\btee\b.*\/etc\//, tier: 'dangerous', description: 'overwrite system file via tee' },
@@ -129,14 +129,19 @@ class CodexResponsesAdapter {
129
129
  const body = {
130
130
  model: this.model,
131
131
  input: items,
132
- tool_choice: 'auto',
133
- parallel_tool_calls: true,
134
132
  store: false,
135
133
  };
136
134
  if (instructions)
137
135
  body.instructions = instructions;
136
+ // Phase v4.1.1-oauth-fix Phase 5: `tool_choice` and
137
+ // `parallel_tool_calls` are only meaningful when tools are present.
138
+ // OpenAI Codex returns HTTP 400 (empty body) for `tool_choice: 'auto'`
139
+ // without a `tools` field — surfaced by `aiden doctor --providers`'s
140
+ // no-tools liveness probe.
138
141
  if (input.tools && input.tools.length > 0) {
139
142
  body.tools = input.tools.map(toWireTool);
143
+ body.tool_choice = 'auto';
144
+ body.parallel_tool_calls = true;
140
145
  }
141
146
  if (typeof input.temperature === 'number') {
142
147
  body.temperature = input.temperature;
@@ -16,9 +16,59 @@
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.ProviderRateLimitError = exports.ProviderTimeoutError = exports.ProviderError = void 0;
19
+ exports.formatRawForMessage = formatRawForMessage;
20
+ /**
21
+ * Format a raw response body for inclusion in the user-facing error
22
+ * message. Recognises the OpenAI / Anthropic JSON envelope shape
23
+ * (`{ error: { message: "..." } }`) and falls back to the raw string
24
+ * for plain-text bodies. Returns null when nothing useful is available
25
+ * so callers can omit the ": <detail>" tail entirely.
26
+ *
27
+ * Truncates to 300 chars to keep multi-line responses from blowing
28
+ * up the user's terminal — full body remains on `error.raw` for
29
+ * programmatic consumers / `aiden doctor --providers` deep mode.
30
+ */
31
+ function formatRawForMessage(raw) {
32
+ if (raw === undefined || raw === null)
33
+ return null;
34
+ // OpenAI / Anthropic JSON envelope: { error: { message: "..." } }
35
+ if (typeof raw === 'object') {
36
+ const err = raw.error;
37
+ if (err && typeof err === 'object') {
38
+ const msg = err.message;
39
+ if (typeof msg === 'string' && msg.length > 0) {
40
+ return msg.length > 300 ? `${msg.slice(0, 300)}…` : msg;
41
+ }
42
+ }
43
+ // Some providers put the message at the top level.
44
+ const topMsg = raw.message;
45
+ if (typeof topMsg === 'string' && topMsg.length > 0) {
46
+ return topMsg.length > 300 ? `${topMsg.slice(0, 300)}…` : topMsg;
47
+ }
48
+ return null;
49
+ }
50
+ // Plain string body.
51
+ if (typeof raw === 'string') {
52
+ const trimmed = raw.trim();
53
+ if (trimmed.length === 0)
54
+ return null;
55
+ return trimmed.length > 300 ? `${trimmed.slice(0, 300)}…` : trimmed;
56
+ }
57
+ return null;
58
+ }
59
+ /**
60
+ * Compose the final `Error.message` from the short summary and (when
61
+ * available) the parsed/truncated raw response body. The body remains
62
+ * stashed on `ProviderError.raw` either way — this only enriches what
63
+ * users see when the error is rendered.
64
+ */
65
+ function composeMessage(message, raw) {
66
+ const tail = formatRawForMessage(raw);
67
+ return tail ? `${message}: ${tail}` : message;
68
+ }
19
69
  class ProviderError extends Error {
20
70
  constructor(message, providerName, statusCode, raw, retryable = false) {
21
- super(message);
71
+ super(composeMessage(message, raw));
22
72
  this.providerName = providerName;
23
73
  this.statusCode = statusCode;
24
74
  this.raw = raw;
@@ -71,7 +71,12 @@ class OllamaPromptToolsAdapter {
71
71
  const status = response.status;
72
72
  const rawText = await this.safeReadText(response);
73
73
  const retryable = status >= 500 || status === 429;
74
- const err = new errors_1.ProviderError(`Provider ${this.providerName} returned ${status}: ${rawText.slice(0, 500)}`, this.providerName, status, rawText, retryable);
74
+ // Phase v4.1.1-oauth-fix Phase 5: short message only. The raw
75
+ // body flows via the .raw arg and composeMessage in errors.ts
76
+ // appends a truncated summary into the final .message — this
77
+ // file used to inline it, producing duplicated output in
78
+ // `aiden doctor --providers` and anywhere else err.message is logged.
79
+ const err = new errors_1.ProviderError(`Provider ${this.providerName} returned ${status}`, this.providerName, status, rawText, retryable);
75
80
  if (!retryable || attempt >= totalAttempts)
76
81
  throw err;
77
82
  lastError = err;
@@ -149,7 +154,9 @@ class OllamaPromptToolsAdapter {
149
154
  clearTimeout(timer);
150
155
  const status = response.status;
151
156
  const rawText = await this.safeReadText(response);
152
- throw new errors_1.ProviderError(`Provider ${this.providerName} returned ${status}: ${rawText.slice(0, 500)}`, this.providerName, status, rawText, status >= 500);
157
+ // Phase v4.1.1-oauth-fix Phase 5: composeMessage handles body
158
+ // rendering centrally; inlining it here would duplicate.
159
+ throw new errors_1.ProviderError(`Provider ${this.providerName} returned ${status}`, this.providerName, status, rawText, status >= 500);
153
160
  }
154
161
  if (!response.body) {
155
162
  clearTimeout(timer);
@@ -73,6 +73,30 @@ function makeSubagentFanoutTool(factory) {
73
73
  tasks: {
74
74
  type: 'array',
75
75
  description: 'Per-child task list (partition mode only). Length must equal n.',
76
+ // Schema mirrors PartitionTask interface in
77
+ // core/v4/subagent/fanout.ts:70-75. If you change one, change
78
+ // the other. OpenAI Codex backend strictly validates schemas
79
+ // and rejects `type: "array"` declarations missing `items`,
80
+ // so the inner shape must be explicit here.
81
+ items: {
82
+ type: 'object',
83
+ description: 'One unit of work for a partition-mode child.',
84
+ properties: {
85
+ goal: {
86
+ type: 'string',
87
+ description: 'The task this child should accomplish.',
88
+ },
89
+ context: {
90
+ type: 'string',
91
+ description: 'Optional shared context for the child.',
92
+ },
93
+ role: {
94
+ type: 'string',
95
+ description: 'Optional role tag, diagnostic only.',
96
+ },
97
+ },
98
+ required: ['goal'],
99
+ },
76
100
  },
77
101
  merge: {
78
102
  type: 'string',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiden-runtime",
3
- "version": "4.1.0",
3
+ "version": "4.1.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },