aiden-runtime 4.0.0 → 4.0.2

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
@@ -59,7 +59,7 @@ Local-first · Self-healing routing · Browser & terminal control · Persistent
59
59
  <p align="center">
60
60
  <img src="https://img.shields.io/badge/Vitest-4-6e9f18?logo=vitest&logoColor=white&style=for-the-badge" alt="Vitest 4" />
61
61
  <img src="https://img.shields.io/badge/esbuild-bundler-ffcf00?logo=esbuild&logoColor=black&style=for-the-badge" alt="esbuild" />
62
- <img src="https://img.shields.io/badge/electron--builder-NSIS-47848f?style=for-the-badge" alt="electron-builder" />
62
+ <img src="https://img.shields.io/badge/install-npm%20global-cb3837?logo=npm&logoColor=white&style=for-the-badge" alt="npm install -g aiden-runtime" />
63
63
  </p>
64
64
 
65
65
  <p align="center">
@@ -139,7 +139,7 @@ Most AI agents answer questions. Aiden runs work end-to-end on your machine.
139
139
  - **Plugin extension** — drop a plugin into `<aiden-home>/plugins/` and call `ctx.commandRegistry.register()` to add slash commands without touching core
140
140
  - **Open source** — AGPL-3.0 core, Apache-2.0 skills. Read every line, modify anything, contribute back.
141
141
 
142
- Aiden is a local-first AI operating system. It runs entirely on your machine — no cloud account required, no telemetry, no data leaving your hardware unless you configure a cloud provider. It ships with a signed Windows installer, Linux AppImage and .deb, and runs in headless API mode on macOS. Features: 68 bundled skills, 42 built-in tools across 11 categories, multi-layer memory architecture, self-healing provider routing across 19 providers, the ability to control your screen, browse the web, run code, send emails and messages, manage files, and hold a full conversation — offline via Ollama.
142
+ Aiden is a local-first AI operating system. It runs entirely on your machine — no cloud account required, no telemetry, no data leaving your hardware unless you configure a cloud provider. It installs as a global npm package (`aiden-runtime`, ~16 MB) on Windows, Linux, WSL, and macOS Node.js 18+ is the only prerequisite. Features: 68 bundled skills, 42 built-in tools across 11 categories, multi-layer memory architecture, self-healing provider routing across 19 providers, the ability to control your screen, browse the web, run code, send emails and messages, manage files, and hold a full conversation — offline via Ollama.
143
143
 
144
144
  ---
145
145
 
@@ -164,12 +164,14 @@ For commercial deployments with support and indemnification, see [aiden.taracod.
164
164
 
165
165
  ## Platform support
166
166
 
167
- | Platform | GUI app | API + CLI | Skills available |
168
- |---|---|---|---|
169
- | **Windows 10/11** | signed installer | | All 68 (including Windows-only skills) |
170
- | **Linux** | ✅ AppImage / .deb | ✅ headless | ~62 (Windows-only skills auto-skipped) |
171
- | **WSL 2** | | headless | ~62 (Windows-only skills auto-skipped) |
172
- | **macOS** | | headless | ~62 (Windows-only skills auto-skipped) |
167
+ All platforms use the same npm-based install path. Node.js 18+ is the only prerequisite.
168
+
169
+ | Platform | Install | Skills available |
170
+ |---|---|---|
171
+ | **Windows 10/11** | `npm install -g aiden-runtime` | All 68 (including Windows-only skills) |
172
+ | **Linux** | `npm install -g aiden-runtime` | ~62 (Windows-only skills auto-skipped) |
173
+ | **WSL 2** | ✅ `npm install -g aiden-runtime` | ~62 (Windows-only skills auto-skipped) |
174
+ | **macOS** | ✅ `npm install -g aiden-runtime` | ~62 (Windows-only skills auto-skipped) |
173
175
 
174
176
  Windows-only skills (clipboard history, Defender, OneNote, Outlook COM, registry, Task Scheduler, etc.) are tagged `platform: windows` and silently skipped on other platforms at load time.
175
177
 
@@ -224,18 +226,18 @@ npm install -g aiden-runtime
224
226
  aiden
225
227
  ```
226
228
 
227
- ### Prerequisites (for installer / manual builds)
229
+ ### Prerequisites (all platforms)
228
230
  - Node.js 18+
229
- - Git
231
+ - Git (only for the manual install path below)
230
232
  - Ollama (optional, for offline mode): [ollama.ai](https://ollama.ai)
231
233
 
232
- ### Windows — signed installer
234
+ ### Windows — one-line install
233
235
 
234
236
  ```powershell
235
237
  irm aiden.taracod.com/install.ps1 | iex
236
238
  ```
237
239
 
238
- Or [download the signed installer](https://github.com/taracodlabs/aiden-releases/releases/latest) manually. Windows 10/11, 64-bit, ~500 MB disk space.
240
+ The installer verifies Node.js 18+ then runs `npm install -g aiden-runtime`. Same package as `npx aiden-runtime` above; just adds the `aiden` command to your PATH so you can launch from any terminal.
239
241
 
240
242
  ### Linux / WSL / macOS — one-line install
241
243
 
@@ -648,7 +650,7 @@ Both the terminal CLI and the browser dashboard (`localhost:4200/ui`) expose the
648
650
 
649
651
  - **TypeScript 5.9** — strict mode, full typing across core, providers, CLI, API.
650
652
  - **Node.js 18+** — runtime; `node-fetch` not needed (built-in `fetch`).
651
- - **Electron 41** — Windows NSIS installer, Linux AppImage + .deb.
653
+ - **Electron 41** — optional desktop wrapper; primary install is npm-based.
652
654
  - **Next.js (app router)** — `dashboard-next/` for the browser UI.
653
655
  - **React 18** — dashboard component model.
654
656
  - **Playwright 1.58** — browser automation backbone.
@@ -658,7 +660,7 @@ Both the terminal CLI and the browser dashboard (`localhost:4200/ui`) expose the
658
660
  - **croner** — cron scheduler.
659
661
  - **discord.js**, **@slack/web-api**, **whatsapp-web.js**, **twilio**, **nodemailer**, **imap-simple** — channel adapters.
660
662
  - **Vitest 4** — test runner; ~1,500 unit + integration tests.
661
- - **esbuild** + **electron-builder** — build pipeline.
663
+ - **esbuild** bundler for the npm package; **electron-builder** — optional desktop wrapper.
662
664
  - **Cloudflare Workers** — landing page + license server + install-script proxy.
663
665
 
664
666
  ---
@@ -58,6 +58,9 @@ var __importStar = (this && this.__importStar) || (function () {
58
58
  return result;
59
59
  };
60
60
  })();
61
+ var __importDefault = (this && this.__importDefault) || function (mod) {
62
+ return (mod && mod.__esModule) ? mod : { "default": mod };
63
+ };
61
64
  Object.defineProperty(exports, "__esModule", { value: true });
62
65
  exports.getEnvSource = exports.loadAidenEnvFile = void 0;
63
66
  exports.main = main;
@@ -71,6 +74,7 @@ exports.runSkillsSubcommand = runSkillsSubcommand;
71
74
  /* eslint-disable @typescript-eslint/no-explicit-any */
72
75
  const commander_1 = require("commander");
73
76
  const node_fs_1 = require("node:fs");
77
+ const node_path_1 = __importDefault(require("node:path"));
74
78
  const chatSession_1 = require("./chatSession");
75
79
  const aidenTUI_1 = require("./aidenTUI");
76
80
  const display_1 = require("./display");
@@ -106,6 +110,7 @@ const runtimeResolver_1 = require("../../providers/v4/runtimeResolver");
106
110
  const chatCompletionsAdapter_1 = require("../../providers/v4/chatCompletionsAdapter");
107
111
  const providerFallback_1 = require("../../core/v4/providerFallback");
108
112
  const skillBundledRestore_1 = require("../../core/v4/skillBundledRestore");
113
+ const providerDetection_1 = require("../../core/v4/firstRun/providerDetection");
109
114
  const aidenLogger_1 = require("../../core/v4/aidenLogger");
110
115
  const plugins_1 = require("../../core/v4/plugins");
111
116
  const providerAuth_1 = require("../../core/v4/auth/providerAuth");
@@ -351,9 +356,70 @@ async function buildAgentRuntime(cliOpts, opts) {
351
356
  }
352
357
  const config = new config_1.ConfigManager(paths);
353
358
  await config.load();
354
- if (await (0, setupWizard_1.isFreshInstall)(paths)) {
355
- process.stdout.write('Aiden is not configured yet. Running setup wizard…\n');
356
- await (0, setupWizard_1.runSetupWizard)({ paths });
359
+ // Phase 30.2 — fresh-user UX. Detection extends the old
360
+ // `isFreshInstall`-only gate so we cover three new failure modes:
361
+ // 1. fresh user with no env / no OAuth / no config → wizard fires
362
+ // (was working under the old gate; still does).
363
+ // 2. user with config.yaml pointing at chatgpt-plus but a stale /
364
+ // missing OAuth token file → wizard fires (was NOT under old
365
+ // code — it saw config.yaml present and proceeded into a
366
+ // broken resolve, which surfaced as a confusing rate-limit
367
+ // error on the user's first chat).
368
+ // 3. user with no config but Ollama running OR an env API key
369
+ // → wizard fires anyway. ConfigManager's DEFAULT_CONFIG points
370
+ // at `anthropic / claude-opus-4-7`, which doesn't match the
371
+ // detected env / Ollama, so skipping the wizard would surface
372
+ // the same confusing "missing ANTHROPIC_API_KEY" error.
373
+ // 4. moat-boot test fixtures that stub `providers.fake.apiKey`
374
+ // inline in config.yaml count as configured — `isFreshInstall`
375
+ // already returns false for them so `wizardNeeded` stays false.
376
+ const detection = await (0, providerDetection_1.detectAvailableProviders)({ paths });
377
+ const configuredProviderBroken = !!detection.configProvider &&
378
+ !detection.configuredProviderHasCredentials;
379
+ const wizardNeeded = !detection.hasAnyProvider ||
380
+ configuredProviderBroken ||
381
+ (await (0, setupWizard_1.isFreshInstall)(paths));
382
+ // Phase 30.2.1: when the wizard returns 'skipped' (explore mode) we
383
+ // boot the REPL with a NullAdapter instead of trying to resolve a
384
+ // real provider. Flagged here, set inside the wizard block, and
385
+ // consumed when building the adapter.
386
+ let exploreMode = false;
387
+ if (wizardNeeded) {
388
+ if (!detection.hasAnyProvider) {
389
+ // Truly empty: no env, no OAuth, no Ollama, no inline config.
390
+ process.stdout.write(`\n${(0, providerDetection_1.summarizeDetection)(detection)}\n`);
391
+ }
392
+ else if (configuredProviderBroken) {
393
+ // Config points at a provider we can't credential-resolve.
394
+ process.stdout.write(`\nConfigured provider '${detection.configProvider}' has no usable credentials ` +
395
+ `at ${node_path_1.default.join(paths.root, 'auth', `${detection.configProvider}.json`)}.\n`);
396
+ }
397
+ else {
398
+ // Detected something (env / oauth / ollama) but config.yaml is
399
+ // missing or empty — DEFAULT_CONFIG would route to anthropic and
400
+ // the resolver would fail. Surface the detection so the user
401
+ // sees what we found, then walk them through proper setup.
402
+ process.stdout.write(`\n${(0, providerDetection_1.summarizeDetection)(detection)}\n`);
403
+ process.stdout.write('config.yaml is empty — let\'s pick a provider that matches.\n');
404
+ }
405
+ process.stdout.write('Launching setup wizard…\n\n');
406
+ const result = await (0, setupWizard_1.runSetupWizard)({ paths });
407
+ // Phase 30.2.1: three exit states.
408
+ if (result.status === 'exited') {
409
+ // Recovery option [5] — clean exit, no REPL.
410
+ process.exit(0);
411
+ }
412
+ if (result.status === 'skipped') {
413
+ // Recovery option [4] "explore mode" OR Ctrl+C cancellation.
414
+ // Boot continues into the REPL with a NullAdapter; chat is
415
+ // intercepted by ChatSession, slash commands work normally.
416
+ // Flagged here and consumed below where the adapter is built.
417
+ exploreMode = true;
418
+ }
419
+ // 'configured' (or 'skipped' — we still want the env/.env reload
420
+ // for slash commands like /providers that read fresh state) →
421
+ // re-load both so the resolver sees what the wizard wrote.
422
+ (0, envSources_1.loadAidenEnvFile)(paths.envFile);
357
423
  await config.load();
358
424
  }
359
425
  const providerId = cliOpts.provider ??
@@ -385,19 +451,32 @@ async function buildAgentRuntime(cliOpts, opts) {
385
451
  const credentialResolver = new credentialResolver_1.CredentialResolver(paths.authJson);
386
452
  const resolver = new runtimeResolver_1.RuntimeResolver(credentialResolver);
387
453
  let adapter;
388
- try {
389
- adapter = await resolver.resolve({ providerId, modelId, config, paths });
454
+ if (exploreMode) {
455
+ // Phase 30.2.1 — wizard skipped. Use a NullAdapter so AidenAgent
456
+ // construction succeeds; ChatSession will intercept chat attempts
457
+ // BEFORE calling the adapter and surface the friendly message.
458
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
459
+ const { NullAdapter } = require('../../providers/v4/nullAdapter');
460
+ adapter = new NullAdapter();
390
461
  }
391
- catch (err) {
392
- display.printError(`Could not resolve provider '${providerId}' / model '${modelId}': ${err.message}`, 'Run `aiden model` to pick a valid provider, or `aiden doctor`.');
393
- process.exit(1);
462
+ else {
463
+ try {
464
+ adapter = await resolver.resolve({ providerId, modelId, config, paths });
465
+ }
466
+ catch (err) {
467
+ display.printError(`Could not resolve provider '${providerId}' / model '${modelId}': ${err.message}`, 'Run `aiden model` to pick a valid provider, or `aiden doctor`.');
468
+ process.exit(1);
469
+ }
394
470
  }
395
471
  // Phase 16b.1: wrap chat_completions providers in a FallbackAdapter so
396
472
  // 429s on Groq slot 1 transparently retry Groq slot 2/3 and Together.
397
473
  // Only activates when there's at least one *additional* slot configured
398
474
  // beyond the primary — otherwise the wrapper would just rethrow.
475
+ // Phase 30.2.1: skip in explore mode — wrapping a NullAdapter in
476
+ // FallbackAdapter would just defer the friendly error one layer.
399
477
  let fallbackAdapter = null;
400
- if (adapter.apiMode === 'chat_completions' &&
478
+ if (!exploreMode &&
479
+ adapter.apiMode === 'chat_completions' &&
401
480
  (providerId === 'groq' || providerId === 'together')) {
402
481
  const slots = buildAgentFallbackSlots(adapter, providerId, modelId);
403
482
  const reachable = slots.filter((s) => s.keyPresent);
@@ -820,6 +899,7 @@ async function buildAgentRuntime(cliOpts, opts) {
820
899
  fallbackAdapter,
821
900
  personalityManager,
822
901
  pluginLoader,
902
+ exploreMode,
823
903
  };
824
904
  }
825
905
  async function runInteractiveChat(cliOpts, opts) {
@@ -846,6 +926,9 @@ async function runInteractiveChat(cliOpts, opts) {
846
926
  paths: runtime.paths,
847
927
  personalityManager: runtime.personalityManager,
848
928
  pluginLoader: runtime.pluginLoader,
929
+ // Phase 30.2.1 — boot card renders "model not configured" and
930
+ // chat attempts get the friendly NotConfiguredError message.
931
+ unconfigured: runtime.exploreMode,
849
932
  };
850
933
  if (cliOpts.tui) {
851
934
  await (0, aidenTUI_1.runTuiMode)({
@@ -212,6 +212,18 @@ class ChatSession {
212
212
  }
213
213
  // ── Inner: a single agent turn ─────────────────────────────────────
214
214
  async runAgentTurn(userInput) {
215
+ // Phase 30.2.1 — explore mode: short-circuit BEFORE building the
216
+ // turn-status spinner / agent call. The wizard skipped, so there's
217
+ // no real provider to talk to. Print a friendly redirect to /setup
218
+ // (or the env-var alternative) and return — REPL stays alive, user
219
+ // can run slash commands or hit /quit.
220
+ if (this.opts.unconfigured) {
221
+ void userInput; // silence unused-arg warning when this branch fires
222
+ this.opts.display.write('\n');
223
+ this.opts.display.printError('No AI provider configured yet.', 'Run /setup to configure a provider, or set an API key environment variable (e.g. GROQ_API_KEY).');
224
+ this.opts.display.write('\n');
225
+ return;
226
+ }
215
227
  // Phase 22 Task 4: status bar reflects the live phase. Set on
216
228
  // entry, cleared in both success and error paths below.
217
229
  this.setStatusState({ kind: 'generating', sinceMs: Date.now() });
@@ -353,11 +365,15 @@ class ChatSession {
353
365
  skillsLoaded = 0;
354
366
  }
355
367
  // PIECE 1 — status pills row.
368
+ // Phase 30.2.1: in explore mode the model pill renders "not
369
+ // configured" instead of the DEFAULT_CONFIG fallback, so a fresh
370
+ // user who skipped the wizard isn't misled by a stale model name.
356
371
  display.write(display.statusPillsRow({
357
372
  coreOnline: true,
358
373
  mode: 'auto',
359
374
  model: this.currentModelId,
360
375
  memoryActive: true,
376
+ providerOk: !this.opts.unconfigured,
361
377
  }) + '\n');
362
378
  display.write(` ${display.rule()}\n`);
363
379
  display.write('\n');
@@ -12,7 +12,7 @@
12
12
  * and registers each on the global CommandRegistry at boot.
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.allCommands = exports.cron = exports.doctor = exports.license = exports.auth = exports.plugins = exports.streaming = exports.debugPrompt = exports.identity = exports.providers = exports.quit = exports.clear = exports.verbose = exports.reasoning = exports.reloadMcp = exports.skills = exports.skin = exports.yolo = exports.usage = exports.compress = exports.title = exports.save = exports.personality = exports.model = exports.tools = exports.help = void 0;
15
+ exports.allCommands = exports.setup = exports.cron = exports.doctor = exports.license = exports.auth = exports.plugins = exports.streaming = exports.debugPrompt = exports.identity = exports.providers = exports.quit = exports.clear = exports.verbose = exports.reasoning = exports.reloadMcp = exports.skills = exports.skin = exports.yolo = exports.usage = exports.compress = exports.title = exports.save = exports.personality = exports.model = exports.tools = exports.help = void 0;
16
16
  const help_1 = require("./help");
17
17
  Object.defineProperty(exports, "help", { enumerable: true, get: function () { return help_1.help; } });
18
18
  const tools_1 = require("./tools");
@@ -63,6 +63,8 @@ const doctor_1 = require("./doctor");
63
63
  Object.defineProperty(exports, "doctor", { enumerable: true, get: function () { return doctor_1.doctor; } });
64
64
  const cron_1 = require("./cron");
65
65
  Object.defineProperty(exports, "cron", { enumerable: true, get: function () { return cron_1.cron; } });
66
+ const setup_1 = require("./setup");
67
+ Object.defineProperty(exports, "setup", { enumerable: true, get: function () { return setup_1.setup; } });
66
68
  /** All built-in system commands, in canonical order. */
67
69
  exports.allCommands = [
68
70
  help_1.help,
@@ -85,6 +87,7 @@ exports.allCommands = [
85
87
  license_1.license,
86
88
  doctor_1.doctor,
87
89
  cron_1.cron,
90
+ setup_1.setup,
88
91
  reloadMcp_1.reloadMcp,
89
92
  reasoning_1.reasoning,
90
93
  verbose_1.verbose,
@@ -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
+ };
@@ -305,11 +305,13 @@ class Display {
305
305
  const lab = (s) => sk.applyColors(s, 'muted');
306
306
  const val = (s) => sk.applyColors(s, 'agent');
307
307
  const pill = (on, label, value) => `${dot(on)} ${lab(label)} ${val(value)}`;
308
+ const providerOk = args.providerOk !== false;
309
+ const modelValue = providerOk ? args.model : 'not configured';
308
310
  return (' ' +
309
311
  [
310
312
  pill(args.coreOnline, 'core', args.coreOnline ? 'online' : 'starting'),
311
313
  pill(true, 'mode', args.mode),
312
- pill(true, 'model', args.model),
314
+ pill(providerOk, 'model', modelValue),
313
315
  pill(args.memoryActive, 'memory', args.memoryActive ? 'active' : 'off'),
314
316
  ].join(' '));
315
317
  }