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/CHANGELOG.md +26 -194
- package/dist/personality.js +125 -3
- package/dist/thin-cli.js +1502 -437
- package/package.json +42 -42
package/dist/thin-cli.js
CHANGED
|
@@ -1,64 +1,1134 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
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
|
-
|
|
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
|
-
//
|
|
18
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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("
|
|
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("
|
|
1127
|
+
console.log(` ${green("\u2713")} ${soma.greetBack(ghUser)}`);
|
|
56
1128
|
}
|
|
57
|
-
|
|
58
1129
|
if (!hasAnyAuth()) {
|
|
59
|
-
console.log(` ${green("
|
|
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.")} ${
|
|
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}
|
|
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("
|
|
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
|
-
|
|
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("
|
|
1159
|
+
console.log(` ${dim("\u2500".repeat(58))}`);
|
|
93
1160
|
console.log("");
|
|
94
|
-
console.log(` ${dim("
|
|
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("
|
|
1173
|
+
console.log(` ${dim("\u2500".repeat(58))}`);
|
|
112
1174
|
console.log("");
|
|
113
|
-
const launch = await confirmYN(` ${
|
|
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}
|
|
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")}
|
|
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(
|
|
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")}
|
|
1230
|
+
console.log(` ${bold("Soma")} \u2014 Install`);
|
|
176
1231
|
console.log("");
|
|
177
|
-
|
|
178
1232
|
try {
|
|
179
|
-
|
|
1233
|
+
execSync2("git --version", { stdio: "ignore" });
|
|
180
1234
|
} catch {
|
|
181
|
-
console.log(` ${red("
|
|
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
|
-
|
|
190
|
-
|
|
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
|
-
|
|
199
|
-
console.log(`
|
|
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 =
|
|
206
|
-
if (
|
|
1252
|
+
const fp = join3(installDir, f);
|
|
1253
|
+
if (existsSync3(fp)) {
|
|
207
1254
|
try {
|
|
208
|
-
preservedFiles[f] =
|
|
209
|
-
console.log(` ${dim("
|
|
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
|
-
|
|
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(
|
|
219
|
-
const fp =
|
|
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}`] =
|
|
222
|
-
console.log(` ${dim("
|
|
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("
|
|
1278
|
+
console.log(` ${yellow("\u23F3")} Repairing...`);
|
|
233
1279
|
try {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
console.log(` ${green("
|
|
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
|
|
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 =
|
|
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
|
-
|
|
248
|
-
console.log(` ${dim("Old files saved to")} ${dim(backup.replace(
|
|
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("
|
|
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("
|
|
1303
|
+
console.log(` ${dim("\u2192")} Runtime already installed.`);
|
|
260
1304
|
try {
|
|
261
|
-
console.log(` ${yellow("
|
|
262
|
-
|
|
263
|
-
console.log(` ${green("
|
|
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("
|
|
1309
|
+
console.log(` ${green("\u2713")} Already current`);
|
|
266
1310
|
}
|
|
267
1311
|
} else {
|
|
268
|
-
console.log(` ${yellow("
|
|
1312
|
+
console.log(` ${yellow("\u23F3")} Downloading Soma runtime...`);
|
|
269
1313
|
try {
|
|
270
|
-
|
|
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("
|
|
1318
|
+
console.log(` ${green("\u2713")} Runtime downloaded`);
|
|
275
1319
|
} catch (err) {
|
|
276
|
-
console.log(` ${red("
|
|
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
|
-
|
|
286
|
-
|
|
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
|
-
|
|
290
|
-
console.log(` ${green("
|
|
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("
|
|
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
|
-
|
|
300
|
-
console.log(` ${green("
|
|
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
|
|
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("
|
|
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("
|
|
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")}
|
|
1377
|
+
console.log(` ${bold("Soma")} \u2014 Update`);
|
|
334
1378
|
console.log("");
|
|
335
|
-
|
|
336
1379
|
const config = readConfig();
|
|
337
|
-
const installPath = config.installPath ||
|
|
338
|
-
|
|
1380
|
+
const installPath = config.installPath || join3(SOMA_HOME, "agent");
|
|
339
1381
|
let currentHash = "";
|
|
340
1382
|
try {
|
|
341
|
-
currentHash =
|
|
342
|
-
cwd: installPath,
|
|
1383
|
+
currentHash = execSync2("git rev-parse --short HEAD", {
|
|
1384
|
+
cwd: installPath,
|
|
1385
|
+
encoding: "utf-8"
|
|
343
1386
|
}).trim();
|
|
344
|
-
console.log(` ${green("
|
|
1387
|
+
console.log(` ${green("\u2713")} Core installed ${dim(`(${currentHash})`)}`);
|
|
345
1388
|
} catch {
|
|
346
|
-
console.log(` ${green("
|
|
1389
|
+
console.log(` ${green("\u2713")} Core installed`);
|
|
347
1390
|
}
|
|
348
|
-
|
|
349
1391
|
let behind = 0;
|
|
350
1392
|
try {
|
|
351
|
-
console.log(` ${yellow("
|
|
352
|
-
|
|
353
|
-
const branch =
|
|
354
|
-
cwd: installPath,
|
|
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 =
|
|
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("
|
|
369
|
-
|
|
1410
|
+
console.log(` ${green("\u2713")} Already up to date.`);
|
|
370
1411
|
const agentV = getAgentVersion();
|
|
371
1412
|
const projectV = getProjectVersion();
|
|
372
|
-
if (agentV && projectV &&
|
|
373
|
-
console.log(` ${yellow("
|
|
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 =
|
|
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("
|
|
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("
|
|
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
|
|
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
|
-
|
|
413
|
-
console.log(` ${green("
|
|
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
|
|
1452
|
+
console.log(` ${yellow("!")} Pull failed \u2014 trying reset...`);
|
|
416
1453
|
try {
|
|
417
|
-
|
|
418
|
-
console.log(` ${green("
|
|
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("
|
|
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 =
|
|
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("
|
|
434
|
-
|
|
435
|
-
console.log(` ${green("
|
|
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 =
|
|
440
|
-
cwd: installPath,
|
|
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("
|
|
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")}
|
|
1503
|
+
console.log(` ${bold("Soma")} \u2014 Update Check`);
|
|
450
1504
|
console.log("");
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if (
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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 (
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
{
|
|
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
|
-
|
|
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")}
|
|
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) {
|
|
506
|
-
|
|
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) {
|
|
510
|
-
|
|
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(
|
|
1592
|
+
check(
|
|
1593
|
+
major > 20 || major === 20 && minor >= 6,
|
|
516
1594
|
`Node.js ${nodeVersion}`,
|
|
517
|
-
`Node.js ${nodeVersion}
|
|
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
|
|
524
|
-
|
|
1599
|
+
check(installed, "Core installed", "Core not installed \u2014 run: soma init");
|
|
525
1600
|
if (installed) {
|
|
526
|
-
const extDir =
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
535
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
1621
|
+
warn(false, "", "Core git repo missing \u2014 run soma init");
|
|
553
1622
|
}
|
|
554
1623
|
} catch {
|
|
555
|
-
warn(false, "", "Core git repo missing
|
|
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("
|
|
1655
|
+
console.log(` ${green("\u2713")} API key configured`);
|
|
563
1656
|
} else {
|
|
564
1657
|
const unloadedKey = detectKeyInShellConfig();
|
|
565
1658
|
if (unloadedKey) {
|
|
566
|
-
console.log(` ${yellow("
|
|
1659
|
+
console.log(` ${yellow("\u26A0")} ${unloadedKey} found in ${dim(getShellConfigPath())} but not loaded \u2014 restart your terminal`);
|
|
567
1660
|
} else {
|
|
568
|
-
console.log(` ${yellow("
|
|
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 =
|
|
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("
|
|
1672
|
+
console.log(` ${green("\u2713 All checks passed")}`);
|
|
582
1673
|
} else if (issues === 0) {
|
|
583
|
-
console.log(` ${green("
|
|
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(` ${
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
655
|
-
|
|
656
|
-
|
|
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(
|
|
1734
|
+
const current = JSON.parse(readFileSync3(settingsPath, "utf-8"));
|
|
660
1735
|
let changed = false;
|
|
661
|
-
const add = (k, v) => {
|
|
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)
|
|
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 =
|
|
1759
|
+
const realCore = readlinkSync2(join3(CORE_DIR, "core"));
|
|
679
1760
|
if (realCore) {
|
|
680
|
-
const devRoot =
|
|
681
|
-
if (
|
|
1761
|
+
const devRoot = dirname3(realCore);
|
|
1762
|
+
if (existsSync3(join3(devRoot, "templates", "default")) || existsSync3(join3(devRoot, "body", "_public"))) agentRoot = devRoot;
|
|
682
1763
|
}
|
|
683
|
-
} catch {
|
|
684
|
-
|
|
685
|
-
const bundledBody =
|
|
686
|
-
|
|
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 (!
|
|
693
|
-
for (const f of readdirSync(bundledBody).filter(
|
|
694
|
-
const dest =
|
|
695
|
-
if (!
|
|
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 =
|
|
701
|
-
const bundledProtos =
|
|
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 (!
|
|
707
|
-
for (const f of readdirSync(bundledProtos).filter(
|
|
708
|
-
const dest =
|
|
709
|
-
if (!
|
|
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
|
|
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 (!
|
|
722
|
-
for (const f of readdirSync(bundledScripts).filter(
|
|
723
|
-
const dest =
|
|
724
|
-
if (!
|
|
725
|
-
|
|
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(
|
|
1806
|
+
const s = JSON.parse(readFileSync3(settingsPath, "utf-8"));
|
|
734
1807
|
s.version = agentV;
|
|
735
|
-
|
|
736
|
-
} catch {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
const
|
|
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 &&
|
|
746
|
-
for (const f of readdirSync(protoDir).filter(
|
|
747
|
-
const bundledFile =
|
|
748
|
-
if (!
|
|
749
|
-
const projRaw =
|
|
750
|
-
const bundledRaw =
|
|
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
|
-
|
|
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("
|
|
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("
|
|
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("
|
|
779
|
-
console.log(` ${dim("No TUI session needed
|
|
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 =
|
|
1854
|
+
const pendingPath = join3(somaDir, "body", "_doctor-pending.md");
|
|
785
1855
|
if (staleSkipped.length === 0) {
|
|
786
1856
|
const done = [
|
|
787
|
-
"---",
|
|
788
|
-
|
|
789
|
-
"
|
|
790
|
-
"
|
|
791
|
-
`
|
|
792
|
-
|
|
793
|
-
"
|
|
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
|
-
|
|
1872
|
+
writeFileSync2(pendingPath, done.join("\n"));
|
|
796
1873
|
}
|
|
797
|
-
} catch {
|
|
1874
|
+
} catch {
|
|
1875
|
+
}
|
|
798
1876
|
} else {
|
|
799
|
-
console.log(` ${green("
|
|
800
|
-
console.log(` ${dim("No file changes needed
|
|
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("
|
|
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 =
|
|
814
|
-
if (!
|
|
815
|
-
console.log(` ${red("
|
|
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 =
|
|
821
|
-
|
|
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("
|
|
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:
|
|
837
|
-
{ path:
|
|
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 =
|
|
842
|
-
if (
|
|
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",
|
|
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 (
|
|
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("
|
|
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(` ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.`);
|