meetsoma 0.1.6 → 0.2.0
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 -0
- package/README.md +74 -22
- package/dist/thin-cli.js +369 -38
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ All notable changes to Soma (`meetsoma` on npm).
|
|
|
4
4
|
|
|
5
5
|
Format follows [Keep a Changelog](https://keepachangelog.com/). Versioning follows [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.2.0] — 2026-04-02
|
|
8
|
+
|
|
9
|
+
Corresponds to Soma agent v0.8.0.
|
|
10
|
+
|
|
11
|
+
### New
|
|
12
|
+
|
|
13
|
+
- **`soma doctor`** — check project health, auto-fix stale settings/body/protocols, run migrations.
|
|
14
|
+
- **`soma status` / `soma health`** — quick project health check (renamed from old `soma doctor`).
|
|
15
|
+
- **`soma --version`** — shows both agent and CLI versions.
|
|
16
|
+
- **`soma --help`** — delegates to core agent for full help output.
|
|
17
|
+
- **`--scan` / `--all` flags** — doctor multi-project support (delegates to core).
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **CLI routing overhaul** — doctor/status/health/update/version all route through thin-cli properly.
|
|
22
|
+
- **Stale protocol detection** — doctor identifies outdated bundled protocols and offers to update them.
|
|
23
|
+
- **Version-aware pending file** — `_doctor-pending.md` injected on boot when migration is available.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **Help header** — shows "CLI v0.2.0" instead of bare version number.
|
|
28
|
+
- **Orphaned `showStatus()`** — removed dead code from thin-cli.
|
|
29
|
+
- **Placeholder text** — replaced thin-cli stubs with real output.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
7
33
|
## [0.1.4] — 2026-03-28
|
|
8
34
|
|
|
9
35
|
Corresponds to Soma agent v0.6.5.
|
package/README.md
CHANGED
|
@@ -25,36 +25,83 @@ An AI coding agent that grows with you. Identity, protocols, and muscle memory
|
|
|
25
25
|
|
|
26
26
|
Most agents forget everything between sessions. Soma doesn't.
|
|
27
27
|
|
|
28
|
-
##
|
|
29
|
-
|
|
30
|
-
On first run, you'll meet the personality engine — no AI, just sentence templates. It introduces Soma and lets you ask questions before committing. Press Enter to install.
|
|
28
|
+
## Install
|
|
31
29
|
|
|
30
|
+
```bash
|
|
31
|
+
npm install -g meetsoma
|
|
32
32
|
```
|
|
33
|
-
σ
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
This installs the Soma CLI — a thin bootstrap layer. On first run, it downloads the full agent runtime (~50MB) from GitHub.
|
|
35
|
+
|
|
36
|
+
### What Happens
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
npm install -g meetsoma ← installs CLI only (~50KB)
|
|
40
|
+
soma ← first run: downloads agent runtime
|
|
41
|
+
soma ← every run after: starts a session
|
|
42
|
+
```
|
|
36
43
|
|
|
37
|
-
|
|
44
|
+
**Requirements:** Node.js ≥ 20.6, git, an API key (Anthropic, Google, or OpenAI).
|
|
38
45
|
|
|
39
|
-
|
|
46
|
+
## First Run
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
```bash
|
|
49
|
+
cd your-project
|
|
50
|
+
soma
|
|
43
51
|
```
|
|
44
52
|
|
|
45
|
-
|
|
53
|
+
Soma detects your project (language, framework, package manager, monorepo signals), creates `.soma/`, and writes its own identity. By session two, it remembers.
|
|
46
54
|
|
|
47
55
|
## Commands
|
|
48
56
|
|
|
57
|
+
### CLI (from your shell)
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
soma # fresh session
|
|
61
|
+
soma inhale # resume from last session's preload
|
|
62
|
+
soma -c # continue last session (full history)
|
|
63
|
+
soma -r # pick a session to resume
|
|
64
|
+
soma focus <keyword> # prime for a topic
|
|
65
|
+
soma map <name> # boot with a workflow MAP
|
|
66
|
+
|
|
67
|
+
soma doctor # project health + migration check
|
|
68
|
+
soma status # infrastructure health check
|
|
69
|
+
soma --version # show agent + CLI versions
|
|
70
|
+
soma --help # full command reference
|
|
71
|
+
soma --help scripts # list installed scripts
|
|
72
|
+
soma --help commands # all CLI + TUI commands
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Session (inside the TUI)
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
/exhale save state + write preload for next session
|
|
79
|
+
/breathe save + rotate into fresh session
|
|
80
|
+
/inhale check preload status
|
|
81
|
+
/rest disable keepalive + exhale
|
|
82
|
+
/pin <name> keep a muscle/protocol hot
|
|
83
|
+
/kill <name> drop to cold
|
|
84
|
+
/hub install install from the community hub
|
|
85
|
+
/soma doctor run migration analysis
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Two Versions
|
|
89
|
+
|
|
90
|
+
Soma has two independently-versioned layers:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
σ Soma v0.7.1 ← agent version (the runtime — features, protocols, templates)
|
|
94
|
+
CLI v0.2.0 ← CLI version (this npm package — install flow, delegation)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The **agent version** is what matters for features. The **CLI version** is the bootstrap.
|
|
98
|
+
|
|
99
|
+
### Updating
|
|
100
|
+
|
|
49
101
|
```bash
|
|
50
|
-
soma
|
|
51
|
-
|
|
52
|
-
soma
|
|
53
|
-
soma --map <name> # load a workflow
|
|
54
|
-
|
|
55
|
-
soma init # install the runtime
|
|
56
|
-
soma doctor # check health
|
|
57
|
-
soma update # check for updates
|
|
102
|
+
soma init # update the agent runtime
|
|
103
|
+
npm install -g meetsoma # update the CLI (rare)
|
|
104
|
+
soma doctor # check if project .soma/ needs migration
|
|
58
105
|
```
|
|
59
106
|
|
|
60
107
|
## The Five Ideas
|
|
@@ -69,11 +116,16 @@ soma update # check for updates
|
|
|
69
116
|
|
|
70
117
|
**Heat** — attention management. Used things stay. Unused things cool.
|
|
71
118
|
|
|
72
|
-
##
|
|
119
|
+
## How It Works
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
~/.soma/agent/ ← global runtime (git clone, updated via soma init)
|
|
123
|
+
your-project/.soma/ ← per-project memory (identity, protocols, sessions)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The CLI delegates to the runtime. The runtime loads your project's `.soma/`, resolves the identity chain (project → parent → global), and boots the agent with your accumulated context.
|
|
73
127
|
|
|
74
|
-
-
|
|
75
|
-
- git
|
|
76
|
-
- API key (Anthropic, Google, or OpenAI)
|
|
128
|
+
On boot, Soma silently adds any missing settings or protocols from newer versions (Tier 1 auto-fix). Bigger migrations are handled by `/soma doctor` inside the TUI.
|
|
77
129
|
|
|
78
130
|
---
|
|
79
131
|
|
package/dist/thin-cli.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* For returning users: detects installed runtime → delegates to it.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, readdirSync, statSync, readlinkSync } from "fs";
|
|
12
12
|
import { join, dirname } from "path";
|
|
13
13
|
import { homedir, platform } from "os";
|
|
14
14
|
import { fileURLToPath } from "url";
|
|
@@ -55,6 +55,29 @@ function isInstalled() {
|
|
|
55
55
|
return (hasDist || hasDev) && hasDeps;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
function getAgentVersion() {
|
|
59
|
+
try {
|
|
60
|
+
// In dev mode, core/ is symlinked — resolve to find the real repo's package.json
|
|
61
|
+
let pkgPath = join(CORE_DIR, "package.json");
|
|
62
|
+
try {
|
|
63
|
+
const realCore = readlinkSync(join(CORE_DIR, "core"));
|
|
64
|
+
if (realCore) {
|
|
65
|
+
// core/ symlink → repos/agent/core → go up one level for package.json
|
|
66
|
+
const devPkg = join(dirname(realCore), "package.json");
|
|
67
|
+
if (existsSync(devPkg)) pkgPath = devPkg;
|
|
68
|
+
}
|
|
69
|
+
} catch { /* not a symlink, use default */ }
|
|
70
|
+
return JSON.parse(readFileSync(pkgPath, "utf-8")).version;
|
|
71
|
+
} catch { return null; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getProjectVersion() {
|
|
75
|
+
try {
|
|
76
|
+
const settingsPath = join(process.cwd(), ".soma", "settings.json");
|
|
77
|
+
return JSON.parse(readFileSync(settingsPath, "utf-8")).version || null;
|
|
78
|
+
} catch { return null; }
|
|
79
|
+
}
|
|
80
|
+
|
|
58
81
|
// ── Browser ──────────────────────────────────────────────────────────
|
|
59
82
|
|
|
60
83
|
function openBrowser(url) {
|
|
@@ -695,7 +718,13 @@ function showHelp() {
|
|
|
695
718
|
}
|
|
696
719
|
|
|
697
720
|
function showVersion() {
|
|
698
|
-
|
|
721
|
+
const agentV = getAgentVersion();
|
|
722
|
+
if (agentV) {
|
|
723
|
+
console.log(`σ Soma v${agentV}`);
|
|
724
|
+
console.log(` CLI v${VERSION}`);
|
|
725
|
+
} else {
|
|
726
|
+
console.log(`soma v${VERSION}`);
|
|
727
|
+
}
|
|
699
728
|
}
|
|
700
729
|
|
|
701
730
|
async function showAbout() {
|
|
@@ -889,7 +918,7 @@ async function initSoma() {
|
|
|
889
918
|
// Save config
|
|
890
919
|
const config = readConfig();
|
|
891
920
|
config.installedAt = config.installedAt || new Date().toISOString();
|
|
892
|
-
config.coreVersion = VERSION;
|
|
921
|
+
config.coreVersion = getAgentVersion() || VERSION;
|
|
893
922
|
config.installPath = installDir;
|
|
894
923
|
writeConfig(config);
|
|
895
924
|
|
|
@@ -938,6 +967,15 @@ async function checkAndUpdate() {
|
|
|
938
967
|
|
|
939
968
|
if (behind === 0) {
|
|
940
969
|
console.log(` ${green("✓")} Already up to date.`);
|
|
970
|
+
|
|
971
|
+
// Check project version staleness even when runtime is current
|
|
972
|
+
const agentV = getAgentVersion();
|
|
973
|
+
const projectV = getProjectVersion();
|
|
974
|
+
if (agentV && projectV && projectV < agentV) {
|
|
975
|
+
console.log(` ${yellow("⚠")} Project .soma/ is at ${cyan(`v${projectV}`)}, agent is at ${cyan(`v${agentV}`)}.`);
|
|
976
|
+
console.log(` Run ${green("soma doctor")} to check for updates.`);
|
|
977
|
+
}
|
|
978
|
+
|
|
941
979
|
console.log("");
|
|
942
980
|
console.log(` ${dim("Soma is set up and ready.")} Run ${green("soma")} ${dim("in a project to start a session.")}`);
|
|
943
981
|
console.log("");
|
|
@@ -1015,12 +1053,13 @@ function checkForUpdates() {
|
|
|
1015
1053
|
printSigma();
|
|
1016
1054
|
console.log(` ${bold("Soma")} — Update Check`);
|
|
1017
1055
|
console.log("");
|
|
1018
|
-
|
|
1056
|
+
const agentV = getAgentVersion();
|
|
1057
|
+
if (agentV) {
|
|
1058
|
+
console.log(` Soma: ${cyan(`v${agentV}`)}`);
|
|
1059
|
+
}
|
|
1060
|
+
console.log(` CLI: ${cyan(`v${VERSION}`)}`);
|
|
1019
1061
|
|
|
1020
1062
|
const config = readConfig();
|
|
1021
|
-
if (config.coreVersion) {
|
|
1022
|
-
console.log(` Core version: ${cyan(`v${config.coreVersion}`)}`);
|
|
1023
|
-
}
|
|
1024
1063
|
|
|
1025
1064
|
// Check npm for CLI updates
|
|
1026
1065
|
try {
|
|
@@ -1061,7 +1100,7 @@ function checkForUpdates() {
|
|
|
1061
1100
|
console.log("");
|
|
1062
1101
|
}
|
|
1063
1102
|
|
|
1064
|
-
async function
|
|
1103
|
+
async function healthCheck() {
|
|
1065
1104
|
printSigma();
|
|
1066
1105
|
console.log(` ${bold("Soma")} — Health Check`);
|
|
1067
1106
|
console.log("");
|
|
@@ -1107,12 +1146,26 @@ async function doctor() {
|
|
|
1107
1146
|
: join(CORE_DIR, "core");
|
|
1108
1147
|
check(existsSync(coreDir), "Core modules present", "Core modules missing");
|
|
1109
1148
|
|
|
1110
|
-
// Git repo health
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1149
|
+
// Git repo health (skip in dev mode — no .git when using symlinks)
|
|
1150
|
+
if (existsSync(join(CORE_DIR, ".git"))) {
|
|
1151
|
+
try {
|
|
1152
|
+
execSync("git status --porcelain", { cwd: CORE_DIR, stdio: "ignore" });
|
|
1153
|
+
check(true, "Git repo healthy", "");
|
|
1154
|
+
} catch {
|
|
1155
|
+
warn(false, "", "Core git repo has issues");
|
|
1156
|
+
}
|
|
1157
|
+
} else {
|
|
1158
|
+
// Dev mode — core/ is symlinked, no .git expected
|
|
1159
|
+
try {
|
|
1160
|
+
const realCore = readlinkSync(join(CORE_DIR, "core"));
|
|
1161
|
+
if (realCore) {
|
|
1162
|
+
check(true, "Dev mode (symlinked)", "");
|
|
1163
|
+
} else {
|
|
1164
|
+
warn(false, "", "Core git repo missing — run soma init");
|
|
1165
|
+
}
|
|
1166
|
+
} catch {
|
|
1167
|
+
warn(false, "", "Core git repo missing — run soma init");
|
|
1168
|
+
}
|
|
1116
1169
|
}
|
|
1117
1170
|
}
|
|
1118
1171
|
|
|
@@ -1148,36 +1201,309 @@ async function doctor() {
|
|
|
1148
1201
|
console.log("");
|
|
1149
1202
|
}
|
|
1150
1203
|
|
|
1151
|
-
function
|
|
1204
|
+
async function projectDoctor() {
|
|
1205
|
+
const doctorArgs = args.slice(1);
|
|
1206
|
+
const wantsScan = doctorArgs.includes("--scan");
|
|
1207
|
+
const wantsAll = doctorArgs.includes("--all");
|
|
1208
|
+
|
|
1209
|
+
// --scan and --all need the runtime — delegate to core
|
|
1210
|
+
if ((wantsScan || wantsAll) && isInstalled()) {
|
|
1211
|
+
await delegateToCore();
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1152
1215
|
printSigma();
|
|
1153
|
-
|
|
1216
|
+
const agentV = getAgentVersion();
|
|
1217
|
+
const projectV = getProjectVersion();
|
|
1218
|
+
const installed = isInstalled();
|
|
1219
|
+
|
|
1220
|
+
console.log(` ${bold("Soma")} — Doctor`);
|
|
1154
1221
|
console.log("");
|
|
1155
1222
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1223
|
+
if (!installed) {
|
|
1224
|
+
console.log(` ${red("✗")} Soma not installed. Run ${green("soma init")} first.`);
|
|
1225
|
+
console.log("");
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1158
1228
|
|
|
1159
|
-
|
|
1160
|
-
console.log(`
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
console.log(` Since: ${dim(config.installedAt.split("T")[0])}`);
|
|
1229
|
+
// Version overview
|
|
1230
|
+
console.log(` Agent: ${cyan(`v${agentV || "unknown"}`)}`);
|
|
1231
|
+
if (projectV) {
|
|
1232
|
+
console.log(` Project: ${cyan(`v${projectV}`)}`);
|
|
1164
1233
|
}
|
|
1234
|
+
console.log(` CLI: ${dim(`v${VERSION}`)}`);
|
|
1235
|
+
console.log("");
|
|
1165
1236
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1237
|
+
const hasSomaDir = existsSync(join(process.cwd(), ".soma"));
|
|
1238
|
+
|
|
1239
|
+
if (!hasSomaDir) {
|
|
1240
|
+
console.log(` ${yellow("⚠")} No .soma/ in current directory.`);
|
|
1241
|
+
console.log(` Run ${green("soma init")} to set up this project, or ${green("soma doctor --scan")} to find projects.`);
|
|
1242
|
+
console.log("");
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
if (!projectV) {
|
|
1247
|
+
console.log(` ${yellow("⚠")} Project .soma/ has no version (pre-versioning).`);
|
|
1248
|
+
console.log(` This project was likely created before v0.6.3.`);
|
|
1249
|
+
console.log(` Run ${green("soma init")} to bring it up to date.`);
|
|
1250
|
+
console.log("");
|
|
1251
|
+
return;
|
|
1176
1252
|
}
|
|
1177
1253
|
|
|
1254
|
+
if (agentV && projectV === agentV) {
|
|
1255
|
+
console.log(` ${green("\u2713")} Project is up to date.`);
|
|
1256
|
+
console.log("");
|
|
1257
|
+
await healthCheck();
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
if (agentV && projectV < agentV) {
|
|
1262
|
+
console.log(` ${yellow("\u26a0")} Project .soma/ is at ${cyan(`v${projectV}`)} , agent is at ${cyan(`v${agentV}`)} .`);
|
|
1263
|
+
console.log("");
|
|
1264
|
+
|
|
1265
|
+
// Tier 1: Auto-fix safe things right now
|
|
1266
|
+
let fixes = 0;
|
|
1267
|
+
const somaDir = join(process.cwd(), ".soma");
|
|
1268
|
+
|
|
1269
|
+
// Add missing settings keys
|
|
1270
|
+
const settingsPath = join(somaDir, "settings.json");
|
|
1271
|
+
if (existsSync(settingsPath)) {
|
|
1272
|
+
try {
|
|
1273
|
+
const current = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
1274
|
+
let changed = false;
|
|
1275
|
+
const add = (k, v) => { if (!(k in current)) { current[k] = v; changed = true; fixes++; } };
|
|
1276
|
+
add("doctor", { autoUpdate: true, declinedVersion: null });
|
|
1277
|
+
add("breathe", { auto: false, triggerAt: 50, rotateAt: 70, graceSeconds: 30 });
|
|
1278
|
+
add("context", { notifyAt: 50, warnAt: 70, urgentAt: 80, autoExhaleAt: 85 });
|
|
1279
|
+
add("preload", { staleAfterHours: 48, lastSessionLogs: 0 });
|
|
1280
|
+
add("scratch", { autoInject: false });
|
|
1281
|
+
add("guard", { coreFiles: "warn", bashCommands: "warn", gitIdentity: null });
|
|
1282
|
+
add("checkpoints", { enabled: true, intervalMinutes: 5, squashOnPush: true });
|
|
1283
|
+
add("persona", { name: null, emoji: "\u03c3" });
|
|
1284
|
+
add("inherit", { identity: true, protocols: true, muscles: true, tools: true });
|
|
1285
|
+
if (changed) writeFileSync(settingsPath, JSON.stringify(current, null, "\t") + "\n");
|
|
1286
|
+
} catch {}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// Add missing body/ + templates
|
|
1290
|
+
const bodyDir = join(somaDir, "body");
|
|
1291
|
+
// Resolve agent root — follow dev symlinks if needed
|
|
1292
|
+
let agentRoot = CORE_DIR;
|
|
1293
|
+
try {
|
|
1294
|
+
const realCore = readlinkSync(join(CORE_DIR, "core"));
|
|
1295
|
+
if (realCore) {
|
|
1296
|
+
const devRoot = dirname(realCore);
|
|
1297
|
+
if (existsSync(join(devRoot, "body", "_public"))) agentRoot = devRoot;
|
|
1298
|
+
}
|
|
1299
|
+
} catch {}
|
|
1300
|
+
// Also check dist/ parent (prod layout)
|
|
1301
|
+
if (!existsSync(join(agentRoot, "body", "_public"))) {
|
|
1302
|
+
const parent = dirname(agentRoot);
|
|
1303
|
+
if (existsSync(join(parent, "body", "_public"))) agentRoot = parent;
|
|
1304
|
+
}
|
|
1305
|
+
const bundledBody = join(agentRoot, "body", "_public");
|
|
1306
|
+
if (existsSync(bundledBody)) {
|
|
1307
|
+
try {
|
|
1308
|
+
if (!existsSync(bodyDir)) mkdirSync(bodyDir, { recursive: true });
|
|
1309
|
+
for (const f of readdirSync(bundledBody).filter(f => f.endsWith(".md"))) {
|
|
1310
|
+
const dest = join(bodyDir, f);
|
|
1311
|
+
if (!existsSync(dest)) { writeFileSync(dest, readFileSync(join(bundledBody, f), "utf-8")); fixes++; }
|
|
1312
|
+
}
|
|
1313
|
+
} catch {}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// Add missing protocols
|
|
1317
|
+
const protoDir = join(somaDir, "amps", "protocols");
|
|
1318
|
+
const bundledProtos = existsSync(join(CORE_DIR, "dist", "content", "protocols"))
|
|
1319
|
+
? join(CORE_DIR, "dist", "content", "protocols")
|
|
1320
|
+
: existsSync(join(CORE_DIR, "content", "protocols"))
|
|
1321
|
+
? join(CORE_DIR, "content", "protocols") : null;
|
|
1322
|
+
if (bundledProtos) {
|
|
1323
|
+
if (!existsSync(protoDir)) mkdirSync(protoDir, { recursive: true });
|
|
1324
|
+
for (const f of readdirSync(bundledProtos).filter(f => f.endsWith(".md") && f !== "_template.md" && f !== "README.md")) {
|
|
1325
|
+
const dest = join(protoDir, f);
|
|
1326
|
+
if (!existsSync(dest)) { writeFileSync(dest, readFileSync(join(bundledProtos, f), "utf-8")); fixes++; }
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// Add missing bundled scripts
|
|
1331
|
+
const scriptsDir = join(somaDir, "amps", "scripts");
|
|
1332
|
+
const bundledScripts = existsSync(join(agentRoot, "dist", "content", "scripts"))
|
|
1333
|
+
? join(agentRoot, "dist", "content", "scripts")
|
|
1334
|
+
: existsSync(join(agentRoot, "content", "scripts"))
|
|
1335
|
+
? join(agentRoot, "content", "scripts")
|
|
1336
|
+
: existsSync(join(agentRoot, "scripts"))
|
|
1337
|
+
? join(agentRoot, "scripts") : null;
|
|
1338
|
+
if (bundledScripts) {
|
|
1339
|
+
if (!existsSync(scriptsDir)) mkdirSync(scriptsDir, { recursive: true });
|
|
1340
|
+
for (const f of readdirSync(bundledScripts).filter(f => f.endsWith(".sh"))) {
|
|
1341
|
+
const dest = join(scriptsDir, f);
|
|
1342
|
+
if (!existsSync(dest)) {
|
|
1343
|
+
writeFileSync(dest, readFileSync(join(bundledScripts, f), "utf-8"), { mode: 0o755 });
|
|
1344
|
+
fixes++;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// Bump version after fixes
|
|
1350
|
+
if (fixes > 0) {
|
|
1351
|
+
try {
|
|
1352
|
+
const s = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
1353
|
+
s.version = agentV;
|
|
1354
|
+
writeFileSync(settingsPath, JSON.stringify(s, null, "\t") + "\n");
|
|
1355
|
+
} catch {}
|
|
1356
|
+
console.log(` ${green("\u2713")} Applied ${fixes} automatic fixes`);
|
|
1357
|
+
const bc = existsSync(join(somaDir, "body")) ? readdirSync(join(somaDir, "body")).filter(f => f.endsWith(".md")).length : 0;
|
|
1358
|
+
const pc = existsSync(protoDir) ? readdirSync(protoDir).filter(f => f.endsWith(".md")).length : 0;
|
|
1359
|
+
console.log(` ${bc} body files, ${pc} protocols, settings updated`);
|
|
1360
|
+
console.log(` Version bumped to ${cyan(`v${agentV}`)}`);
|
|
1361
|
+
|
|
1362
|
+
// Scan + fix stale protocols (exist but differ from bundled)
|
|
1363
|
+
let staleUpdated = 0;
|
|
1364
|
+
let staleSkipped = [];
|
|
1365
|
+
if (bundledProtos && existsSync(protoDir)) {
|
|
1366
|
+
for (const f of readdirSync(protoDir).filter(f => f.endsWith(".md") && f !== "_template.md" && f !== "README.md")) {
|
|
1367
|
+
const bundledFile = join(bundledProtos, f);
|
|
1368
|
+
if (!existsSync(bundledFile)) continue;
|
|
1369
|
+
const projRaw = readFileSync(join(protoDir, f), "utf-8");
|
|
1370
|
+
const bundledRaw = readFileSync(bundledFile, "utf-8");
|
|
1371
|
+
const strip = s => s.replace(/^(heat|loads|runs|last-run|heat-default):.*\n?/gm, "").trim();
|
|
1372
|
+
if (strip(projRaw) === strip(bundledRaw)) continue;
|
|
1373
|
+
// Preserve user's runtime fields (heat, loads) when updating content
|
|
1374
|
+
const heatLine = projRaw.match(/^heat:.*$/m);
|
|
1375
|
+
const loadsLine = projRaw.match(/^loads:.*$/m);
|
|
1376
|
+
let updated = bundledRaw;
|
|
1377
|
+
if (heatLine) updated = updated.replace(/^heat:.*$/m, heatLine[0]);
|
|
1378
|
+
if (loadsLine) updated = updated.replace(/^loads:.*$/m, loadsLine[0]);
|
|
1379
|
+
writeFileSync(join(protoDir, f), updated);
|
|
1380
|
+
staleUpdated++;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
console.log("");
|
|
1385
|
+
if (staleUpdated > 0) {
|
|
1386
|
+
console.log(` ${green("\u2713")} ${staleUpdated} protocols updated to latest version`);
|
|
1387
|
+
console.log(` ${dim("Heat and load counts preserved. Content updated (coaching-voice rewrites, new TL;DR).")}`);
|
|
1388
|
+
}
|
|
1389
|
+
if (staleSkipped.length > 0) {
|
|
1390
|
+
console.log(` ${yellow("\u26a0")} ${staleSkipped.length} protocols skipped (may be customized)`);
|
|
1391
|
+
}
|
|
1392
|
+
console.log("");
|
|
1393
|
+
const totalRemaining = staleSkipped.length;
|
|
1394
|
+
if (totalRemaining > 0) {
|
|
1395
|
+
console.log(` ${dim("CLI handled: files, scripts, settings, protocols, version bump.")}`);
|
|
1396
|
+
console.log(` ${dim("Remaining: " + totalRemaining + " items need review.")}`);
|
|
1397
|
+
console.log(` ${dim("For full migration:")} ${green("soma")} ${dim("then")} ${green("/soma doctor")}`);
|
|
1398
|
+
} else {
|
|
1399
|
+
console.log(` ${green("\u2713")} Full migration complete from CLI.`);
|
|
1400
|
+
console.log(` ${dim("No TUI session needed — all updates applied.")}`);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// Write _doctor-pending.md — only if there are remaining items for the agent
|
|
1404
|
+
// If CLI handled everything, write a brief "complete" note instead
|
|
1405
|
+
try {
|
|
1406
|
+
const pendingPath = join(somaDir, "body", "_doctor-pending.md");
|
|
1407
|
+
if (staleSkipped.length === 0) {
|
|
1408
|
+
// Full migration done — write brief completion note
|
|
1409
|
+
const done = [
|
|
1410
|
+
"---",
|
|
1411
|
+
"type: template",
|
|
1412
|
+
"name: doctor-pending",
|
|
1413
|
+
"status: complete",
|
|
1414
|
+
`created: ${new Date().toISOString().split("T")[0]}`,
|
|
1415
|
+
"description: CLI doctor completed full migration — verify with /soma doctor then delete this file",
|
|
1416
|
+
"---",
|
|
1417
|
+
"",
|
|
1418
|
+
"# Doctor Update — Complete",
|
|
1419
|
+
"",
|
|
1420
|
+
`The CLI doctor migrated this project from v${projectV} to v${agentV} on ${new Date().toISOString().split("T")[0]}.`,
|
|
1421
|
+
"",
|
|
1422
|
+
`Applied: ${fixes} file fixes + ${staleUpdated} protocol updates. All settings, body, scripts, protocols current.`,
|
|
1423
|
+
"",
|
|
1424
|
+
"Run \`/soma doctor\` to verify, then delete this file.",
|
|
1425
|
+
];
|
|
1426
|
+
writeFileSync(pendingPath, done.join("\n"));
|
|
1427
|
+
} else {
|
|
1428
|
+
const pending = [
|
|
1429
|
+
"---",
|
|
1430
|
+
"type: template",
|
|
1431
|
+
"name: doctor-pending",
|
|
1432
|
+
"status: active",
|
|
1433
|
+
`created: ${new Date().toISOString().split("T")[0]}`,
|
|
1434
|
+
"description: Written by soma doctor CLI — tells the agent what was fixed and what needs attention",
|
|
1435
|
+
"---",
|
|
1436
|
+
"",
|
|
1437
|
+
"# Doctor Update — Pending",
|
|
1438
|
+
"",
|
|
1439
|
+
`The CLI doctor ran on ${new Date().toISOString().split("T")[0]} and updated this project from v${projectV} to v${agentV}.`,
|
|
1440
|
+
"",
|
|
1441
|
+
"## What the CLI fixed (Tier 1 — automatic, safe)",
|
|
1442
|
+
"",
|
|
1443
|
+
`- ${bc} body template files (added missing ones, preserved your customized files)`,
|
|
1444
|
+
`- ${pc} protocols present (added missing bundled protocols)`,
|
|
1445
|
+
`- Settings: added missing configuration sections with safe defaults`,
|
|
1446
|
+
`- Version bumped to v${agentV}`,
|
|
1447
|
+
"",
|
|
1448
|
+
`## What the CLI also fixed`,
|
|
1449
|
+
"",
|
|
1450
|
+
`- ${staleUpdated} protocols updated to latest version (heat/loads preserved)`,
|
|
1451
|
+
"",
|
|
1452
|
+
staleSkipped.length > 0 ? `## What still needs attention (${staleSkipped.length} items)` : "## Status: Complete",
|
|
1453
|
+
"",
|
|
1454
|
+
staleSkipped.length > 0
|
|
1455
|
+
? `**${staleSkipped.length} protocols** may be customized and were not auto-updated. The agent needs to:\n- Diff each against bundled version\n- Decide whether to update or preserve`
|
|
1456
|
+
: "All mechanical updates applied by CLI. The agent can verify with /soma doctor.",
|
|
1457
|
+
"",
|
|
1458
|
+
"**To complete the migration:**",
|
|
1459
|
+
"",
|
|
1460
|
+
"Read the migration phases that apply to your version jump:",
|
|
1461
|
+
"```",
|
|
1462
|
+
];
|
|
1463
|
+
// Find which phases apply (only from projectV onward)
|
|
1464
|
+
let agentRoot2 = CORE_DIR;
|
|
1465
|
+
try {
|
|
1466
|
+
const rc = readlinkSync(join(CORE_DIR, "core"));
|
|
1467
|
+
if (rc) { const dr = dirname(rc); if (existsSync(join(dr, "migrations", "phases"))) agentRoot2 = dr; }
|
|
1468
|
+
} catch {}
|
|
1469
|
+
if (!existsSync(join(agentRoot2, "migrations", "phases"))) {
|
|
1470
|
+
const pr = dirname(agentRoot2);
|
|
1471
|
+
if (existsSync(join(pr, "migrations", "phases"))) agentRoot2 = pr;
|
|
1472
|
+
}
|
|
1473
|
+
const phasesDir2 = join(agentRoot2, "migrations", "phases");
|
|
1474
|
+
if (existsSync(phasesDir2)) {
|
|
1475
|
+
const allPhases = readdirSync(phasesDir2).filter(f => f.endsWith(".md")).sort();
|
|
1476
|
+
const relevant = allPhases.filter(f => {
|
|
1477
|
+
const m = f.match(/^v([\d.]+)-to-v/);
|
|
1478
|
+
return m && m[1] >= projectV;
|
|
1479
|
+
});
|
|
1480
|
+
pending.push(`${relevant.length} phases apply (v${projectV} \u2192 v${agentV}):`);
|
|
1481
|
+
pending.push("\`\`\`");
|
|
1482
|
+
for (const f of relevant) {
|
|
1483
|
+
pending.push(" " + join(phasesDir2, f));
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
pending.push("```");
|
|
1487
|
+
pending.push("");
|
|
1488
|
+
pending.push("Execute each phase's **Actions** section in order.");
|
|
1489
|
+
pending.push("After completing all phases, delete this file — it's no longer needed.");
|
|
1490
|
+
pending.push("");
|
|
1491
|
+
writeFileSync(pendingPath, pending.join("\n"));
|
|
1492
|
+
}
|
|
1493
|
+
} catch { /* non-fatal */ }
|
|
1494
|
+
} else {
|
|
1495
|
+
console.log(` ${green("\u2713")} Version bumped to ${cyan(`v${agentV}`)}`);
|
|
1496
|
+
console.log(` ${dim("No file changes needed — project structure is current.")}`);
|
|
1497
|
+
}
|
|
1498
|
+
} else {
|
|
1499
|
+
console.log(` ${green("\u2713")} Project version: ${cyan(`v${projectV}`)}`);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1178
1502
|
console.log("");
|
|
1503
|
+
await healthCheck();
|
|
1179
1504
|
}
|
|
1180
1505
|
|
|
1506
|
+
|
|
1181
1507
|
// ── Delegation ───────────────────────────────────────────────────────
|
|
1182
1508
|
|
|
1183
1509
|
async function delegateToCore() {
|
|
@@ -1276,7 +1602,12 @@ const cmd = args[0];
|
|
|
1276
1602
|
if (cmd === "--version" || cmd === "-v" || cmd === "-V") {
|
|
1277
1603
|
showVersion();
|
|
1278
1604
|
} else if (cmd === "--help" || cmd === "-h") {
|
|
1279
|
-
|
|
1605
|
+
if (isInstalled()) {
|
|
1606
|
+
// Delegate to core — richer help with scripts, commands, hub
|
|
1607
|
+
await delegateToCore();
|
|
1608
|
+
} else {
|
|
1609
|
+
showHelp();
|
|
1610
|
+
}
|
|
1280
1611
|
} else if (cmd === "about") {
|
|
1281
1612
|
await showAbout();
|
|
1282
1613
|
} else if (cmd === "init") {
|
|
@@ -1291,15 +1622,15 @@ if (cmd === "--version" || cmd === "-v" || cmd === "-V") {
|
|
|
1291
1622
|
// Installed, project init (new project or --template/--orphan)
|
|
1292
1623
|
await delegateToCore();
|
|
1293
1624
|
} else {
|
|
1294
|
-
// Installed + .soma/ exists — check for updates
|
|
1625
|
+
// Installed + .soma/ exists — check for updates + project staleness
|
|
1295
1626
|
await checkAndUpdate();
|
|
1296
1627
|
}
|
|
1297
1628
|
} else if (cmd === "update") {
|
|
1298
1629
|
checkForUpdates();
|
|
1299
1630
|
} else if (cmd === "doctor") {
|
|
1300
|
-
await
|
|
1301
|
-
} else if (cmd === "status") {
|
|
1302
|
-
|
|
1631
|
+
await projectDoctor();
|
|
1632
|
+
} else if (cmd === "status" || cmd === "health") {
|
|
1633
|
+
await healthCheck();
|
|
1303
1634
|
} else if (isInstalled()) {
|
|
1304
1635
|
// Core installed — delegate to runtime
|
|
1305
1636
|
await delegateToCore();
|