meetsoma 0.1.3 → 0.1.5

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/CHANGELOG.md CHANGED
@@ -4,6 +4,33 @@ All notable changes to Soma (`meetsoma` on npm).
4
4
 
5
5
  Format follows [Keep a Changelog](https://keepachangelog.com/). Versioning follows [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.1.4] — 2026-03-28
8
+
9
+ Corresponds to Soma agent v0.6.5.
10
+
11
+ ### New
12
+
13
+ - **`soma inhale --list`** — see available preloads with age and staleness markers.
14
+ - **`soma inhale <name>`** — load a specific preload by date, session ID, or substring.
15
+ - **`soma inhale --load <path>`** — load any file as a preload.
16
+ - **`soma map <name>`** — run a MAP with prompt-config and targeted preload.
17
+ - **`soma map --list`** — see available MAPs with status and description.
18
+ - **`--preload` flag deprecated** — use `soma inhale` or `soma map` instead (still works).
19
+
20
+ ### Changed
21
+
22
+ - **Settings-driven heat overrides** — per-project AMPS heat control via `settings.heat.overrides`.
23
+ - **Statusline** shows preload status. Smart `/exhale` detects edit vs write mode.
24
+ - **Restart signal** — auto-detects when extensions change, prompts for restart.
25
+
26
+ ### Fixed
27
+
28
+ - **Crash on partial settings** — `settings.heat` access now defensive (won't crash with old config files).
29
+ - **Breathe grace period** — was 60s, now correctly 30s matching settings default.
30
+ - **5 auto-breathe UX gaps** — smarter context warnings, resume awareness.
31
+
32
+ ---
33
+
7
34
  ## [0.1.3] — 2026-03-23
8
35
 
9
36
  ### New
@@ -246,9 +246,9 @@ const TOPICS = {
246
246
  // ── Practical / instructional ───────────────────────────────────────
247
247
 
248
248
  how_to_install: [
249
- `{Three steps|Here's the short version|Pretty quick}. {First|Step one}: run ${"`soma init`"} {right here in your terminal|from any directory|from this CLI}. It'll {check your GitHub identity|verify who you are via GitHub|use your GitHub account} and {set everything up|install the runtime|get you running}. {You'll need|Requirements}: {Node.js 20+|Node 20 or newer} and the {GitHub CLI|gh command-line tool} (${"`brew install gh`"} {if you don't have it|on Mac|to get it}). {That's it|Nothing else|No other dependencies}.`,
249
+ `{Press Enter|Just hit Enter|Enter} and {I'll walk you through it|Soma handles the rest|the setup takes about a minute}. It {downloads the runtime|grabs everything you need|installs automatically}, {sets up your API key|walks you through authentication|helps you connect an AI provider}, and {you're ready to go|you can start right away|launches your first session}. {All you need is|Requirements:} {Node.js 20+|Node 20 or newer} and {git|git installed}. {That's it|Nothing else to do|One flow, start to finish}.`,
250
250
 
251
- `Run ${"`soma init`"} — {it handles everything|that's the whole process|one command}. It {downloads the runtime|grabs the latest version|pulls everything you need} and {sets up Soma|installs dependencies|gets you running}. {Takes about a minute|Under 60 seconds|Quick}. {Just need|Requirements:} {Node.js 20+|Node 20 or newer} and {git|git installed}.`,
251
+ `{Hit Enter|Press Enter|Just Enter} — {Soma walks you through everything|the setup is guided|it's step by step}. {Downloads the runtime|Installs the engine|Gets everything ready}, {helps you set up an API key|handles authentication|connects you to an AI provider}, {done in about a minute|quick setup|takes sixty seconds}. {Need|Requirements:} {Node.js 20+|Node 20 or newer} and {git|git installed}.`,
252
252
  ],
253
253
 
254
254
  how_to_source: [
@@ -266,7 +266,7 @@ const TOPICS = {
266
266
  ],
267
267
 
268
268
  how_to_api_key: [
269
- `{Yes|You'll need one|Required for sessions}. Soma uses {an LLM provider|an AI model|a language model} under the hood {Anthropic (Claude)|Claude by default|Anthropic's Claude}, but {also supports|works with} {Google (Gemini)|OpenAI|other providers}. Set {your API key|it} as an environment variable: ${"`export ANTHROPIC_API_KEY=sk-...`"} {before running|in your shell|in your .bashrc/.zshrc}. {The key is yours|You bring your own key|We don't provide one} {Soma never sees your key|it stays on your machine|nothing gets sent to us}.`,
269
+ `{Yes|You'll need one|Required}, but {Soma walks you through it|the setup handles it|we'll set it up together} when you install. {You bring your own key|It's your key|You get one from Anthropic} {Soma stores it locally|it stays on your machine|nothing gets sent to us}. {If you have a Claude Pro or Max subscription|Got a Claude subscription?|Claude Pro/Max users}, you can {log in with your account instead|skip the API key entirely|use OAuth no key needed}. {Press Enter to get started|Hit Enter and I'll walk you through it|Ready? Just press Enter}.`,
270
270
  ],
271
271
 
272
272
  how_to_model: [
@@ -274,7 +274,7 @@ const TOPICS = {
274
274
  ],
275
275
 
276
276
  how_to_start: [
277
- `{Here's the path|Three steps|Quick start}: {1.|First,} run ${"`soma init`"} to {install the runtime|get set up|install everything}. {2.|Then} ${"`cd`"} into {any project|your project|a directory with code}. {3.|Finally,} run ${"`soma`"} — {it creates|Soma creates|it'll make} a ${"`~/.soma/`"} directory, {detects your stack|scans your project|figures out what you're building}, and {starts learning|begins adapting|you're off}. {First session is minimal|Session one is bare|It starts simple}. {By session five|After a few sessions|Give it a week} — {it knows your patterns|it's adapted to how you work|you'll feel the difference}.`,
277
+ `{Press Enter|Hit Enter|Just Enter} {Soma handles everything|the setup is guided|I'll walk you through it}. {Installs the runtime|Downloads what you need|Gets everything ready}, {helps you connect an AI provider|sets up your API key|handles auth}, and {you can launch right away|your first session starts immediately|you're coding in about a minute}. After that, {cd into any project|go to a project directory} and run ${"`soma`"} — {it creates a .soma/ directory|Soma sets up in your project} and {starts learning how you work|begins adapting|picks up your patterns}. {By session five|After a few sessions|Give it a week} — {you'll feel the difference|it knows your workflow|it remembers everything}.`,
278
278
  ],
279
279
 
280
280
  how_to_try: [
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * postinstall.js — runs after `npm install -g meetsoma`
4
+ *
5
+ * If we're in an interactive terminal, launch the welcome/setup flow.
6
+ * If not (CI, piped, scripts), just print a short message.
7
+ */
8
+
9
+ import { fileURLToPath } from "url";
10
+ import { dirname, join } from "path";
11
+ import { execFileSync } from "child_process";
12
+
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+
15
+ if (process.stdin.isTTY && process.stdout.isTTY) {
16
+ // Interactive — run the full welcome + setup
17
+ try {
18
+ execFileSync("node", [join(__dirname, "thin-cli.js")], {
19
+ stdio: "inherit",
20
+ cwd: process.env.HOME || process.env.USERPROFILE || process.cwd(),
21
+ });
22
+ } catch {
23
+ // Non-zero exit is fine (user ctrl+c, etc.)
24
+ }
25
+ } else {
26
+ // Non-interactive — just tell them what to do
27
+ console.log("");
28
+ console.log(" ✓ Soma installed. Run \x1b[32msoma\x1b[0m to get started.");
29
+ console.log("");
30
+ }
package/dist/thin-cli.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * For returning users: detects installed runtime → delegates to it.
9
9
  */
10
10
 
11
- import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from "fs";
11
+ import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, readdirSync, statSync } from "fs";
12
12
  import { join, dirname } from "path";
13
13
  import { homedir, platform } from "os";
14
14
  import { fileURLToPath } from "url";
@@ -89,6 +89,11 @@ async function confirm(prompt) {
89
89
  return true;
90
90
  }
91
91
 
92
+ async function confirmYN(prompt) {
93
+ const key = await waitForKey(`${prompt} ${dim("[y/n]")} `);
94
+ return key.toLowerCase() === "y";
95
+ }
96
+
92
97
  // ── Typing effect ────────────────────────────────────────────────────
93
98
 
94
99
  function sleep(ms) {
@@ -281,6 +286,42 @@ function readLine(prompt) {
281
286
  });
282
287
  }
283
288
 
289
+ async function handleQuestion(input) {
290
+ const match = matchQuestion(input);
291
+ if (match) {
292
+ const answer = voice.ask(match.topic);
293
+ console.log("");
294
+ await typeParagraph(answer);
295
+ return true;
296
+ }
297
+
298
+ // Edge case routing — detect intent even without a topic match
299
+ const lower = input.toLowerCase();
300
+ const rude = /suck|stupid|dumb|trash|garbage|hate|worst|bad|ugly|boring|lame|waste/.test(lower);
301
+ const impressed = /cool|amazing|wow|nice|love|awesome|brilliant|impressive|neat|sick|fire|goat/.test(lower);
302
+ const meta = /are you|what are you|how do you|who are you|real|alive|ai\b|bot\b/.test(lower);
303
+ const greeting = /^(hi|hey|hello|sup|yo|howdy|hola|greetings|good morning|good evening)\b/.test(lower);
304
+
305
+ console.log("");
306
+ if (greeting) {
307
+ await typeOut(` ${voice.greet()} ${voice.spin("{Ask me anything.|What do you want to know?|I know about 9 topics — pick one.}")}\n`);
308
+ } else if (rude) {
309
+ await typeParagraph(voice.ask("meta_rude"));
310
+ } else if (impressed) {
311
+ await typeParagraph(voice.ask("meta_impressed"));
312
+ } else if (meta) {
313
+ await typeParagraph(voice.ask("meta_self"));
314
+ } else {
315
+ // Anything with a question mark → try harder, then admit we don't know
316
+ if (input.includes("?")) {
317
+ await typeParagraph(voice.ask("meta_nonsense"));
318
+ } else {
319
+ await typeParagraph(voice.ask("meta_nonsense"));
320
+ }
321
+ }
322
+ return false;
323
+ }
324
+
284
325
  async function interactiveQ() {
285
326
  console.log("");
286
327
  console.log(` ${bold("Ask me anything.")}`);
@@ -290,7 +331,7 @@ async function interactiveQ() {
290
331
  console.log(` ${dim("•")} Why no compaction? ${dim("•")} Are you AI?`);
291
332
  console.log(` ${dim("•")} How does it compare? ${dim("•")} Who made this?`);
292
333
  console.log("");
293
- console.log(` ${dim("...or ask anything. I'll do my best.")}`);
334
+ console.log(` ${dim("...or ask anything. Press")} ${green("Enter")} ${dim("when you're ready to install.")}`);
294
335
 
295
336
  let rounds = 0;
296
337
  const maxRounds = 8;
@@ -299,56 +340,23 @@ async function interactiveQ() {
299
340
  console.log("");
300
341
  const input = await readLine(` ${cyan("?")} `);
301
342
 
343
+ // Empty input or quit → exit Q&A, proceed to install
302
344
  if (!input || input === "q" || input === "quit" || input === "exit") {
303
- console.log("");
304
- await typeOut(` ${voice.say("bye")}\n`);
305
345
  break;
306
346
  }
307
347
 
308
- const match = matchQuestion(input);
309
- if (match) {
310
- const answer = voice.ask(match.topic);
311
- console.log("");
312
- await typeParagraph(answer);
313
- rounds++;
314
-
315
- if (rounds < maxRounds) {
316
- console.log("");
317
- console.log(` ${dim("Ask another, or")} ${green("Enter")} ${dim("to install Soma.")}`);
318
- }
319
- } else {
320
- // Edge case routing — detect intent even without a topic match
321
- const lower = input.toLowerCase();
322
- const rude = /suck|stupid|dumb|trash|garbage|hate|worst|bad|ugly|boring|lame|waste/.test(lower);
323
- const impressed = /cool|amazing|wow|nice|love|awesome|brilliant|impressive|neat|sick|fire|goat/.test(lower);
324
- const meta = /are you|what are you|how do you|who are you|real|alive|ai\b|bot\b/.test(lower);
325
- const greeting = /^(hi|hey|hello|sup|yo|howdy|hola|greetings|good morning|good evening)\b/.test(lower);
348
+ await handleQuestion(input);
349
+ rounds++;
326
350
 
351
+ if (rounds < maxRounds) {
327
352
  console.log("");
328
- if (greeting) {
329
- await typeOut(` ${voice.greet()} ${voice.spin("{Ask me anything.|What do you want to know?|I know about 9 topics — pick one.}")}\n`);
330
- } else if (rude) {
331
- await typeParagraph(voice.ask("meta_rude"));
332
- } else if (impressed) {
333
- await typeParagraph(voice.ask("meta_impressed"));
334
- } else if (meta) {
335
- await typeParagraph(voice.ask("meta_self"));
336
- } else {
337
- await typeParagraph(voice.ask("meta_nonsense"));
338
- }
353
+ console.log(` ${dim("Ask another, or")} ${green("Enter")} ${dim("to install Soma.")}`);
339
354
  }
340
355
  }
341
356
 
342
357
  if (rounds >= maxRounds) {
343
358
  console.log("");
344
- await typeOut(` ${voice.spin("{Curious enough?|Intrigued?|Want to see it in action?}")} ${dim("Let's get you in.")}\n`);
345
- console.log("");
346
- await confirm(` ${dim("→")} Press ${bold("Enter")} to install Soma`);
347
- if (openBrowser(SITE_URL)) {
348
- console.log(` ${green("✓")} Opened ${cyan(SITE_URL)}`);
349
- } else {
350
- console.log(` ${dim("→")} Visit: ${cyan(SITE_URL)}`);
351
- }
359
+ await typeOut(` ${voice.spin("{Curious enough?|Intrigued?|Want to see it in action?}")} ${dim("Let's set you up.")}\n`);
352
360
  }
353
361
  }
354
362
 
@@ -369,6 +377,201 @@ function getGitHubUsername() {
369
377
 
370
378
  // ── Commands ─────────────────────────────────────────────────────────
371
379
 
380
+ // ── Auth helpers ─────────────────────────────────────────────────────
381
+
382
+ function hasAnyAuth() {
383
+ const hasEnvKey = !!(process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY || process.env.GEMINI_API_KEY || process.env.GROQ_API_KEY || process.env.XAI_API_KEY);
384
+ if (hasEnvKey) return true;
385
+ try {
386
+ const authData = JSON.parse(readFileSync(join(CORE_DIR, "auth.json"), "utf-8"));
387
+ return Object.keys(authData).length > 0;
388
+ } catch { return false; }
389
+ }
390
+
391
+ function getShellConfigPath() {
392
+ return process.env.SHELL?.includes("zsh") ? "~/.zshrc" : "~/.bashrc";
393
+ }
394
+
395
+ function getShellConfigAbsPath() {
396
+ const home = homedir();
397
+ return process.env.SHELL?.includes("zsh") ? join(home, ".zshrc") : join(home, ".bashrc");
398
+ }
399
+
400
+ function detectKeyInShellConfig() {
401
+ // Check if key is in shell config but not loaded (user hasn't restarted terminal)
402
+ try {
403
+ const configContent = readFileSync(getShellConfigAbsPath(), "utf-8");
404
+ const keyPattern = /export\s+(ANTHROPIC_API_KEY|OPENAI_API_KEY|GEMINI_API_KEY)=/;
405
+ const match = configContent.match(keyPattern);
406
+ if (match) return match[1];
407
+ } catch {}
408
+ return null;
409
+ }
410
+
411
+ // ── API key setup wizard ─────────────────────────────────────────────
412
+
413
+ async function apiKeySetup() {
414
+ console.log(` ${yellow("!")} One more thing — Soma needs an AI provider to work.`);
415
+ console.log("");
416
+ await typeOut(` ${voice.spin("{Do you have an Anthropic API key?|Got a Claude API key?|Have an API key for Claude?}")}\n`);
417
+ console.log("");
418
+ console.log(` ${green("y")} ${dim("Yes, I have a key")}`);
419
+ console.log(` ${green("n")} ${dim("No, I need one")}`);
420
+ console.log(` ${green("s")} ${dim("I have a Claude Pro/Max subscription")}`);
421
+ console.log(` ${green("?")} ${dim("What's an API key?")}`);
422
+ console.log("");
423
+
424
+ const key = await waitForKey(` ${dim("→")} `);
425
+ const choice = key.toLowerCase();
426
+
427
+ if (choice === "y") {
428
+ await apiKeyEntry();
429
+ } else if (choice === "s") {
430
+ await oauthGuide();
431
+ } else if (choice === "?") {
432
+ await apiKeyExplain();
433
+ } else {
434
+ await apiKeyGetOne();
435
+ }
436
+ }
437
+
438
+ async function apiKeyExplain() {
439
+ console.log("");
440
+ await typeParagraph("An API key is like a password that lets Soma talk to an AI model. You get one from Anthropic (the company that makes Claude), paste it into your terminal config, and Soma handles the rest. Your key stays on your machine — Soma never sends it anywhere.");
441
+ console.log("");
442
+ await typeParagraph("If you have a Claude Pro or Max subscription, you don't need a separate key — you can log in with your account instead.");
443
+ console.log("");
444
+
445
+ console.log(` ${green("g")} ${dim("Get a key (I'll show you how)")}`);
446
+ console.log(` ${green("s")} ${dim("I have Claude Pro/Max — log in instead")}`);
447
+ console.log("");
448
+
449
+ const key = await waitForKey(` ${dim("→")} `);
450
+ if (key.toLowerCase() === "s") {
451
+ await oauthGuide();
452
+ } else {
453
+ await apiKeyGetOne();
454
+ }
455
+ }
456
+
457
+ async function apiKeyGetOne() {
458
+ console.log("");
459
+ await typeOut(` ${voice.spin("{Here's how.|Let me walk you through it.|Quick steps.}")}\n`);
460
+ console.log("");
461
+ console.log(` ${cyan("Step 1:")} Open this link to create a key:`);
462
+ console.log("");
463
+ console.log(` ${cyan("https://console.anthropic.com/settings/keys")}`);
464
+ console.log("");
465
+ openBrowser("https://console.anthropic.com/settings/keys");
466
+ console.log(` ${dim("(opened in your browser)")}`);
467
+ console.log("");
468
+ await confirm(` ${dim("→")} Press ${bold("Enter")} when you have your key`);
469
+ await apiKeyEntry();
470
+ }
471
+
472
+ function readSecret(prompt) {
473
+ return new Promise(resolve => {
474
+ process.stdout.write(prompt);
475
+ if (!process.stdin.isTTY) { resolve(""); return; }
476
+ process.stdin.setRawMode(true);
477
+ process.stdin.resume();
478
+ process.stdin.setEncoding("utf-8");
479
+ let buf = "";
480
+ const onData = chunk => {
481
+ for (const ch of chunk) {
482
+ if (ch === "\r" || ch === "\n") {
483
+ process.stdin.setRawMode(false);
484
+ process.stdin.pause();
485
+ process.stdin.removeListener("data", onData);
486
+ process.stdout.write("\n");
487
+ resolve(buf);
488
+ return;
489
+ } else if (ch === "\u007F" || ch === "\b") {
490
+ // Backspace
491
+ if (buf.length > 0) {
492
+ buf = buf.slice(0, -1);
493
+ process.stdout.write("\b \b");
494
+ }
495
+ } else if (ch === "\u0003") {
496
+ // Ctrl+C
497
+ process.stdout.write("\n");
498
+ process.exit(0);
499
+ } else if (ch >= " ") {
500
+ buf += ch;
501
+ process.stdout.write("•");
502
+ }
503
+ }
504
+ };
505
+ process.stdin.on("data", onData);
506
+ });
507
+ }
508
+
509
+ async function apiKeyEntry() {
510
+ console.log("");
511
+ console.log(` ${cyan("Step 2:")} Paste your key below.`);
512
+ console.log(` ${dim("It starts with")} sk-ant-...`);
513
+ console.log("");
514
+
515
+ const apiKey = await readSecret(` ${dim("Key:")} `);
516
+
517
+ if (!apiKey || !apiKey.startsWith("sk-")) {
518
+ console.log("");
519
+ if (!apiKey) {
520
+ console.log(` ${dim("No key entered. You can set it up later.")}`);
521
+ } else {
522
+ console.log(` ${yellow("!")} That doesn't look like an Anthropic key.`);
523
+ console.log(` ${dim("Keys start with")} sk-ant-...`);
524
+ }
525
+ console.log("");
526
+ const sc = getShellConfigPath();
527
+ console.log(` ${dim("When you have your key, add it to")} ${dim(sc)}${dim(":")}`);
528
+ console.log(` ${green('export ANTHROPIC_API_KEY="your-key-here"')}`);
529
+ console.log(` ${dim("Then restart your terminal and run")} ${green("soma")}`);
530
+ console.log("");
531
+ return;
532
+ }
533
+
534
+ // Write to shell config
535
+ const shellConfigPath = getShellConfigAbsPath();
536
+ const shellConfigName = getShellConfigPath();
537
+ const exportLine = `\nexport ANTHROPIC_API_KEY="${apiKey}"\n`;
538
+
539
+ try {
540
+ appendFileSync(shellConfigPath, exportLine);
541
+ console.log("");
542
+ console.log(` ${green("✓")} Key saved to ${dim(shellConfigName)}`);
543
+ console.log("");
544
+
545
+ // Set it for the current process too so we can launch immediately
546
+ process.env.ANTHROPIC_API_KEY = apiKey;
547
+
548
+ await typeOut(` ${voice.spin("{You're all set.|Good to go.|Ready.}")} ${dim("Soma can start now.")}\n`);
549
+ console.log("");
550
+ } catch {
551
+ console.log("");
552
+ console.log(` ${yellow("!")} Couldn't write to ${dim(shellConfigName)}.`);
553
+ console.log(` ${dim("Add this line manually:")}`);
554
+ console.log(` ${green(`export ANTHROPIC_API_KEY="${apiKey}"`)}`);
555
+ console.log(` ${dim("Then restart your terminal and run")} ${green("soma")}`);
556
+ console.log("");
557
+ }
558
+ }
559
+
560
+ async function oauthGuide() {
561
+ console.log("");
562
+ await typeParagraph("Nice — with a Pro or Max subscription, you can log in with your Anthropic account. No API key needed.");
563
+ console.log("");
564
+ console.log(` ${dim("When Soma starts, type")} ${green("/login")} ${dim("and follow the prompts.")}`);
565
+ console.log(` ${dim("It'll open your browser to authenticate.")}`);
566
+ console.log("");
567
+ await typeOut(` ${voice.spin("{Let's launch.|Starting up.|Here we go.}")}\n`);
568
+ console.log("");
569
+ // Mark that user chose OAuth so we don't block launch
570
+ process.env._SOMA_OAUTH_PENDING = "1";
571
+ }
572
+
573
+ // ── Welcome / First Run ─────────────────────────────────────────────
574
+
372
575
  async function showWelcome() {
373
576
  printSigma();
374
577
  console.log(` ${bold("Soma")} ${dim("—")} ${white("the AI agent that remembers")}`);
@@ -381,32 +584,74 @@ async function showWelcome() {
381
584
  if (ghUser) {
382
585
  console.log(` ${green("✓")} ${voice.greetBack(ghUser)}`);
383
586
  }
384
- console.log(` ${green("✓")} Core installed. Starting Soma...`);
587
+
588
+ if (!hasAnyAuth()) {
589
+ console.log(` ${green("✓")} Core installed`);
590
+ console.log("");
591
+
592
+ // Check if key exists in shell config but isn't loaded
593
+ const unloadedKey = detectKeyInShellConfig();
594
+ if (unloadedKey) {
595
+ console.log(` ${yellow("!")} Found ${bold(unloadedKey)} in ${dim(getShellConfigPath())} but it's not loaded.`);
596
+ console.log(` ${dim("Restart your terminal and run")} ${green("soma")} ${dim("again.")}`);
597
+ console.log("");
598
+ return;
599
+ }
600
+
601
+ await apiKeySetup();
602
+
603
+ // If they set a key or chose OAuth, launch. If not, exit gracefully.
604
+ if (!hasAnyAuth() && !process.env.ANTHROPIC_API_KEY && !process.env._SOMA_OAUTH_PENDING) {
605
+ console.log(` ${dim("No worries.")} ${voice.spin("{Come back when you're ready.|Set up a key and run soma again.|We'll be here.}")}`);
606
+ console.log("");
607
+ console.log(` ${dim(`v${VERSION} · BSL 1.1 · soma.gravicity.ai`)}`);
608
+ console.log("");
609
+ return;
610
+ }
611
+ } else {
612
+ console.log(` ${green("✓")} Core installed. Starting Soma...`);
613
+ }
385
614
  console.log("");
386
615
  await delegateToCore();
387
616
  return;
388
617
  }
389
618
 
390
- // Not installed — show a concept + offer to set up
391
- const concept = CONCEPTS[getConceptIndex()];
392
- const body = getConceptBody(concept.topic);
619
+ // ── Not installed — first time ever ────────────────────────────────
393
620
 
394
- console.log(` ${magenta("❝")} ${bold(concept.title)}`);
621
+ await typeOut(` ${voice.greet()}\n`);
395
622
  console.log("");
396
- await typeParagraph(body);
623
+ await typeParagraph("Soma is an AI coding agent that remembers across sessions. It learns your patterns, builds its own tools, and picks up where it left off.");
397
624
  console.log("");
398
625
  console.log(` ${dim("─".repeat(58))}`);
399
626
  console.log("");
400
- console.log(` ${dim("→")} ${green("Enter")} Install Soma`);
401
- console.log(` ${dim("→")} ${green("?")} Ask me something first`);
627
+ console.log(` ${dim("→")} Press ${green("Enter")} to set up, or type a question.`);
402
628
  console.log("");
403
629
 
404
- const key = await waitForKey(` ${dim("Your move:")} `);
630
+ const input = await readLine(` ${dim("")} `);
405
631
 
406
- if (key === "?") {
632
+ if (input && input !== "") {
633
+ await handleQuestion(input);
407
634
  await interactiveQ();
408
- } else {
409
- await initSoma();
635
+ }
636
+
637
+ // Install the runtime
638
+ await initSoma();
639
+
640
+ // If install succeeded, run the API key setup
641
+ if (isInstalled() && !hasAnyAuth()) {
642
+ await apiKeySetup();
643
+ }
644
+
645
+ // If they have auth now (or chose OAuth), offer to launch
646
+ if (isInstalled() && (hasAnyAuth() || process.env.ANTHROPIC_API_KEY || process.env._SOMA_OAUTH_PENDING)) {
647
+ console.log(` ${dim("─".repeat(58))}`);
648
+ console.log("");
649
+ const launch = await confirmYN(` ${voice.spin("{Ready to go?|Want to start your first session?|Launch Soma?}")}`);
650
+ if (launch) {
651
+ console.log("");
652
+ await delegateToCore();
653
+ return;
654
+ }
410
655
  }
411
656
 
412
657
  console.log("");
@@ -429,7 +674,10 @@ function showHelp() {
429
674
  console.log(` ${green("soma")} Start a session`);
430
675
  console.log(` ${green("soma focus <keyword>")} Start a focused session`);
431
676
  console.log(` ${green("soma inhale")} Resume from last session's preload`);
432
- console.log(` ${green("soma --map <name>")} Load a specific workflow`);
677
+ console.log(` ${green("soma inhale <name>")} Load a specific preload by name`);
678
+ console.log(` ${green("soma inhale --list")} Show available preloads`);
679
+ console.log(` ${green("soma map <name>")} Load a specific workflow (MAP)`);
680
+ console.log(` ${green("soma map --list")} Show available MAPs`);
433
681
  console.log("");
434
682
  console.log(` ${bold("Maintenance")}`);
435
683
  console.log(` ${green("soma doctor")} Verify installation health`);
@@ -512,7 +760,65 @@ async function initSoma() {
512
760
  const installDir = join(SOMA_HOME, "agent");
513
761
  mkdirSync(SOMA_HOME, { recursive: true });
514
762
 
515
- if (existsSync(installDir)) {
763
+ // Validate existing install — check it's a real git repo with dist/ content
764
+ const isValidInstall = existsSync(installDir)
765
+ && existsSync(join(installDir, ".git"))
766
+ && (existsSync(join(installDir, "dist", "extensions")) || existsSync(join(installDir, "extensions")));
767
+
768
+ // Track user files to preserve across repair/reinstall
769
+ let preservedFiles = {};
770
+
771
+ if (existsSync(installDir) && !isValidInstall) {
772
+ // Broken/partial install — try to repair, preserve user files
773
+ console.log(` ${yellow("⚠")} Incomplete installation detected.`);
774
+ console.log(` ${dim("Missing:")} ${!existsSync(join(installDir, ".git")) ? "git repo" : "core files"}`);
775
+ console.log("");
776
+
777
+ // Save user files before touching anything
778
+ const userFileNames = ["auth.json", "models.json"];
779
+ for (const f of userFileNames) {
780
+ const fp = join(installDir, f);
781
+ if (existsSync(fp)) {
782
+ try {
783
+ preservedFiles[f] = readFileSync(fp, "utf-8");
784
+ console.log(` ${dim("→")} Preserving ${f}`);
785
+ } catch {}
786
+ }
787
+ }
788
+
789
+ // Try repair: if it's a git repo, fetch + reset. Otherwise move aside for fresh clone.
790
+ const hasGit = existsSync(join(installDir, ".git"));
791
+ let repaired = false;
792
+
793
+ if (hasGit) {
794
+ console.log(` ${yellow("⏳")} Repairing...`);
795
+ try {
796
+ execSync("git fetch origin", { cwd: installDir, stdio: "ignore", timeout: 30000 });
797
+ execSync("git reset --hard origin/main", { cwd: installDir, stdio: "ignore" });
798
+ console.log(` ${green("✓")} Repaired from remote`);
799
+ repaired = true;
800
+ } catch {
801
+ console.log(` ${yellow("!")} Repair failed — will re-download.`);
802
+ }
803
+ }
804
+
805
+ if (!repaired) {
806
+ // Move broken dir aside (never delete), then clone will happen below
807
+ const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
808
+ const backup = join(SOMA_HOME, `agent-backup-${ts}`);
809
+ try {
810
+ execSync(`mv "${installDir}" "${backup}"`, { stdio: "ignore" });
811
+ console.log(` ${dim("Old files saved to")} ${dim(backup.replace(homedir(), "~"))}`);
812
+ } catch {
813
+ console.log(` ${red("✗")} Could not move old installation aside.`);
814
+ console.log(` ${dim("Try:")} mv ~/.soma/agent ~/.soma/agent-old && soma init`);
815
+ console.log("");
816
+ return;
817
+ }
818
+ }
819
+ }
820
+
821
+ if (isValidInstall) {
516
822
  console.log(` ${dim("→")} Runtime already installed.`);
517
823
 
518
824
  // Pull latest
@@ -554,13 +860,30 @@ async function initSoma() {
554
860
  }
555
861
  }
556
862
 
557
- // Verify
863
+ // Restore preserved user files (from broken install repair)
864
+ if (Object.keys(preservedFiles).length > 0) {
865
+ for (const [f, content] of Object.entries(preservedFiles)) {
866
+ try {
867
+ writeFileSync(join(installDir, f), content, { mode: 0o600 });
868
+ console.log(` ${green("✓")} Restored ${f}`);
869
+ } catch {}
870
+ }
871
+ }
872
+
873
+ // Verify — gate success on actual working install
558
874
  const hasExts = existsSync(join(installDir, "dist", "extensions"));
559
875
  const hasCore = existsSync(join(installDir, "dist", "core"));
560
- if (hasExts && hasCore) {
561
- console.log(` ${green("✓")} Extensions and core ready`);
876
+
877
+ if (!hasExts || !hasCore) {
878
+ console.log("");
879
+ console.log(` ${red("✗")} Installation incomplete — core files missing.`);
880
+ console.log(` ${dim("Try:")} rm -rf ~/.soma/agent && soma init`);
881
+ console.log("");
882
+ return;
562
883
  }
563
884
 
885
+ console.log(` ${green("✓")} Extensions and core ready`);
886
+
564
887
  // Save config
565
888
  const config = readConfig();
566
889
  config.installedAt = config.installedAt || new Date().toISOString();
@@ -571,12 +894,118 @@ async function initSoma() {
571
894
  console.log("");
572
895
  console.log(` ${green("✓")} ${bold("Soma is installed!")}`);
573
896
  console.log("");
574
- console.log(` Next steps:`);
575
- console.log(` ${cyan("1.")} ${green("cd <your-project>")}`);
576
- console.log(` ${cyan("2.")} ${green("soma")} to start your first session`);
897
+ }
898
+
899
+ async function checkAndUpdate() {
900
+ printSigma();
901
+ console.log(` ${bold("Soma")} — Status`);
902
+ console.log("");
903
+
904
+ const config = readConfig();
905
+ const installPath = config.installPath || join(SOMA_HOME, "agent");
906
+
907
+ // Check current version
908
+ let currentHash = "";
909
+ try {
910
+ currentHash = execSync("git rev-parse --short HEAD", {
911
+ cwd: installPath, encoding: "utf-8"
912
+ }).trim();
913
+ console.log(` ${green("✓")} Core installed ${dim(`(${currentHash})`)}`);
914
+ } catch {
915
+ console.log(` ${green("✓")} Core installed`);
916
+ }
917
+
918
+ // Check for updates
919
+ let behind = 0;
920
+ try {
921
+ console.log(` ${yellow("⏳")} Checking for updates...`);
922
+ execSync("git fetch origin --quiet", { cwd: installPath, stdio: "ignore", timeout: 15000 });
923
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
924
+ cwd: installPath, encoding: "utf-8"
925
+ }).trim();
926
+ const behindStr = execSync(
927
+ `git rev-list HEAD..origin/${branch} --count`,
928
+ { cwd: installPath, encoding: "utf-8" }
929
+ ).trim();
930
+ behind = parseInt(behindStr) || 0;
931
+ } catch {
932
+ console.log(` ${dim("Could not check for updates.")}`);
933
+ console.log("");
934
+ return;
935
+ }
936
+
937
+ if (behind === 0) {
938
+ console.log(` ${green("✓")} Already up to date.`);
939
+ console.log("");
940
+ console.log(` ${dim("Soma is set up and ready.")} Run ${green("soma")} ${dim("in a project to start a session.")}`);
941
+ console.log("");
942
+ return;
943
+ }
944
+
945
+ console.log(` ${cyan("⬆")} ${bold(`${behind} update${behind !== 1 ? "s" : ""} available.`)}`);
946
+ console.log("");
947
+
948
+ // Show what changed
949
+ try {
950
+ const log = execSync(
951
+ `git log HEAD..origin/main --oneline --no-decorate -5`,
952
+ { cwd: installPath, encoding: "utf-8" }
953
+ ).trim();
954
+ if (log) {
955
+ for (const line of log.split("\n")) {
956
+ console.log(` ${dim("•")} ${line.slice(8)}`);
957
+ }
958
+ if (behind > 5) {
959
+ console.log(` ${dim(`...and ${behind - 5} more`)}`);
960
+ }
961
+ console.log("");
962
+ }
963
+ } catch {}
964
+
965
+ const shouldUpdate = await confirmYN(` ${dim("→")} Update now?`);
966
+ if (!shouldUpdate) {
967
+ console.log("");
968
+ console.log(` ${dim("Skipped. Run")} ${green("soma init")} ${dim("anytime to update.")}`);
969
+ console.log("");
970
+ return;
971
+ }
972
+
973
+ // Pull + reinstall deps
974
+ console.log("");
975
+ try {
976
+ execSync("git pull --ff-only", { cwd: installPath, stdio: "ignore" });
977
+ console.log(` ${green("✓")} Updated`);
978
+ } catch {
979
+ console.log(` ${yellow("!")} Pull failed — trying reset...`);
980
+ try {
981
+ execSync("git reset --hard origin/main", { cwd: installPath, stdio: "ignore" });
982
+ console.log(` ${green("✓")} Updated (reset)`);
983
+ } catch {
984
+ console.log(` ${red("✗")} Update failed.`);
985
+ console.log(` ${dim("Try:")} cd ~/.soma/agent && git pull`);
986
+ console.log("");
987
+ return;
988
+ }
989
+ }
990
+
991
+ // Reinstall deps if package.json changed
992
+ try {
993
+ const pkgChanged = execSync(
994
+ `git diff HEAD~${behind} HEAD --name-only -- package.json package-lock.json`,
995
+ { cwd: installPath, encoding: "utf-8" }
996
+ ).trim();
997
+ if (pkgChanged) {
998
+ console.log(` ${yellow("⏳")} Updating dependencies...`);
999
+ execSync("npm install --omit=dev", { cwd: installPath, stdio: ["ignore", "ignore", "inherit"] });
1000
+ console.log(` ${green("✓")} Dependencies updated`);
1001
+ }
1002
+ } catch {}
1003
+
1004
+ const newHash = execSync("git rev-parse --short HEAD", {
1005
+ cwd: installPath, encoding: "utf-8"
1006
+ }).trim();
577
1007
  console.log("");
578
- console.log(` Soma will create a ${dim(".soma/")} directory in your project`);
579
- console.log(` and begin learning how you work.`);
1008
+ console.log(` ${green("✓")} ${bold("Soma is up to date")} ${dim(`(${currentHash} ${newHash})`)}`);
580
1009
  console.log("");
581
1010
  }
582
1011
 
@@ -685,11 +1114,18 @@ async function doctor() {
685
1114
  }
686
1115
  }
687
1116
 
688
- // API key
689
- warn(!!process.env.ANTHROPIC_API_KEY,
690
- "ANTHROPIC_API_KEY set",
691
- "ANTHROPIC_API_KEY not set — needed for sessions"
692
- );
1117
+ // API key (check env + auth.json + shell config)
1118
+ const hasAuth = hasAnyAuth();
1119
+ if (hasAuth) {
1120
+ console.log(` ${green("✓")} API key configured`);
1121
+ } else {
1122
+ const unloadedKey = detectKeyInShellConfig();
1123
+ if (unloadedKey) {
1124
+ console.log(` ${yellow("⚠")} ${unloadedKey} found in ${dim(getShellConfigPath())} but not loaded — restart your terminal`);
1125
+ } else {
1126
+ console.log(` ${yellow("⚠")} No API key — run ${green("soma")} to set one up`);
1127
+ }
1128
+ }
693
1129
 
694
1130
  // Git
695
1131
  try {
@@ -813,7 +1249,20 @@ if (cmd === "--version" || cmd === "-v" || cmd === "-V") {
813
1249
  } else if (cmd === "about") {
814
1250
  await showAbout();
815
1251
  } else if (cmd === "init") {
816
- await initSoma();
1252
+ const hasProjectArgs = args.includes("--template") || args.includes("--orphan") || args.includes("-o");
1253
+ const runtimeInstalled = isInstalled();
1254
+ const hasSomaDir = existsSync(join(process.cwd(), ".soma"));
1255
+
1256
+ if (!runtimeInstalled) {
1257
+ // Not installed — run full install + setup
1258
+ await initSoma();
1259
+ } else if (hasProjectArgs || !hasSomaDir) {
1260
+ // Installed, project init (new project or --template/--orphan)
1261
+ await delegateToCore();
1262
+ } else {
1263
+ // Installed + .soma/ exists — check for updates, don't re-run setup
1264
+ await checkAndUpdate();
1265
+ }
817
1266
  } else if (cmd === "update") {
818
1267
  checkForUpdates();
819
1268
  } else if (cmd === "doctor") {
@@ -825,7 +1274,7 @@ if (cmd === "--version" || cmd === "-v" || cmd === "-V") {
825
1274
  await delegateToCore();
826
1275
  } else {
827
1276
  // Check if user typed a known post-install command
828
- const postInstallCmds = ["focus", "inhale", "content", "install", "list", "--map", "--preload"];
1277
+ const postInstallCmds = ["focus", "inhale", "content", "install", "list", "map", "--map", "--preload"];
829
1278
  if (cmd && postInstallCmds.includes(cmd)) {
830
1279
  printSigma();
831
1280
  console.log(` ${bold("soma " + cmd)} requires the Soma runtime.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meetsoma",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Soma \u2014 the AI coding agent with self-growing memory",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,6 +9,7 @@
9
9
  "files": [
10
10
  "dist/thin-cli.js",
11
11
  "dist/personality.js",
12
+ "dist/postinstall.js",
12
13
  "LICENSE",
13
14
  "README.md",
14
15
  "CHANGELOG.md"
@@ -34,6 +35,9 @@
34
35
  "type": "git",
35
36
  "url": "git+https://github.com/meetsoma/soma-agent.git"
36
37
  },
38
+ "scripts": {
39
+ "postinstall": "node dist/postinstall.js"
40
+ },
37
41
  "engines": {
38
42
  "node": ">=20.6.0"
39
43
  }