meetsoma 0.1.4 → 0.1.6

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.
@@ -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";
@@ -50,7 +50,9 @@ function isInstalled() {
50
50
  // Check both layouts: soma-beta (dist/extensions) and dev setup (extensions/)
51
51
  const hasDist = existsSync(join(CORE_DIR, "dist", "extensions")) && existsSync(join(CORE_DIR, "dist", "core"));
52
52
  const hasDev = existsSync(join(CORE_DIR, "extensions")) && existsSync(join(CORE_DIR, "core"));
53
- return hasDist || hasDev;
53
+ // Check deps exist — Pi is the critical dependency
54
+ const hasDeps = existsSync(join(CORE_DIR, "node_modules", "@mariozechner"));
55
+ return (hasDist || hasDev) && hasDeps;
54
56
  }
55
57
 
56
58
  // ── Browser ──────────────────────────────────────────────────────────
@@ -89,6 +91,11 @@ async function confirm(prompt) {
89
91
  return true;
90
92
  }
91
93
 
94
+ async function confirmYN(prompt) {
95
+ const key = await waitForKey(`${prompt} ${dim("[y/n]")} `);
96
+ return key.toLowerCase() === "y";
97
+ }
98
+
92
99
  // ── Typing effect ────────────────────────────────────────────────────
93
100
 
94
101
  function sleep(ms) {
@@ -281,6 +288,42 @@ function readLine(prompt) {
281
288
  });
282
289
  }
283
290
 
291
+ async function handleQuestion(input) {
292
+ const match = matchQuestion(input);
293
+ if (match) {
294
+ const answer = voice.ask(match.topic);
295
+ console.log("");
296
+ await typeParagraph(answer);
297
+ return true;
298
+ }
299
+
300
+ // Edge case routing — detect intent even without a topic match
301
+ const lower = input.toLowerCase();
302
+ const rude = /suck|stupid|dumb|trash|garbage|hate|worst|bad|ugly|boring|lame|waste/.test(lower);
303
+ const impressed = /cool|amazing|wow|nice|love|awesome|brilliant|impressive|neat|sick|fire|goat/.test(lower);
304
+ const meta = /are you|what are you|how do you|who are you|real|alive|ai\b|bot\b/.test(lower);
305
+ const greeting = /^(hi|hey|hello|sup|yo|howdy|hola|greetings|good morning|good evening)\b/.test(lower);
306
+
307
+ console.log("");
308
+ if (greeting) {
309
+ await typeOut(` ${voice.greet()} ${voice.spin("{Ask me anything.|What do you want to know?|I know about 9 topics — pick one.}")}\n`);
310
+ } else if (rude) {
311
+ await typeParagraph(voice.ask("meta_rude"));
312
+ } else if (impressed) {
313
+ await typeParagraph(voice.ask("meta_impressed"));
314
+ } else if (meta) {
315
+ await typeParagraph(voice.ask("meta_self"));
316
+ } else {
317
+ // Anything with a question mark → try harder, then admit we don't know
318
+ if (input.includes("?")) {
319
+ await typeParagraph(voice.ask("meta_nonsense"));
320
+ } else {
321
+ await typeParagraph(voice.ask("meta_nonsense"));
322
+ }
323
+ }
324
+ return false;
325
+ }
326
+
284
327
  async function interactiveQ() {
285
328
  console.log("");
286
329
  console.log(` ${bold("Ask me anything.")}`);
@@ -290,7 +333,7 @@ async function interactiveQ() {
290
333
  console.log(` ${dim("•")} Why no compaction? ${dim("•")} Are you AI?`);
291
334
  console.log(` ${dim("•")} How does it compare? ${dim("•")} Who made this?`);
292
335
  console.log("");
293
- console.log(` ${dim("...or ask anything. I'll do my best.")}`);
336
+ console.log(` ${dim("...or ask anything. Press")} ${green("Enter")} ${dim("when you're ready to install.")}`);
294
337
 
295
338
  let rounds = 0;
296
339
  const maxRounds = 8;
@@ -299,56 +342,23 @@ async function interactiveQ() {
299
342
  console.log("");
300
343
  const input = await readLine(` ${cyan("?")} `);
301
344
 
345
+ // Empty input or quit → exit Q&A, proceed to install
302
346
  if (!input || input === "q" || input === "quit" || input === "exit") {
303
- console.log("");
304
- await typeOut(` ${voice.say("bye")}\n`);
305
347
  break;
306
348
  }
307
349
 
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);
350
+ await handleQuestion(input);
351
+ rounds++;
326
352
 
353
+ if (rounds < maxRounds) {
327
354
  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
- }
355
+ console.log(` ${dim("Ask another, or")} ${green("Enter")} ${dim("to install Soma.")}`);
339
356
  }
340
357
  }
341
358
 
342
359
  if (rounds >= maxRounds) {
343
360
  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
- }
361
+ await typeOut(` ${voice.spin("{Curious enough?|Intrigued?|Want to see it in action?}")} ${dim("Let's set you up.")}\n`);
352
362
  }
353
363
  }
354
364
 
@@ -369,6 +379,201 @@ function getGitHubUsername() {
369
379
 
370
380
  // ── Commands ─────────────────────────────────────────────────────────
371
381
 
382
+ // ── Auth helpers ─────────────────────────────────────────────────────
383
+
384
+ function hasAnyAuth() {
385
+ 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);
386
+ if (hasEnvKey) return true;
387
+ try {
388
+ const authData = JSON.parse(readFileSync(join(CORE_DIR, "auth.json"), "utf-8"));
389
+ return Object.keys(authData).length > 0;
390
+ } catch { return false; }
391
+ }
392
+
393
+ function getShellConfigPath() {
394
+ return process.env.SHELL?.includes("zsh") ? "~/.zshrc" : "~/.bashrc";
395
+ }
396
+
397
+ function getShellConfigAbsPath() {
398
+ const home = homedir();
399
+ return process.env.SHELL?.includes("zsh") ? join(home, ".zshrc") : join(home, ".bashrc");
400
+ }
401
+
402
+ function detectKeyInShellConfig() {
403
+ // Check if key is in shell config but not loaded (user hasn't restarted terminal)
404
+ try {
405
+ const configContent = readFileSync(getShellConfigAbsPath(), "utf-8");
406
+ const keyPattern = /export\s+(ANTHROPIC_API_KEY|OPENAI_API_KEY|GEMINI_API_KEY)=/;
407
+ const match = configContent.match(keyPattern);
408
+ if (match) return match[1];
409
+ } catch {}
410
+ return null;
411
+ }
412
+
413
+ // ── API key setup wizard ─────────────────────────────────────────────
414
+
415
+ async function apiKeySetup() {
416
+ console.log(` ${yellow("!")} One more thing — Soma needs an AI provider to work.`);
417
+ console.log("");
418
+ await typeOut(` ${voice.spin("{Do you have an Anthropic API key?|Got a Claude API key?|Have an API key for Claude?}")}\n`);
419
+ console.log("");
420
+ console.log(` ${green("y")} ${dim("Yes, I have a key")}`);
421
+ console.log(` ${green("n")} ${dim("No, I need one")}`);
422
+ console.log(` ${green("s")} ${dim("I have a Claude Pro/Max subscription")}`);
423
+ console.log(` ${green("?")} ${dim("What's an API key?")}`);
424
+ console.log("");
425
+
426
+ const key = await waitForKey(` ${dim("→")} `);
427
+ const choice = key.toLowerCase();
428
+
429
+ if (choice === "y") {
430
+ await apiKeyEntry();
431
+ } else if (choice === "s") {
432
+ await oauthGuide();
433
+ } else if (choice === "?") {
434
+ await apiKeyExplain();
435
+ } else {
436
+ await apiKeyGetOne();
437
+ }
438
+ }
439
+
440
+ async function apiKeyExplain() {
441
+ console.log("");
442
+ 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.");
443
+ console.log("");
444
+ 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.");
445
+ console.log("");
446
+
447
+ console.log(` ${green("g")} ${dim("Get a key (I'll show you how)")}`);
448
+ console.log(` ${green("s")} ${dim("I have Claude Pro/Max — log in instead")}`);
449
+ console.log("");
450
+
451
+ const key = await waitForKey(` ${dim("→")} `);
452
+ if (key.toLowerCase() === "s") {
453
+ await oauthGuide();
454
+ } else {
455
+ await apiKeyGetOne();
456
+ }
457
+ }
458
+
459
+ async function apiKeyGetOne() {
460
+ console.log("");
461
+ await typeOut(` ${voice.spin("{Here's how.|Let me walk you through it.|Quick steps.}")}\n`);
462
+ console.log("");
463
+ console.log(` ${cyan("Step 1:")} Open this link to create a key:`);
464
+ console.log("");
465
+ console.log(` ${cyan("https://console.anthropic.com/settings/keys")}`);
466
+ console.log("");
467
+ openBrowser("https://console.anthropic.com/settings/keys");
468
+ console.log(` ${dim("(opened in your browser)")}`);
469
+ console.log("");
470
+ await confirm(` ${dim("→")} Press ${bold("Enter")} when you have your key`);
471
+ await apiKeyEntry();
472
+ }
473
+
474
+ function readSecret(prompt) {
475
+ return new Promise(resolve => {
476
+ process.stdout.write(prompt);
477
+ if (!process.stdin.isTTY) { resolve(""); return; }
478
+ process.stdin.setRawMode(true);
479
+ process.stdin.resume();
480
+ process.stdin.setEncoding("utf-8");
481
+ let buf = "";
482
+ const onData = chunk => {
483
+ for (const ch of chunk) {
484
+ if (ch === "\r" || ch === "\n") {
485
+ process.stdin.setRawMode(false);
486
+ process.stdin.pause();
487
+ process.stdin.removeListener("data", onData);
488
+ process.stdout.write("\n");
489
+ resolve(buf);
490
+ return;
491
+ } else if (ch === "\u007F" || ch === "\b") {
492
+ // Backspace
493
+ if (buf.length > 0) {
494
+ buf = buf.slice(0, -1);
495
+ process.stdout.write("\b \b");
496
+ }
497
+ } else if (ch === "\u0003") {
498
+ // Ctrl+C
499
+ process.stdout.write("\n");
500
+ process.exit(0);
501
+ } else if (ch >= " ") {
502
+ buf += ch;
503
+ process.stdout.write("•");
504
+ }
505
+ }
506
+ };
507
+ process.stdin.on("data", onData);
508
+ });
509
+ }
510
+
511
+ async function apiKeyEntry() {
512
+ console.log("");
513
+ console.log(` ${cyan("Step 2:")} Paste your key below.`);
514
+ console.log(` ${dim("It starts with")} sk-ant-...`);
515
+ console.log("");
516
+
517
+ const apiKey = await readSecret(` ${dim("Key:")} `);
518
+
519
+ if (!apiKey || !apiKey.startsWith("sk-")) {
520
+ console.log("");
521
+ if (!apiKey) {
522
+ console.log(` ${dim("No key entered. You can set it up later.")}`);
523
+ } else {
524
+ console.log(` ${yellow("!")} That doesn't look like an Anthropic key.`);
525
+ console.log(` ${dim("Keys start with")} sk-ant-...`);
526
+ }
527
+ console.log("");
528
+ const sc = getShellConfigPath();
529
+ console.log(` ${dim("When you have your key, add it to")} ${dim(sc)}${dim(":")}`);
530
+ console.log(` ${green('export ANTHROPIC_API_KEY="your-key-here"')}`);
531
+ console.log(` ${dim("Then restart your terminal and run")} ${green("soma")}`);
532
+ console.log("");
533
+ return;
534
+ }
535
+
536
+ // Write to shell config
537
+ const shellConfigPath = getShellConfigAbsPath();
538
+ const shellConfigName = getShellConfigPath();
539
+ const exportLine = `\nexport ANTHROPIC_API_KEY="${apiKey}"\n`;
540
+
541
+ try {
542
+ appendFileSync(shellConfigPath, exportLine);
543
+ console.log("");
544
+ console.log(` ${green("✓")} Key saved to ${dim(shellConfigName)}`);
545
+ console.log("");
546
+
547
+ // Set it for the current process too so we can launch immediately
548
+ process.env.ANTHROPIC_API_KEY = apiKey;
549
+
550
+ await typeOut(` ${voice.spin("{You're all set.|Good to go.|Ready.}")} ${dim("Soma can start now.")}\n`);
551
+ console.log("");
552
+ } catch {
553
+ console.log("");
554
+ console.log(` ${yellow("!")} Couldn't write to ${dim(shellConfigName)}.`);
555
+ console.log(` ${dim("Add this line manually:")}`);
556
+ console.log(` ${green(`export ANTHROPIC_API_KEY="${apiKey}"`)}`);
557
+ console.log(` ${dim("Then restart your terminal and run")} ${green("soma")}`);
558
+ console.log("");
559
+ }
560
+ }
561
+
562
+ async function oauthGuide() {
563
+ console.log("");
564
+ await typeParagraph("Nice — with a Pro or Max subscription, you can log in with your Anthropic account. No API key needed.");
565
+ console.log("");
566
+ console.log(` ${dim("When Soma starts, type")} ${green("/login")} ${dim("and follow the prompts.")}`);
567
+ console.log(` ${dim("It'll open your browser to authenticate.")}`);
568
+ console.log("");
569
+ await typeOut(` ${voice.spin("{Let's launch.|Starting up.|Here we go.}")}\n`);
570
+ console.log("");
571
+ // Mark that user chose OAuth so we don't block launch
572
+ process.env._SOMA_OAUTH_PENDING = "1";
573
+ }
574
+
575
+ // ── Welcome / First Run ─────────────────────────────────────────────
576
+
372
577
  async function showWelcome() {
373
578
  printSigma();
374
579
  console.log(` ${bold("Soma")} ${dim("—")} ${white("the AI agent that remembers")}`);
@@ -381,32 +586,74 @@ async function showWelcome() {
381
586
  if (ghUser) {
382
587
  console.log(` ${green("✓")} ${voice.greetBack(ghUser)}`);
383
588
  }
384
- console.log(` ${green("✓")} Core installed. Starting Soma...`);
589
+
590
+ if (!hasAnyAuth()) {
591
+ console.log(` ${green("✓")} Core installed`);
592
+ console.log("");
593
+
594
+ // Check if key exists in shell config but isn't loaded
595
+ const unloadedKey = detectKeyInShellConfig();
596
+ if (unloadedKey) {
597
+ console.log(` ${yellow("!")} Found ${bold(unloadedKey)} in ${dim(getShellConfigPath())} but it's not loaded.`);
598
+ console.log(` ${dim("Restart your terminal and run")} ${green("soma")} ${dim("again.")}`);
599
+ console.log("");
600
+ return;
601
+ }
602
+
603
+ await apiKeySetup();
604
+
605
+ // If they set a key or chose OAuth, launch. If not, exit gracefully.
606
+ if (!hasAnyAuth() && !process.env.ANTHROPIC_API_KEY && !process.env._SOMA_OAUTH_PENDING) {
607
+ 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.}")}`);
608
+ console.log("");
609
+ console.log(` ${dim(`v${VERSION} · BSL 1.1 · soma.gravicity.ai`)}`);
610
+ console.log("");
611
+ return;
612
+ }
613
+ } else {
614
+ console.log(` ${green("✓")} Core installed. Starting Soma...`);
615
+ }
385
616
  console.log("");
386
617
  await delegateToCore();
387
618
  return;
388
619
  }
389
620
 
390
- // Not installed — show a concept + offer to set up
391
- const concept = CONCEPTS[getConceptIndex()];
392
- const body = getConceptBody(concept.topic);
621
+ // ── Not installed — first time ever ────────────────────────────────
393
622
 
394
- console.log(` ${magenta("❝")} ${bold(concept.title)}`);
623
+ await typeOut(` ${voice.greet()}\n`);
395
624
  console.log("");
396
- await typeParagraph(body);
625
+ 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
626
  console.log("");
398
627
  console.log(` ${dim("─".repeat(58))}`);
399
628
  console.log("");
400
- console.log(` ${dim("→")} ${green("Enter")} Install Soma`);
401
- console.log(` ${dim("→")} ${green("?")} Ask me something first`);
629
+ console.log(` ${dim("→")} Press ${green("Enter")} to set up, or type a question.`);
402
630
  console.log("");
403
631
 
404
- const key = await waitForKey(` ${dim("Your move:")} `);
632
+ const input = await readLine(` ${dim("")} `);
405
633
 
406
- if (key === "?") {
634
+ if (input && input !== "") {
635
+ await handleQuestion(input);
407
636
  await interactiveQ();
408
- } else {
409
- await initSoma();
637
+ }
638
+
639
+ // Install the runtime
640
+ await initSoma();
641
+
642
+ // If install succeeded, run the API key setup
643
+ if (isInstalled() && !hasAnyAuth()) {
644
+ await apiKeySetup();
645
+ }
646
+
647
+ // If they have auth now (or chose OAuth), offer to launch
648
+ if (isInstalled() && (hasAnyAuth() || process.env.ANTHROPIC_API_KEY || process.env._SOMA_OAUTH_PENDING)) {
649
+ console.log(` ${dim("─".repeat(58))}`);
650
+ console.log("");
651
+ const launch = await confirmYN(` ${voice.spin("{Ready to go?|Want to start your first session?|Launch Soma?}")}`);
652
+ if (launch) {
653
+ console.log("");
654
+ await delegateToCore();
655
+ return;
656
+ }
410
657
  }
411
658
 
412
659
  console.log("");
@@ -515,7 +762,65 @@ async function initSoma() {
515
762
  const installDir = join(SOMA_HOME, "agent");
516
763
  mkdirSync(SOMA_HOME, { recursive: true });
517
764
 
518
- if (existsSync(installDir)) {
765
+ // Validate existing install — check it's a real git repo with dist/ content
766
+ const isValidInstall = existsSync(installDir)
767
+ && existsSync(join(installDir, ".git"))
768
+ && (existsSync(join(installDir, "dist", "extensions")) || existsSync(join(installDir, "extensions")));
769
+
770
+ // Track user files to preserve across repair/reinstall
771
+ let preservedFiles = {};
772
+
773
+ if (existsSync(installDir) && !isValidInstall) {
774
+ // Broken/partial install — try to repair, preserve user files
775
+ console.log(` ${yellow("⚠")} Incomplete installation detected.`);
776
+ console.log(` ${dim("Missing:")} ${!existsSync(join(installDir, ".git")) ? "git repo" : "core files"}`);
777
+ console.log("");
778
+
779
+ // Save user files before touching anything
780
+ const userFileNames = ["auth.json", "models.json"];
781
+ for (const f of userFileNames) {
782
+ const fp = join(installDir, f);
783
+ if (existsSync(fp)) {
784
+ try {
785
+ preservedFiles[f] = readFileSync(fp, "utf-8");
786
+ console.log(` ${dim("→")} Preserving ${f}`);
787
+ } catch {}
788
+ }
789
+ }
790
+
791
+ // Try repair: if it's a git repo, fetch + reset. Otherwise move aside for fresh clone.
792
+ const hasGit = existsSync(join(installDir, ".git"));
793
+ let repaired = false;
794
+
795
+ if (hasGit) {
796
+ console.log(` ${yellow("⏳")} Repairing...`);
797
+ try {
798
+ execSync("git fetch origin", { cwd: installDir, stdio: "ignore", timeout: 30000 });
799
+ execSync("git reset --hard origin/main", { cwd: installDir, stdio: "ignore" });
800
+ console.log(` ${green("✓")} Repaired from remote`);
801
+ repaired = true;
802
+ } catch {
803
+ console.log(` ${yellow("!")} Repair failed — will re-download.`);
804
+ }
805
+ }
806
+
807
+ if (!repaired) {
808
+ // Move broken dir aside (never delete), then clone will happen below
809
+ const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
810
+ const backup = join(SOMA_HOME, `agent-backup-${ts}`);
811
+ try {
812
+ execSync(`mv "${installDir}" "${backup}"`, { stdio: "ignore" });
813
+ console.log(` ${dim("Old files saved to")} ${dim(backup.replace(homedir(), "~"))}`);
814
+ } catch {
815
+ console.log(` ${red("✗")} Could not move old installation aside.`);
816
+ console.log(` ${dim("Try:")} mv ~/.soma/agent ~/.soma/agent-old && soma init`);
817
+ console.log("");
818
+ return;
819
+ }
820
+ }
821
+ }
822
+
823
+ if (isValidInstall) {
519
824
  console.log(` ${dim("→")} Runtime already installed.`);
520
825
 
521
826
  // Pull latest
@@ -557,13 +862,30 @@ async function initSoma() {
557
862
  }
558
863
  }
559
864
 
560
- // Verify
865
+ // Restore preserved user files (from broken install repair)
866
+ if (Object.keys(preservedFiles).length > 0) {
867
+ for (const [f, content] of Object.entries(preservedFiles)) {
868
+ try {
869
+ writeFileSync(join(installDir, f), content, { mode: 0o600 });
870
+ console.log(` ${green("✓")} Restored ${f}`);
871
+ } catch {}
872
+ }
873
+ }
874
+
875
+ // Verify — gate success on actual working install
561
876
  const hasExts = existsSync(join(installDir, "dist", "extensions"));
562
877
  const hasCore = existsSync(join(installDir, "dist", "core"));
563
- if (hasExts && hasCore) {
564
- console.log(` ${green("✓")} Extensions and core ready`);
878
+
879
+ if (!hasExts || !hasCore) {
880
+ console.log("");
881
+ console.log(` ${red("✗")} Installation incomplete — core files missing.`);
882
+ console.log(` ${dim("Try:")} rm -rf ~/.soma/agent && soma init`);
883
+ console.log("");
884
+ return;
565
885
  }
566
886
 
887
+ console.log(` ${green("✓")} Extensions and core ready`);
888
+
567
889
  // Save config
568
890
  const config = readConfig();
569
891
  config.installedAt = config.installedAt || new Date().toISOString();
@@ -574,12 +896,118 @@ async function initSoma() {
574
896
  console.log("");
575
897
  console.log(` ${green("✓")} ${bold("Soma is installed!")}`);
576
898
  console.log("");
577
- console.log(` Next steps:`);
578
- console.log(` ${cyan("1.")} ${green("cd <your-project>")}`);
579
- console.log(` ${cyan("2.")} ${green("soma")} to start your first session`);
899
+ }
900
+
901
+ async function checkAndUpdate() {
902
+ printSigma();
903
+ console.log(` ${bold("Soma")} — Status`);
904
+ console.log("");
905
+
906
+ const config = readConfig();
907
+ const installPath = config.installPath || join(SOMA_HOME, "agent");
908
+
909
+ // Check current version
910
+ let currentHash = "";
911
+ try {
912
+ currentHash = execSync("git rev-parse --short HEAD", {
913
+ cwd: installPath, encoding: "utf-8"
914
+ }).trim();
915
+ console.log(` ${green("✓")} Core installed ${dim(`(${currentHash})`)}`);
916
+ } catch {
917
+ console.log(` ${green("✓")} Core installed`);
918
+ }
919
+
920
+ // Check for updates
921
+ let behind = 0;
922
+ try {
923
+ console.log(` ${yellow("⏳")} Checking for updates...`);
924
+ execSync("git fetch origin --quiet", { cwd: installPath, stdio: "ignore", timeout: 15000 });
925
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
926
+ cwd: installPath, encoding: "utf-8"
927
+ }).trim();
928
+ const behindStr = execSync(
929
+ `git rev-list HEAD..origin/${branch} --count`,
930
+ { cwd: installPath, encoding: "utf-8" }
931
+ ).trim();
932
+ behind = parseInt(behindStr) || 0;
933
+ } catch {
934
+ console.log(` ${dim("Could not check for updates.")}`);
935
+ console.log("");
936
+ return;
937
+ }
938
+
939
+ if (behind === 0) {
940
+ console.log(` ${green("✓")} Already up to date.`);
941
+ console.log("");
942
+ console.log(` ${dim("Soma is set up and ready.")} Run ${green("soma")} ${dim("in a project to start a session.")}`);
943
+ console.log("");
944
+ return;
945
+ }
946
+
947
+ console.log(` ${cyan("⬆")} ${bold(`${behind} update${behind !== 1 ? "s" : ""} available.`)}`);
948
+ console.log("");
949
+
950
+ // Show what changed
951
+ try {
952
+ const log = execSync(
953
+ `git log HEAD..origin/main --oneline --no-decorate -5`,
954
+ { cwd: installPath, encoding: "utf-8" }
955
+ ).trim();
956
+ if (log) {
957
+ for (const line of log.split("\n")) {
958
+ console.log(` ${dim("•")} ${line.slice(8)}`);
959
+ }
960
+ if (behind > 5) {
961
+ console.log(` ${dim(`...and ${behind - 5} more`)}`);
962
+ }
963
+ console.log("");
964
+ }
965
+ } catch {}
966
+
967
+ const shouldUpdate = await confirmYN(` ${dim("→")} Update now?`);
968
+ if (!shouldUpdate) {
969
+ console.log("");
970
+ console.log(` ${dim("Skipped. Run")} ${green("soma init")} ${dim("anytime to update.")}`);
971
+ console.log("");
972
+ return;
973
+ }
974
+
975
+ // Pull + reinstall deps
580
976
  console.log("");
581
- console.log(` Soma will create a ${dim(".soma/")} directory in your project`);
582
- console.log(` and begin learning how you work.`);
977
+ try {
978
+ execSync("git pull --ff-only", { cwd: installPath, stdio: "ignore" });
979
+ console.log(` ${green("✓")} Updated`);
980
+ } catch {
981
+ console.log(` ${yellow("!")} Pull failed — trying reset...`);
982
+ try {
983
+ execSync("git reset --hard origin/main", { cwd: installPath, stdio: "ignore" });
984
+ console.log(` ${green("✓")} Updated (reset)`);
985
+ } catch {
986
+ console.log(` ${red("✗")} Update failed.`);
987
+ console.log(` ${dim("Try:")} cd ~/.soma/agent && git pull`);
988
+ console.log("");
989
+ return;
990
+ }
991
+ }
992
+
993
+ // Reinstall deps if package.json changed
994
+ try {
995
+ const pkgChanged = execSync(
996
+ `git diff HEAD~${behind} HEAD --name-only -- package.json package-lock.json`,
997
+ { cwd: installPath, encoding: "utf-8" }
998
+ ).trim();
999
+ if (pkgChanged) {
1000
+ console.log(` ${yellow("⏳")} Updating dependencies...`);
1001
+ execSync("npm install --omit=dev", { cwd: installPath, stdio: ["ignore", "ignore", "inherit"] });
1002
+ console.log(` ${green("✓")} Dependencies updated`);
1003
+ }
1004
+ } catch {}
1005
+
1006
+ const newHash = execSync("git rev-parse --short HEAD", {
1007
+ cwd: installPath, encoding: "utf-8"
1008
+ }).trim();
1009
+ console.log("");
1010
+ console.log(` ${green("✓")} ${bold("Soma is up to date")} ${dim(`(${currentHash} → ${newHash})`)}`);
583
1011
  console.log("");
584
1012
  }
585
1013
 
@@ -688,11 +1116,18 @@ async function doctor() {
688
1116
  }
689
1117
  }
690
1118
 
691
- // API key
692
- warn(!!process.env.ANTHROPIC_API_KEY,
693
- "ANTHROPIC_API_KEY set",
694
- "ANTHROPIC_API_KEY not set — needed for sessions"
695
- );
1119
+ // API key (check env + auth.json + shell config)
1120
+ const hasAuth = hasAnyAuth();
1121
+ if (hasAuth) {
1122
+ console.log(` ${green("✓")} API key configured`);
1123
+ } else {
1124
+ const unloadedKey = detectKeyInShellConfig();
1125
+ if (unloadedKey) {
1126
+ console.log(` ${yellow("⚠")} ${unloadedKey} found in ${dim(getShellConfigPath())} but not loaded — restart your terminal`);
1127
+ } else {
1128
+ console.log(` ${yellow("⚠")} No API key — run ${green("soma")} to set one up`);
1129
+ }
1130
+ }
696
1131
 
697
1132
  // Git
698
1133
  try {
@@ -746,6 +1181,27 @@ function showStatus() {
746
1181
  // ── Delegation ───────────────────────────────────────────────────────
747
1182
 
748
1183
  async function delegateToCore() {
1184
+ // Pre-flight: verify runtime can start before delegating
1185
+ const piPkg = join(CORE_DIR, "node_modules", "@mariozechner", "pi-coding-agent");
1186
+ if (!existsSync(piPkg)) {
1187
+ console.log(` ${red("✗")} Runtime dependencies missing.`);
1188
+ console.log(` ${dim("Run")} ${green("soma init")} ${dim("to repair the installation.")}`);
1189
+ console.log("");
1190
+ return;
1191
+ }
1192
+ const cliEntry = existsSync(join(CORE_DIR, "dist", "cli.js"))
1193
+ ? join(CORE_DIR, "dist", "cli.js")
1194
+ : null;
1195
+ const mainEntry = existsSync(join(CORE_DIR, "dist", "main.js"))
1196
+ ? join(CORE_DIR, "dist", "main.js")
1197
+ : null;
1198
+ if (!cliEntry && !mainEntry) {
1199
+ console.log(` ${red("✗")} Runtime entry point missing.`);
1200
+ console.log(` ${dim("Run")} ${green("soma init")} ${dim("to repair the installation.")}`);
1201
+ console.log("");
1202
+ return;
1203
+ }
1204
+
749
1205
  const { execFileSync: execF } = await import("child_process");
750
1206
  const passArgs = process.argv.slice(2);
751
1207
 
@@ -792,7 +1248,15 @@ async function delegateToCore() {
792
1248
  }
793
1249
  return;
794
1250
  } catch (err) {
1251
+ // Non-zero exit from session is normal (user quit, ctrl+c)
795
1252
  if (err.status) process.exit(err.status);
1253
+ // Module errors = broken install
1254
+ if (err.message && err.message.includes("MODULE_NOT_FOUND")) {
1255
+ console.log("");
1256
+ console.log(` ${red("✗")} Soma failed to start — missing dependencies.`);
1257
+ console.log(` ${dim("Run")} ${green("soma init")} ${dim("to repair the installation.")}`);
1258
+ console.log("");
1259
+ }
796
1260
  return;
797
1261
  }
798
1262
  }
@@ -816,17 +1280,19 @@ if (cmd === "--version" || cmd === "-v" || cmd === "-V") {
816
1280
  } else if (cmd === "about") {
817
1281
  await showAbout();
818
1282
  } else if (cmd === "init") {
819
- // If runtime is installed AND (has --template/--orphan args OR no .soma/ in cwd),
820
- // route to project init via content-cli instead of runtime install
821
1283
  const hasProjectArgs = args.includes("--template") || args.includes("--orphan") || args.includes("-o");
822
1284
  const runtimeInstalled = isInstalled();
823
1285
  const hasSomaDir = existsSync(join(process.cwd(), ".soma"));
824
1286
 
825
- if (runtimeInstalled && (hasProjectArgs || !hasSomaDir)) {
826
- // Delegate to content-cli for project init
1287
+ if (!runtimeInstalled) {
1288
+ // Not installed run full install + setup
1289
+ await initSoma();
1290
+ } else if (hasProjectArgs || !hasSomaDir) {
1291
+ // Installed, project init (new project or --template/--orphan)
827
1292
  await delegateToCore();
828
1293
  } else {
829
- await initSoma();
1294
+ // Installed + .soma/ exists — check for updates, don't re-run setup
1295
+ await checkAndUpdate();
830
1296
  }
831
1297
  } else if (cmd === "update") {
832
1298
  checkForUpdates();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meetsoma",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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
  }