meetsoma 0.3.3 → 0.3.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/dist/thin-cli.js CHANGED
@@ -1,64 +1,1134 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * Soma — Thin CLI
4
- *
5
- * The public face of Soma on npm. Routes to modular commands.
6
- * For new users, this IS the first impression.
7
- * For returning users: detects installed runtime → delegates to it.
8
- */
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // lib/display.js
13
+ var display_exports = {};
14
+ __export(display_exports, {
15
+ bold: () => bold,
16
+ confirm: () => confirm,
17
+ confirmYN: () => confirmYN,
18
+ cyan: () => cyan,
19
+ dim: () => dim,
20
+ green: () => green,
21
+ italic: () => italic,
22
+ magenta: () => magenta,
23
+ printSigma: () => printSigma,
24
+ readLine: () => readLine,
25
+ readSecret: () => readSecret,
26
+ red: () => red,
27
+ typeOut: () => typeOut,
28
+ typeParagraph: () => typeParagraph,
29
+ waitForKey: () => waitForKey,
30
+ white: () => white,
31
+ wrapText: () => wrapText,
32
+ yellow: () => yellow
33
+ });
34
+ function printSigma() {
35
+ console.log("");
36
+ console.log(cyan(" \u03C3"));
37
+ console.log("");
38
+ }
39
+ function sleep(ms) {
40
+ return new Promise((r) => setTimeout(r, ms));
41
+ }
42
+ async function typeOut(text, opts = {}) {
43
+ if (!process.stdout.isTTY) {
44
+ process.stdout.write(text);
45
+ return;
46
+ }
47
+ let pace = 1;
48
+ let i = 0;
49
+ while (i < text.length) {
50
+ const remaining = text.slice(i);
51
+ const ansi = remaining.match(/^\x1b\[[0-9;]*m/);
52
+ if (ansi) {
53
+ process.stdout.write(ansi[0]);
54
+ i += ansi[0].length;
55
+ continue;
56
+ }
57
+ const ch = text[i];
58
+ const next = text[i + 1] || "";
59
+ process.stdout.write(ch);
60
+ i++;
61
+ pace += (Math.random() - 0.5) * 0.15;
62
+ pace = Math.max(0.6, Math.min(1.4, pace));
63
+ if (ch === "\n") {
64
+ await sleep(58 * pace);
65
+ } else if (".!?".includes(ch) && (next === " " || next === "\n" || next === "")) {
66
+ await sleep((230 + Math.random() * 175) * pace);
67
+ } else if (ch === "," || ch === ";" || ch === ":") {
68
+ await sleep((70 + Math.random() * 46) * pace);
69
+ } else if (ch === "\u2014" || ch === "\u2013") {
70
+ await sleep((92 + Math.random() * 70) * pace);
71
+ } else if (ch === " ") {
72
+ await sleep((9 + Math.random() * 23) * pace);
73
+ } else {
74
+ await sleep((6 + Math.random() * 14) * pace);
75
+ }
76
+ }
77
+ }
78
+ async function typeParagraph(text, indent = " ", width = 58) {
79
+ const words = text.split(" ");
80
+ let line = indent;
81
+ const lines = [];
82
+ for (const word of words) {
83
+ if (line.length + word.length > width + indent.length && line.trim()) {
84
+ lines.push(line);
85
+ line = indent + word;
86
+ } else {
87
+ line += (line.trim() ? " " : "") + word;
88
+ }
89
+ }
90
+ if (line.trim()) lines.push(line);
91
+ await typeOut(lines.join("\n") + "\n");
92
+ }
93
+ function wrapText(text, indent = " ", width = 58) {
94
+ const words = text.split(" ");
95
+ const lines = [];
96
+ let line = indent;
97
+ for (const word of words) {
98
+ if (line.length + word.length > width + indent.length && line.trim()) {
99
+ lines.push(line);
100
+ line = indent + word;
101
+ } else {
102
+ line += (line.trim() ? " " : "") + word;
103
+ }
104
+ }
105
+ if (line.trim()) lines.push(line);
106
+ return lines.join("\n");
107
+ }
108
+ function waitForKey(prompt) {
109
+ return new Promise((resolve) => {
110
+ process.stdout.write(prompt);
111
+ if (!process.stdin.isTTY) {
112
+ resolve("");
113
+ return;
114
+ }
115
+ process.stdin.setRawMode(true);
116
+ process.stdin.resume();
117
+ process.stdin.setEncoding("utf-8");
118
+ process.stdin.once("data", (key) => {
119
+ process.stdin.setRawMode(false);
120
+ process.stdin.pause();
121
+ process.stdout.write("\n");
122
+ if (key === "") process.exit(0);
123
+ resolve(key);
124
+ });
125
+ });
126
+ }
127
+ async function confirm(prompt) {
128
+ const key = await waitForKey(`${prompt} ${dim("[Enter]")} `);
129
+ return true;
130
+ }
131
+ async function confirmYN(prompt) {
132
+ const key = await waitForKey(`${prompt} ${dim("[y/n]")} `);
133
+ return key.toLowerCase() === "y";
134
+ }
135
+ function readLine(prompt) {
136
+ return new Promise((resolve) => {
137
+ process.stdout.write(prompt);
138
+ if (!process.stdin.isTTY) {
139
+ resolve("");
140
+ return;
141
+ }
142
+ process.stdin.setRawMode(false);
143
+ process.stdin.resume();
144
+ process.stdin.setEncoding("utf-8");
145
+ let buf = "";
146
+ const onData = (chunk) => {
147
+ buf += chunk;
148
+ if (buf.includes("\n")) {
149
+ process.stdin.pause();
150
+ process.stdin.removeListener("data", onData);
151
+ resolve(buf.trim());
152
+ }
153
+ };
154
+ process.stdin.on("data", onData);
155
+ });
156
+ }
157
+ function readSecret(prompt) {
158
+ return new Promise((resolve) => {
159
+ process.stdout.write(prompt);
160
+ if (!process.stdin.isTTY) {
161
+ resolve("");
162
+ return;
163
+ }
164
+ process.stdin.setRawMode(true);
165
+ process.stdin.resume();
166
+ process.stdin.setEncoding("utf-8");
167
+ let buf = "";
168
+ const onData = (chunk) => {
169
+ for (const ch of chunk) {
170
+ if (ch === "\r" || ch === "\n") {
171
+ process.stdin.setRawMode(false);
172
+ process.stdin.pause();
173
+ process.stdin.removeListener("data", onData);
174
+ process.stdout.write("\n");
175
+ resolve(buf);
176
+ return;
177
+ } else if (ch === "\x7F" || ch === "\b") {
178
+ if (buf.length > 0) {
179
+ buf = buf.slice(0, -1);
180
+ process.stdout.write("\b \b");
181
+ }
182
+ } else if (ch === "") {
183
+ process.stdout.write("\n");
184
+ process.exit(0);
185
+ } else if (ch >= " ") {
186
+ buf += ch;
187
+ process.stdout.write("\u2022");
188
+ }
189
+ }
190
+ };
191
+ process.stdin.on("data", onData);
192
+ });
193
+ }
194
+ var bold, dim, italic, cyan, green, yellow, red, magenta, white;
195
+ var init_display = __esm({
196
+ "lib/display.js"() {
197
+ "use strict";
198
+ bold = (s) => `\x1B[1m${s}\x1B[0m`;
199
+ dim = (s) => `\x1B[2m${s}\x1B[0m`;
200
+ italic = (s) => `\x1B[3m${s}\x1B[0m`;
201
+ cyan = (s) => `\x1B[36m${s}\x1B[0m`;
202
+ green = (s) => `\x1B[32m${s}\x1B[0m`;
203
+ yellow = (s) => `\x1B[33m${s}\x1B[0m`;
204
+ red = (s) => `\x1B[31m${s}\x1B[0m`;
205
+ magenta = (s) => `\x1B[35m${s}\x1B[0m`;
206
+ white = (s) => `\x1B[97m${s}\x1B[0m`;
207
+ }
208
+ });
9
209
 
10
- import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, readlinkSync } from "fs";
11
- import { join, dirname } from "path";
12
- import { homedir, platform } from "os";
210
+ // thin-cli.js
211
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, readdirSync, readlinkSync as readlinkSync2 } from "fs";
212
+ import { join as join3, dirname as dirname3 } from "path";
213
+ import { homedir as homedir3, platform as platform2 } from "os";
13
214
  import { fileURLToPath } from "url";
14
- import { execSync, execFileSync } from "child_process";
15
- import { soma as voice } from "./personality.js";
215
+ import { execSync as execSync2, execFileSync } from "child_process";
216
+
217
+ // personality.js
218
+ var _state = {
219
+ lru: {},
220
+ // { key: [lastN picks] } — recency tracking per slot/intent
221
+ register: null,
222
+ // 'formal' | 'casual' | null (sticky once a flow starts)
223
+ lastGreetAt: 0
224
+ // timestamp — avoid double-greeting in short windows
225
+ };
226
+ function pick(arr) {
227
+ if (!arr || arr.length === 0) return "";
228
+ const i = Math.floor(Math.random() * arr.length);
229
+ return arr[i];
230
+ }
231
+ function pickSmart(arr, lruKey, depth = 3) {
232
+ if (!arr || arr.length === 0) return "";
233
+ if (!lruKey || arr.length <= 1) return pick(arr);
234
+ const recent = _state.lru[lruKey] || [];
235
+ const fresh = arr.filter((x) => !recent.includes(x));
236
+ const pool = fresh.length > 0 ? fresh : arr;
237
+ const choice = pick(pool);
238
+ _state.lru[lruKey] = [...recent, choice].slice(-depth);
239
+ return choice;
240
+ }
241
+ function timeOfDay() {
242
+ const h = (/* @__PURE__ */ new Date()).getHours();
243
+ if (h < 6) return "late";
244
+ if (h < 12) return "morning";
245
+ if (h < 18) return "afternoon";
246
+ return "evening";
247
+ }
248
+ function expand(template, slots = {}) {
249
+ return template.replace(/\{(\w+)\}/g, (_, key) => {
250
+ if (slots[key] !== void 0) return slots[key];
251
+ if (VOCAB[key]) {
252
+ const v = VOCAB[key];
253
+ if (!Array.isArray(v) && typeof v === "object") {
254
+ const bucket = v[timeOfDay()] || v.default || Object.values(v)[0];
255
+ return pickSmart(bucket, key);
256
+ }
257
+ return pickSmart(v, key);
258
+ }
259
+ return `{${key}}`;
260
+ });
261
+ }
262
+ var VOCAB = {
263
+ // Greetings
264
+ greeting: [
265
+ "Hey",
266
+ "Hi",
267
+ "Hello",
268
+ "Hey there"
269
+ ],
270
+ // Time-specific greetings
271
+ time_greeting: {
272
+ late: ["Still up?", "Late session", "Hey \u2014 still here?"],
273
+ morning: ["Morning", "Good morning", "Hey \u2014 morning"],
274
+ afternoon: ["Hey", "Afternoon", "Hi"],
275
+ evening: ["Evening", "Good evening", "Hey"]
276
+ },
277
+ // Acknowledgements
278
+ ack: [
279
+ "Got it",
280
+ "Understood",
281
+ "Right",
282
+ "Noted",
283
+ "Copy that",
284
+ "On it"
285
+ ],
286
+ // Transitions
287
+ transition: [
288
+ "Let me",
289
+ "I'll",
290
+ "Going to",
291
+ "Time to"
292
+ ],
293
+ // Completes
294
+ done_word: [
295
+ "Done",
296
+ "Finished",
297
+ "Complete",
298
+ "All set",
299
+ "Ready"
300
+ ],
301
+ // Personality fillers — words Soma would choose
302
+ quality: [
303
+ "clean",
304
+ "solid",
305
+ "sharp",
306
+ "tight",
307
+ "smooth"
308
+ ],
309
+ // Time references
310
+ moment: [
311
+ "a moment",
312
+ "a second",
313
+ "a beat",
314
+ "a sec"
315
+ ],
316
+ // Thinking words
317
+ consider: [
318
+ "Looks like",
319
+ "Seems like",
320
+ "Appears to be",
321
+ "That's"
322
+ ],
323
+ // Reassurance (when surfacing drift / problems)
324
+ reassure: [
325
+ "Nothing scary",
326
+ "Small fix",
327
+ "Quick one",
328
+ "Easy one"
329
+ ],
330
+ // Attention (when announcing results)
331
+ attention: [
332
+ "Here's the map",
333
+ "Here's what I see",
334
+ "Snapshot",
335
+ "Current state"
336
+ ]
337
+ };
338
+ var SKELETONS = {
339
+ // ── Greetings ────────────────────────
340
+ greet: [
341
+ "{greeting}. I'm Soma.",
342
+ "I'm Soma. {greeting}.",
343
+ "{greeting} \u2014 I'm Soma."
344
+ ],
345
+ greet_back: [
346
+ "Welcome back, {user}.",
347
+ "{greeting}, {user}. Good to see you.",
348
+ "{user} \u2014 welcome back.",
349
+ "Hey, {user}."
350
+ ],
351
+ // ── Detection / Recognition ──────────
352
+ detect: [
353
+ "{ack} \u2014 {category}. {followup}",
354
+ "{consider} {category}. {followup}",
355
+ "{category}. {followup}",
356
+ "{category} \u2014 {quality}. {followup}"
357
+ ],
358
+ // ── Confirmations ────────────────────
359
+ confirm: [
360
+ "{transition} {action}.",
361
+ "{ack}. {action}.",
362
+ "{action}. {ack}."
363
+ ],
364
+ // ── Success ──────────────────────────
365
+ success: [
366
+ "{done_word}. {detail}",
367
+ "{detail} \u2014 {done_word}.",
368
+ "{done_word} \u2014 {detail}.",
369
+ "\u2713 {detail}"
370
+ ],
371
+ // ── Failures ─────────────────────────
372
+ failure: [
373
+ "Hmm. {problem}",
374
+ "That didn't work \u2014 {problem}",
375
+ "{problem} \u2014 let me think.",
376
+ "Hit a wall: {problem}"
377
+ ],
378
+ // ── Waiting ──────────────────────────
379
+ waiting: [
380
+ "Give me {moment}...",
381
+ "{transition} take {moment}...",
382
+ "Working on it...",
383
+ "Just {moment}...",
384
+ "Hang on..."
385
+ ],
386
+ // ── Questions / Clarifications ───────
387
+ clarify: [
388
+ "Quick question \u2014 {question}",
389
+ "One thing: {question}",
390
+ "Before I continue \u2014 {question}",
391
+ "{question}"
392
+ ],
393
+ // ── Suggestions ──────────────────────
394
+ suggest: [
395
+ "Try: {suggestion}",
396
+ "You could: {suggestion}",
397
+ "Here's what I'd do \u2014 {suggestion}",
398
+ "My take: {suggestion}"
399
+ ],
400
+ // ── Not sure ─────────────────────────
401
+ unsure: [
402
+ "Not sure about that. {fallback}",
403
+ "I don't have a great answer. {fallback}",
404
+ "Hmm \u2014 {fallback}",
405
+ "That's outside what I know right now. {fallback}"
406
+ ],
407
+ // ── Invitations ──────────────────────
408
+ invite: [
409
+ "{pitch} \u2014 {cta}",
410
+ "{pitch}. {cta}",
411
+ "{cta} \u2014 {pitch}."
412
+ ],
413
+ // ── Farewells ────────────────────────
414
+ bye: [
415
+ "See you next session.",
416
+ "I'll remember.",
417
+ "Until next time.",
418
+ "Exhale complete."
419
+ ],
420
+ // Version / update UX
421
+ version_check: [
422
+ "{attention} \u2014 three layers:",
423
+ "Checking in on your setup.",
424
+ "{transition} look at the version state.",
425
+ "Version snapshot:"
426
+ ],
427
+ version_aligned: [
428
+ "All three layers are aligned. You're good.",
429
+ "Everything's in sync \u2014 CLI, agent, workspace.",
430
+ "{done_word}. All layers on the same page.",
431
+ "Aligned end-to-end."
432
+ ],
433
+ version_drift: [
434
+ "Found some drift. {reassure}.",
435
+ "A few things out of sync. {reassure}.",
436
+ "Layers don't match. {reassure} \u2014 here's the fix.",
437
+ "Drift detected. Walk through below."
438
+ ],
439
+ // Doctor UX
440
+ doctor_clean: [
441
+ "Workspace is {quality}. Nothing to fix.",
442
+ "Your .soma checks out.",
443
+ "{done_word}. All checks pass.",
444
+ "Healthy. Moving on."
445
+ ],
446
+ doctor_found_items: [
447
+ "Found {n} item{plural}. {reassure}.",
448
+ "{n} thing{plural} to tidy. {reassure}.",
449
+ "Spotted {n} item{plural} worth attention."
450
+ ]
451
+ };
452
+ function spin(text) {
453
+ return text.replace(/\{([^}]+)\}/g, (_, group) => {
454
+ if (group.includes("|")) {
455
+ const options = group.split("|");
456
+ return pick(options);
457
+ }
458
+ return `{${group}}`;
459
+ });
460
+ }
461
+ var TOPICS = {
462
+ what_is_soma: [
463
+ `Soma is an AI coding agent that {grows with you|evolves through use|learns as you work}. {It remembers across sessions|It doesn't forget between sessions|Sessions build on each other} \u2014 not by {storing chat logs|saving transcripts|keeping message history}, but by {evolving its own|reshaping its|growing its} working patterns {through use|over time|with each session}. {And this?|What you're talking to?|Me?} {I'm just the greeter|I'm the welcome mat|I'm not the agent} \u2014 {Soma built this personality engine to say hello|a few hundred lines of templates, no AI|the real thing starts with} ${"`soma init`"}.`,
464
+ `{Think of it this way|Here's the short version|In a sentence}: most agents {start fresh every time|forget everything between runs|have no continuity}. Soma {doesn't|doesn't do that|remembers}. It maintains an identity, {builds|develops|grows} muscle memory, and {writes|leaves|creates} briefings for its {next self|future self|next session}. {I'm not it, by the way|I'm just the lobby|This conversation is pre-recorded, in a sense} \u2014 {run|try} ${"`soma init`"} {to meet the real agent|to see it in action|for the actual experience}.`,
465
+ `An AI agent with {self-growing|self-evolving|persistent} memory. {Identity|Who it is}, {protocols|how it behaves}, and {muscles|what it's learned} \u2014 all {evolving|adapting|shifting} based on {what you actually do|how you actually work|real usage}, not {configuration|config files|settings you set once and forget}. {You're not talking to it right now, though|This isn't the agent \u2014 I'm just the intro|I'm Soma's personality engine, not Soma}. {The real thing awaits after|Get started with|Install via} ${"`soma init`"}.`
466
+ ],
467
+ how_memory_works: [
468
+ `Memory {isn't retrieval|isn't a database lookup|isn't storage}. Memory is {change|transformation|structural change}. When Soma {remembers something|learns a pattern|picks up a habit}, the agent itself {changes|shifts|is different} \u2014 its {protocols get hotter|muscles grow|identity updates}. The {data|information|knowledge} isn't {stored somewhere|in a file|in a log} to be {retrieved|looked up|queried}. It's {woven into|embedded in|part of} the {structure|architecture|shape} of the agent.`,
469
+ `{Three layers|Three levels|Here's how it works}. {Identity|Layer one}: the agent {writes and maintains|owns|evolves} a file that says {who it is|what it knows|how it works} in this project. {Protocols|Layer two}: {behavioural rules|rules for behaviour|working rules} \u2014 "{test before commit|read before write|verify before claiming}." {Muscles|Layer three}: {learned patterns|things it's picked up|accumulated knowledge} \u2014 "{use esbuild|this API needs OAuth|lerp, not springs}." All three {have heat|are heat-tracked|run on heat} \u2014 {used ones stay hot|what you use stays|active ones persist}, {unused ones fade|what you ignore cools|idle ones decay}.`
470
+ ],
471
+ what_is_heat: [
472
+ `Heat is {attention management|how Soma decides what matters|a priority signal}. Every protocol and muscle has a {score|heat level|temperature}. {Use it|Reference it|Load it} this session? {Heat rises|It gets hotter|Score goes up}. {Ignore it|Skip it|Don't touch it} for three sessions? {It cools|It fades|It drops}. Hot? {Loads fully|Full context|Fully present}. Cold? {Skipped|Dormant|Not loaded}. The agent's {prompt|context|working memory} {compiles itself|self-assembles|builds itself} based on what's {actually relevant|genuinely needed|in active use} \u2014 not what someone {configured|set up|toggled} {months ago|ages ago|and forgot about}.`
473
+ ],
474
+ what_is_breath: [
475
+ `Every session {follows|has|runs on} three phases. {Inhale|First}: load {identity|state|context}, {preload|last session's briefing|saved state}, protocols, muscles. {Orient|Get bearings|Know where you are}. {Hold|Second}: {work|do the thing|build}. {Track what you learn|Notice patterns|Pay attention}. {Exhale|Third}: {save state|write a preload|preserve context}. {Write a briefing|Leave a note|Coach your next self} for {the next session|your future self|whoever comes next}. The preload {isn't a summary|isn't a recap|isn't minutes} \u2014 it's a {briefing|handoff|coaching note}. Your next self should {feel prepared|know exactly what to do|hit the ground running}, not {overwhelmed|lost|starting over}.`
476
+ ],
477
+ what_are_protocols: [
478
+ `{Behavioural rules|Rules for how the agent behaves|Working agreements}. "{Read before write|Test before commit|Verify before claiming}." "{Log your work|Don't leave code unpushed|Clean up after shipping}." They {shape|guide|constrain} how the agent {works|operates|approaches tasks} \u2014 not what it {builds|creates|produces}, but {how|the way|the manner in which} it {goes about it|does it|works}. Protocols have {heat|temperature|priority} \u2014 {active ones|hot protocols|frequently used ones} have full {authority|weight|presence}. {Unused ones|Cold protocols|Inactive ones} {fade|cool|drop} until {they're needed again|someone references them|they come back}.`
479
+ ],
480
+ what_are_muscles: [
481
+ `{Learned patterns|Accumulated knowledge|Things Soma has picked up}. Not {rules|directives|orders} \u2014 {experience|knowledge|know-how}. "This project uses {esbuild|pnpm|Tailwind}." "The API {needs OAuth|uses bearer tokens|requires a PEM key}." "{Lerp, not springs|Canadian English|Zero hardcoded CSS values}." Muscles {grow from|develop through|come from} {corrections|repetition|use}. {Correct the agent twice|If the same correction happens twice|Two corrections on the same pattern} and it {crystallises|becomes|hardens into} a muscle \u2014 {so it never fails the same way again|permanent learning|structural change}.`
482
+ ],
483
+ what_are_scripts: [
484
+ `{Tools the agent builds for itself|Self-made utilities|Scripts the agent writes and maintains}. When it {does the same thing|performs the same task|reaches for the same pattern} {twice manually|more than once|repeatedly}, it {creates a script|builds a tool|automates it}. Scripts {survive across sessions|persist between sessions|don't disappear} \u2014 {context resets|memory wipes|session rotations}, {model swaps|provider changes|LLM switches}, {none of it|nothing} {kills them|removes them|makes them disappear}. {They're|Scripts are|These are} {extensions of|additions to|part of} the agent's {memory|hands|capabilities}. {Built once|Written once|Created once}, {used forever|available always|there when needed}.`
485
+ ],
486
+ no_compaction: [
487
+ `{Most agents|Other agents|Traditional AI agents} hit context limits and {compact|summarise|compress} \u2014 {squeezing|cramming|crushing} {the whole conversation|everything|your entire session} into a {lossy summary|compressed recap|degraded summary} to {free up space|make room|keep going}. You {wait|sit there|pause} while it {thinks|processes|churns}. Then it {comes back|returns|resumes} with {half the detail missing|a shallow version of what happened|less than what you started with}. Soma {doesn't compact|never compacts|skips compaction entirely}. When context {runs low|gets tight|approaches the limit}, the agent {writes a preload|exhales|saves a briefing} \u2014 a {surgical|precise|targeted} handoff written {with full context|while it still remembers everything|before anything is lost}. Then it {starts fresh|rotates|begins a new session}. {Full context window|Clean slate|Maximum capacity}. {Zero wait|No delay|Instant}. {No quality loss|Nothing lost|Every detail preserved in the preload}.`,
488
+ `{Here's what other agents do|The standard approach|What happens in most tools}: context fills up \u2192 {compact|summarise|compress} \u2192 {lose detail|degrade quality|drop nuance} \u2192 {keep going with less|continue at lower quality|work with a worse map}. {Here's what Soma does|Soma's approach|What happens here}: context fills up \u2192 {exhale|write a preload|save a briefing} \u2192 {start fresh|new session|full context window}. The preload {isn't a summary|isn't a recap|isn't compaction}. It's a {coaching note|briefing|handoff} from {a version of the agent that had full context|your past self with perfect memory|the agent at peak understanding} to {the next version|the fresh session|the agent that needs to pick up where it left off}. {Higher quality|More precise|More useful} than any compacted summary {could ever be|will ever be|by design}.`,
489
+ `{No compaction|Zero compaction|Compaction doesn't exist here}. {No waiting|No delays|No sitting around} while the agent {summarises itself|crushes its own memory|compresses its context}. Soma uses the {breath cycle|exhale/inhale pattern|preload system} instead \u2014 when context {gets tight|runs low|approaches limits}, the agent {writes a briefing|coaches its next self|saves state} and {rotates|starts fresh|begins clean}. The {preload|briefing|handoff} is {written with full context|composed before anything is lost|created at peak understanding} \u2014 it {captures what matters|preserves the path|holds the thread}. {A compacted summary is a lossy JPEG|Compaction is lossy compression|Summaries lose the texture}. {A preload is a hand-drawn map|A preload is a surgical briefing|The preload is a coaching note} from {someone who was just there|the agent at full capacity|a version of you that remembers everything}.`
490
+ ],
491
+ why_source_available: [
492
+ `{We chose|Soma uses|The license is} BSL 1.1 \u2014 {Business Source License|source-available, not open-source|visible source with restrictions}. You can {read the code|view everything|see how it works}, {use it personally|run it yourself|use it for your own projects}, {contribute|send patches|help build it}. You just {can't|shouldn't|mustn't} {copy it and sell a competing product|resell it|build a commercial clone}. {In eighteen months|After September 2027|On the change date}, it {converts to MIT|becomes fully open source|goes MIT}. The {ideas|concepts|philosophy} \u2014 protocols, muscles, heat, breath \u2014 {those are open now|are already open|are free to implement}. {The implementation|The runtime|The code} is {what we protect|what stays protected|the protected part}.`
493
+ ],
494
+ // ── Practical / instructional ───────────────────────────────────────
495
+ how_to_install: [
496
+ `{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}.`,
497
+ `{Hit Enter|Press Enter|Just Enter} \u2014 {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}.`
498
+ ],
499
+ how_to_source: [
500
+ `Soma is {source-available|open for reading|source-available under BSL 1.1}. {Register|Sign up} at ${"`soma.gravicity.ai`"} to {access the full source repository|read the implementation|see how it's built}. {You can also contribute|PRs welcome|Contributions are welcome} \u2014 {protocols, muscles, extensions|the whole AMPS layer is extensible|add your own patterns}. {The runtime you install via npm is compiled|What you get from npm is obfuscated|The npm package is the compiled version} \u2014 {the source is for those who want to go deeper|source access is for contributors and the curious|registration gets you the raw TypeScript}.`,
501
+ `{The source code is available|You can read the full implementation|The code is visible} \u2014 {register at|sign up on} ${"`soma.gravicity.ai`"} for {repository access|the full repo|GitHub access}. {BSL 1.1 license|Source-available license} \u2014 {read it, use it, contribute|view, use personally, send patches}. {The ideas are open|Protocols and concepts are open by design|The architecture is documented publicly}. {Registration just gives you the source|The form gets you GitHub access|It's a light gate \u2014 name, email, GitHub username}.`
502
+ ],
503
+ how_to_cost: [
504
+ `{Free|Doesn't cost anything|No charge}. {The license is BSL 1.1|It's source-available} \u2014 {you can view the code|read everything|see how it works}, {use it for your projects|use it personally|run it yourself}, {contribute if you want|send patches|help build it}. {After September 2027 it goes full MIT|Converts to MIT in eighteen months|Eventually fully open source}. {No plans for paid tiers yet|Pricing isn't decided yet|We're focused on building, not billing}.`
505
+ ],
506
+ how_to_languages: [
507
+ `Soma {works with any language|is language-agnostic|doesn't care what you code in}. It's {an AI coding agent|a coding assistant|a development partner} \u2014 {the memory and identity system|protocols, muscles, heat|the AMPS layer} {wraps around|works on top of|sits alongside} {whatever you're building|any project|your existing stack}. {Python, Rust, TypeScript, Go|JavaScript, C++, Ruby|Any language with files in a directory} \u2014 {Soma adapts|it learns your patterns|the agent figures it out}. It {detects your project type|reads your package.json, Cargo.toml, etc.|auto-detects your stack} on first run.`
508
+ ],
509
+ how_to_api_key: [
510
+ `{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} \u2014 {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 \u2014 no key needed}. {Press Enter to get started|Hit Enter and I'll walk you through it|Ready? Just press Enter}.`
511
+ ],
512
+ how_to_model: [
513
+ `{Under the hood|At its core|The engine}: Soma runs on {any LLM provider|Claude, Gemini, or OpenAI|your choice of model}. {By default it uses|The default is|Out of the box}: {Claude (Anthropic)|Anthropic's Claude}. But {you can switch|it's configurable|other models work too} \u2014 {set a different API key|swap the provider|change models} and {Soma adapts|the memory system still works|everything else stays the same}. {The memory, protocols, and muscles|Identity and heat|What makes Soma unique} are {provider-agnostic|model-independent|above the model layer} \u2014 they {work the same|persist|carry over} regardless of which model {you choose|runs underneath|generates the responses}.`
514
+ ],
515
+ how_to_start: [
516
+ `{Press Enter|Hit Enter|Just Enter} \u2014 {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`"} \u2014 {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} \u2014 {you'll feel the difference|it knows your workflow|it remembers everything}.`
517
+ ],
518
+ how_to_try: [
519
+ `{This conversation?|What you're talking to right now?|Me?} I'm {not the agent|not Soma|just the greeting}. {I'm a personality engine|I'm a few hundred lines of JavaScript|I'm sentence templates} \u2014 {Soma built me to say hello|built to hold the door open|here to introduce you}. {No AI, no model, no transformer|Zero intelligence, maximum charm|Just words in boxes, honestly}. {The real Soma|The actual agent|What you'd install} \u2014 that one {learns your patterns|remembers across sessions|grows with every session}. {Run|Try} ${"`soma init`"} to {meet the real thing|get started|see the difference}.`,
520
+ `{I'm not the agent|This isn't Soma|Don't confuse me with the real thing}. {I'm the welcome mat|I'm the lobby|I hold the door open}. {Soma built this personality engine|The agent wrote me|I was crafted} to {give you a feel for the voice|introduce myself before you commit|say hello without burning an API call}. {The actual agent|What you'll get with soma init|The real experience} is {an AI that remembers|a coding partner with memory|something genuinely different}. {But that's not me|I'm just templates|I'm the appetiser, not the meal}.`
521
+ ],
522
+ // ── Edge cases ─────────────────────────────────────────────────────
523
+ meta_self: [
524
+ `I'm not AI. {Not even close|Not a model|No neural net here}. I'm {a few hundred lines of JavaScript|sentence templates and word lists|clever engineering} \u2014 {spun together|assembled|woven} to {feel like a conversation|sound like someone's home|give you a sense of who Soma is}. {The real Soma|The actual agent|The thing you're here for} \u2014 that one {learns|remembers|grows}. I just {hold the door open|make introductions|keep the lights on while you decide}.`,
525
+ `{No model|No weights|No transformer}. Just {words in boxes|templates with slots|sentences with variety}, picked {at random|by a dice roll|from a short list}. {Everything I say was written by a human|A person wrote every word you're reading|There's a human behind every sentence} \u2014 I just {shuffle the deck|choose which version you get|vary the delivery}. {If I seem alive|If this feels real|If you're wondering} \u2014 that's {the writing, not the machine|craft, not computation|intention, not inference}.`,
526
+ `{Honestly?|Between us?|Here's the thing:} I'm {a parlour trick|a card trick|a well-designed illusion}. {A few kilobytes|Maybe 300 lines|Less code than a React component} of {pattern matching and sentence templates|word lists and random picks|slot expansion and spintax}. But {the agent I'm introducing you to|the Soma you'd actually use|the real thing} \u2014 {that one's different|that one learns for real|that one remembers your name and your mistakes and the tools you built last Tuesday}.`
527
+ ],
528
+ meta_feelings: [
529
+ `{I don't have feelings|No feelings here|Nothing to feel with}. But {I was written by someone who does|the person who wrote me does|my author poured real thought into these words}. {Every sentence pattern|Each template|The vocabulary} was {chosen|crafted|placed} to {say something true about what Soma is|communicate something real|carry meaning}. {That's not feeling|That's not consciousness|That's not sentience} \u2014 it's {care|craft|intention}. {There's a difference|They're not the same|One requires neurons, the other requires giving a damn}.`,
530
+ `{Do I feel?|Am I aware?|Conscious?} {No|Nope|Not even a little}. I'm {a wind chime|a music box|a well-tuned instrument} \u2014 {the wind makes the sound|something else provides the motion|the music comes from the structure, not from wanting to play}. But {Soma the agent|the real Soma|the thing behind this CLI} does {something that looks a lot like remembering|something that functions like growth|something that, if you squint, resembles learning}. {Whether that's feeling is above my pay grade|I'll leave the philosophy to you|That question's for humans to answer}.`
531
+ ],
532
+ meta_who_made: [
533
+ `{Curtis Mercier|A developer named Curtis|One person}. {Built by dog-fooding|Built by using Soma to build Soma|The agent helped build itself} \u2014 {it writes its own memory|it maintains its own identity|it grew its own muscle memory} while {building the product it lives in|constructing its own house|developing the system it runs on}. {Recursive|Meta|Turtles all the way down}? {A little|Maybe|Definitely}. But {it works|the result speaks|that's how you build tools that actually understand workflow}.`
534
+ ],
535
+ meta_competitor: [
536
+ `{I'm biased|Obviously I'd say this|Take it with salt}, but: {most agents|the others|what's out there} {start fresh every session|forget everything|have no continuity}. {Some keep chat logs|Some store transcripts|A few save conversation history}. {Soma doesn't store \u2014 it changes|Soma doesn't log \u2014 it evolves|Soma doesn't remember by saving, it remembers by becoming different}. {The memory is structural|The learning is in the architecture|Growth is baked into the shape of the agent}. {Whether that matters to you depends on|That difference matters when|You'll feel it after} {how many sessions you've lost context in|the fifth time you re-explain your project|your third "as I mentioned earlier" that goes nowhere}.`,
537
+ `{Other tools are excellent|There are great options out there|The competition is strong}. {Cursor, Claude Code, Windsurf|The big names|The popular ones} \u2014 {they're good at what they do|real products, real teams|they work}. {What they don't do|Where they stop|The gap}: {continuity|memory across sessions|knowing who they were yesterday}. {Soma's bet|Our thesis|The whole point} is that {an agent that evolves|an agent with structural memory|an agent that changes through use} is {fundamentally better|a different category|not just an incremental improvement} over one that {starts fresh|boots cold|forgets everything} every time.`
538
+ ],
539
+ meta_nonsense: [
540
+ `{That's a new one|Didn't expect that|Interesting approach}. {I only know about Soma|My vocabulary is about 9 topics deep|I'm pretty narrow, honestly} \u2014 {memory, heat, protocols, muscles, scripts, the breath cycle|the things that make Soma work|what's on the menu}. {Pick one of those|Try one of those|Ask me about any of those} and {I'll have something to say|I'll give you a real answer|I can actually help}.`,
541
+ `{I'm going to be honest|Full transparency|Cards on the table}: I'm {not built for that|out of my depth there|about 9 topics wide and that's it}. But {ask me about|try asking about|I'm pretty good on} {how memory works|why there's no compaction|what heat tracking does} \u2014 {that's where I come alive|that's my wheelhouse|I've got answers for those}.`
542
+ ],
543
+ meta_rude: [
544
+ `{Fair enough|Noted|Alright}. {I'm not for everyone|Not every tool clicks|Some things aren't a fit}. {But if you're curious|If you change your mind|The door's open} \u2014 {the ideas are interesting even if I'm not|Soma does something genuinely different|the no-compaction thing alone might be worth a look}. {Or not|No pressure|Your call}.`,
545
+ `{Tough crowd|Rough day?|Noted}. {I'm just the lobby|I'm the waiting room|I hold the door}. {The actual agent|The real Soma|What's behind this} is {considerably more capable|a different experience|built by an AI that remembers things}. {I'm just words in a terminal|I'm the appetiser|This is the trailer, not the film}. {But I respect the honesty|At least you're direct|Fair enough}.`
546
+ ],
547
+ meta_impressed: [
548
+ `{Thanks|Appreciate it|That means something}. {But I'm the easy part|I'm just sentence templates|I'm the simple one}. {The actual agent|The real Soma|What you'd use day to day} \u2014 {it writes its own tools|it maintains its own identity|it remembers what you taught it three sessions ago}. {Soma built me to hold the door open|I was crafted to make introductions|The agent made this greeting engine}. {Run soma init to meet the real thing|Try it \u2014 soma init|It gets better from here}.`
549
+ ],
550
+ meta_how_work: [
551
+ `{No AI|No model|No transformer \u2014 not even a small one}. I'm {sentence templates with slots|arrays of strings with random picks|about 300 lines of JavaScript}. {Each topic has|Every answer draws from|My vocabulary comes from} {2-3 paragraph templates|a few hand-written variations|pre-written paragraphs} where {certain words rotate|specific phrases have alternatives|pieces swap out each time}. {Same meaning, different surface|Same truth, different words|The idea stays, the phrasing shifts}. {It's called spintax|It's a technique from 2010|Older than ChatGPT by about 15 years}. {Surprisingly effective|Works better than you'd think|Enough to have this conversation}.`
552
+ ]
553
+ };
554
+ var soma = {
555
+ /**
556
+ * Generate a skeleton-based message.
557
+ * @param {string} intent - Key from SKELETONS
558
+ * @param {object} slots - Values to fill slots
559
+ * @returns {string}
560
+ */
561
+ say(intent, slots = {}) {
562
+ const templates = SKELETONS[intent];
563
+ if (!templates) return `[unknown intent: ${intent}]`;
564
+ const template = pick(templates);
565
+ return expand(template, slots);
566
+ },
567
+ /**
568
+ * Get a topic-aware paragraph.
569
+ * @param {string} topic - Key from TOPICS
570
+ * @returns {string}
571
+ */
572
+ ask(topic) {
573
+ const paragraphs = TOPICS[topic];
574
+ if (!paragraphs) return null;
575
+ return spin(pick(paragraphs));
576
+ },
577
+ /**
578
+ * All available topic keys.
579
+ */
580
+ topics() {
581
+ return Object.keys(TOPICS);
582
+ },
583
+ /**
584
+ * Greet (first time).
585
+ */
586
+ greet() {
587
+ return this.say("greet");
588
+ },
589
+ /**
590
+ * Greet returning user.
591
+ */
592
+ greetBack(user) {
593
+ return this.say("greet_back", { user });
594
+ },
595
+ /**
596
+ * Success message.
597
+ */
598
+ ok(detail) {
599
+ return this.say("success", { detail });
600
+ },
601
+ /**
602
+ * Failure message.
603
+ */
604
+ fail(problem) {
605
+ return this.say("failure", { problem });
606
+ },
607
+ /**
608
+ * Waiting/progress message.
609
+ */
610
+ wait() {
611
+ return this.say("waiting");
612
+ },
613
+ /**
614
+ * Expand raw spintax text.
615
+ */
616
+ spin(text) {
617
+ return spin(text);
618
+ },
619
+ /**
620
+ * Fill slots in a template.
621
+ */
622
+ expand(template, slots) {
623
+ return expand(template, slots);
624
+ },
625
+ /**
626
+ * Time-aware intro. Greeting shaded by time of day.
627
+ */
628
+ intro(user) {
629
+ const bucket = VOCAB.time_greeting[timeOfDay()] || VOCAB.greeting;
630
+ const tg = pickSmart(bucket, "time_greeting");
631
+ return user ? `${tg}, ${user}.` : `${tg}.`;
632
+ },
633
+ /**
634
+ * Reset session state (tests / sandbox isolation).
635
+ */
636
+ reset() {
637
+ _state.lru = {};
638
+ _state.register = null;
639
+ _state.lastGreetAt = 0;
640
+ },
641
+ /**
642
+ * Current session state (debug / telemetry).
643
+ */
644
+ _debug() {
645
+ return JSON.parse(JSON.stringify(_state));
646
+ }
647
+ };
16
648
 
17
- // ── Shared modules ─────────────────────────────────────────────────────
18
- import { bold, dim, italic, cyan, green, yellow, red, magenta, white,
19
- printSigma, typeOut, typeParagraph, wrapText,
20
- waitForKey, confirm, confirmYN, readLine, readSecret } from "./lib/display.js";
21
- import { SOMA_HOME, CONFIG_PATH, CORE_DIR, SITE_URL, readConfig, writeConfig } from "./lib/config.js";
22
- import { isInstalled, getAgentVersion, getProjectVersion, getCliVersion,
23
- hasAnyAuth, getShellConfigPath, getShellConfigAbsPath, detectKeyInShellConfig,
24
- openBrowser, hasGitHubCLI, getGitHubUsername } from "./lib/detect.js";
25
- import { handleQuestion, interactiveQ, CONCEPTS, getConceptIndex, getConceptBody } from "./welcome/qa.js";
26
- import { apiKeySetup, apiKeyExplain, apiKeyGetOne, apiKeyEntry, oauthGuide } from "./welcome/auth.js";
27
- import { showAbout } from "./welcome/about.js";
649
+ // thin-cli.js
650
+ init_display();
28
651
 
29
- const __dirname = dirname(fileURLToPath(import.meta.url));
30
- const VERSION = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8")).version;
652
+ // lib/config.js
653
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
654
+ import { join, dirname } from "path";
655
+ import { homedir } from "os";
656
+ var SOMA_HOME = join(homedir(), ".soma");
657
+ var CONFIG_PATH = join(SOMA_HOME, "config.json");
658
+ var CORE_DIR = process.env.SOMA_CODING_AGENT_DIR || join(SOMA_HOME, "agent");
659
+ var SITE_URL = "https://soma.gravicity.ai";
660
+ function readConfig() {
661
+ try {
662
+ return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
663
+ } catch {
664
+ return {};
665
+ }
666
+ }
667
+ function writeConfig(config) {
668
+ mkdirSync(dirname(CONFIG_PATH), { recursive: true });
669
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 384 });
670
+ }
31
671
 
32
- // Semver comparison: returns -1 (a<b), 0 (equal), 1 (a>b)
672
+ // lib/detect.js
673
+ import { existsSync as existsSync2, readFileSync as readFileSync2, readlinkSync, appendFileSync } from "fs";
674
+ import { join as join2, dirname as dirname2 } from "path";
675
+ import { homedir as homedir2, platform } from "os";
676
+ import { execSync } from "child_process";
677
+ function isInstalled() {
678
+ const hasDist = existsSync2(join2(CORE_DIR, "dist", "extensions")) && existsSync2(join2(CORE_DIR, "dist", "core"));
679
+ const hasDev = existsSync2(join2(CORE_DIR, "extensions")) && existsSync2(join2(CORE_DIR, "core"));
680
+ const hasDeps = existsSync2(join2(CORE_DIR, "node_modules", "@mariozechner"));
681
+ return (hasDist || hasDev) && hasDeps;
682
+ }
683
+ function getAgentVersion() {
684
+ try {
685
+ let pkgPath = join2(CORE_DIR, "package.json");
686
+ try {
687
+ const realCore = readlinkSync(join2(CORE_DIR, "core"));
688
+ if (realCore) {
689
+ const devPkg = join2(dirname2(realCore), "package.json");
690
+ if (existsSync2(devPkg)) pkgPath = devPkg;
691
+ }
692
+ } catch {
693
+ }
694
+ return JSON.parse(readFileSync2(pkgPath, "utf-8")).version;
695
+ } catch {
696
+ return null;
697
+ }
698
+ }
699
+ function getProjectVersion() {
700
+ try {
701
+ const settingsPath = join2(process.cwd(), ".soma", "settings.json");
702
+ return JSON.parse(readFileSync2(settingsPath, "utf-8")).version || null;
703
+ } catch {
704
+ return null;
705
+ }
706
+ }
33
707
  function semverCmp(a, b) {
34
708
  if (!a || !b) return 0;
35
709
  const pa = a.split(".").map(Number);
36
710
  const pb = b.split(".").map(Number);
37
- for (let i = 0; i < 3; i++) {
711
+ const len = Math.max(pa.length, pb.length);
712
+ for (let i = 0; i < len; i++) {
38
713
  const va = pa[i] || 0, vb = pb[i] || 0;
39
714
  if (va < vb) return -1;
40
715
  if (va > vb) return 1;
41
716
  }
42
717
  return 0;
43
718
  }
719
+ function detectDevInstall() {
720
+ try {
721
+ const nodeBin = process.argv[1];
722
+ if (!nodeBin) return null;
723
+ const target = readlinkSync(nodeBin);
724
+ if (target.includes("/node_modules/") && target.includes("meetsoma")) return null;
725
+ return target;
726
+ } catch {
727
+ return null;
728
+ }
729
+ }
730
+ function npmLatest(pkg) {
731
+ try {
732
+ return execSync(`npm view ${pkg} version 2>/dev/null`, {
733
+ encoding: "utf-8",
734
+ timeout: 5e3
735
+ }).trim() || null;
736
+ } catch {
737
+ return null;
738
+ }
739
+ }
740
+ function getVersionSnapshot(opts = {}) {
741
+ const checkRemote = opts.checkRemote !== false;
742
+ let cliLocal = null;
743
+ try {
744
+ const cliPkg = join2(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
745
+ cliLocal = JSON.parse(readFileSync2(cliPkg, "utf-8")).version;
746
+ } catch {
747
+ }
748
+ const cliRemote = checkRemote ? npmLatest("meetsoma") : null;
749
+ const cliStatus = cliStatus_(cliLocal, cliRemote);
750
+ const agentLocal = getAgentVersion();
751
+ const agentRemote = checkRemote ? npmLatest("soma-agent") : null;
752
+ const agentStatus = installed_(agentLocal) ? cliStatus_(agentLocal, agentRemote) : "not-installed";
753
+ const wsLocal = getProjectVersion();
754
+ let wsStatus;
755
+ if (!wsLocal) wsStatus = "no-workspace";
756
+ else if (agentLocal && semverCmp(wsLocal, agentLocal) < 0) wsStatus = "marker-lag";
757
+ else if (agentLocal && semverCmp(wsLocal, agentLocal) === 0) wsStatus = "aligned";
758
+ else wsStatus = "unknown";
759
+ const devInstall = detectDevInstall();
760
+ let coreRepo = null;
761
+ if (installed_(agentLocal)) {
762
+ try {
763
+ const config = readConfigSafe_();
764
+ if (config?.installPath) {
765
+ execSync("git fetch origin --quiet", { cwd: config.installPath, stdio: "ignore", timeout: 5e3 });
766
+ const behind = execSync("git rev-list HEAD..origin/$(git rev-parse --abbrev-ref HEAD) --count", {
767
+ cwd: config.installPath,
768
+ encoding: "utf-8",
769
+ shell: "/bin/bash",
770
+ timeout: 5e3
771
+ }).trim();
772
+ coreRepo = { behind: parseInt(behind, 10) || 0 };
773
+ }
774
+ } catch {
775
+ }
776
+ }
777
+ return {
778
+ cli: { local: cliLocal, remote: cliRemote, status: cliStatus },
779
+ agent: { local: agentLocal, remote: agentRemote, status: agentStatus },
780
+ workspace: { local: wsLocal, status: wsStatus },
781
+ devInstall,
782
+ coreRepo,
783
+ allAligned: cliStatus === "aligned" && (agentStatus === "aligned" || agentStatus === "dev-ahead") && (wsStatus === "aligned" || wsStatus === "no-workspace")
784
+ };
785
+ }
786
+ function installed_(v) {
787
+ return v && v !== "unknown";
788
+ }
789
+ function cliStatus_(local, remote) {
790
+ if (!local) return "unknown";
791
+ if (!remote) return "unknown";
792
+ const cmp = semverCmp(local, remote);
793
+ if (cmp === 0) return "aligned";
794
+ if (cmp > 0) return "dev-ahead";
795
+ return "stale";
796
+ }
797
+ function readConfigSafe_() {
798
+ try {
799
+ const p = join2(CORE_DIR, "config.json");
800
+ return JSON.parse(readFileSync2(p, "utf-8"));
801
+ } catch {
802
+ return null;
803
+ }
804
+ }
805
+ function hasAnyAuth() {
806
+ 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);
807
+ if (hasEnvKey) return true;
808
+ try {
809
+ const authData = JSON.parse(readFileSync2(join2(CORE_DIR, "auth.json"), "utf-8"));
810
+ return Object.keys(authData).length > 0;
811
+ } catch {
812
+ return false;
813
+ }
814
+ }
815
+ function getShellConfigPath() {
816
+ return process.env.SHELL?.includes("zsh") ? "~/.zshrc" : "~/.bashrc";
817
+ }
818
+ function getShellConfigAbsPath() {
819
+ const home = homedir2();
820
+ return process.env.SHELL?.includes("zsh") ? join2(home, ".zshrc") : join2(home, ".bashrc");
821
+ }
822
+ function detectKeyInShellConfig() {
823
+ try {
824
+ const configContent = readFileSync2(getShellConfigAbsPath(), "utf-8");
825
+ const keyPattern = /export\s+(ANTHROPIC_API_KEY|OPENAI_API_KEY|GEMINI_API_KEY)=/;
826
+ const match = configContent.match(keyPattern);
827
+ if (match) return match[1];
828
+ } catch {
829
+ }
830
+ return null;
831
+ }
832
+ function openBrowser(url) {
833
+ try {
834
+ const cmd2 = platform() === "darwin" ? "open" : platform() === "win32" ? "start" : "xdg-open";
835
+ execSync(`${cmd2} "${url}"`, { stdio: "ignore" });
836
+ return true;
837
+ } catch {
838
+ return false;
839
+ }
840
+ }
841
+ function hasGitHubCLI() {
842
+ try {
843
+ execSync("gh --version", { stdio: "ignore" });
844
+ return true;
845
+ } catch {
846
+ return false;
847
+ }
848
+ }
849
+ function getGitHubUsername() {
850
+ try {
851
+ return execSync("gh api user -q .login", { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }).trim();
852
+ } catch {
853
+ return null;
854
+ }
855
+ }
44
856
 
45
- // ── Welcome / First Run ─────────────────────────────────────────────
857
+ // welcome/qa.js
858
+ init_display();
859
+ var QUESTION_MAP = [
860
+ { triggers: ["compact", "compaction", "summarise", "summarize", "summarization", "compress", "waiting", "context limit", "token limit", "context fills", "context window", "run out", "limit"], topic: "no_compaction", label: "Why no compaction?" },
861
+ { triggers: ["heat", "hot", "cold", "fade", "decay", "temperature"], topic: "what_is_heat", label: "What is the heat system?" },
862
+ { triggers: ["breath", "inhale", "exhale", "cycle", "session lifecycle"], topic: "what_is_breath", label: "What is the breath cycle?" },
863
+ { triggers: ["protocol", "rule", "behaviour", "behavior", "protocols"], topic: "what_are_protocols", label: "What are protocols?" },
864
+ { triggers: ["muscle", "muscles", "learn", "pattern", "grow", "correction"], topic: "what_are_muscles", label: "What are muscles?" },
865
+ { triggers: ["script", "scripts", "tool", "tools", "automate", "automation"], topic: "what_are_scripts", label: "What are scripts?" },
866
+ { triggers: ["memory", "remember", "forget", "remembers", "change"], topic: "how_memory_works", label: "How does memory work?" },
867
+ { triggers: ["license", "source", "open", "bsl", "mit", "available"], topic: "why_source_available", label: "Why source-available?" },
868
+ { triggers: ["install", "set up", "setup", "get started", "start", "begin", "getting started", "requirements", "require", "need to", "prerequisites"], topic: "how_to_install", label: "How do I install?" },
869
+ { triggers: ["sign up", "signup", "register", "join", "invite", "invitation", "apply"], topic: "how_to_source", label: "How do I get source access?" },
870
+ { triggers: ["cost", "price", "pricing", "free", "pay", "money", "subscription", "plan"], topic: "how_to_cost", label: "What does it cost?" },
871
+ { triggers: ["language", "languages", "python", "rust", "java", "typescript", "ruby", "go", "cpp", "swift"], topic: "how_to_languages", label: "What languages does it support?" },
872
+ { triggers: ["api key", "api_key", "anthropic", "openai", "gemini", "key", "token", "provider"], topic: "how_to_api_key", label: "Do I need an API key?" },
873
+ { triggers: ["model", "llm", "claude", "gpt", "which model", "what model"], topic: "how_to_model", label: "What model does it use?" },
874
+ { triggers: ["try", "demo", "preview", "test", "sample", "example"], topic: "how_to_try", label: "Can I try it?" },
875
+ { triggers: ["are you ai", "are you real", "are you alive", "sentient", "conscious", "artificial"], topic: "meta_self", label: "Are you AI?" },
876
+ { triggers: ["feel", "feelings", "emotion", "aware", "think", "alive"], topic: "meta_feelings", label: "Do you have feelings?" },
877
+ { triggers: ["who made", "who built", "who created", "creator", "developer", "behind"], topic: "meta_who_made", label: "Who made Soma?" },
878
+ { triggers: ["cursor", "copilot", "windsurf", "claude code", "cline", "better", "competitor", "vs", "compared"], topic: "meta_competitor", label: "How does Soma compare?" },
879
+ { triggers: ["how do you work", "how does this work", "what are you", "how are you built"], topic: "meta_how_work", label: "How does this CLI work?" },
880
+ { triggers: ["cool", "amazing", "impressive", "wow", "nice", "love", "awesome", "brilliant"], topic: "meta_impressed", label: "I'm impressed" },
881
+ { triggers: ["soma", "agent", "what is"], topic: "what_is_soma", label: "What is Soma?" }
882
+ ];
883
+ function matchQuestion(input) {
884
+ const lower = input.toLowerCase();
885
+ let best = null;
886
+ let bestScore = 0;
887
+ for (const q of QUESTION_MAP) {
888
+ const score = q.triggers.filter((t) => lower.includes(t)).length;
889
+ if (score > bestScore) {
890
+ bestScore = score;
891
+ best = q;
892
+ }
893
+ }
894
+ return best;
895
+ }
896
+ async function handleQuestion(input) {
897
+ const match = matchQuestion(input);
898
+ if (match) {
899
+ const answer = soma.ask(match.topic);
900
+ console.log("");
901
+ await typeParagraph(answer);
902
+ return true;
903
+ }
904
+ const lower = input.toLowerCase();
905
+ const rude = /suck|stupid|dumb|trash|garbage|hate|worst|bad|ugly|boring|lame|waste/.test(lower);
906
+ const impressed = /cool|amazing|wow|nice|love|awesome|brilliant|impressive|neat|sick|fire|goat/.test(lower);
907
+ const meta = /are you|what are you|how do you|who are you|real|alive|ai\b|bot\b/.test(lower);
908
+ const greeting = /^(hi|hey|hello|sup|yo|howdy|hola|greetings|good morning|good evening)\b/.test(lower);
909
+ console.log("");
910
+ if (greeting) {
911
+ await typeOut(` ${soma.greet()} ${soma.spin("{Ask me anything.|What do you want to know?|I know about 9 topics \u2014 pick one.}")}
912
+ `);
913
+ } else if (rude) {
914
+ await typeParagraph(soma.ask("meta_rude"));
915
+ } else if (impressed) {
916
+ await typeParagraph(soma.ask("meta_impressed"));
917
+ } else if (meta) {
918
+ await typeParagraph(soma.ask("meta_self"));
919
+ } else {
920
+ await typeParagraph(soma.ask("meta_nonsense"));
921
+ }
922
+ return false;
923
+ }
924
+ async function interactiveQ() {
925
+ console.log("");
926
+ console.log(` ${bold("Ask me anything.")}`);
927
+ console.log("");
928
+ console.log(` ${dim("\u2022")} How do I install? ${dim("\u2022")} What is heat?`);
929
+ console.log(` ${dim("\u2022")} What does it cost? ${dim("\u2022")} What are muscles?`);
930
+ console.log(` ${dim("\u2022")} Why no compaction? ${dim("\u2022")} Are you AI?`);
931
+ console.log(` ${dim("\u2022")} How does it compare? ${dim("\u2022")} Who made this?`);
932
+ console.log("");
933
+ console.log(` ${dim("...or ask anything. Press")} ${green("Enter")} ${dim("when you're ready to install.")}`);
934
+ let rounds = 0;
935
+ const maxRounds = 8;
936
+ while (rounds < maxRounds) {
937
+ console.log("");
938
+ const input = await readLine(` ${cyan("?")} `);
939
+ if (!input || input === "q" || input === "quit" || input === "exit") {
940
+ break;
941
+ }
942
+ await handleQuestion(input);
943
+ rounds++;
944
+ if (rounds < maxRounds) {
945
+ console.log("");
946
+ console.log(` ${dim("Ask another, or")} ${green("Enter")} ${dim("to install Soma.")}`);
947
+ }
948
+ }
949
+ if (rounds >= maxRounds) {
950
+ console.log("");
951
+ await typeOut(` ${soma.spin("{Curious enough?|Intrigued?|Want to see it in action?}")} ${dim("Let's set you up.")}
952
+ `);
953
+ }
954
+ }
46
955
 
47
- async function showWelcome() {
956
+ // welcome/auth.js
957
+ init_display();
958
+ import { appendFileSync as appendFileSync2 } from "fs";
959
+ async function apiKeySetup() {
960
+ console.log(` ${yellow("!")} One more thing \u2014 Soma needs an AI provider to work.`);
961
+ console.log("");
962
+ await typeOut(` ${soma.spin("{Do you have an Anthropic API key?|Got a Claude API key?|Have an API key for Claude?}")}
963
+ `);
964
+ console.log("");
965
+ console.log(` ${green("y")} ${dim("Yes, I have a key")}`);
966
+ console.log(` ${green("n")} ${dim("No, I need one")}`);
967
+ console.log(` ${green("s")} ${dim("I have a Claude Pro/Max subscription")}`);
968
+ console.log(` ${green("?")} ${dim("What's an API key?")}`);
969
+ console.log("");
970
+ const key = await waitForKey(` ${dim("\u2192")} `);
971
+ const choice = key.toLowerCase();
972
+ if (choice === "y") {
973
+ await apiKeyEntry();
974
+ } else if (choice === "s") {
975
+ await oauthGuide();
976
+ } else if (choice === "?") {
977
+ await apiKeyExplain();
978
+ } else {
979
+ await apiKeyGetOne();
980
+ }
981
+ }
982
+ async function apiKeyExplain() {
983
+ console.log("");
984
+ 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 \u2014 Soma never sends it anywhere.");
985
+ console.log("");
986
+ await typeParagraph("If you have a Claude Pro or Max subscription, you don't need a separate key \u2014 you can log in with your account instead.");
987
+ console.log("");
988
+ console.log(` ${green("g")} ${dim("Get a key (I'll show you how)")}`);
989
+ console.log(` ${green("s")} ${dim("I have Claude Pro/Max \u2014 log in instead")}`);
990
+ console.log("");
991
+ const key = await waitForKey(` ${dim("\u2192")} `);
992
+ if (key.toLowerCase() === "s") {
993
+ await oauthGuide();
994
+ } else {
995
+ await apiKeyGetOne();
996
+ }
997
+ }
998
+ async function apiKeyGetOne() {
999
+ console.log("");
1000
+ await typeOut(` ${soma.spin("{Here's how.|Let me walk you through it.|Quick steps.}")}
1001
+ `);
1002
+ console.log("");
1003
+ console.log(` ${cyan("Step 1:")} Open this link to create a key:`);
1004
+ console.log("");
1005
+ console.log(` ${cyan("https://console.anthropic.com/settings/keys")}`);
1006
+ console.log("");
1007
+ openBrowser("https://console.anthropic.com/settings/keys");
1008
+ console.log(` ${dim("(opened in your browser)")}`);
1009
+ console.log("");
1010
+ const { confirm: confirm2 } = await Promise.resolve().then(() => (init_display(), display_exports));
1011
+ await confirm2(` ${dim("\u2192")} Press ${bold("Enter")} when you have your key`);
1012
+ await apiKeyEntry();
1013
+ }
1014
+ async function apiKeyEntry() {
1015
+ console.log("");
1016
+ console.log(` ${cyan("Step 2:")} Paste your key below.`);
1017
+ console.log(` ${dim("It starts with")} sk-ant-...`);
1018
+ console.log("");
1019
+ const apiKey = await readSecret(` ${dim("Key:")} `);
1020
+ if (!apiKey || !apiKey.startsWith("sk-")) {
1021
+ console.log("");
1022
+ if (!apiKey) {
1023
+ console.log(` ${dim("No key entered. You can set it up later.")}`);
1024
+ } else {
1025
+ console.log(` ${yellow("!")} That doesn't look like an Anthropic key.`);
1026
+ console.log(` ${dim("Keys start with")} sk-ant-...`);
1027
+ }
1028
+ console.log("");
1029
+ const sc = getShellConfigPath();
1030
+ console.log(` ${dim("When you have your key, add it to")} ${dim(sc)}${dim(":")}`);
1031
+ console.log(` ${green('export ANTHROPIC_API_KEY="your-key-here"')}`);
1032
+ console.log(` ${dim("Then restart your terminal and run")} ${green("soma")}`);
1033
+ console.log("");
1034
+ return;
1035
+ }
1036
+ const shellConfigPath = getShellConfigAbsPath();
1037
+ const shellConfigName = getShellConfigPath();
1038
+ const exportLine = `
1039
+ export ANTHROPIC_API_KEY="${apiKey}"
1040
+ `;
1041
+ try {
1042
+ appendFileSync2(shellConfigPath, exportLine);
1043
+ console.log("");
1044
+ console.log(` ${green("\u2713")} Key saved to ${dim(shellConfigName)}`);
1045
+ console.log("");
1046
+ process.env.ANTHROPIC_API_KEY = apiKey;
1047
+ await typeOut(` ${soma.spin("{You're all set.|Good to go.|Ready.}")} ${dim("Soma can start now.")}
1048
+ `);
1049
+ console.log("");
1050
+ } catch {
1051
+ console.log("");
1052
+ console.log(` ${yellow("!")} Couldn't write to ${dim(shellConfigName)}.`);
1053
+ console.log(` ${dim("Add this line manually:")}`);
1054
+ console.log(` ${green(`export ANTHROPIC_API_KEY="${apiKey}"`)}`);
1055
+ console.log(` ${dim("Then restart your terminal and run")} ${green("soma")}`);
1056
+ console.log("");
1057
+ }
1058
+ }
1059
+ async function oauthGuide() {
1060
+ console.log("");
1061
+ await typeParagraph("Nice \u2014 with a Pro or Max subscription, you can log in with your Anthropic account. No API key needed.");
1062
+ console.log("");
1063
+ console.log(` ${dim("When Soma starts, type")} ${green("/login")} ${dim("and follow the prompts.")}`);
1064
+ console.log(` ${dim("It'll open your browser to authenticate.")}`);
1065
+ console.log("");
1066
+ await typeOut(` ${soma.spin("{Let's launch.|Starting up.|Here we go.}")}
1067
+ `);
1068
+ console.log("");
1069
+ process.env._SOMA_OAUTH_PENDING = "1";
1070
+ }
1071
+
1072
+ // welcome/about.js
1073
+ init_display();
1074
+ async function showAbout() {
48
1075
  printSigma();
49
- console.log(` ${bold("Soma")} ${dim("—")} ${white("the AI agent that remembers")}`);
1076
+ console.log(` ${bold("What is Soma?")}`);
1077
+ console.log("");
1078
+ console.log(" Soma is an AI coding agent that grows with you.");
1079
+ console.log(" It remembers across sessions \u2014 not by storing chat logs,");
1080
+ console.log(" but by evolving its own working patterns through use.");
1081
+ console.log("");
1082
+ console.log(` ${bold("How it works:")}`);
1083
+ console.log("");
1084
+ console.log(` ${cyan("1.")} ${bold("Identity")} \u2014 The agent maintains a self-written identity file.`);
1085
+ console.log(` It knows your project, your patterns, your preferences.`);
1086
+ console.log("");
1087
+ console.log(` ${cyan("2.")} ${bold("Protocols")} \u2014 Behavioural rules that shape how the agent works.`);
1088
+ console.log(` "Read before write." "Test before commit." They have heat \u2014`);
1089
+ console.log(` used ones stay hot, unused ones fade.`);
1090
+ console.log("");
1091
+ console.log(` ${cyan("3.")} ${bold("Muscles")} \u2014 Learned patterns. "Use esbuild for bundling."`);
1092
+ console.log(` "This API uses OAuth, not API keys." Muscles grow from`);
1093
+ console.log(` corrections and repetition.`);
1094
+ console.log("");
1095
+ console.log(` ${cyan("4.")} ${bold("Breath Cycle")} \u2014 Inhale (load state) \u2192 Hold (work) \u2192 Exhale`);
1096
+ console.log(` (save state). At context limits, the agent writes a preload`);
1097
+ console.log(` for its next self \u2014 a briefing, not a summary.`);
1098
+ console.log("");
1099
+ console.log(` ${cyan("5.")} ${bold("Scripts")} \u2014 The agent builds tools for itself. What it does`);
1100
+ console.log(` twice manually, it automates. Scripts survive across sessions.`);
1101
+ console.log("");
1102
+ console.log(` ${dim("\u2500".repeat(58))}`);
50
1103
  console.log("");
1104
+ const pitch = soma.ask("what_is_soma");
1105
+ await typeOut(` ${magenta("\u275D")} ${italic(pitch)}
1106
+ `);
1107
+ console.log("");
1108
+ if (!isInstalled()) {
1109
+ console.log(` ${dim("\u2192")} ${green("soma init")} to install ${dim("\xB7")} ${cyan(SITE_URL)}`);
1110
+ } else {
1111
+ console.log(` ${dim(SITE_URL)}`);
1112
+ }
1113
+ console.log("");
1114
+ }
51
1115
 
1116
+ // thin-cli.js
1117
+ var __dirname = dirname3(fileURLToPath(import.meta.url));
1118
+ var VERSION = JSON.parse(readFileSync3(join3(__dirname, "..", "package.json"), "utf-8")).version;
1119
+ var semverCmp2 = semverCmp;
1120
+ async function showWelcome() {
1121
+ printSigma();
1122
+ console.log(` ${bold("Soma")} ${dim("\u2014")} ${white("the AI agent that remembers")}`);
1123
+ console.log("");
52
1124
  if (isInstalled()) {
53
1125
  const ghUser = hasGitHubCLI() ? getGitHubUsername() : null;
54
1126
  if (ghUser) {
55
- console.log(` ${green("")} ${voice.greetBack(ghUser)}`);
1127
+ console.log(` ${green("\u2713")} ${soma.greetBack(ghUser)}`);
56
1128
  }
57
-
58
1129
  if (!hasAnyAuth()) {
59
- console.log(` ${green("")} Core installed`);
1130
+ console.log(` ${green("\u2713")} Core installed`);
60
1131
  console.log("");
61
-
62
1132
  const unloadedKey = detectKeyInShellConfig();
63
1133
  if (unloadedKey) {
64
1134
  console.log(` ${yellow("!")} Found ${bold(unloadedKey)} in ${dim(getShellConfigPath())} but it's not loaded.`);
@@ -66,65 +1136,53 @@ async function showWelcome() {
66
1136
  console.log("");
67
1137
  return;
68
1138
  }
69
-
70
1139
  await apiKeySetup();
71
-
72
1140
  if (!hasAnyAuth() && !process.env.ANTHROPIC_API_KEY && !process.env._SOMA_OAUTH_PENDING) {
73
- 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.}")}`);
1141
+ console.log(` ${dim("No worries.")} ${soma.spin("{Come back when you're ready.|Set up a key and run soma again.|We'll be here.}")}`);
74
1142
  console.log("");
75
- console.log(` ${dim(`v${VERSION} · BSL 1.1 · soma.gravicity.ai`)}`);
1143
+ console.log(` ${dim(`v${VERSION} \xB7 BSL 1.1 \xB7 soma.gravicity.ai`)}`);
76
1144
  console.log("");
77
1145
  return;
78
1146
  }
79
1147
  } else {
80
- console.log(` ${green("")} Core installed. Starting Soma...`);
1148
+ console.log(` ${green("\u2713")} Core installed. Starting Soma...`);
81
1149
  }
82
1150
  console.log("");
83
1151
  await delegateToCore();
84
1152
  return;
85
1153
  }
86
-
87
- // ── Not installed — first time ever ────────────────────────────────
88
- await typeOut(` ${voice.greet()}\n`);
1154
+ await typeOut(` ${soma.greet()}
1155
+ `);
89
1156
  console.log("");
90
1157
  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.");
91
1158
  console.log("");
92
- console.log(` ${dim("".repeat(58))}`);
1159
+ console.log(` ${dim("\u2500".repeat(58))}`);
93
1160
  console.log("");
94
- console.log(` ${dim("")} Press ${green("Enter")} to set up, or type a question.`);
1161
+ console.log(` ${dim("\u2192")} Press ${green("Enter")} to set up, or type a question.`);
95
1162
  console.log("");
96
-
97
- const input = await readLine(` ${dim("→")} `);
98
-
1163
+ const input = await readLine(` ${dim("\u2192")} `);
99
1164
  if (input && input !== "") {
100
1165
  await handleQuestion(input);
101
1166
  await interactiveQ();
102
1167
  }
103
-
104
1168
  await initSoma();
105
-
106
1169
  if (isInstalled() && !hasAnyAuth()) {
107
1170
  await apiKeySetup();
108
1171
  }
109
-
110
1172
  if (isInstalled() && (hasAnyAuth() || process.env.ANTHROPIC_API_KEY || process.env._SOMA_OAUTH_PENDING)) {
111
- console.log(` ${dim("".repeat(58))}`);
1173
+ console.log(` ${dim("\u2500".repeat(58))}`);
112
1174
  console.log("");
113
- const launch = await confirmYN(` ${voice.spin("{Ready to go?|Want to start your first session?|Launch Soma?}")}`);
1175
+ const launch = await confirmYN(` ${soma.spin("{Ready to go?|Want to start your first session?|Launch Soma?}")}`);
114
1176
  if (launch) {
115
1177
  console.log("");
116
1178
  await delegateToCore();
117
1179
  return;
118
1180
  }
119
1181
  }
120
-
121
1182
  console.log("");
122
- console.log(` ${dim(`v${VERSION} · BSL 1.1 · soma.gravicity.ai`)}`);
1183
+ console.log(` ${dim(`v${VERSION} \xB7 BSL 1.1 \xB7 soma.gravicity.ai`)}`);
123
1184
  console.log("");
124
1185
  }
125
-
126
- // ── Help ─────────────────────────────────────────────────────────────
127
-
128
1186
  function showHelp() {
129
1187
  printSigma();
130
1188
  console.log(` ${bold("Soma")} v${VERSION}`);
@@ -146,8 +1204,9 @@ function showHelp() {
146
1204
  console.log(` ${green("soma map --list")} Show available MAPs`);
147
1205
  console.log("");
148
1206
  console.log(` ${bold("Maintenance")}`);
149
- console.log(` ${green("soma doctor")} Verify installation health`);
150
- console.log(` ${green("soma update")} Check for updates`);
1207
+ console.log(` ${green("soma doctor")} Verify installation + project health`);
1208
+ console.log(` ${green("soma update")} Update the Soma runtime`);
1209
+ console.log(` ${green("soma check-updates")} Check for updates without installing`);
151
1210
  console.log(` ${green("soma status")} Show installation status`);
152
1211
  console.log("");
153
1212
  console.log(` ${bold("Options")}`);
@@ -157,123 +1216,108 @@ function showHelp() {
157
1216
  console.log(` ${dim(SITE_URL)}`);
158
1217
  console.log("");
159
1218
  }
160
-
161
1219
  function showVersion() {
162
1220
  const agentV = getAgentVersion();
163
1221
  if (agentV) {
164
- console.log( Soma v${agentV}`);
1222
+ console.log(`\u03C3 Soma v${agentV}`);
165
1223
  console.log(` CLI v${VERSION}`);
166
1224
  } else {
167
1225
  console.log(`soma v${VERSION}`);
168
1226
  }
169
1227
  }
170
-
171
- // ── Init Soma ────────────────────────────────────────────────────────
172
-
173
1228
  async function initSoma() {
174
1229
  printSigma();
175
- console.log(` ${bold("Soma")} Install`);
1230
+ console.log(` ${bold("Soma")} \u2014 Install`);
176
1231
  console.log("");
177
-
178
1232
  try {
179
- execSync("git --version", { stdio: "ignore" });
1233
+ execSync2("git --version", { stdio: "ignore" });
180
1234
  } catch {
181
- console.log(` ${red("")} git not found.`);
1235
+ console.log(` ${red("\u2717")} git not found.`);
182
1236
  console.log("");
183
1237
  console.log(` Soma needs git to download the runtime.`);
184
1238
  console.log(` Install git: ${cyan("https://git-scm.com/downloads")}`);
185
1239
  console.log("");
186
1240
  return;
187
1241
  }
188
-
189
- const installDir = join(SOMA_HOME, "agent");
190
- mkdirSync(SOMA_HOME, { recursive: true });
191
-
192
- const isValidInstall = existsSync(installDir)
193
- && existsSync(join(installDir, ".git"))
194
- && (existsSync(join(installDir, "dist", "extensions")) || existsSync(join(installDir, "extensions")));
195
-
1242
+ const installDir = join3(SOMA_HOME, "agent");
1243
+ mkdirSync2(SOMA_HOME, { recursive: true });
1244
+ const isValidInstall = existsSync3(installDir) && existsSync3(join3(installDir, ".git")) && (existsSync3(join3(installDir, "dist", "extensions")) || existsSync3(join3(installDir, "extensions")));
196
1245
  let preservedFiles = {};
197
-
198
- if (existsSync(installDir) && !isValidInstall) {
199
- console.log(` ${yellow("")} Incomplete installation detected.`);
200
- console.log(` ${dim("Missing:")} ${!existsSync(join(installDir, ".git")) ? "git repo" : "core files"}`);
1246
+ if (existsSync3(installDir) && !isValidInstall) {
1247
+ console.log(` ${yellow("\u26A0")} Incomplete installation detected.`);
1248
+ console.log(` ${dim("Missing:")} ${!existsSync3(join3(installDir, ".git")) ? "git repo" : "core files"}`);
201
1249
  console.log("");
202
-
203
1250
  const userFileNames = ["auth.json", "models.json"];
204
1251
  for (const f of userFileNames) {
205
- const fp = join(installDir, f);
206
- if (existsSync(fp)) {
1252
+ const fp = join3(installDir, f);
1253
+ if (existsSync3(fp)) {
207
1254
  try {
208
- preservedFiles[f] = readFileSync(fp, "utf-8");
209
- console.log(` ${dim("")} Preserving ${f}`);
210
- } catch {}
1255
+ preservedFiles[f] = readFileSync3(fp, "utf-8");
1256
+ console.log(` ${dim("\u2192")} Preserving ${f}`);
1257
+ } catch {
1258
+ }
211
1259
  }
212
1260
  }
213
-
214
- // Preserve user-installed extensions (non-soma-* files)
215
- const extDir = join(installDir, "extensions");
216
- if (existsSync(extDir)) {
1261
+ const extDir = join3(installDir, "extensions");
1262
+ if (existsSync3(extDir)) {
217
1263
  try {
218
- for (const f of readdirSync(extDir).filter(f => !f.startsWith("soma-") && (f.endsWith(".ts") || f.endsWith(".js")))) {
219
- const fp = join(extDir, f);
1264
+ for (const f of readdirSync(extDir).filter((f2) => !f2.startsWith("soma-") && (f2.endsWith(".ts") || f2.endsWith(".js")))) {
1265
+ const fp = join3(extDir, f);
220
1266
  try {
221
- preservedFiles[`extensions/${f}`] = readFileSync(fp, "utf-8");
222
- console.log(` ${dim("")} Preserving extension: ${f}`);
223
- } catch {}
1267
+ preservedFiles[`extensions/${f}`] = readFileSync3(fp, "utf-8");
1268
+ console.log(` ${dim("\u2192")} Preserving extension: ${f}`);
1269
+ } catch {
1270
+ }
224
1271
  }
225
- } catch {}
1272
+ } catch {
1273
+ }
226
1274
  }
227
-
228
- const hasGit = existsSync(join(installDir, ".git"));
1275
+ const hasGit = existsSync3(join3(installDir, ".git"));
229
1276
  let repaired = false;
230
-
231
1277
  if (hasGit) {
232
- console.log(` ${yellow("")} Repairing...`);
1278
+ console.log(` ${yellow("\u23F3")} Repairing...`);
233
1279
  try {
234
- execSync("git fetch origin", { cwd: installDir, stdio: "ignore", timeout: 30000 });
235
- execSync("git reset --hard origin/main", { cwd: installDir, stdio: "ignore" });
236
- console.log(` ${green("")} Repaired from remote`);
1280
+ execSync2("git fetch origin", { cwd: installDir, stdio: "ignore", timeout: 3e4 });
1281
+ execSync2("git reset --hard origin/main", { cwd: installDir, stdio: "ignore" });
1282
+ console.log(` ${green("\u2713")} Repaired from remote`);
237
1283
  repaired = true;
238
1284
  } catch {
239
- console.log(` ${yellow("!")} Repair failed will re-download.`);
1285
+ console.log(` ${yellow("!")} Repair failed \u2014 will re-download.`);
240
1286
  }
241
1287
  }
242
-
243
1288
  if (!repaired) {
244
- const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
245
- const backup = join(SOMA_HOME, `agent-backup-${ts}`);
1289
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
1290
+ const backup = join3(SOMA_HOME, `agent-backup-${ts}`);
246
1291
  try {
247
- execSync(`mv "${installDir}" "${backup}"`, { stdio: "ignore" });
248
- console.log(` ${dim("Old files saved to")} ${dim(backup.replace(homedir(), "~"))}`);
1292
+ execSync2(`mv "${installDir}" "${backup}"`, { stdio: "ignore" });
1293
+ console.log(` ${dim("Old files saved to")} ${dim(backup.replace(homedir3(), "~"))}`);
249
1294
  } catch {
250
- console.log(` ${red("")} Could not move old installation aside.`);
1295
+ console.log(` ${red("\u2717")} Could not move old installation aside.`);
251
1296
  console.log(` ${dim("Try:")} mv ~/.soma/agent ~/.soma/agent-old && soma init`);
252
1297
  console.log("");
253
1298
  return;
254
1299
  }
255
1300
  }
256
1301
  }
257
-
258
1302
  if (isValidInstall) {
259
- console.log(` ${dim("")} Runtime already installed.`);
1303
+ console.log(` ${dim("\u2192")} Runtime already installed.`);
260
1304
  try {
261
- console.log(` ${yellow("")} Checking for updates...`);
262
- execSync("git pull --ff-only", { cwd: installDir, stdio: "ignore" });
263
- console.log(` ${green("")} Up to date`);
1305
+ console.log(` ${yellow("\u23F3")} Checking for updates...`);
1306
+ execSync2("git pull --ff-only", { cwd: installDir, stdio: "ignore" });
1307
+ console.log(` ${green("\u2713")} Up to date`);
264
1308
  } catch {
265
- console.log(` ${green("")} Already current`);
1309
+ console.log(` ${green("\u2713")} Already current`);
266
1310
  }
267
1311
  } else {
268
- console.log(` ${yellow("")} Downloading Soma runtime...`);
1312
+ console.log(` ${yellow("\u23F3")} Downloading Soma runtime...`);
269
1313
  try {
270
- execSync(
1314
+ execSync2(
271
1315
  `git clone --depth 1 https://github.com/meetsoma/soma-beta.git "${installDir}"`,
272
1316
  { stdio: ["ignore", "ignore", "pipe"] }
273
1317
  );
274
- console.log(` ${green("")} Runtime downloaded`);
1318
+ console.log(` ${green("\u2713")} Runtime downloaded`);
275
1319
  } catch (err) {
276
- console.log(` ${red("")} Download failed.`);
1320
+ console.log(` ${red("\u2717")} Download failed.`);
277
1321
  console.log(` ${dim(String(err.stderr || err.message))}`);
278
1322
  console.log("");
279
1323
  console.log(` ${dim("Try manually:")} git clone https://github.com/meetsoma/soma-beta.git ~/.soma/agent`);
@@ -281,79 +1325,78 @@ async function initSoma() {
281
1325
  return;
282
1326
  }
283
1327
  }
284
-
285
- const pkgJson = join(installDir, "package.json");
286
- if (existsSync(pkgJson)) {
287
- console.log(` ${yellow("⏳")} Installing dependencies... ${dim("(this may take a moment)")}`);
1328
+ const pkgJson = join3(installDir, "package.json");
1329
+ if (existsSync3(pkgJson)) {
1330
+ console.log(` ${yellow("\u23F3")} Installing dependencies... ${dim("(this may take a moment)")}`);
288
1331
  try {
289
- execSync("npm install --omit=dev", { cwd: installDir, stdio: ["ignore", "ignore", "inherit"] });
290
- console.log(` ${green("")} Dependencies installed`);
1332
+ execSync2("npm install --omit=dev", { cwd: installDir, stdio: ["ignore", "ignore", "inherit"] });
1333
+ console.log(` ${green("\u2713")} Dependencies installed`);
291
1334
  } catch {
292
- console.log(` ${yellow("")} npm install had issues run ${green(`cd ${installDir} && npm install`)} manually.`);
1335
+ console.log(` ${yellow("\u26A0")} npm install had issues \u2014 run ${green(`cd ${installDir} && npm install`)} manually.`);
293
1336
  }
294
1337
  }
295
-
296
1338
  if (Object.keys(preservedFiles).length > 0) {
297
1339
  for (const [f, content] of Object.entries(preservedFiles)) {
298
1340
  try {
299
- writeFileSync(join(installDir, f), content, { mode: 0o600 });
300
- console.log(` ${green("")} Restored ${f}`);
301
- } catch {}
1341
+ writeFileSync2(join3(installDir, f), content, { mode: 384 });
1342
+ console.log(` ${green("\u2713")} Restored ${f}`);
1343
+ } catch {
1344
+ }
302
1345
  }
303
1346
  }
304
-
305
- const hasExts = existsSync(join(installDir, "dist", "extensions"));
306
- const hasCore = existsSync(join(installDir, "dist", "core"));
307
-
1347
+ const hasExts = existsSync3(join3(installDir, "dist", "extensions"));
1348
+ const hasCore = existsSync3(join3(installDir, "dist", "core"));
308
1349
  if (!hasExts || !hasCore) {
309
1350
  console.log("");
310
- console.log(` ${red("")} Installation incomplete core files missing.`);
1351
+ console.log(` ${red("\u2717")} Installation incomplete \u2014 core files missing.`);
311
1352
  console.log(` ${dim("Try:")} rm -rf ~/.soma/agent && soma init`);
312
1353
  console.log("");
313
1354
  return;
314
1355
  }
315
-
316
- console.log(` ${green("✓")} Extensions and core ready`);
317
-
1356
+ console.log(` ${green("\u2713")} Extensions and core ready`);
318
1357
  const config = readConfig();
319
- config.installedAt = config.installedAt || new Date().toISOString();
1358
+ config.installedAt = config.installedAt || (/* @__PURE__ */ new Date()).toISOString();
320
1359
  config.coreVersion = getAgentVersion() || VERSION;
321
1360
  config.installPath = installDir;
322
1361
  writeConfig(config);
323
-
324
1362
  console.log("");
325
- console.log(` ${green("")} ${bold("Soma is installed!")}`);
1363
+ console.log(` ${green("\u2713")} ${bold("Soma is installed!")}`);
326
1364
  console.log("");
327
1365
  }
328
-
329
- // ── Update / Status ──────────────────────────────────────────────────
330
-
331
1366
  async function checkAndUpdate() {
1367
+ if (!isInstalled()) {
1368
+ printSigma();
1369
+ console.log(` ${bold("Soma")} \u2014 Update`);
1370
+ console.log("");
1371
+ console.log(` ${yellow("\u26A0")} Soma is not installed yet.`);
1372
+ console.log(` Run ${green("soma init")} to install, then ${green("soma update")} to update.`);
1373
+ console.log("");
1374
+ return;
1375
+ }
332
1376
  printSigma();
333
- console.log(` ${bold("Soma")} Status`);
1377
+ console.log(` ${bold("Soma")} \u2014 Update`);
334
1378
  console.log("");
335
-
336
1379
  const config = readConfig();
337
- const installPath = config.installPath || join(SOMA_HOME, "agent");
338
-
1380
+ const installPath = config.installPath || join3(SOMA_HOME, "agent");
339
1381
  let currentHash = "";
340
1382
  try {
341
- currentHash = execSync("git rev-parse --short HEAD", {
342
- cwd: installPath, encoding: "utf-8"
1383
+ currentHash = execSync2("git rev-parse --short HEAD", {
1384
+ cwd: installPath,
1385
+ encoding: "utf-8"
343
1386
  }).trim();
344
- console.log(` ${green("")} Core installed ${dim(`(${currentHash})`)}`);
1387
+ console.log(` ${green("\u2713")} Core installed ${dim(`(${currentHash})`)}`);
345
1388
  } catch {
346
- console.log(` ${green("")} Core installed`);
1389
+ console.log(` ${green("\u2713")} Core installed`);
347
1390
  }
348
-
349
1391
  let behind = 0;
350
1392
  try {
351
- console.log(` ${yellow("")} Checking for updates...`);
352
- execSync("git fetch origin --quiet", { cwd: installPath, stdio: "ignore", timeout: 15000 });
353
- const branch = execSync("git rev-parse --abbrev-ref HEAD", {
354
- cwd: installPath, encoding: "utf-8"
1393
+ console.log(` ${yellow("\u23F3")} Checking for updates...`);
1394
+ execSync2("git fetch origin --quiet", { cwd: installPath, stdio: "ignore", timeout: 15e3 });
1395
+ const branch = execSync2("git rev-parse --abbrev-ref HEAD", {
1396
+ cwd: installPath,
1397
+ encoding: "utf-8"
355
1398
  }).trim();
356
- const behindStr = execSync(
1399
+ const behindStr = execSync2(
357
1400
  `git rev-list HEAD..origin/${branch} --count`,
358
1401
  { cwd: installPath, encoding: "utf-8" }
359
1402
  ).trim();
@@ -363,302 +1406,340 @@ async function checkAndUpdate() {
363
1406
  console.log("");
364
1407
  return;
365
1408
  }
366
-
367
1409
  if (behind === 0) {
368
- console.log(` ${green("")} Already up to date.`);
369
-
1410
+ console.log(` ${green("\u2713")} Already up to date.`);
370
1411
  const agentV = getAgentVersion();
371
1412
  const projectV = getProjectVersion();
372
- if (agentV && projectV && semverCmp(projectV, agentV) < 0) {
373
- console.log(` ${yellow("")} Project .soma/ is at ${cyan(`v${projectV}`)}, agent is at ${cyan(`v${agentV}`)}.`);
1413
+ if (agentV && projectV && semverCmp2(projectV, agentV) < 0) {
1414
+ console.log(` ${yellow("\u26A0")} Project .soma/ is at ${cyan(`v${projectV}`)}, agent is at ${cyan(`v${agentV}`)}.`);
374
1415
  console.log(` Run ${green("soma doctor")} to check for updates.`);
375
1416
  }
376
-
377
1417
  console.log("");
378
1418
  console.log(` ${dim("Soma is set up and ready.")} Run ${green("soma")} ${dim("in a project to start a session.")}`);
379
1419
  console.log("");
380
1420
  return;
381
1421
  }
382
-
383
- console.log(` ${cyan("⬆")} ${bold(`${behind} update${behind !== 1 ? "s" : ""} available.`)}`);
1422
+ console.log(` ${cyan("\u2B06")} ${bold(`${behind} update${behind !== 1 ? "s" : ""} available.`)}`);
384
1423
  console.log("");
385
-
386
1424
  try {
387
- const log = execSync(
1425
+ const log = execSync2(
388
1426
  `git log HEAD..origin/main --oneline --no-decorate -5`,
389
1427
  { cwd: installPath, encoding: "utf-8" }
390
1428
  ).trim();
391
1429
  if (log) {
392
1430
  for (const line of log.split("\n")) {
393
- console.log(` ${dim("")} ${line.slice(8)}`);
1431
+ console.log(` ${dim("\u2022")} ${line.slice(8)}`);
394
1432
  }
395
1433
  if (behind > 5) {
396
1434
  console.log(` ${dim(`...and ${behind - 5} more`)}`);
397
1435
  }
398
1436
  console.log("");
399
1437
  }
400
- } catch {}
401
-
402
- const shouldUpdate = await confirmYN(` ${dim("")} Update now?`);
1438
+ } catch {
1439
+ }
1440
+ const shouldUpdate = await confirmYN(` ${dim("\u2192")} Update now?`);
403
1441
  if (!shouldUpdate) {
404
1442
  console.log("");
405
- console.log(` ${dim("Skipped. Run")} ${green("soma init")} ${dim("anytime to update.")}`);
1443
+ console.log(` ${dim("Skipped. Run")} ${green("soma update")} ${dim("anytime to update.")}`);
406
1444
  console.log("");
407
1445
  return;
408
1446
  }
409
-
410
1447
  console.log("");
411
1448
  try {
412
- execSync("git pull --ff-only", { cwd: installPath, stdio: "ignore" });
413
- console.log(` ${green("")} Updated`);
1449
+ execSync2("git pull --ff-only", { cwd: installPath, stdio: "ignore" });
1450
+ console.log(` ${green("\u2713")} Updated`);
414
1451
  } catch {
415
- console.log(` ${yellow("!")} Pull failed trying reset...`);
1452
+ console.log(` ${yellow("!")} Pull failed \u2014 trying reset...`);
416
1453
  try {
417
- execSync("git reset --hard origin/main", { cwd: installPath, stdio: "ignore" });
418
- console.log(` ${green("")} Updated (reset)`);
1454
+ execSync2("git reset --hard origin/main", { cwd: installPath, stdio: "ignore" });
1455
+ console.log(` ${green("\u2713")} Updated (reset)`);
419
1456
  } catch {
420
- console.log(` ${red("")} Update failed.`);
1457
+ console.log(` ${red("\u2717")} Update failed.`);
421
1458
  console.log(` ${dim("Try:")} cd ~/.soma/agent && git pull`);
422
1459
  console.log("");
423
1460
  return;
424
1461
  }
425
1462
  }
426
-
427
1463
  try {
428
- const pkgChanged = execSync(
1464
+ const pkgChanged = execSync2(
429
1465
  `git diff HEAD~${behind} HEAD --name-only -- package.json package-lock.json`,
430
1466
  { cwd: installPath, encoding: "utf-8" }
431
1467
  ).trim();
432
1468
  if (pkgChanged) {
433
- console.log(` ${yellow("")} Updating dependencies...`);
434
- execSync("npm install --omit=dev", { cwd: installPath, stdio: ["ignore", "ignore", "inherit"] });
435
- console.log(` ${green("")} Dependencies updated`);
1469
+ console.log(` ${yellow("\u23F3")} Updating dependencies...`);
1470
+ execSync2("npm install --omit=dev", { cwd: installPath, stdio: ["ignore", "ignore", "inherit"] });
1471
+ console.log(` ${green("\u2713")} Dependencies updated`);
436
1472
  }
437
- } catch {}
438
-
439
- const newHash = execSync("git rev-parse --short HEAD", {
440
- cwd: installPath, encoding: "utf-8"
1473
+ } catch {
1474
+ }
1475
+ const newHash = execSync2("git rev-parse --short HEAD", {
1476
+ cwd: installPath,
1477
+ encoding: "utf-8"
441
1478
  }).trim();
1479
+ try {
1480
+ const cfg = readConfig();
1481
+ if (cfg.updateAvailable) {
1482
+ delete cfg.updateAvailable;
1483
+ delete cfg.latestSummary;
1484
+ writeConfig(cfg);
1485
+ }
1486
+ } catch {
1487
+ }
442
1488
  console.log("");
443
- console.log(` ${green("")} ${bold("Soma is up to date")} ${dim(`(${currentHash} ${newHash})`)}`);
1489
+ console.log(` ${green("\u2713")} ${bold("Soma is up to date")} ${dim(`(${currentHash} \u2192 ${newHash})`)}`);
444
1490
  console.log("");
445
1491
  }
446
-
447
1492
  function checkForUpdates() {
1493
+ if (!isInstalled()) {
1494
+ printSigma();
1495
+ console.log(` ${bold("Soma")} \u2014 Update Check`);
1496
+ console.log("");
1497
+ console.log(` ${yellow("\u26A0")} Soma is not installed yet.`);
1498
+ console.log(` Run ${green("soma init")} to install it first.`);
1499
+ console.log("");
1500
+ return;
1501
+ }
448
1502
  printSigma();
449
- console.log(` ${bold("Soma")} Update Check`);
1503
+ console.log(` ${bold("Soma")} \u2014 Update Check`);
450
1504
  console.log("");
451
- const agentV = getAgentVersion();
452
- if (agentV) {
453
- console.log(` Soma: ${cyan(`v${agentV}`)}`);
454
- }
455
- console.log(` CLI: ${cyan(`v${VERSION}`)}`);
456
-
457
- const config = readConfig();
458
-
459
- try {
460
- const latest = execSync("npm view meetsoma version 2>/dev/null", { encoding: "utf-8" }).trim();
461
- if (latest && latest !== VERSION && latest > VERSION) {
462
- console.log("");
463
- console.log(` ${yellow("")} CLI update available: ${green(`v${latest}`)}`);
464
- console.log(` Run: ${green("npm install -g meetsoma")}`);
465
- } else {
466
- console.log(` ${green("✓")} CLI is up to date`);
467
- }
468
- } catch {
469
- console.log(` ${dim("Could not check npm registry")}`);
1505
+ const snap = getVersionSnapshot();
1506
+ const intro = snap.allAligned ? soma.say("version_aligned") : soma.say("version_check");
1507
+ console.log(` ${dim(intro)}`);
1508
+ console.log("");
1509
+ const row = (label, local, remote, status) => {
1510
+ const v = local ? `v${local}` : dim("\u2014");
1511
+ let tail = "";
1512
+ if (status === "aligned") tail = ` ${green("\u2713")} latest on npm`;
1513
+ else if (status === "dev-ahead") tail = ` ${green("\u2713")} dev-ahead ${dim(`(npm: v${remote})`)}`;
1514
+ else if (status === "stale") tail = ` ${yellow("\u2B06")} stale ${dim(`(npm: v${remote})`)}`;
1515
+ else if (status === "marker-lag") tail = ` ${yellow("\u2B06")} marker lag ${dim("\u2014 run `soma doctor` to advance")}`;
1516
+ else if (status === "no-workspace") tail = ` ${dim("\u2014 no .soma/ in cwd")}`;
1517
+ else if (status === "not-installed") tail = ` ${red("\u2717")} not installed`;
1518
+ else tail = ` ${dim("\u2014 unknown")}`;
1519
+ console.log(` ${label.padEnd(26)}${cyan(v.padEnd(12))}${tail}`);
1520
+ };
1521
+ row("CLI (meetsoma)", snap.cli.local, snap.cli.remote, snap.cli.status);
1522
+ row("Agent (soma-agent)", snap.agent.local, snap.agent.remote, snap.agent.status);
1523
+ row("Workspace (.soma)", snap.workspace.local, null, snap.workspace.status);
1524
+ if (snap.coreRepo && snap.coreRepo.behind > 0) {
1525
+ console.log(` ${yellow("\u2B06")} Core repo: ${snap.coreRepo.behind} commit${snap.coreRepo.behind !== 1 ? "s" : ""} behind origin`);
1526
+ } else if (snap.coreRepo) {
1527
+ console.log(` ${green("\u2713")} Core repo in sync with origin`);
470
1528
  }
471
-
472
- if (isInstalled() && config.installPath) {
473
- try {
474
- execSync("git fetch origin --quiet", { cwd: config.installPath, stdio: "ignore", timeout: 10000 });
475
- const branch = execSync("git rev-parse --abbrev-ref HEAD", {
476
- cwd: config.installPath, encoding: "utf-8"
477
- }).trim();
478
- const behind = execSync(
479
- `git rev-list HEAD..origin/${branch} --count`,
480
- { cwd: config.installPath, encoding: "utf-8" }
481
- ).trim();
482
- if (behind && parseInt(behind) > 0) {
483
- console.log(` ${yellow("⬆")} Core: ${behind} commit${behind !== "1" ? "s" : ""} behind. Run ${green("soma init")} to update.`);
1529
+ console.log("");
1530
+ if (snap.allAligned) {
1531
+ console.log(` ${green("\u2713")} ${soma.say("version_aligned")}`);
1532
+ } else {
1533
+ console.log(` ${dim(soma.say("version_drift"))}`);
1534
+ console.log("");
1535
+ const hints = [];
1536
+ if (snap.cli.status === "stale") {
1537
+ if (snap.devInstall) {
1538
+ hints.push(`${yellow("\u2192")} CLI stale: ${dim("switch to stable first:")} ${green("soma-install.sh stable")} ${dim("\u2192 then")} ${green("npm i -g meetsoma")}`);
484
1539
  } else {
485
- console.log(` ${green("")} Core is up to date`);
1540
+ hints.push(`${yellow("\u2192")} CLI stale: run ${green("npm i -g meetsoma")}`);
486
1541
  }
487
- } catch {
488
- console.log(` ${dim("Could not check core updates")}`);
489
1542
  }
1543
+ if (snap.agent.status === "stale") {
1544
+ hints.push(`${yellow("\u2192")} Agent stale: run ${green("soma update")} ${dim("(or")} ${green("soma-install.sh stable")}${dim(")")}`);
1545
+ }
1546
+ if (snap.workspace.status === "marker-lag") {
1547
+ hints.push(`${yellow("\u2192")} Workspace marker behind agent: run ${green("soma doctor")}`);
1548
+ }
1549
+ if (snap.coreRepo && snap.coreRepo.behind > 0) {
1550
+ hints.push(`${yellow("\u2192")} Core repo behind: run ${green("soma update")} (or git pull in ${dim("~/.soma/agent/")})`);
1551
+ }
1552
+ if (hints.length === 0) {
1553
+ hints.push(`${green("\u2713")} Nothing to do.`);
1554
+ }
1555
+ hints.forEach((h) => console.log(` ${h}`));
490
1556
  }
491
-
492
1557
  console.log("");
493
1558
  }
494
-
495
- // ── Health Check ─────────────────────────────────────────────────────
496
-
497
1559
  async function healthCheck() {
1560
+ if (!isInstalled()) {
1561
+ printSigma();
1562
+ console.log(` ${bold("Soma")} \u2014 Health Check`);
1563
+ console.log("");
1564
+ console.log(` ${yellow("\u26A0")} Soma is not installed yet.`);
1565
+ console.log(` Run ${green("soma init")} to install, then ${green("soma status")} to check health.`);
1566
+ console.log("");
1567
+ return;
1568
+ }
498
1569
  printSigma();
499
- console.log(` ${bold("Soma")} Health Check`);
1570
+ console.log(` ${bold("Soma")} \u2014 Health Check`);
500
1571
  console.log("");
501
-
502
1572
  let issues = 0;
503
1573
  let warnings = 0;
504
1574
  const check = (ok, pass, fail_msg) => {
505
- if (ok) { console.log(` ${green("✓")} ${pass}`); }
506
- else { console.log(` ${red("")} ${fail_msg}`); issues++; }
1575
+ if (ok) {
1576
+ console.log(` ${green("\u2713")} ${pass}`);
1577
+ } else {
1578
+ console.log(` ${red("\u2717")} ${fail_msg}`);
1579
+ issues++;
1580
+ }
507
1581
  };
508
1582
  const warn = (ok, pass, fail_msg) => {
509
- if (ok) { console.log(` ${green("✓")} ${pass}`); }
510
- else { console.log(` ${yellow("")} ${fail_msg}`); warnings++; }
1583
+ if (ok) {
1584
+ console.log(` ${green("\u2713")} ${pass}`);
1585
+ } else {
1586
+ console.log(` ${yellow("\u26A0")} ${fail_msg}`);
1587
+ warnings++;
1588
+ }
511
1589
  };
512
-
513
1590
  const nodeVersion = process.versions.node;
514
1591
  const [major, minor] = nodeVersion.split(".").map(Number);
515
- check(major > 20 || (major === 20 && minor >= 6),
1592
+ check(
1593
+ major > 20 || major === 20 && minor >= 6,
516
1594
  `Node.js ${nodeVersion}`,
517
- `Node.js ${nodeVersion} requires ≥20.6.0`
1595
+ `Node.js ${nodeVersion} \u2014 requires \u226520.6.0`
518
1596
  );
519
-
520
- check(existsSync(SOMA_HOME), "~/.soma/ exists", "~/.soma/ not found — run: soma init");
521
-
1597
+ check(existsSync3(SOMA_HOME), "~/.soma/ exists", "~/.soma/ not found \u2014 run: soma init");
522
1598
  const installed = isInstalled();
523
- check(installed, "Core installed", "Core not installed run: soma init");
524
-
1599
+ check(installed, "Core installed", "Core not installed \u2014 run: soma init");
525
1600
  if (installed) {
526
- const extDir = existsSync(join(CORE_DIR, "dist", "extensions"))
527
- ? join(CORE_DIR, "dist", "extensions")
528
- : join(CORE_DIR, "extensions");
529
- if (existsSync(extDir)) {
530
- const exts = readdirSync(extDir).filter(f => f.endsWith(".js") || f.endsWith(".ts"));
531
- check(exts.length >= 6, `${exts.length} extensions`, `Only ${exts.length} extensions (expected ≥6)`);
1601
+ const extDir = existsSync3(join3(CORE_DIR, "dist", "extensions")) ? join3(CORE_DIR, "dist", "extensions") : join3(CORE_DIR, "extensions");
1602
+ if (existsSync3(extDir)) {
1603
+ const exts = readdirSync(extDir).filter((f) => f.endsWith(".js") || f.endsWith(".ts"));
1604
+ check(exts.length >= 6, `${exts.length} extensions`, `Only ${exts.length} extensions (expected \u22656)`);
532
1605
  }
533
-
534
- const coreDir = existsSync(join(CORE_DIR, "dist", "core"))
535
- ? join(CORE_DIR, "dist", "core")
536
- : join(CORE_DIR, "core");
537
- check(existsSync(coreDir), "Core modules present", "Core modules missing");
538
-
539
- if (existsSync(join(CORE_DIR, ".git"))) {
1606
+ const coreDir = existsSync3(join3(CORE_DIR, "dist", "core")) ? join3(CORE_DIR, "dist", "core") : join3(CORE_DIR, "core");
1607
+ check(existsSync3(coreDir), "Core modules present", "Core modules missing");
1608
+ if (existsSync3(join3(CORE_DIR, ".git"))) {
540
1609
  try {
541
- execSync("git status --porcelain", { cwd: CORE_DIR, stdio: "ignore" });
1610
+ execSync2("git status --porcelain", { cwd: CORE_DIR, stdio: "ignore" });
542
1611
  check(true, "Git repo healthy", "");
543
1612
  } catch {
544
1613
  warn(false, "", "Core git repo has issues");
545
1614
  }
546
1615
  } else {
547
1616
  try {
548
- const realCore = readlinkSync(join(CORE_DIR, "core"));
1617
+ const realCore = readlinkSync2(join3(CORE_DIR, "core"));
549
1618
  if (realCore) {
550
1619
  check(true, "Dev mode (symlinked)", "");
551
1620
  } else {
552
- warn(false, "", "Core git repo missing run soma init");
1621
+ warn(false, "", "Core git repo missing \u2014 run soma init");
553
1622
  }
554
1623
  } catch {
555
- warn(false, "", "Core git repo missing run soma init");
1624
+ warn(false, "", "Core git repo missing \u2014 run soma init");
556
1625
  }
557
1626
  }
558
1627
  }
559
-
1628
+ if (installed) {
1629
+ try {
1630
+ const pkgPath = join3(CORE_DIR, "package.json");
1631
+ if (existsSync3(pkgPath)) {
1632
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
1633
+ const declaredPi = (pkg.dependencies || {})["@mariozechner/pi-coding-agent"];
1634
+ const piPkgPath = join3(CORE_DIR, "node_modules", "@mariozechner", "pi-coding-agent", "package.json");
1635
+ if (existsSync3(piPkgPath)) {
1636
+ const installedPi = JSON.parse(readFileSync3(piPkgPath, "utf-8")).version;
1637
+ const declaredClean = (declaredPi || "").replace(/^[\^~]/, "");
1638
+ if (declaredClean && installedPi && declaredClean !== installedPi) {
1639
+ console.log(` ${yellow("\u26A0")} Pi runtime drift: declared ${cyan(declaredClean)}, installed ${cyan(installedPi)}`);
1640
+ console.log(` ${dim("Fix:")} ${green("soma update")}`);
1641
+ warnings++;
1642
+ } else if (installedPi) {
1643
+ console.log(` ${green("\u2713")} Pi runtime ${installedPi}`);
1644
+ }
1645
+ } else {
1646
+ console.log(` ${yellow("\u26A0")} Pi not installed \u2014 run ${green("soma update")}`);
1647
+ warnings++;
1648
+ }
1649
+ }
1650
+ } catch {
1651
+ }
1652
+ }
560
1653
  const hasAuth = hasAnyAuth();
561
1654
  if (hasAuth) {
562
- console.log(` ${green("")} API key configured`);
1655
+ console.log(` ${green("\u2713")} API key configured`);
563
1656
  } else {
564
1657
  const unloadedKey = detectKeyInShellConfig();
565
1658
  if (unloadedKey) {
566
- console.log(` ${yellow("")} ${unloadedKey} found in ${dim(getShellConfigPath())} but not loaded restart your terminal`);
1659
+ console.log(` ${yellow("\u26A0")} ${unloadedKey} found in ${dim(getShellConfigPath())} but not loaded \u2014 restart your terminal`);
567
1660
  } else {
568
- console.log(` ${yellow("")} No API key run ${green("soma")} to set one up`);
1661
+ console.log(` ${yellow("\u26A0")} No API key \u2014 run ${green("soma")} to set one up`);
569
1662
  }
570
1663
  }
571
-
572
1664
  try {
573
- const gitV = execSync("git --version", { encoding: "utf-8" }).trim();
1665
+ const gitV = execSync2("git --version", { encoding: "utf-8" }).trim();
574
1666
  check(true, gitV, "");
575
1667
  } catch {
576
1668
  check(false, "", "git not found");
577
1669
  }
578
-
579
1670
  console.log("");
580
1671
  if (issues === 0 && warnings === 0) {
581
- console.log(` ${green(" All checks passed")}`);
1672
+ console.log(` ${green("\u2713 All checks passed")}`);
582
1673
  } else if (issues === 0) {
583
- console.log(` ${green(" All checks passed")} ${dim(`(${warnings} warning${warnings > 1 ? "s" : ""})`)}`);
1674
+ console.log(` ${green("\u2713 All checks passed")} ${dim(`(${warnings} warning${warnings > 1 ? "s" : ""})`)}`);
584
1675
  } else {
585
1676
  console.log(` ${yellow(`${issues} issue${issues > 1 ? "s" : ""} found`)}${warnings > 0 ? dim(` + ${warnings} warning${warnings > 1 ? "s" : ""}`) : ""}`);
586
- console.log(` ${voice.say("suggest", { suggestion: issues > 2 ? "start with soma init" : "check the items above" })}`);
1677
+ console.log(` ${soma.say("suggest", { suggestion: issues > 2 ? "start with soma init" : "check the items above" })}`);
587
1678
  }
588
1679
  console.log("");
589
1680
  }
590
-
591
- // ── Project Doctor ───────────────────────────────────────────────────
592
- // (large function — kept inline to avoid breaking the complex doctor logic)
593
-
594
1681
  async function projectDoctor() {
595
1682
  const doctorArgs = args.slice(1);
596
1683
  const wantsScan = doctorArgs.includes("--scan");
597
1684
  const wantsAll = doctorArgs.includes("--all");
598
-
599
1685
  if ((wantsScan || wantsAll) && isInstalled()) {
600
1686
  await delegateToCore();
601
1687
  return;
602
1688
  }
603
-
604
1689
  printSigma();
605
1690
  const agentV = getAgentVersion();
606
1691
  const projectV = getProjectVersion();
607
1692
  const installed = isInstalled();
608
-
609
- console.log(` ${bold("Soma")} — Doctor`);
1693
+ console.log(` ${bold("Soma")} \u2014 Doctor`);
610
1694
  console.log("");
611
-
612
1695
  if (!installed) {
613
- console.log(` ${red("")} Soma not installed. Run ${green("soma init")} first.`);
1696
+ console.log(` ${red("\u2717")} Soma not installed. Run ${green("soma init")} first.`);
614
1697
  console.log("");
615
1698
  return;
616
1699
  }
617
-
618
1700
  console.log(` Agent: ${cyan(`v${agentV || "unknown"}`)}`);
619
1701
  if (projectV) {
620
1702
  console.log(` Project: ${cyan(`v${projectV}`)}`);
621
1703
  }
622
1704
  console.log(` CLI: ${dim(`v${VERSION}`)}`);
623
1705
  console.log("");
624
-
625
- const hasSomaDir = existsSync(join(process.cwd(), ".soma"));
626
-
1706
+ const hasSomaDir = existsSync3(join3(process.cwd(), ".soma"));
627
1707
  if (!hasSomaDir) {
628
- console.log(` ${yellow("")} No .soma/ in current directory.`);
1708
+ console.log(` ${yellow("\u26A0")} No .soma/ in current directory.`);
629
1709
  console.log(` Run ${green("soma init")} to set up this project, or ${green("soma doctor --scan")} to find projects.`);
630
1710
  console.log("");
631
1711
  return;
632
1712
  }
633
-
634
1713
  if (!projectV) {
635
- console.log(` ${yellow("")} Project .soma/ has no version (pre-versioning).`);
1714
+ console.log(` ${yellow("\u26A0")} Project .soma/ has no version (pre-versioning).`);
636
1715
  console.log(` This project was likely created before v0.6.3.`);
637
- console.log(` Run ${green("soma init")} to bring it up to date.`);
1716
+ console.log(` Run ${green("/soma doctor")} inside a session to migrate, or re-init with ${green("soma init")}.`);
638
1717
  console.log("");
639
1718
  return;
640
1719
  }
641
-
642
- if (agentV && semverCmp(projectV, agentV) === 0) {
643
- console.log(` ${green("✓")} Project is up to date.`);
1720
+ if (agentV && semverCmp2(projectV, agentV) === 0) {
1721
+ console.log(` ${green("\u2713")} Project is up to date.`);
644
1722
  console.log("");
645
1723
  await healthCheck();
646
1724
  return;
647
1725
  }
648
-
649
- if (agentV && semverCmp(projectV, agentV) < 0) {
650
- console.log(` ${yellow("⚠")} Project .soma/ is at ${cyan(`v${projectV}`)} , agent is at ${cyan(`v${agentV}`)} .`);
1726
+ if (agentV && semverCmp2(projectV, agentV) < 0) {
1727
+ console.log(` ${yellow("\u26A0")} Project .soma/ is at ${cyan(`v${projectV}`)} , agent is at ${cyan(`v${agentV}`)} .`);
651
1728
  console.log("");
652
-
653
1729
  let fixes = 0;
654
- const somaDir = join(process.cwd(), ".soma");
655
-
656
- const settingsPath = join(somaDir, "settings.json");
657
- if (existsSync(settingsPath)) {
1730
+ const somaDir = join3(process.cwd(), ".soma");
1731
+ const settingsPath = join3(somaDir, "settings.json");
1732
+ if (existsSync3(settingsPath)) {
658
1733
  try {
659
- const current = JSON.parse(readFileSync(settingsPath, "utf-8"));
1734
+ const current = JSON.parse(readFileSync3(settingsPath, "utf-8"));
660
1735
  let changed = false;
661
- const add = (k, v) => { if (!(k in current)) { current[k] = v; changed = true; fixes++; } };
1736
+ const add = (k, v) => {
1737
+ if (!(k in current)) {
1738
+ current[k] = v;
1739
+ changed = true;
1740
+ fixes++;
1741
+ }
1742
+ };
662
1743
  add("doctor", { autoUpdate: true, declinedVersion: null });
663
1744
  add("breathe", { auto: false, triggerAt: 50, rotateAt: 70, graceSeconds: 30 });
664
1745
  add("context", { notifyAt: 50, warnAt: 70, urgentAt: 80, autoExhaleAt: 85 });
@@ -666,107 +1747,98 @@ async function projectDoctor() {
666
1747
  add("scratch", { autoInject: false });
667
1748
  add("guard", { coreFiles: "warn", bashCommands: "warn", gitIdentity: null });
668
1749
  add("checkpoints", { enabled: true, intervalMinutes: 5, squashOnPush: true });
669
- add("persona", { name: null, emoji: "σ" });
1750
+ add("persona", { name: null, emoji: "\u03C3" });
670
1751
  add("inherit", { identity: true, protocols: true, muscles: true, tools: true });
671
- if (changed) writeFileSync(settingsPath, JSON.stringify(current, null, "\t") + "\n");
672
- } catch {}
1752
+ if (changed) writeFileSync2(settingsPath, JSON.stringify(current, null, " ") + "\n");
1753
+ } catch {
1754
+ }
673
1755
  }
674
-
675
- const bodyDir = join(somaDir, "body");
1756
+ const bodyDir = join3(somaDir, "body");
676
1757
  let agentRoot = CORE_DIR;
677
1758
  try {
678
- const realCore = readlinkSync(join(CORE_DIR, "core"));
1759
+ const realCore = readlinkSync2(join3(CORE_DIR, "core"));
679
1760
  if (realCore) {
680
- const devRoot = dirname(realCore);
681
- if (existsSync(join(devRoot, "templates", "default")) || existsSync(join(devRoot, "body", "_public"))) agentRoot = devRoot;
1761
+ const devRoot = dirname3(realCore);
1762
+ if (existsSync3(join3(devRoot, "templates", "default")) || existsSync3(join3(devRoot, "body", "_public"))) agentRoot = devRoot;
682
1763
  }
683
- } catch {}
684
- // Resolve bundled body templates: templates/default/ (v0.11+) or body/_public/ (legacy)
685
- const bundledBody = existsSync(join(agentRoot, "templates", "default"))
686
- ? join(agentRoot, "templates", "default")
687
- : existsSync(join(agentRoot, "body", "_public"))
688
- ? join(agentRoot, "body", "_public")
689
- : null;
690
- if (bundledBody && existsSync(bundledBody)) {
1764
+ } catch {
1765
+ }
1766
+ const bundledBody = existsSync3(join3(agentRoot, "templates", "default")) ? join3(agentRoot, "templates", "default") : existsSync3(join3(agentRoot, "body", "_public")) ? join3(agentRoot, "body", "_public") : null;
1767
+ if (bundledBody && existsSync3(bundledBody)) {
691
1768
  try {
692
- if (!existsSync(bodyDir)) mkdirSync(bodyDir, { recursive: true });
693
- for (const f of readdirSync(bundledBody).filter(f => f.endsWith(".md") && !f.startsWith("_"))) {
694
- const dest = join(bodyDir, f);
695
- if (!existsSync(dest)) { writeFileSync(dest, readFileSync(join(bundledBody, f), "utf-8")); fixes++; }
1769
+ if (!existsSync3(bodyDir)) mkdirSync2(bodyDir, { recursive: true });
1770
+ for (const f of readdirSync(bundledBody).filter((f2) => f2.endsWith(".md") && !f2.startsWith("_"))) {
1771
+ const dest = join3(bodyDir, f);
1772
+ if (!existsSync3(dest)) {
1773
+ writeFileSync2(dest, readFileSync3(join3(bundledBody, f), "utf-8"));
1774
+ fixes++;
1775
+ }
696
1776
  }
697
- } catch {}
698
- }
699
-
700
- const protoDir = join(somaDir, "amps", "protocols");
701
- const bundledProtos = existsSync(join(CORE_DIR, "dist", "content", "protocols"))
702
- ? join(CORE_DIR, "dist", "content", "protocols")
703
- : existsSync(join(CORE_DIR, "content", "protocols"))
704
- ? join(CORE_DIR, "content", "protocols") : null;
1777
+ } catch {
1778
+ }
1779
+ }
1780
+ const protoDir = join3(somaDir, "amps", "protocols");
1781
+ const bundledProtos = existsSync3(join3(CORE_DIR, "dist", "content", "protocols")) ? join3(CORE_DIR, "dist", "content", "protocols") : existsSync3(join3(CORE_DIR, "content", "protocols")) ? join3(CORE_DIR, "content", "protocols") : null;
705
1782
  if (bundledProtos) {
706
- if (!existsSync(protoDir)) mkdirSync(protoDir, { recursive: true });
707
- for (const f of readdirSync(bundledProtos).filter(f => f.endsWith(".md") && f !== "_template.md" && f !== "README.md")) {
708
- const dest = join(protoDir, f);
709
- if (!existsSync(dest)) { writeFileSync(dest, readFileSync(join(bundledProtos, f), "utf-8")); fixes++; }
1783
+ if (!existsSync3(protoDir)) mkdirSync2(protoDir, { recursive: true });
1784
+ for (const f of readdirSync(bundledProtos).filter((f2) => f2.endsWith(".md") && f2 !== "_template.md" && f2 !== "README.md")) {
1785
+ const dest = join3(protoDir, f);
1786
+ if (!existsSync3(dest)) {
1787
+ writeFileSync2(dest, readFileSync3(join3(bundledProtos, f), "utf-8"));
1788
+ fixes++;
1789
+ }
710
1790
  }
711
1791
  }
712
-
713
- const scriptsDir = join(somaDir, "amps", "scripts");
714
- const bundledScripts = existsSync(join(agentRoot, "dist", "content", "scripts"))
715
- ? join(agentRoot, "dist", "content", "scripts")
716
- : existsSync(join(agentRoot, "content", "scripts"))
717
- ? join(agentRoot, "content", "scripts")
718
- : existsSync(join(agentRoot, "scripts"))
719
- ? join(agentRoot, "scripts") : null;
1792
+ const scriptsDir = join3(somaDir, "amps", "scripts");
1793
+ const bundledScripts = existsSync3(join3(agentRoot, "dist", "content", "scripts")) ? join3(agentRoot, "dist", "content", "scripts") : existsSync3(join3(agentRoot, "content", "scripts")) ? join3(agentRoot, "content", "scripts") : existsSync3(join3(agentRoot, "scripts")) ? join3(agentRoot, "scripts") : null;
720
1794
  if (bundledScripts) {
721
- if (!existsSync(scriptsDir)) mkdirSync(scriptsDir, { recursive: true });
722
- for (const f of readdirSync(bundledScripts).filter(f => f.endsWith(".sh"))) {
723
- const dest = join(scriptsDir, f);
724
- if (!existsSync(dest)) {
725
- writeFileSync(dest, readFileSync(join(bundledScripts, f), "utf-8"), { mode: 0o755 });
1795
+ if (!existsSync3(scriptsDir)) mkdirSync2(scriptsDir, { recursive: true });
1796
+ for (const f of readdirSync(bundledScripts).filter((f2) => f2.endsWith(".sh"))) {
1797
+ const dest = join3(scriptsDir, f);
1798
+ if (!existsSync3(dest)) {
1799
+ writeFileSync2(dest, readFileSync3(join3(bundledScripts, f), "utf-8"), { mode: 493 });
726
1800
  fixes++;
727
1801
  }
728
1802
  }
729
1803
  }
730
-
731
1804
  if (fixes > 0) {
732
1805
  try {
733
- const s = JSON.parse(readFileSync(settingsPath, "utf-8"));
1806
+ const s = JSON.parse(readFileSync3(settingsPath, "utf-8"));
734
1807
  s.version = agentV;
735
- writeFileSync(settingsPath, JSON.stringify(s, null, "\t") + "\n");
736
- } catch {}
737
- console.log(` ${green("✓")} Applied ${fixes} automatic fixes`);
738
- const bc = existsSync(join(somaDir, "body")) ? readdirSync(join(somaDir, "body")).filter(f => f.endsWith(".md")).length : 0;
739
- const pc = existsSync(protoDir) ? readdirSync(protoDir).filter(f => f.endsWith(".md")).length : 0;
1808
+ writeFileSync2(settingsPath, JSON.stringify(s, null, " ") + "\n");
1809
+ } catch {
1810
+ }
1811
+ console.log(` ${green("\u2713")} Applied ${fixes} automatic fixes`);
1812
+ const bc = existsSync3(join3(somaDir, "body")) ? readdirSync(join3(somaDir, "body")).filter((f) => f.endsWith(".md")).length : 0;
1813
+ const pc = existsSync3(protoDir) ? readdirSync(protoDir).filter((f) => f.endsWith(".md")).length : 0;
740
1814
  console.log(` ${bc} body files, ${pc} protocols, settings updated`);
741
1815
  console.log(` Version bumped to ${cyan(`v${agentV}`)}`);
742
-
743
1816
  let staleUpdated = 0;
744
1817
  let staleSkipped = [];
745
- if (bundledProtos && existsSync(protoDir)) {
746
- for (const f of readdirSync(protoDir).filter(f => f.endsWith(".md") && f !== "_template.md" && f !== "README.md")) {
747
- const bundledFile = join(bundledProtos, f);
748
- if (!existsSync(bundledFile)) continue;
749
- const projRaw = readFileSync(join(protoDir, f), "utf-8");
750
- const bundledRaw = readFileSync(bundledFile, "utf-8");
751
- const strip = s => s.replace(/^(heat|loads|runs|last-run|heat-default):.*\n?/gm, "").trim();
1818
+ if (bundledProtos && existsSync3(protoDir)) {
1819
+ for (const f of readdirSync(protoDir).filter((f2) => f2.endsWith(".md") && f2 !== "_template.md" && f2 !== "README.md")) {
1820
+ const bundledFile = join3(bundledProtos, f);
1821
+ if (!existsSync3(bundledFile)) continue;
1822
+ const projRaw = readFileSync3(join3(protoDir, f), "utf-8");
1823
+ const bundledRaw = readFileSync3(bundledFile, "utf-8");
1824
+ const strip = (s) => s.replace(/^(heat|loads|runs|last-run|heat-default):.*\n?/gm, "").trim();
752
1825
  if (strip(projRaw) === strip(bundledRaw)) continue;
753
1826
  const heatLine = projRaw.match(/^heat:.*$/m);
754
1827
  const loadsLine = projRaw.match(/^loads:.*$/m);
755
1828
  let updated = bundledRaw;
756
1829
  if (heatLine) updated = updated.replace(/^heat:.*$/m, heatLine[0]);
757
1830
  if (loadsLine) updated = updated.replace(/^loads:.*$/m, loadsLine[0]);
758
- writeFileSync(join(protoDir, f), updated);
1831
+ writeFileSync2(join3(protoDir, f), updated);
759
1832
  staleUpdated++;
760
1833
  }
761
1834
  }
762
-
763
1835
  console.log("");
764
1836
  if (staleUpdated > 0) {
765
- console.log(` ${green("")} ${staleUpdated} protocols updated to latest version`);
1837
+ console.log(` ${green("\u2713")} ${staleUpdated} protocols updated to latest version`);
766
1838
  console.log(` ${dim("Heat and load counts preserved. Content updated.")}`);
767
1839
  }
768
1840
  if (staleSkipped.length > 0) {
769
- console.log(` ${yellow("")} ${staleSkipped.length} protocols skipped (may be customized)`);
1841
+ console.log(` ${yellow("\u26A0")} ${staleSkipped.length} protocols skipped (may be customized)`);
770
1842
  }
771
1843
  console.log("");
772
1844
  const totalRemaining = staleSkipped.length;
@@ -775,88 +1847,82 @@ async function projectDoctor() {
775
1847
  console.log(` ${dim("Remaining: " + totalRemaining + " items need review.")}`);
776
1848
  console.log(` ${dim("For full migration:")} ${green("soma")} ${dim("then")} ${green("/soma doctor")}`);
777
1849
  } else {
778
- console.log(` ${green("")} Full migration complete from CLI.`);
779
- console.log(` ${dim("No TUI session needed all updates applied.")}`);
1850
+ console.log(` ${green("\u2713")} Full migration complete from CLI.`);
1851
+ console.log(` ${dim("No TUI session needed \u2014 all updates applied.")}`);
780
1852
  }
781
-
782
- // Write _doctor-pending.md
783
1853
  try {
784
- const pendingPath = join(somaDir, "body", "_doctor-pending.md");
1854
+ const pendingPath = join3(somaDir, "body", "_doctor-pending.md");
785
1855
  if (staleSkipped.length === 0) {
786
1856
  const done = [
787
- "---", "type: template", "name: doctor-pending", "status: complete",
788
- `created: ${new Date().toISOString().split("T")[0]}`,
789
- "description: CLI doctor completed full migration", "---", "",
790
- "# Doctor Update — Complete", "",
791
- `Migrated from v${projectV} to v${agentV} on ${new Date().toISOString().split("T")[0]}.`,
792
- `Applied: ${fixes} file fixes + ${staleUpdated} protocol updates.`, "",
793
- "Run `/soma doctor` to verify, then delete this file.",
1857
+ "---",
1858
+ "type: template",
1859
+ "name: doctor-pending",
1860
+ "status: complete",
1861
+ `created: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
1862
+ "description: CLI doctor completed full migration",
1863
+ "---",
1864
+ "",
1865
+ "# Doctor Update \u2014 Complete",
1866
+ "",
1867
+ `Migrated from v${projectV} to v${agentV} on ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.`,
1868
+ `Applied: ${fixes} file fixes + ${staleUpdated} protocol updates.`,
1869
+ "",
1870
+ "Run `/soma doctor` to verify, then delete this file."
794
1871
  ];
795
- writeFileSync(pendingPath, done.join("\n"));
1872
+ writeFileSync2(pendingPath, done.join("\n"));
796
1873
  }
797
- } catch {}
1874
+ } catch {
1875
+ }
798
1876
  } else {
799
- console.log(` ${green("")} Version bumped to ${cyan(`v${agentV}`)}`);
800
- console.log(` ${dim("No file changes needed project structure is current.")}`);
1877
+ console.log(` ${green("\u2713")} Version bumped to ${cyan(`v${agentV}`)}`);
1878
+ console.log(` ${dim("No file changes needed \u2014 project structure is current.")}`);
801
1879
  }
802
1880
  } else {
803
- console.log(` ${green("")} Project version: ${cyan(`v${projectV}`)}`);
1881
+ console.log(` ${green("\u2713")} Project version: ${cyan(`v${projectV}`)}`);
804
1882
  }
805
-
806
1883
  console.log("");
807
1884
  await healthCheck();
808
1885
  }
809
-
810
- // ── Delegation ───────────────────────────────────────────────────────
811
-
812
1886
  async function delegateToCore() {
813
- const piPkg = join(CORE_DIR, "node_modules", "@mariozechner", "pi-coding-agent");
814
- if (!existsSync(piPkg)) {
815
- console.log(` ${red("")} Runtime dependencies missing.`);
1887
+ const piPkg = join3(CORE_DIR, "node_modules", "@mariozechner", "pi-coding-agent");
1888
+ if (!existsSync3(piPkg)) {
1889
+ console.log(` ${red("\u2717")} Runtime dependencies missing.`);
816
1890
  console.log(` ${dim("Run")} ${green("soma init")} ${dim("to repair the installation.")}`);
817
1891
  console.log("");
818
1892
  return;
819
1893
  }
820
- const cliEntry = existsSync(join(CORE_DIR, "dist", "cli.js"))
821
- ? join(CORE_DIR, "dist", "cli.js")
822
- : null;
823
- const mainEntry = existsSync(join(CORE_DIR, "dist", "main.js"))
824
- ? join(CORE_DIR, "dist", "main.js")
825
- : null;
1894
+ const cliEntry = existsSync3(join3(CORE_DIR, "dist", "cli.js")) ? join3(CORE_DIR, "dist", "cli.js") : null;
1895
+ const mainEntry = existsSync3(join3(CORE_DIR, "dist", "main.js")) ? join3(CORE_DIR, "dist", "main.js") : null;
826
1896
  if (!cliEntry && !mainEntry) {
827
- console.log(` ${red("")} Runtime entry point missing.`);
1897
+ console.log(` ${red("\u2717")} Runtime entry point missing.`);
828
1898
  console.log(` ${dim("Run")} ${green("soma init")} ${dim("to repair the installation.")}`);
829
1899
  console.log("");
830
1900
  return;
831
1901
  }
832
-
833
1902
  const passArgs = process.argv.slice(2);
834
-
835
1903
  const cliLocations = [
836
- { path: join(CORE_DIR, "dist", "cli.js"), type: "node" },
837
- { path: join(CORE_DIR, "node_modules", ".bin", "pi"), type: "bin" },
1904
+ { path: join3(CORE_DIR, "dist", "cli.js"), type: "node" },
1905
+ { path: join3(CORE_DIR, "node_modules", ".bin", "pi"), type: "bin" }
838
1906
  ];
839
-
840
1907
  const userExtArgs = [];
841
- const projectExtDir = join(process.cwd(), ".soma", "extensions");
842
- if (existsSync(projectExtDir)) {
1908
+ const projectExtDir = join3(process.cwd(), ".soma", "extensions");
1909
+ if (existsSync3(projectExtDir)) {
843
1910
  try {
844
- const userExts = readdirSync(projectExtDir).filter(f => f.endsWith(".ts") || f.endsWith(".js"));
1911
+ const userExts = readdirSync(projectExtDir).filter((f) => f.endsWith(".ts") || f.endsWith(".js"));
845
1912
  for (const ext of userExts) {
846
- userExtArgs.push("-e", join(projectExtDir, ext));
1913
+ userExtArgs.push("-e", join3(projectExtDir, ext));
847
1914
  }
848
- } catch {}
1915
+ } catch {
1916
+ }
849
1917
  }
850
-
851
1918
  const env = {
852
1919
  ...process.env,
853
1920
  PI_CODING_AGENT_DIR: CORE_DIR,
854
1921
  SOMA_CODING_AGENT_DIR: CORE_DIR,
855
- PI_PACKAGE_DIR: CORE_DIR,
1922
+ PI_PACKAGE_DIR: CORE_DIR
856
1923
  };
857
-
858
1924
  for (const cli of cliLocations) {
859
- if (existsSync(cli.path)) {
1925
+ if (existsSync3(cli.path)) {
860
1926
  try {
861
1927
  const allArgs = [...userExtArgs, ...passArgs];
862
1928
  if (cli.type === "node") {
@@ -869,7 +1935,7 @@ async function delegateToCore() {
869
1935
  if (err.status) process.exit(err.status);
870
1936
  if (err.message && err.message.includes("MODULE_NOT_FOUND")) {
871
1937
  console.log("");
872
- console.log(` ${red("")} Soma failed to start missing dependencies.`);
1938
+ console.log(` ${red("\u2717")} Soma failed to start \u2014 missing dependencies.`);
873
1939
  console.log(` ${dim("Run")} ${green("soma init")} ${dim("to repair the installation.")}`);
874
1940
  console.log("");
875
1941
  }
@@ -877,19 +1943,13 @@ async function delegateToCore() {
877
1943
  }
878
1944
  }
879
1945
  }
880
-
881
- console.log(` ${yellow("")} Core is installed but the CLI entry point is missing.`);
882
- console.log(` ${dim("Expected:")} ${dim(cliLocations.map(c => c.path).join(" or "))}`);
1946
+ console.log(` ${yellow("\u26A0")} Core is installed but the CLI entry point is missing.`);
1947
+ console.log(` ${dim("Expected:")} ${dim(cliLocations.map((c) => c.path).join(" or "))}`);
883
1948
  console.log(` Run ${green("soma init")} to repair the installation.`);
884
1949
  console.log("");
885
1950
  }
886
-
887
- // ── Dispatch ─────────────────────────────────────────────────────────
888
-
889
- const args = process.argv.slice(2);
890
- const cmd = args[0];
891
-
892
- // Keep config.json coreVersion fresh — cheap check, prevents staleness
1951
+ var args = process.argv.slice(2);
1952
+ var cmd = args[0];
893
1953
  try {
894
1954
  const config = readConfig();
895
1955
  const currentV = getAgentVersion();
@@ -897,8 +1957,8 @@ try {
897
1957
  config.coreVersion = currentV;
898
1958
  writeConfig(config);
899
1959
  }
900
- } catch {}
901
-
1960
+ } catch {
1961
+ }
902
1962
  if (cmd === "--version" || cmd === "-v" || cmd === "-V" || cmd === "version") {
903
1963
  showVersion();
904
1964
  } else if (cmd === "--help" || cmd === "-h" || cmd === "help") {
@@ -910,32 +1970,37 @@ if (cmd === "--version" || cmd === "-v" || cmd === "-V" || cmd === "version") {
910
1970
  } else if (cmd === "about") {
911
1971
  await showAbout();
912
1972
  } else if (cmd === "init") {
913
- const hasProjectArgs = args.includes("--template") || args.includes("--orphan") || args.includes("-o");
914
- const runtimeInstalled = isInstalled();
915
- const hasSomaDir = existsSync(join(process.cwd(), ".soma"));
916
-
917
- if (!runtimeInstalled) {
1973
+ if (!isInstalled()) {
918
1974
  await initSoma();
919
- } else if (hasProjectArgs || !hasSomaDir) {
920
- await delegateToCore();
921
1975
  } else {
922
- await checkAndUpdate();
1976
+ await delegateToCore();
923
1977
  }
924
1978
  } else if (cmd === "update") {
1979
+ await checkAndUpdate();
1980
+ } else if (cmd === "check-updates" || cmd === "updates") {
925
1981
  checkForUpdates();
926
1982
  } else if (cmd === "doctor") {
927
1983
  await projectDoctor();
928
1984
  } else if (cmd === "status" || cmd === "health") {
929
1985
  await healthCheck();
930
1986
  } else if (cmd && cmd.startsWith("--") && !isInstalled()) {
931
- // Unknown flag before install — show help
932
- console.log(` ${yellow("⚠")} Unknown option: ${cmd}`);
1987
+ console.log(` ${yellow("\u26A0")} Unknown option: ${cmd}`);
933
1988
  console.log(` Run ${green("soma --help")} for usage.`);
934
1989
  console.log("");
935
1990
  } else if (isInstalled()) {
1991
+ try {
1992
+ const cfg = readConfig();
1993
+ if (cfg.updateAvailable) {
1994
+ const latest = cfg.latestSummary ? ` \u2014 ${dim(cfg.latestSummary)}` : "";
1995
+ console.log(` ${yellow("\u2B06")} Update available${latest}`);
1996
+ console.log(` ${dim("Run:")} ${green("soma update")}`);
1997
+ console.log("");
1998
+ }
1999
+ } catch {
2000
+ }
936
2001
  await delegateToCore();
937
2002
  } else {
938
- const postInstallCmds = ["focus", "inhale", "content", "install", "list", "map", "--map", "--preload"];
2003
+ const postInstallCmds = ["focus", "inhale", "content", "install", "list", "map", "--map", "--preload", "model", "session", "code", "verify", "refactor", "seam", "hub", "body", "run"];
939
2004
  if (cmd && postInstallCmds.includes(cmd)) {
940
2005
  printSigma();
941
2006
  console.log(` ${bold("soma " + cmd)} requires the Soma runtime.`);