@sechroom/cli 2026.6.9 → 2026.6.10-rc.78b29b91
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/README.md +1 -2
- package/dist/index.js +2092 -379
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/auth.ts
|
|
@@ -12,12 +12,16 @@ import open from "open";
|
|
|
12
12
|
// src/config.ts
|
|
13
13
|
import { homedir } from "os";
|
|
14
14
|
import { join, dirname } from "path";
|
|
15
|
-
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "fs";
|
|
15
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from "fs";
|
|
16
16
|
var CONFIG_DIR = join(homedir(), ".config", "sechroom");
|
|
17
17
|
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
18
18
|
var TOKEN_FILE = join(CONFIG_DIR, "token.json");
|
|
19
|
-
var
|
|
19
|
+
var STATE_DIR_NAME = ".sechroom";
|
|
20
|
+
var BASELINE_CONFIG_NAME = ".sechroom.json";
|
|
21
|
+
var OVERRIDE_CONFIG_NAME = join(STATE_DIR_NAME, "config.json");
|
|
22
|
+
var BINDING_FIELDS = ["schemaVersion", "baseUrl", "tenant", "workspaceId", "defaultProjectId"];
|
|
20
23
|
var DEFAULT_BASE_URL = "https://app.sechroom.ai/api";
|
|
24
|
+
var LOCAL_CONFIG_SCHEMA_VERSION = 2;
|
|
21
25
|
function ensureDir() {
|
|
22
26
|
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
23
27
|
}
|
|
@@ -53,35 +57,65 @@ function writeToken(tok) {
|
|
|
53
57
|
ensureDir();
|
|
54
58
|
writeFileSync(TOKEN_FILE, JSON.stringify(tok, null, 2), { mode: 384 });
|
|
55
59
|
}
|
|
56
|
-
function
|
|
60
|
+
function clearToken() {
|
|
61
|
+
if (!existsSync(TOKEN_FILE)) return void 0;
|
|
62
|
+
rmSync(TOKEN_FILE);
|
|
63
|
+
return TOKEN_FILE;
|
|
64
|
+
}
|
|
65
|
+
function clearPersisted() {
|
|
66
|
+
if (!existsSync(CONFIG_FILE)) return void 0;
|
|
67
|
+
rmSync(CONFIG_FILE);
|
|
68
|
+
return CONFIG_FILE;
|
|
69
|
+
}
|
|
70
|
+
function readJsonConfig(path) {
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
73
|
+
} catch {
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function findConfigHome(start = process.cwd()) {
|
|
57
78
|
let dir = start;
|
|
58
79
|
for (; ; ) {
|
|
59
|
-
|
|
60
|
-
if (existsSync(candidate)) return candidate;
|
|
80
|
+
if (existsSync(join(dir, BASELINE_CONFIG_NAME)) || existsSync(join(dir, OVERRIDE_CONFIG_NAME))) return dir;
|
|
61
81
|
const parent = dirname(dir);
|
|
62
82
|
if (parent === dir) return void 0;
|
|
63
83
|
dir = parent;
|
|
64
84
|
}
|
|
65
85
|
}
|
|
66
86
|
function readLocalConfig() {
|
|
67
|
-
const
|
|
68
|
-
if (!
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
87
|
+
const home = findConfigHome();
|
|
88
|
+
if (!home) return {};
|
|
89
|
+
const baselinePath = join(home, BASELINE_CONFIG_NAME);
|
|
90
|
+
const overridePath = join(home, OVERRIDE_CONFIG_NAME);
|
|
91
|
+
const merged = { ...readJsonConfig(baselinePath) ?? {}, ...readJsonConfig(overridePath) ?? {} };
|
|
92
|
+
return {
|
|
93
|
+
schemaVersion: merged.schemaVersion,
|
|
94
|
+
baseUrl: merged.baseUrl,
|
|
95
|
+
tenant: merged.tenant,
|
|
96
|
+
workspaceId: merged.workspaceId,
|
|
97
|
+
defaultProjectId: merged.defaultProjectId,
|
|
98
|
+
path: existsSync(baselinePath) ? baselinePath : overridePath
|
|
99
|
+
};
|
|
75
100
|
}
|
|
76
101
|
function writeLocalConfig(patch) {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
102
|
+
const home = findConfigHome() ?? process.cwd();
|
|
103
|
+
const baselinePath = join(home, BASELINE_CONFIG_NAME);
|
|
104
|
+
const overridePath = join(home, OVERRIDE_CONFIG_NAME);
|
|
105
|
+
const current = readJsonConfig(baselinePath) ?? {};
|
|
106
|
+
const next = { ...current, ...patch, schemaVersion: LOCAL_CONFIG_SCHEMA_VERSION };
|
|
107
|
+
writeFileSync(baselinePath, JSON.stringify(next, null, 2), { mode: 420 });
|
|
108
|
+
const override = readJsonConfig(overridePath);
|
|
109
|
+
if (override) {
|
|
110
|
+
for (const f of BINDING_FIELDS) delete override[f];
|
|
111
|
+
if (Object.keys(override).length === 0) rmSync(overridePath, { force: true });
|
|
112
|
+
else writeFileSync(overridePath, JSON.stringify(override, null, 2), { mode: 384 });
|
|
82
113
|
}
|
|
83
|
-
|
|
84
|
-
|
|
114
|
+
return baselinePath;
|
|
115
|
+
}
|
|
116
|
+
function committedBindingPath(dir) {
|
|
117
|
+
const p = join(dir, BASELINE_CONFIG_NAME);
|
|
118
|
+
return existsSync(p) ? p : void 0;
|
|
85
119
|
}
|
|
86
120
|
function resolveConfig(flags) {
|
|
87
121
|
const local = readLocalConfig();
|
|
@@ -93,7 +127,9 @@ function resolveConfig(flags) {
|
|
|
93
127
|
"No tenant set. The Sechroom API rejects untenanted requests (HTTP 400). Pass --tenant <id>, set SECHROOM_TENANT, run `sechroom config set tenant <id>`, or `sechroom config set --local tenant <id>` for this directory."
|
|
94
128
|
);
|
|
95
129
|
}
|
|
96
|
-
|
|
130
|
+
const workspaceId = process.env.SECHROOM_WORKSPACE ?? local.workspaceId ?? persisted.workspaceId ?? void 0;
|
|
131
|
+
const defaultProjectId = local.defaultProjectId ?? persisted.defaultProjectId ?? void 0;
|
|
132
|
+
return { baseUrl: baseUrl.replace(/\/$/, ""), tenant, workspaceId, defaultProjectId, clientId: persisted.clientId };
|
|
97
133
|
}
|
|
98
134
|
function describeConfig(flags) {
|
|
99
135
|
const local = readLocalConfig();
|
|
@@ -111,6 +147,7 @@ function describeConfig(flags) {
|
|
|
111
147
|
return {
|
|
112
148
|
baseUrl: { value: baseUrl.value, source: baseUrl.source },
|
|
113
149
|
tenant: pick(flags.tenant, process.env.SECHROOM_TENANT, local.tenant, g.tenant),
|
|
150
|
+
workspaceId: pick(void 0, process.env.SECHROOM_WORKSPACE, local.workspaceId, g.workspaceId),
|
|
114
151
|
localPath: local.path
|
|
115
152
|
};
|
|
116
153
|
}
|
|
@@ -292,6 +329,7 @@ var style = {
|
|
|
292
329
|
cyan: wrap(36, 39)
|
|
293
330
|
};
|
|
294
331
|
var ok = (s) => style.green(s);
|
|
332
|
+
var warn = (s) => style.yellow(s);
|
|
295
333
|
var err = (s) => style.red(s);
|
|
296
334
|
function active() {
|
|
297
335
|
return !quiet && Boolean(process.stderr.isTTY);
|
|
@@ -337,8 +375,8 @@ async function promptYesNo(question) {
|
|
|
337
375
|
const { createInterface } = await import("readline");
|
|
338
376
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
339
377
|
try {
|
|
340
|
-
const answer = await new Promise((
|
|
341
|
-
rl.question(`${question} [y/N] `,
|
|
378
|
+
const answer = await new Promise((resolve3) => {
|
|
379
|
+
rl.question(`${question} [y/N] `, resolve3);
|
|
342
380
|
});
|
|
343
381
|
return /^y(es)?$/i.test(answer.trim());
|
|
344
382
|
} finally {
|
|
@@ -351,8 +389,8 @@ async function promptText(question, def) {
|
|
|
351
389
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
352
390
|
try {
|
|
353
391
|
const suffix = def ? ` [${def}]` : "";
|
|
354
|
-
const answer = await new Promise((
|
|
355
|
-
rl.question(`${question}${suffix} `,
|
|
392
|
+
const answer = await new Promise((resolve3) => {
|
|
393
|
+
rl.question(`${question}${suffix} `, resolve3);
|
|
356
394
|
});
|
|
357
395
|
const trimmed = answer.trim();
|
|
358
396
|
return trimmed.length > 0 ? trimmed : def ?? "";
|
|
@@ -378,8 +416,8 @@ async function promptSelect(question, choices, def) {
|
|
|
378
416
|
process.stderr.write(` ${marker} ${style.bold(String(i + 1))}. ${c.label}${hint}
|
|
379
417
|
`);
|
|
380
418
|
});
|
|
381
|
-
const answer = await new Promise((
|
|
382
|
-
rl.question(`Choose ${style.dim(`[${defIdx + 1}]`)} `,
|
|
419
|
+
const answer = await new Promise((resolve3) => {
|
|
420
|
+
rl.question(`Choose ${style.dim(`[${defIdx + 1}]`)} `, resolve3);
|
|
383
421
|
});
|
|
384
422
|
const trimmed = answer.trim();
|
|
385
423
|
if (!trimmed) return choices[defIdx].value;
|
|
@@ -411,8 +449,8 @@ async function promptMultiSelect(question, choices, preselected = []) {
|
|
|
411
449
|
process.stderr.write(` ${box} ${style.bold(String(i + 1))}. ${c.label}${hint}
|
|
412
450
|
`);
|
|
413
451
|
});
|
|
414
|
-
const answer = await new Promise((
|
|
415
|
-
rl.question(`Select ${style.dim("[Enter = \u25C9]")} `,
|
|
452
|
+
const answer = await new Promise((resolve3) => {
|
|
453
|
+
rl.question(`Select ${style.dim("[Enter = \u25C9]")} `, resolve3);
|
|
416
454
|
});
|
|
417
455
|
const trimmed = answer.trim().toLowerCase();
|
|
418
456
|
if (!trimmed) return preValues();
|
|
@@ -453,6 +491,7 @@ async function makeClient(cfg) {
|
|
|
453
491
|
onRequest({ request }) {
|
|
454
492
|
request.headers.set("authorization", `Bearer ${token}`);
|
|
455
493
|
request.headers.set("tenant", cfg.tenant);
|
|
494
|
+
request.headers.set("x-sechroom-surface", "cli");
|
|
456
495
|
return request;
|
|
457
496
|
}
|
|
458
497
|
});
|
|
@@ -468,6 +507,12 @@ function emit(data, json) {
|
|
|
468
507
|
function publicUrl(url) {
|
|
469
508
|
return url.replace(/^https?:\/\/localhost:5012/, "https://sechroom.yi.ocd.codes");
|
|
470
509
|
}
|
|
510
|
+
function resolveViewUrl(baseUrl, url) {
|
|
511
|
+
if (!url) return void 0;
|
|
512
|
+
if (/^https?:\/\//i.test(url)) return publicUrl(url);
|
|
513
|
+
const origin = baseUrl.replace(/\/api\/?$/i, "").replace(/\/+$/, "");
|
|
514
|
+
return publicUrl(`${origin}${url.startsWith("/") ? url : `/${url}`}`);
|
|
515
|
+
}
|
|
471
516
|
function emitAction(summary, data, json) {
|
|
472
517
|
if (json) {
|
|
473
518
|
process.stdout.write(JSON.stringify(data) + "\n");
|
|
@@ -485,9 +530,10 @@ async function runApi(label, fn) {
|
|
|
485
530
|
s.fail();
|
|
486
531
|
fail(err2);
|
|
487
532
|
}
|
|
488
|
-
|
|
533
|
+
const httpFailed = res.response !== void 0 && !res.response.ok;
|
|
534
|
+
if (res.error !== void 0 && res.error !== null || httpFailed) {
|
|
489
535
|
s.fail();
|
|
490
|
-
fail(res.error);
|
|
536
|
+
fail(res.error ?? (res.response ? `HTTP ${res.response.status} ${res.response.statusText}`.trim() : "request failed"));
|
|
491
537
|
}
|
|
492
538
|
s.succeed();
|
|
493
539
|
return res.data;
|
|
@@ -538,7 +584,8 @@ Examples:
|
|
|
538
584
|
});
|
|
539
585
|
});
|
|
540
586
|
const titlePart = opts.title ? ` ${style.dim(`"${opts.title}"`)}` : "";
|
|
541
|
-
const
|
|
587
|
+
const view = resolveViewUrl(cfg.baseUrl, data.url);
|
|
588
|
+
const urlPart = view ? ` ${style.dim("\u2192")} ${view}` : "";
|
|
542
589
|
emitAction(`created memory ${style.bold(data.id)}${titlePart}${urlPart}`, data, cmd.optsWithGlobals().json);
|
|
543
590
|
});
|
|
544
591
|
memory.command("get <memoryId>").description("Fetch a memory by id (GET /memories/{memoryId})").action(async (memoryId, _opts, cmd) => {
|
|
@@ -847,7 +894,8 @@ Examples:
|
|
|
847
894
|
});
|
|
848
895
|
});
|
|
849
896
|
const inversePart = data.inverseId ? ` ${style.dim(`(inverse ${data.inverseId})`)}` : "";
|
|
850
|
-
const
|
|
897
|
+
const view = resolveViewUrl(cfg.baseUrl, data.url);
|
|
898
|
+
const urlPart = view ? ` ${style.dim("\u2192")} ${view}` : "";
|
|
851
899
|
emitAction(
|
|
852
900
|
`created relationship ${style.bold(data.id)} ${style.dim(`${fromMemoryId} \u2192 ${toMemoryId}`)}${inversePart}${urlPart}`,
|
|
853
901
|
data,
|
|
@@ -997,7 +1045,8 @@ Examples:
|
|
|
997
1045
|
}
|
|
998
1046
|
});
|
|
999
1047
|
});
|
|
1000
|
-
const
|
|
1048
|
+
const view = resolveViewUrl(cfg.baseUrl, data.url);
|
|
1049
|
+
const urlPart = view ? ` ${style.dim("\u2192")} ${view}` : "";
|
|
1001
1050
|
emitAction(
|
|
1002
1051
|
`created workspace ${style.bold(data.id)} ${style.dim(`"${opts.name}"`)}${urlPart}`,
|
|
1003
1052
|
data,
|
|
@@ -1140,7 +1189,8 @@ Examples:
|
|
|
1140
1189
|
}
|
|
1141
1190
|
});
|
|
1142
1191
|
});
|
|
1143
|
-
const
|
|
1192
|
+
const view = resolveViewUrl(cfg.baseUrl, data.url);
|
|
1193
|
+
const urlPart = view ? ` ${style.dim("\u2192")} ${view}` : "";
|
|
1144
1194
|
emitAction(`created project ${style.bold(data.id)}${urlPart}`, data, cmd.optsWithGlobals().json);
|
|
1145
1195
|
});
|
|
1146
1196
|
project.command("list").description("List projects (GET /projects)").option("--workspace <workspaceId>", "Scope to a workspace").option("--status <status>", "Draft | Active | OnHold | Completed | Cancelled").option("--include-archived", "Include archived projects", false).action(async (opts, cmd) => {
|
|
@@ -1523,189 +1573,131 @@ Examples:
|
|
|
1523
1573
|
});
|
|
1524
1574
|
}
|
|
1525
1575
|
|
|
1526
|
-
// src/commands/
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
1540
|
-
const data = await runApi("Allocating id", async () => {
|
|
1541
|
-
const client = await makeClient(cfg);
|
|
1542
|
-
return client.POST("/id-registry/allocate", {
|
|
1543
|
-
body: { namespaceKind, scope, clientNonce: null }
|
|
1544
|
-
});
|
|
1545
|
-
});
|
|
1546
|
-
emitAction(`allocated ${style.bold(data.id)} ${style.dim(`(seq ${data.seq})`)}`, data, cmd.optsWithGlobals().json);
|
|
1547
|
-
});
|
|
1548
|
-
id.command("peek <namespaceKind> <scope>").description("Inspect a sequence without consuming (GET /id-registry/state)").action(async (namespaceKind, scope, _opts, cmd) => {
|
|
1549
|
-
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
1550
|
-
const data = await runApi("Peeking id sequence", async () => {
|
|
1551
|
-
const client = await makeClient(cfg);
|
|
1552
|
-
return client.GET("/id-registry/state", {
|
|
1553
|
-
params: { query: { namespaceKind, scope } }
|
|
1554
|
-
});
|
|
1555
|
-
});
|
|
1556
|
-
emit(data, cmd.optsWithGlobals().json);
|
|
1557
|
-
});
|
|
1576
|
+
// src/commands/hook.ts
|
|
1577
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1578
|
+
import { homedir as homedir3 } from "os";
|
|
1579
|
+
import { delimiter, dirname as dirname4, join as join4 } from "path";
|
|
1580
|
+
|
|
1581
|
+
// src/sem.ts
|
|
1582
|
+
import { basename as basename2, dirname as dirname2, join as join2 } from "path";
|
|
1583
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1584
|
+
var SEM_FILE = join2(".sechroom", "lane.json");
|
|
1585
|
+
var LEGACY_SEM_FILE = ".sem";
|
|
1586
|
+
var STATE_DIR_NAME2 = ".sechroom";
|
|
1587
|
+
function localSemPath(cwd = process.cwd()) {
|
|
1588
|
+
return join2(cwd, SEM_FILE);
|
|
1558
1589
|
}
|
|
1559
|
-
function
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
);
|
|
1571
|
-
account.command("profile").description("Show your resolved profile (GET /me/profile)").action(async (_opts, cmd) => {
|
|
1572
|
-
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
1573
|
-
const data = await runApi("Fetching profile", async () => {
|
|
1574
|
-
const client = await makeClient(cfg);
|
|
1575
|
-
return client.GET("/me/profile");
|
|
1576
|
-
});
|
|
1577
|
-
emit(data, cmd.optsWithGlobals().json);
|
|
1578
|
-
});
|
|
1579
|
-
account.command("set-profile").description("Update your profile (PUT /me/profile)").option("--display-name <displayName>", "Display name").option("--timezone <timezone>", "IANA timezone (e.g. Europe/London)").option("--bio <bio>", "Short bio").option("--photo-url <photoUrl>", "Avatar / photo URL").action(async (opts, cmd) => {
|
|
1580
|
-
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
1581
|
-
const data = await runApi("Updating profile", async () => {
|
|
1582
|
-
const client = await makeClient(cfg);
|
|
1583
|
-
return client.PUT("/me/profile", {
|
|
1584
|
-
body: {
|
|
1585
|
-
displayName: opts.displayName ?? null,
|
|
1586
|
-
timezone: opts.timezone ?? null,
|
|
1587
|
-
bio: opts.bio ?? null,
|
|
1588
|
-
photoUrl: opts.photoUrl ?? null
|
|
1589
|
-
}
|
|
1590
|
-
});
|
|
1591
|
-
});
|
|
1592
|
-
emitAction("updated profile", data, cmd.optsWithGlobals().json);
|
|
1593
|
-
});
|
|
1594
|
-
account.command("feed").description("Your recent memory feed (GET /me/memories/feed)").option("--limit <n>", "Max results", "20").option("--cursor <cursor>", "Opaque paging cursor").option("--query <query>", "Free-text filter").option("--filter-tags <tags>", "Comma-separated tag filter").option("--include-archived", "Include archived memories", false).option("--include-text", "Include memory body text", false).action(async (opts, cmd) => {
|
|
1595
|
-
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
1596
|
-
const data = await runApi("Fetching feed", async () => {
|
|
1597
|
-
const client = await makeClient(cfg);
|
|
1598
|
-
return client.GET("/me/memories/feed", {
|
|
1599
|
-
params: {
|
|
1600
|
-
query: {
|
|
1601
|
-
limit: Number(opts.limit),
|
|
1602
|
-
includeArchived: Boolean(opts.includeArchived),
|
|
1603
|
-
includeText: Boolean(opts.includeText),
|
|
1604
|
-
...opts.cursor ? { cursor: opts.cursor } : {},
|
|
1605
|
-
...opts.query ? { query: opts.query } : {},
|
|
1606
|
-
...opts.filterTags ? { filterTags: opts.filterTags } : {}
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
});
|
|
1610
|
-
});
|
|
1611
|
-
emit(data, cmd.optsWithGlobals().json);
|
|
1612
|
-
});
|
|
1613
|
-
account.command("reviews").description("Your review queue (GET /reviews)").option("--status <status>", "Pending | Resolved | Empty").option("--scope-kind <scopeKind>", "Memory | Project | Workspace").option("--scope-target-id <id>", "Scope target id").option("--limit <n>", "Max results", "20").action(async (opts, cmd) => {
|
|
1614
|
-
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
1615
|
-
const data = await runApi("Fetching reviews", async () => {
|
|
1616
|
-
const client = await makeClient(cfg);
|
|
1617
|
-
return client.GET("/reviews", {
|
|
1618
|
-
params: {
|
|
1619
|
-
query: {
|
|
1620
|
-
limit: Number(opts.limit),
|
|
1621
|
-
...opts.status ? { status: opts.status } : {},
|
|
1622
|
-
...opts.scopeKind ? { scopeKind: opts.scopeKind } : {},
|
|
1623
|
-
...opts.scopeTargetId ? { scopeTargetId: opts.scopeTargetId } : {}
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
});
|
|
1627
|
-
});
|
|
1628
|
-
emit(data, cmd.optsWithGlobals().json);
|
|
1629
|
-
});
|
|
1630
|
-
account.command("review-get <reviewId>").description("Fetch one review bundle (GET /reviews/{reviewId})").action(async (reviewId, _opts, cmd) => {
|
|
1631
|
-
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
1632
|
-
const data = await runApi("Fetching review", async () => {
|
|
1633
|
-
const client = await makeClient(cfg);
|
|
1634
|
-
return client.GET("/reviews/{reviewId}", { params: { path: { reviewId } } });
|
|
1635
|
-
});
|
|
1636
|
-
emit(data, cmd.optsWithGlobals().json);
|
|
1637
|
-
});
|
|
1638
|
-
account.command("review-accept <reviewId>").description("Accept a review bundle (POST /reviews/{reviewId}/accept)").action(async (reviewId, _opts, cmd) => {
|
|
1639
|
-
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
1640
|
-
const data = await runApi("Accepting review", async () => {
|
|
1641
|
-
const client = await makeClient(cfg);
|
|
1642
|
-
return client.POST("/reviews/{reviewId}/accept", {
|
|
1643
|
-
params: { path: { reviewId } },
|
|
1644
|
-
body: { decisions: {} }
|
|
1645
|
-
});
|
|
1646
|
-
});
|
|
1647
|
-
emitAction(`accepted review ${style.bold(reviewId)}`, data, cmd.optsWithGlobals().json);
|
|
1648
|
-
});
|
|
1649
|
-
account.command("lookup-batch <ids...>").description("Resolve many ids at once (POST /lookup/batch)").action(async (ids, _opts, cmd) => {
|
|
1650
|
-
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
1651
|
-
const data = await runApi(`Resolving ${ids.length} ids`, async () => {
|
|
1652
|
-
const client = await makeClient(cfg);
|
|
1653
|
-
return client.POST("/lookup/batch", { body: { ids } });
|
|
1654
|
-
});
|
|
1655
|
-
emit(data, cmd.optsWithGlobals().json);
|
|
1656
|
-
});
|
|
1590
|
+
function resolveSemPathForRead(start = process.cwd()) {
|
|
1591
|
+
let dir = start;
|
|
1592
|
+
while (true) {
|
|
1593
|
+
const candidate = join2(dir, SEM_FILE);
|
|
1594
|
+
if (existsSync2(candidate)) return candidate;
|
|
1595
|
+
const legacy = join2(dir, LEGACY_SEM_FILE);
|
|
1596
|
+
if (existsSync2(legacy)) return legacy;
|
|
1597
|
+
const parent = dirname2(dir);
|
|
1598
|
+
if (parent === dir) return void 0;
|
|
1599
|
+
dir = parent;
|
|
1600
|
+
}
|
|
1657
1601
|
}
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
"
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1602
|
+
function parseSem(text) {
|
|
1603
|
+
const out = {};
|
|
1604
|
+
for (const raw of text.split("\n")) {
|
|
1605
|
+
const line = raw.trim();
|
|
1606
|
+
if (!line || line.startsWith("#")) continue;
|
|
1607
|
+
const eq = line.indexOf("=");
|
|
1608
|
+
if (eq === -1) continue;
|
|
1609
|
+
const key = line.slice(0, eq).trim();
|
|
1610
|
+
const value = line.slice(eq + 1).trim();
|
|
1611
|
+
if (key) out[key] = value;
|
|
1612
|
+
}
|
|
1613
|
+
return out;
|
|
1614
|
+
}
|
|
1615
|
+
function serializeSem(values) {
|
|
1616
|
+
return JSON.stringify(values, null, 2) + "\n";
|
|
1617
|
+
}
|
|
1618
|
+
function readSem(path) {
|
|
1619
|
+
const p = path ?? resolveSemPathForRead();
|
|
1620
|
+
if (!p || !existsSync2(p)) return void 0;
|
|
1621
|
+
const text = readFileSync2(p, "utf8");
|
|
1622
|
+
const values = basename2(p) === LEGACY_SEM_FILE ? parseSem(text) : parseLaneJson(text);
|
|
1623
|
+
return { path: p, values };
|
|
1624
|
+
}
|
|
1625
|
+
function readLocalSemValues(cwd = process.cwd()) {
|
|
1626
|
+
const next = join2(cwd, SEM_FILE);
|
|
1627
|
+
if (existsSync2(next)) return readSem(next)?.values ?? {};
|
|
1628
|
+
const legacy = join2(cwd, LEGACY_SEM_FILE);
|
|
1629
|
+
if (existsSync2(legacy)) return readSem(legacy)?.values ?? {};
|
|
1630
|
+
return {};
|
|
1631
|
+
}
|
|
1632
|
+
function parseLaneJson(text) {
|
|
1633
|
+
try {
|
|
1634
|
+
const parsed = JSON.parse(text);
|
|
1635
|
+
const out = {};
|
|
1636
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
1637
|
+
if (typeof v === "string") out[k] = v;
|
|
1638
|
+
}
|
|
1639
|
+
return out;
|
|
1640
|
+
} catch {
|
|
1641
|
+
return {};
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
var STATE_DIR_IGNORE = `${STATE_DIR_NAME2}/`;
|
|
1645
|
+
function writeSem(values, path = localSemPath()) {
|
|
1646
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
1647
|
+
writeFileSync2(path, serializeSem(values));
|
|
1648
|
+
ensureSemIgnored(path);
|
|
1649
|
+
return path;
|
|
1650
|
+
}
|
|
1651
|
+
function ignoresSem(content) {
|
|
1652
|
+
return content.split("\n").some((line) => {
|
|
1653
|
+
const t = line.trim();
|
|
1654
|
+
return t === STATE_DIR_NAME2 || t === STATE_DIR_IGNORE || t === `/${STATE_DIR_NAME2}` || t === `/${STATE_DIR_IGNORE}` || t === `**/${STATE_DIR_NAME2}` || t === `**/${STATE_DIR_IGNORE}`;
|
|
1703
1655
|
});
|
|
1704
1656
|
}
|
|
1657
|
+
function inGitRepo(startDir) {
|
|
1658
|
+
let dir = startDir;
|
|
1659
|
+
for (; ; ) {
|
|
1660
|
+
if (existsSync2(join2(dir, ".git"))) return true;
|
|
1661
|
+
const parent = dirname2(dir);
|
|
1662
|
+
if (parent === dir) return false;
|
|
1663
|
+
dir = parent;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
function resolveGitignoreTarget(startDir) {
|
|
1667
|
+
let dir = startDir;
|
|
1668
|
+
for (; ; ) {
|
|
1669
|
+
const gi = join2(dir, ".gitignore");
|
|
1670
|
+
if (existsSync2(gi)) return { path: gi, exists: true };
|
|
1671
|
+
const parent = dirname2(dir);
|
|
1672
|
+
if (existsSync2(join2(dir, ".git")) || parent === dir) {
|
|
1673
|
+
return { path: join2(startDir, ".gitignore"), exists: false };
|
|
1674
|
+
}
|
|
1675
|
+
dir = parent;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
function ensureSemIgnored(semPath) {
|
|
1679
|
+
try {
|
|
1680
|
+
const checkoutDir = dirname2(dirname2(semPath));
|
|
1681
|
+
if (!inGitRepo(checkoutDir)) return;
|
|
1682
|
+
const target = resolveGitignoreTarget(checkoutDir);
|
|
1683
|
+
if (target.exists) {
|
|
1684
|
+
const content = readFileSync2(target.path, "utf8");
|
|
1685
|
+
if (ignoresSem(content)) return;
|
|
1686
|
+
const sep = content.length === 0 || content.endsWith("\n") ? "" : "\n";
|
|
1687
|
+
appendFileSync(target.path, `${sep}${STATE_DIR_IGNORE}
|
|
1688
|
+
`);
|
|
1689
|
+
} else {
|
|
1690
|
+
writeFileSync2(target.path, `${STATE_DIR_IGNORE}
|
|
1691
|
+
`);
|
|
1692
|
+
}
|
|
1693
|
+
} catch {
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1705
1696
|
|
|
1706
|
-
// src/setup/
|
|
1707
|
-
import {
|
|
1708
|
-
import {
|
|
1697
|
+
// src/setup/clients.ts
|
|
1698
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1699
|
+
import { homedir as homedir2 } from "os";
|
|
1700
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
1709
1701
|
|
|
1710
1702
|
// src/setup/operator-surface.ts
|
|
1711
1703
|
var SectionType = {
|
|
@@ -1713,11 +1705,17 @@ var SectionType = {
|
|
|
1713
1705
|
McpConfigToml: "mcp-config-toml",
|
|
1714
1706
|
InstructionFile: "instruction-file",
|
|
1715
1707
|
ProjectConfig: "project-config",
|
|
1716
|
-
Verify: "verify"
|
|
1708
|
+
Verify: "verify",
|
|
1709
|
+
/** SBC-999 — workspace-pinned conventions, emitted only when the request
|
|
1710
|
+
* carried a workspaceId and that workspace has agent-setup-bundle memories. */
|
|
1711
|
+
WorkspaceConventions: "workspace-conventions"
|
|
1717
1712
|
};
|
|
1718
1713
|
async function fetchSetup(cfg) {
|
|
1719
1714
|
const client = await makeClient(cfg);
|
|
1720
|
-
const { data, error } = await client.GET(
|
|
1715
|
+
const { data, error } = await client.GET(
|
|
1716
|
+
"/operator-surface/setup",
|
|
1717
|
+
cfg.workspaceId ? { params: { query: { workspaceId: cfg.workspaceId } } } : {}
|
|
1718
|
+
);
|
|
1721
1719
|
if (error) throw new Error(`GET /operator-surface/setup failed: ${JSON.stringify(error)}`);
|
|
1722
1720
|
return data;
|
|
1723
1721
|
}
|
|
@@ -1749,7 +1747,10 @@ async function fetchMemoryFields(cfg, id) {
|
|
|
1749
1747
|
const client = await makeClient(cfg);
|
|
1750
1748
|
const { data } = await client.GET("/memories/{memoryId}", { params: { path: { memoryId: id } } });
|
|
1751
1749
|
const env = data;
|
|
1752
|
-
|
|
1750
|
+
const m = env?.item ?? env;
|
|
1751
|
+
if (!m) return null;
|
|
1752
|
+
const version = typeof m.currentVersion === "string" ? Number(m.currentVersion) : m.currentVersion;
|
|
1753
|
+
return { text: m.text, title: m.title, tags: m.tags, version: Number.isFinite(version) ? version : void 0 };
|
|
1753
1754
|
}
|
|
1754
1755
|
async function resolveInstruction(cfg, section, personalWorkspaceId) {
|
|
1755
1756
|
const client = await makeClient(cfg);
|
|
@@ -1757,7 +1758,7 @@ async function resolveInstruction(cfg, section, personalWorkspaceId) {
|
|
|
1757
1758
|
const tags = parseTagArtifactId(artifact.id);
|
|
1758
1759
|
if (!tags) continue;
|
|
1759
1760
|
const { data } = await client.POST("/memories/search", {
|
|
1760
|
-
body: { query: null, textQuery: null, semanticQuery:
|
|
1761
|
+
body: { query: null, textQuery: null, semanticQuery: null, hybrid: false, limit: 1, includeArchived: false, includeSystem: false, tags }
|
|
1761
1762
|
});
|
|
1762
1763
|
const hits = data ?? [];
|
|
1763
1764
|
if (hits.length === 0) continue;
|
|
@@ -1767,20 +1768,34 @@ async function resolveInstruction(cfg, section, personalWorkspaceId) {
|
|
|
1767
1768
|
const templateTags = template.tags ?? tags;
|
|
1768
1769
|
if (personalWorkspaceId) {
|
|
1769
1770
|
const { data: ovr } = await client.POST("/memories/search", {
|
|
1770
|
-
body: { query: null, textQuery: null, semanticQuery:
|
|
1771
|
+
body: { query: null, textQuery: null, semanticQuery: null, hybrid: false, limit: 1, includeArchived: false, includeSystem: false, tags: ["sechroom:role:override", `sechroom:template-ref:${templateId}`], owner: { type: "Workspace", id: personalWorkspaceId } }
|
|
1771
1772
|
});
|
|
1772
1773
|
const ovrHits = ovr ?? [];
|
|
1773
1774
|
if (ovrHits.length > 0) {
|
|
1774
1775
|
const override = await fetchMemoryFields(cfg, ovrHits[0].id);
|
|
1775
1776
|
if (typeof override?.text === "string" && override.text.length > 0) {
|
|
1776
|
-
return { title: override.title ?? template.title ?? artifact.title, body: override.text, source: "override", templateId, templateTags };
|
|
1777
|
+
return { title: override.title ?? template.title ?? artifact.title, body: override.text, source: "override", templateId, templateTags, sourceRef: `${ovrHits[0].id}@v${override.version ?? 1}` };
|
|
1777
1778
|
}
|
|
1778
1779
|
}
|
|
1779
1780
|
}
|
|
1780
|
-
return { title: template.title ?? artifact.title, body: template.text, source: "template", templateId, templateTags };
|
|
1781
|
+
return { title: template.title ?? artifact.title, body: template.text, source: "template", templateId, templateTags, sourceRef: `${templateId}@v${template.version ?? 1}` };
|
|
1781
1782
|
}
|
|
1782
1783
|
return null;
|
|
1783
1784
|
}
|
|
1785
|
+
async function resolveWorkspaceConventions(cfg, section) {
|
|
1786
|
+
const parts = [];
|
|
1787
|
+
const refs = [];
|
|
1788
|
+
for (const artifact of section.artifacts) {
|
|
1789
|
+
if (parseTagArtifactId(artifact.id)) continue;
|
|
1790
|
+
const mem = await fetchMemoryFields(cfg, artifact.id);
|
|
1791
|
+
if (typeof mem?.text === "string" && mem.text.trim().length > 0) {
|
|
1792
|
+
parts.push(mem.text.trim());
|
|
1793
|
+
refs.push(`${artifact.id}@v${mem.version ?? 1}`);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
if (parts.length === 0) return null;
|
|
1797
|
+
return { body: parts.join("\n\n---\n\n"), refs };
|
|
1798
|
+
}
|
|
1784
1799
|
async function createOverride(cfg, template, personalWorkspaceId) {
|
|
1785
1800
|
const client = await makeClient(cfg);
|
|
1786
1801
|
const overrideTags = template.templateTags.filter(
|
|
@@ -1803,73 +1818,760 @@ async function createOverride(cfg, template, personalWorkspaceId) {
|
|
|
1803
1818
|
if (error) throw new Error(`creating personal copy failed: ${JSON.stringify(error)}`);
|
|
1804
1819
|
}
|
|
1805
1820
|
|
|
1806
|
-
// src/setup/
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
} catch {
|
|
1816
|
-
return fallback;
|
|
1821
|
+
// src/setup/clients.ts
|
|
1822
|
+
function claudeDesktopConfigPath(home) {
|
|
1823
|
+
switch (process.platform) {
|
|
1824
|
+
case "darwin":
|
|
1825
|
+
return join3(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
1826
|
+
case "win32":
|
|
1827
|
+
return join3(process.env.APPDATA ?? join3(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
1828
|
+
default:
|
|
1829
|
+
return join3(home, ".config", "Claude", "claude_desktop_config.json");
|
|
1817
1830
|
}
|
|
1818
1831
|
}
|
|
1819
|
-
function
|
|
1820
|
-
const
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1832
|
+
function clientTargets(cwd) {
|
|
1833
|
+
const home = homedir2();
|
|
1834
|
+
return {
|
|
1835
|
+
"claude-code": {
|
|
1836
|
+
key: "claude-code",
|
|
1837
|
+
label: "Claude Code",
|
|
1838
|
+
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join3(cwd, ".mcp.json"), format: "json" },
|
|
1839
|
+
instruction: { surfaceKey: "claude-code", path: join3(cwd, "CLAUDE.md") }
|
|
1840
|
+
},
|
|
1841
|
+
"claude-desktop": {
|
|
1842
|
+
key: "claude-desktop",
|
|
1843
|
+
label: "Claude Desktop",
|
|
1844
|
+
mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
|
|
1845
|
+
instruction: { surfaceKey: "claude-desktop", path: join3(home, ".claude", "CLAUDE.md") }
|
|
1846
|
+
},
|
|
1847
|
+
codex: {
|
|
1848
|
+
key: "codex",
|
|
1849
|
+
label: "Codex CLI",
|
|
1850
|
+
mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join3(home, ".codex", "config.toml"), format: "toml" },
|
|
1851
|
+
instruction: { surfaceKey: "chatgpt", path: join3(cwd, "AGENTS.md") }
|
|
1852
|
+
},
|
|
1853
|
+
cursor: {
|
|
1854
|
+
key: "cursor",
|
|
1855
|
+
label: "Cursor",
|
|
1856
|
+
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join3(cwd, ".cursor", "mcp.json"), format: "json" },
|
|
1857
|
+
instruction: { surfaceKey: "chatgpt", path: join3(cwd, "AGENTS.md") }
|
|
1858
|
+
}
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
var ALL_CLIENT_KEYS = ["claude-code", "claude-desktop", "codex", "cursor"];
|
|
1862
|
+
var DEFAULT_CLIENT_KEY = "claude-code";
|
|
1863
|
+
function detectInstalledClients(cwd) {
|
|
1864
|
+
const home = homedir2();
|
|
1865
|
+
const detected = [];
|
|
1866
|
+
if (existsSync3(join3(home, ".claude"))) detected.push("claude-code");
|
|
1867
|
+
if (existsSync3(dirname3(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
|
|
1868
|
+
if (existsSync3(join3(home, ".codex"))) detected.push("codex");
|
|
1869
|
+
if (existsSync3(join3(home, ".cursor")) || existsSync3(join3(cwd, ".cursor"))) detected.push("cursor");
|
|
1870
|
+
return detected;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
// src/commands/hook.ts
|
|
1874
|
+
async function readStdin() {
|
|
1875
|
+
if (process.stdin.isTTY) return "";
|
|
1876
|
+
const chunks = [];
|
|
1877
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
1878
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1879
|
+
}
|
|
1880
|
+
function parseHookInput(raw) {
|
|
1881
|
+
if (!raw.trim()) return {};
|
|
1882
|
+
try {
|
|
1883
|
+
return JSON.parse(raw);
|
|
1884
|
+
} catch {
|
|
1885
|
+
return {};
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
function resolveLane(flagLane, cwd) {
|
|
1889
|
+
if (flagLane) return flagLane;
|
|
1890
|
+
const env = process.env.SECHROOM_LANE;
|
|
1891
|
+
if (env) return env;
|
|
1892
|
+
const start = cwd ?? process.cwd();
|
|
1893
|
+
const sem = readSem(resolveSemPathForRead(start));
|
|
1894
|
+
return sem?.values["code-lane"];
|
|
1895
|
+
}
|
|
1896
|
+
var INTENT_FILE = join4(".sechroom", "continuity.json");
|
|
1897
|
+
function resolveIntentPath(start) {
|
|
1898
|
+
let dir = start;
|
|
1899
|
+
for (; ; ) {
|
|
1900
|
+
const candidate = join4(dir, INTENT_FILE);
|
|
1901
|
+
if (existsSync4(candidate)) return candidate;
|
|
1902
|
+
const parent = dirname4(dir);
|
|
1903
|
+
if (parent === dir) return void 0;
|
|
1904
|
+
dir = parent;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
function readIntent(start) {
|
|
1908
|
+
const path = resolveIntentPath(start);
|
|
1909
|
+
if (!path) return void 0;
|
|
1910
|
+
try {
|
|
1911
|
+
return JSON.parse(readFileSync3(path, "utf8"));
|
|
1912
|
+
} catch {
|
|
1913
|
+
return void 0;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
function hasRequiredIntent(i) {
|
|
1917
|
+
return Boolean(
|
|
1918
|
+
i.objective?.trim() && i.state?.trim() && i.lastAction?.trim() && i.nextAction?.trim() && i.resumeInstruction?.trim()
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1921
|
+
function formatContext(bundle, lane) {
|
|
1922
|
+
const s = bundle?.latestSnapshot;
|
|
1923
|
+
if (!s) return null;
|
|
1924
|
+
const lines = [];
|
|
1925
|
+
lines.push(`[sechroom continuity \u2014 resumed lane ${lane}]`);
|
|
1926
|
+
if (s.currentObjective) lines.push(`Objective: ${s.currentObjective}`);
|
|
1927
|
+
if (s.currentState) lines.push(`State: ${s.currentState}`);
|
|
1928
|
+
if (s.lastMeaningfulAction) lines.push(`Last action: ${s.lastMeaningfulAction}`);
|
|
1929
|
+
if (s.nextIntendedAction) lines.push(`Next: ${s.nextIntendedAction}`);
|
|
1930
|
+
if (s.resumeInstruction) lines.push(`Resume: ${s.resumeInstruction}`);
|
|
1931
|
+
const constraints = s.activeConstraints ?? [];
|
|
1932
|
+
if (constraints.length) {
|
|
1933
|
+
lines.push("Active constraints:");
|
|
1934
|
+
for (const c of constraints) lines.push(` - ${c}`);
|
|
1935
|
+
}
|
|
1936
|
+
const questions = s.openQuestions ?? [];
|
|
1937
|
+
if (questions.length) {
|
|
1938
|
+
lines.push("Open questions:");
|
|
1939
|
+
for (const q of questions) lines.push(` - ${q}`);
|
|
1940
|
+
}
|
|
1941
|
+
const artifacts = s.relevantArtifactIds ?? [];
|
|
1942
|
+
if (artifacts.length) lines.push(`Relevant artifacts: ${artifacts.join(", ")}`);
|
|
1943
|
+
const marker = [s.id, s.createdAt].filter(Boolean).join(" @ ");
|
|
1944
|
+
if (marker) lines.push(`(snapshot ${marker})`);
|
|
1945
|
+
return lines.join("\n");
|
|
1946
|
+
}
|
|
1947
|
+
function emitSessionStart(additionalContext) {
|
|
1948
|
+
process.stdout.write(
|
|
1949
|
+
JSON.stringify({
|
|
1950
|
+
hookSpecificOutput: {
|
|
1951
|
+
hookEventName: "SessionStart",
|
|
1952
|
+
additionalContext
|
|
1953
|
+
}
|
|
1954
|
+
}) + "\n"
|
|
1955
|
+
);
|
|
1956
|
+
}
|
|
1957
|
+
var HOOK_COMMANDS = {
|
|
1958
|
+
SessionStart: "sechroom hook session-start",
|
|
1959
|
+
PreCompact: "sechroom hook pre-compact"
|
|
1960
|
+
};
|
|
1961
|
+
var HOOK_EVENTS = ["SessionStart", "PreCompact"];
|
|
1962
|
+
function hasHookCommand(config2, event, command) {
|
|
1963
|
+
const groups = config2.hooks?.[event] ?? [];
|
|
1964
|
+
return groups.some((g) => (g.hooks ?? []).some((h) => h.type === "command" && h.command === command));
|
|
1965
|
+
}
|
|
1966
|
+
function mergeHooks(config2) {
|
|
1967
|
+
config2.hooks ??= {};
|
|
1968
|
+
let added = 0;
|
|
1969
|
+
for (const event of HOOK_EVENTS) {
|
|
1970
|
+
const command = HOOK_COMMANDS[event];
|
|
1971
|
+
if (hasHookCommand(config2, event, command)) continue;
|
|
1972
|
+
const groups = config2.hooks[event] ??= [];
|
|
1973
|
+
groups.push({ hooks: [{ type: "command", command }] });
|
|
1974
|
+
added += 1;
|
|
1975
|
+
}
|
|
1976
|
+
return added;
|
|
1977
|
+
}
|
|
1978
|
+
function readJsonConfig2(path) {
|
|
1979
|
+
if (!existsSync4(path)) return {};
|
|
1980
|
+
const raw = readFileSync3(path, "utf8");
|
|
1981
|
+
if (!raw.trim()) return {};
|
|
1982
|
+
return JSON.parse(raw);
|
|
1983
|
+
}
|
|
1984
|
+
function installHooksJson(path, dryRun) {
|
|
1985
|
+
const existed = existsSync4(path) && readFileSync3(path, "utf8").trim().length > 0;
|
|
1986
|
+
const config2 = readJsonConfig2(path);
|
|
1987
|
+
const added = mergeHooks(config2);
|
|
1988
|
+
if (added === 0 && existed) return { path, status: "current" };
|
|
1989
|
+
if (!dryRun) {
|
|
1990
|
+
mkdirSync3(dirname4(path), { recursive: true });
|
|
1991
|
+
writeFileSync3(path, JSON.stringify(config2, null, 2) + "\n");
|
|
1992
|
+
}
|
|
1993
|
+
return { path, status: existed ? "merged" : "created" };
|
|
1994
|
+
}
|
|
1995
|
+
function ensureCodexFeaturesHooks(content) {
|
|
1996
|
+
const lines = content.split("\n");
|
|
1997
|
+
const headerIdx = lines.findIndex((l) => l.trim() === "[features]");
|
|
1998
|
+
if (headerIdx === -1) {
|
|
1999
|
+
const base = content.length === 0 || content.endsWith("\n") ? content : content + "\n";
|
|
2000
|
+
return { next: base + "\n[features]\nhooks = true\n", changed: true };
|
|
2001
|
+
}
|
|
2002
|
+
for (let i = headerIdx + 1; i < lines.length; i += 1) {
|
|
2003
|
+
const trimmed = lines[i].trim();
|
|
2004
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) break;
|
|
2005
|
+
const m = lines[i].match(/^(\s*)hooks(\s*)=(\s*)(.*)$/);
|
|
2006
|
+
if (!m) continue;
|
|
2007
|
+
const value = m[4].replace(/\s*#.*$/, "").trim();
|
|
2008
|
+
if (value === "true") return { next: content, changed: false };
|
|
2009
|
+
lines[i] = `${m[1]}hooks${m[2]}=${m[3]}true`;
|
|
2010
|
+
return { next: lines.join("\n"), changed: true };
|
|
2011
|
+
}
|
|
2012
|
+
lines.splice(headerIdx + 1, 0, "hooks = true");
|
|
2013
|
+
return { next: lines.join("\n"), changed: true };
|
|
2014
|
+
}
|
|
2015
|
+
function installCodexFeatureFlag(path, dryRun) {
|
|
2016
|
+
const existed = existsSync4(path);
|
|
2017
|
+
const content = existed ? readFileSync3(path, "utf8") : "";
|
|
2018
|
+
const { next, changed } = ensureCodexFeaturesHooks(content);
|
|
2019
|
+
if (!changed) return { path, status: "current" };
|
|
2020
|
+
if (!dryRun) {
|
|
2021
|
+
mkdirSync3(dirname4(path), { recursive: true });
|
|
2022
|
+
writeFileSync3(path, next);
|
|
2023
|
+
}
|
|
2024
|
+
return { path, status: existed ? "merged" : "created" };
|
|
2025
|
+
}
|
|
2026
|
+
function resolveSurfaces(surface, cwd) {
|
|
2027
|
+
if (surface === "claude") return ["claude"];
|
|
2028
|
+
if (surface === "codex") return ["codex"];
|
|
2029
|
+
if (surface === "both") return ["claude", "codex"];
|
|
2030
|
+
if (surface) throw new Error(`--surface must be one of claude | codex | both (got '${surface}')`);
|
|
2031
|
+
const surfaces = detectHookSurfaces(cwd);
|
|
2032
|
+
return surfaces.length > 0 ? surfaces : ["claude", "codex"];
|
|
2033
|
+
}
|
|
2034
|
+
function describe(result, dryRun) {
|
|
2035
|
+
if (result.status === "current") return ` \u2713 ${result.path} (already configured)`;
|
|
2036
|
+
const verb = dryRun ? "would" : result.status === "created" ? "created" : "updated";
|
|
2037
|
+
return ` \u2713 ${result.path} (${dryRun ? `${verb} ${result.status === "created" ? "create" : "update"}` : verb})`;
|
|
2038
|
+
}
|
|
2039
|
+
var HOOK_SURFACE_LABEL = {
|
|
2040
|
+
claude: "Claude Code",
|
|
2041
|
+
codex: "Codex"
|
|
2042
|
+
};
|
|
2043
|
+
function installHookSurfaces(surfaces, opts) {
|
|
2044
|
+
const out = [];
|
|
2045
|
+
for (const surface of surfaces) {
|
|
2046
|
+
if (surface === "claude") {
|
|
2047
|
+
const path = opts.local ? join4(opts.cwd, ".claude", "settings.json") : join4(opts.home, ".claude", "settings.json");
|
|
2048
|
+
out.push({ surface, results: [installHooksJson(path, opts.dryRun)] });
|
|
2049
|
+
} else {
|
|
2050
|
+
const hooksJson = installHooksJson(join4(opts.home, ".codex", "hooks.json"), opts.dryRun);
|
|
2051
|
+
const featureFlag = installCodexFeatureFlag(join4(opts.home, ".codex", "config.toml"), opts.dryRun);
|
|
2052
|
+
out.push({ surface, results: [hooksJson, featureFlag] });
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
return out;
|
|
2056
|
+
}
|
|
2057
|
+
function detectHookSurfaces(cwd) {
|
|
2058
|
+
const detected = detectInstalledClients(cwd);
|
|
2059
|
+
const surfaces = [];
|
|
2060
|
+
if (detected.includes("claude-code")) surfaces.push("claude");
|
|
2061
|
+
if (detected.includes("codex")) surfaces.push("codex");
|
|
2062
|
+
return surfaces;
|
|
2063
|
+
}
|
|
2064
|
+
function isSechroomOnPath() {
|
|
2065
|
+
const pathEnv = process.env.PATH ?? "";
|
|
2066
|
+
if (!pathEnv) return false;
|
|
2067
|
+
const names = process.platform === "win32" ? ["sechroom.cmd", "sechroom.exe", "sechroom.bat", "sechroom"] : ["sechroom"];
|
|
2068
|
+
for (const dir of pathEnv.split(delimiter)) {
|
|
2069
|
+
if (!dir) continue;
|
|
2070
|
+
for (const name of names) {
|
|
2071
|
+
if (existsSync4(join4(dir, name))) return true;
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
return false;
|
|
2075
|
+
}
|
|
2076
|
+
function warnIfSechroomNotOnPath(write = (s) => void process.stderr.write(s)) {
|
|
2077
|
+
if (isSechroomOnPath()) return false;
|
|
2078
|
+
write(
|
|
2079
|
+
"\n\u26A0 `sechroom` isn't on your PATH. The hooks run a bare `sechroom hook \u2026` command\n when your agent fires them, so a non-global install (npx / local) will fail at\n that point. Install globally so the command resolves:\n npm i -g @sechroom/cli\n"
|
|
2080
|
+
);
|
|
2081
|
+
return true;
|
|
2082
|
+
}
|
|
2083
|
+
function registerHook(program2) {
|
|
2084
|
+
const hook = program2.command("hook").description("Agent-lifecycle hook adapter (Claude Code / Codex) \u2014 bridges hooks to continuity");
|
|
2085
|
+
hook.addHelpText(
|
|
2086
|
+
"after",
|
|
2087
|
+
`
|
|
2088
|
+
Examples:
|
|
2089
|
+
# SessionStart (load): inject the lane's latest snapshot as context.
|
|
2090
|
+
$ echo '{"hook_event_name":"SessionStart","cwd":"'"$PWD"'"}' | sechroom hook session-start
|
|
2091
|
+
# PreCompact (save): snapshot from ./${INTENT_FILE} before the agent compacts.
|
|
2092
|
+
$ echo '{"hook_event_name":"PreCompact","cwd":"'"$PWD"'"}' | sechroom hook pre-compact
|
|
2093
|
+
# Wire both hooks into the installed surface(s)' config (no hand-editing):
|
|
2094
|
+
$ sechroom hook install auto-detect Claude Code / Codex
|
|
2095
|
+
$ sechroom hook install --surface codex Codex only
|
|
2096
|
+
$ sechroom hook install --local --dry-run preview the project .claude/settings.json
|
|
2097
|
+
|
|
2098
|
+
Lane source (high -> low): --lane > SECHROOM_LANE > ./.sem code-lane (D-binding-5).
|
|
2099
|
+
Fail-soft: no lane / no auth / no-or-partial intent file / API error -> exit 0, never blocks.`
|
|
2100
|
+
);
|
|
2101
|
+
hook.command("session-start").description("Resume the checkout's lane and emit continuity context for a SessionStart hook").option("--lane <laneId>", "Override the resolved lane (else SECHROOM_LANE, else ./.sem code-lane)").option("--surface <surface>", "Target surface: claude | codex (output is identical for session-start)", "claude").option("--max-artifacts <n>", "Cap artifacts in the resume bundle").action(async (opts, cmd) => {
|
|
2102
|
+
try {
|
|
2103
|
+
const raw = await readStdin();
|
|
2104
|
+
const input = parseHookInput(raw);
|
|
2105
|
+
const lane = resolveLane(opts.lane, input.cwd);
|
|
2106
|
+
if (!lane) return process.exit(0);
|
|
2107
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2108
|
+
const client = await makeClient(cfg);
|
|
2109
|
+
const { data } = await client.POST("/continuity/resume/lane", {
|
|
2110
|
+
body: {
|
|
2111
|
+
laneId: lane,
|
|
2112
|
+
workspaceId: null,
|
|
2113
|
+
maxArtifacts: opts.maxArtifacts != null ? Number(opts.maxArtifacts) : null,
|
|
2114
|
+
includeLookingAtMyself: null,
|
|
2115
|
+
changedSince: null
|
|
2116
|
+
}
|
|
2117
|
+
});
|
|
2118
|
+
const context = formatContext(data, lane);
|
|
2119
|
+
if (context) emitSessionStart(context);
|
|
2120
|
+
return process.exit(0);
|
|
2121
|
+
} catch {
|
|
2122
|
+
return process.exit(0);
|
|
2123
|
+
}
|
|
2124
|
+
});
|
|
2125
|
+
hook.command("pre-compact").description("Save a continuity snapshot from the agent-maintained intent file on a PreCompact hook").option("--lane <laneId>", "Override the resolved lane (else SECHROOM_LANE, else ./.sem code-lane)").option("--scope <scope>", "Snapshot scope (else the intent file's `scope`, else 'compaction')").option("--surface <surface>", "Target surface: claude | codex (lifecycle-only on both)", "claude").action(async (opts, cmd) => {
|
|
2126
|
+
try {
|
|
2127
|
+
const raw = await readStdin();
|
|
2128
|
+
const input = parseHookInput(raw);
|
|
2129
|
+
const cwd = input.cwd ?? process.cwd();
|
|
2130
|
+
const lane = resolveLane(opts.lane, input.cwd);
|
|
2131
|
+
if (!lane) return process.exit(0);
|
|
2132
|
+
const intent = readIntent(cwd);
|
|
2133
|
+
if (!intent || !hasRequiredIntent(intent)) return process.exit(0);
|
|
2134
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2135
|
+
const client = await makeClient(cfg);
|
|
2136
|
+
await client.POST("/continuity/snapshots", {
|
|
2137
|
+
body: {
|
|
2138
|
+
laneId: lane,
|
|
2139
|
+
scope: opts.scope ?? intent.scope ?? "compaction",
|
|
2140
|
+
currentObjective: intent.objective,
|
|
2141
|
+
currentState: intent.state,
|
|
2142
|
+
lastMeaningfulAction: intent.lastAction,
|
|
2143
|
+
nextIntendedAction: intent.nextAction,
|
|
2144
|
+
resumeInstruction: intent.resumeInstruction,
|
|
2145
|
+
activeConstraints: intent.constraints ?? null,
|
|
2146
|
+
openQuestions: intent.questions ?? null,
|
|
2147
|
+
surfaceMarkers: intent.surfaceMarkers ?? null,
|
|
2148
|
+
relevantArtifactIds: intent.artifacts ?? null,
|
|
2149
|
+
confidence: intent.confidence ?? null,
|
|
2150
|
+
// Compaction is infrequent, so the FR-051 clobber guard doesn't bite;
|
|
2151
|
+
// Acknowledge lets a within-window checkpoint land on the lane.
|
|
2152
|
+
concurrentSessionPolicy: "Acknowledge"
|
|
2153
|
+
}
|
|
2154
|
+
});
|
|
2155
|
+
return process.exit(0);
|
|
2156
|
+
} catch {
|
|
2157
|
+
return process.exit(0);
|
|
2158
|
+
}
|
|
2159
|
+
});
|
|
2160
|
+
hook.command("install").description("Wire the session-start + pre-compact hooks into Claude Code and/or Codex config").option("--surface <surface>", "Target surface: claude | codex | both (default: auto-detect installed surfaces)").option("--local", "Claude Code only: write <cwd>/.claude/settings.json instead of ~/.claude/settings.json").option("--dry-run", "Print what would change; write nothing").action((opts) => {
|
|
2161
|
+
const dryRun = Boolean(opts.dryRun);
|
|
2162
|
+
const cwd = process.cwd();
|
|
2163
|
+
let surfaces;
|
|
2164
|
+
try {
|
|
2165
|
+
surfaces = resolveSurfaces(opts.surface, cwd);
|
|
2166
|
+
} catch (err2) {
|
|
2167
|
+
process.stderr.write(`${err2.message}
|
|
2168
|
+
`);
|
|
2169
|
+
return process.exit(2);
|
|
2170
|
+
}
|
|
2171
|
+
const results = [];
|
|
2172
|
+
try {
|
|
2173
|
+
const installed = installHookSurfaces(surfaces, { dryRun, local: opts.local, cwd, home: homedir3() });
|
|
2174
|
+
for (const { surface, results: surfaceResults } of installed) {
|
|
2175
|
+
process.stdout.write(`${HOOK_SURFACE_LABEL[surface]}:
|
|
2176
|
+
`);
|
|
2177
|
+
for (const r of surfaceResults) {
|
|
2178
|
+
results.push(r);
|
|
2179
|
+
process.stdout.write(describe(r, dryRun) + "\n");
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
} catch (err2) {
|
|
2183
|
+
process.stderr.write(`hook install failed: ${err2.message}
|
|
2184
|
+
`);
|
|
2185
|
+
return process.exit(1);
|
|
2186
|
+
}
|
|
2187
|
+
if (dryRun) {
|
|
2188
|
+
process.stdout.write("\n(dry run \u2014 no files were written.)\n");
|
|
2189
|
+
} else if (results.every((r) => r.status === "current")) {
|
|
2190
|
+
process.stdout.write("\nAlready up to date \u2014 nothing to change.\n");
|
|
2191
|
+
} else {
|
|
2192
|
+
process.stdout.write("\nRestart (or reload) your agent for the hooks to take effect.\n");
|
|
2193
|
+
}
|
|
2194
|
+
warnIfSechroomNotOnPath();
|
|
2195
|
+
return process.exit(0);
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// src/commands/account.ts
|
|
2200
|
+
function registerId(program2) {
|
|
2201
|
+
const id = program2.command("id").description("Allocate human-authored id sequences (FR-*, D-*)");
|
|
2202
|
+
id.addHelpText(
|
|
2203
|
+
"after",
|
|
2204
|
+
`
|
|
2205
|
+
Examples:
|
|
2206
|
+
$ sechroom id next FR sechroom allocate the next FR-sechroom-NNN id
|
|
2207
|
+
$ sechroom id next D Backend allocate the next D-Backend-NNN id
|
|
2208
|
+
$ sechroom id peek FR sechroom inspect the sequence without consuming
|
|
2209
|
+
$ sechroom id peek FR sechroom --json`
|
|
2210
|
+
);
|
|
2211
|
+
id.command("next <namespaceKind> <scope>").description("Allocate the next id in a sequence (POST /id-registry/allocate)").action(async (namespaceKind, scope, _opts, cmd) => {
|
|
2212
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2213
|
+
const data = await runApi("Allocating id", async () => {
|
|
2214
|
+
const client = await makeClient(cfg);
|
|
2215
|
+
return client.POST("/id-registry/allocate", {
|
|
2216
|
+
body: { namespaceKind, scope, clientNonce: null }
|
|
2217
|
+
});
|
|
2218
|
+
});
|
|
2219
|
+
emitAction(`allocated ${style.bold(data.id)} ${style.dim(`(seq ${data.seq})`)}`, data, cmd.optsWithGlobals().json);
|
|
2220
|
+
});
|
|
2221
|
+
id.command("peek <namespaceKind> <scope>").description("Inspect a sequence without consuming (GET /id-registry/state)").action(async (namespaceKind, scope, _opts, cmd) => {
|
|
2222
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2223
|
+
const data = await runApi("Peeking id sequence", async () => {
|
|
2224
|
+
const client = await makeClient(cfg);
|
|
2225
|
+
return client.GET("/id-registry/state", {
|
|
2226
|
+
params: { query: { namespaceKind, scope } }
|
|
2227
|
+
});
|
|
2228
|
+
});
|
|
2229
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
function registerAccount(program2) {
|
|
2233
|
+
const account = program2.command("account").description("Your profile, feeds, and review queue");
|
|
2234
|
+
account.addHelpText(
|
|
2235
|
+
"after",
|
|
2236
|
+
`
|
|
2237
|
+
Examples:
|
|
2238
|
+
$ sechroom account profile
|
|
2239
|
+
$ sechroom account set-profile --display-name "Chris" --timezone "Europe/London"
|
|
2240
|
+
$ sechroom account feed --limit 20
|
|
2241
|
+
$ sechroom account reviews --status Pending
|
|
2242
|
+
$ sechroom account lookup-batch mem_XXXX wsp_YYYY --json`
|
|
2243
|
+
);
|
|
2244
|
+
account.command("profile").description("Show your resolved profile (GET /me/profile)").action(async (_opts, cmd) => {
|
|
2245
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2246
|
+
const data = await runApi("Fetching profile", async () => {
|
|
2247
|
+
const client = await makeClient(cfg);
|
|
2248
|
+
return client.GET("/me/profile");
|
|
2249
|
+
});
|
|
2250
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
2251
|
+
});
|
|
2252
|
+
account.command("set-profile").description("Update your profile (PUT /me/profile)").option("--display-name <displayName>", "Display name").option("--timezone <timezone>", "IANA timezone (e.g. Europe/London)").option("--bio <bio>", "Short bio").option("--photo-url <photoUrl>", "Avatar / photo URL").action(async (opts, cmd) => {
|
|
2253
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2254
|
+
const data = await runApi("Updating profile", async () => {
|
|
2255
|
+
const client = await makeClient(cfg);
|
|
2256
|
+
return client.PUT("/me/profile", {
|
|
2257
|
+
body: {
|
|
2258
|
+
displayName: opts.displayName ?? null,
|
|
2259
|
+
timezone: opts.timezone ?? null,
|
|
2260
|
+
bio: opts.bio ?? null,
|
|
2261
|
+
photoUrl: opts.photoUrl ?? null
|
|
2262
|
+
}
|
|
2263
|
+
});
|
|
2264
|
+
});
|
|
2265
|
+
emitAction("updated profile", data, cmd.optsWithGlobals().json);
|
|
2266
|
+
});
|
|
2267
|
+
account.command("feed").description("Your recent memory feed (GET /me/memories/feed)").option("--limit <n>", "Max results", "20").option("--cursor <cursor>", "Opaque paging cursor").option("--query <query>", "Free-text filter").option("--filter-tags <tags>", "Comma-separated tag filter").option("--include-archived", "Include archived memories", false).option("--include-text", "Include memory body text", false).action(async (opts, cmd) => {
|
|
2268
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2269
|
+
const data = await runApi("Fetching feed", async () => {
|
|
2270
|
+
const client = await makeClient(cfg);
|
|
2271
|
+
return client.GET("/me/memories/feed", {
|
|
2272
|
+
params: {
|
|
2273
|
+
query: {
|
|
2274
|
+
limit: Number(opts.limit),
|
|
2275
|
+
includeArchived: Boolean(opts.includeArchived),
|
|
2276
|
+
includeText: Boolean(opts.includeText),
|
|
2277
|
+
...opts.cursor ? { cursor: opts.cursor } : {},
|
|
2278
|
+
...opts.query ? { query: opts.query } : {},
|
|
2279
|
+
...opts.filterTags ? { filterTags: opts.filterTags } : {}
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
});
|
|
2283
|
+
});
|
|
2284
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
2285
|
+
});
|
|
2286
|
+
account.command("reviews").description("Your review queue (GET /reviews)").option("--status <status>", "Pending | Resolved | Empty").option("--scope-kind <scopeKind>", "Memory | Project | Workspace").option("--scope-target-id <id>", "Scope target id").option("--limit <n>", "Max results", "20").action(async (opts, cmd) => {
|
|
2287
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2288
|
+
const data = await runApi("Fetching reviews", async () => {
|
|
2289
|
+
const client = await makeClient(cfg);
|
|
2290
|
+
return client.GET("/reviews", {
|
|
2291
|
+
params: {
|
|
2292
|
+
query: {
|
|
2293
|
+
limit: Number(opts.limit),
|
|
2294
|
+
...opts.status ? { status: opts.status } : {},
|
|
2295
|
+
...opts.scopeKind ? { scopeKind: opts.scopeKind } : {},
|
|
2296
|
+
...opts.scopeTargetId ? { scopeTargetId: opts.scopeTargetId } : {}
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
});
|
|
2300
|
+
});
|
|
2301
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
2302
|
+
});
|
|
2303
|
+
account.command("review-get <reviewId>").description("Fetch one review bundle (GET /reviews/{reviewId})").action(async (reviewId, _opts, cmd) => {
|
|
2304
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2305
|
+
const data = await runApi("Fetching review", async () => {
|
|
2306
|
+
const client = await makeClient(cfg);
|
|
2307
|
+
return client.GET("/reviews/{reviewId}", { params: { path: { reviewId } } });
|
|
2308
|
+
});
|
|
2309
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
2310
|
+
});
|
|
2311
|
+
account.command("review-accept <reviewId>").description("Accept a review bundle (POST /reviews/{reviewId}/accept)").action(async (reviewId, _opts, cmd) => {
|
|
2312
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2313
|
+
const data = await runApi("Accepting review", async () => {
|
|
2314
|
+
const client = await makeClient(cfg);
|
|
2315
|
+
return client.POST("/reviews/{reviewId}/accept", {
|
|
2316
|
+
params: { path: { reviewId } },
|
|
2317
|
+
body: { decisions: {} }
|
|
2318
|
+
});
|
|
2319
|
+
});
|
|
2320
|
+
emitAction(`accepted review ${style.bold(reviewId)}`, data, cmd.optsWithGlobals().json);
|
|
2321
|
+
});
|
|
2322
|
+
account.command("lookup-batch <ids...>").description("Resolve many ids at once (POST /lookup/batch)").action(async (ids, _opts, cmd) => {
|
|
2323
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2324
|
+
const data = await runApi(`Resolving ${ids.length} ids`, async () => {
|
|
2325
|
+
const client = await makeClient(cfg);
|
|
2326
|
+
return client.POST("/lookup/batch", { body: { ids } });
|
|
2327
|
+
});
|
|
2328
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
// src/commands/chat.ts
|
|
2333
|
+
function registerChat(program2) {
|
|
2334
|
+
const chat = program2.command("chat").description("Send and read Slack / Discord channel messages").option("--surface <surface>", "slack | discord", "slack");
|
|
2335
|
+
chat.addHelpText(
|
|
2336
|
+
"after",
|
|
2337
|
+
`
|
|
2338
|
+
Examples:
|
|
2339
|
+
$ sechroom chat send C0123456789 "deploy is green" --surface slack
|
|
2340
|
+
$ sechroom chat send 987654321098765432 "deploy is green" --surface discord --guild 123456789012345678
|
|
2341
|
+
$ sechroom chat send C0123456789 "lgtm" --surface slack --as user --parent 1718049600.123456
|
|
2342
|
+
$ sechroom chat messages --surface slack
|
|
2343
|
+
$ sechroom chat replies 1718049600.123456 --surface slack
|
|
2344
|
+
$ sechroom chat stop-tracking 1718049600.123456 --surface slack`
|
|
2345
|
+
);
|
|
2346
|
+
chat.command("send <channelId> <text>").description("Send a message to a channel (POST /chat/channel-messages/{surface})").option("--guild <guildId>", "Discord guild snowflake \u2014 required for --surface discord").option("--memory <memoryId>", "Attach a sechroom memory id").option("--no-track", "Don't capture replies to this message").option("--parent <parentMessage>", "Thread under a parent (Slack thread_ts / Discord message id)").option("--source <source>", "Source / lane stamp (renders an attribution footer)", "cli").option("--as <as>", "Slack only: 'bot' (default) or 'user' (your linked Slack identity)", "bot").action(async (channelId, text, opts, cmd) => {
|
|
2347
|
+
const { surface, ...globals } = cmd.optsWithGlobals();
|
|
2348
|
+
const json = Boolean(cmd.optsWithGlobals().json);
|
|
2349
|
+
const cfg = resolveConfig(globals);
|
|
2350
|
+
const data = await runApi("Sending message", async () => {
|
|
2351
|
+
const client = await makeClient(cfg);
|
|
2352
|
+
return client.POST("/chat/channel-messages/{surface}", {
|
|
2353
|
+
params: { path: { surface: String(surface) } },
|
|
2354
|
+
body: {
|
|
2355
|
+
channelId,
|
|
2356
|
+
text,
|
|
2357
|
+
guildId: opts.guild ?? null,
|
|
2358
|
+
attachedMemoryId: opts.memory ?? null,
|
|
2359
|
+
trackReplies: opts.track,
|
|
2360
|
+
parentMessage: opts.parent ?? null,
|
|
2361
|
+
source: opts.source,
|
|
2362
|
+
as: opts.as
|
|
2363
|
+
}
|
|
2364
|
+
});
|
|
2365
|
+
});
|
|
2366
|
+
if (!data.ok) {
|
|
2367
|
+
if (json) {
|
|
2368
|
+
emit(data, true);
|
|
2369
|
+
} else {
|
|
2370
|
+
process.stderr.write(
|
|
2371
|
+
`${err("\u2717")} send failed: ${data.upstreamError ?? "error"}${data.errorDescription ? ` \u2014 ${data.errorDescription}` : ""}
|
|
2372
|
+
`
|
|
2373
|
+
);
|
|
2374
|
+
}
|
|
2375
|
+
process.exit(1);
|
|
2376
|
+
}
|
|
2377
|
+
const idPart = data.persistedId ? ` ${style.dim(`(${data.persistedId})`)}` : "";
|
|
2378
|
+
emitAction(`sent to ${surface} ${style.bold(channelId)}${idPart}`, data, json);
|
|
2379
|
+
});
|
|
2380
|
+
chat.command("messages").description("List recent channel messages (GET /chat/channel-messages/{surface})").action(async (_opts, cmd) => {
|
|
2381
|
+
const { surface, ...globals } = cmd.optsWithGlobals();
|
|
2382
|
+
const cfg = resolveConfig(globals);
|
|
2383
|
+
const data = await runApi("Fetching messages", async () => {
|
|
2384
|
+
const client = await makeClient(cfg);
|
|
2385
|
+
return client.GET("/chat/channel-messages/{surface}", {
|
|
2386
|
+
params: { path: { surface: String(surface) } }
|
|
2387
|
+
});
|
|
2388
|
+
});
|
|
2389
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
2390
|
+
});
|
|
2391
|
+
chat.command("replies <messageId>").description("List thread replies for a message (GET /chat/channel-messages/by-id/{id}/replies)").action(async (messageId, _opts, cmd) => {
|
|
2392
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2393
|
+
const data = await runApi("Fetching replies", async () => {
|
|
2394
|
+
const client = await makeClient(cfg);
|
|
2395
|
+
return client.GET("/chat/channel-messages/by-id/{id}/replies", {
|
|
2396
|
+
params: { path: { id: messageId } }
|
|
2397
|
+
});
|
|
2398
|
+
});
|
|
2399
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
2400
|
+
});
|
|
2401
|
+
chat.command("stop-tracking <messageId>").description("Stop watching a message for replies (POST .../by-id/{id}/stop-tracking-replies)").action(async (messageId, _opts, cmd) => {
|
|
2402
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2403
|
+
const data = await runApi("Stopping reply tracking", async () => {
|
|
2404
|
+
const client = await makeClient(cfg);
|
|
2405
|
+
return client.POST("/chat/channel-messages/by-id/{id}/stop-tracking-replies", {
|
|
2406
|
+
params: { path: { id: messageId } },
|
|
2407
|
+
body: {}
|
|
2408
|
+
});
|
|
2409
|
+
});
|
|
2410
|
+
emitAction(`stopped tracking replies on ${style.bold(messageId)}`, data, cmd.optsWithGlobals().json);
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
// src/setup/apply.ts
|
|
2415
|
+
import { createHash as createHash2 } from "crypto";
|
|
2416
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
|
|
2417
|
+
import { dirname as dirname5 } from "path";
|
|
2418
|
+
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
2419
|
+
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
2420
|
+
function normalizeBody(s) {
|
|
2421
|
+
return s.replace(/\r\n/g, "\n").trim();
|
|
2422
|
+
}
|
|
2423
|
+
function bodySha256(body) {
|
|
2424
|
+
return createHash2("sha256").update(normalizeBody(body), "utf8").digest("hex");
|
|
2425
|
+
}
|
|
2426
|
+
function renderBlock(write) {
|
|
2427
|
+
const body = normalizeBody(write.body);
|
|
2428
|
+
const attrs = [`block=${write.block}`];
|
|
2429
|
+
if (write.source) attrs.push(`source=${write.source}`);
|
|
2430
|
+
attrs.push(`sha256=${bodySha256(body)}`);
|
|
2431
|
+
return `${MARKER_BEGIN} ${attrs.join(" ")} -->
|
|
2432
|
+
${body}
|
|
2433
|
+
${MARKER_END} block=${write.block} -->
|
|
2434
|
+
`;
|
|
2435
|
+
}
|
|
2436
|
+
function keyedBlockRe(block) {
|
|
2437
|
+
const b = escapeRe(block);
|
|
2438
|
+
return new RegExp(
|
|
2439
|
+
`${escapeRe(MARKER_BEGIN)}[^\\n]*?\\bblock=${b}\\b[^\\n]*?-->\\n[\\s\\S]*?${escapeRe(MARKER_END)}[^\\n]*?\\bblock=${b}\\b[^\\n]*?-->\\n?`
|
|
2440
|
+
);
|
|
2441
|
+
}
|
|
2442
|
+
function legacyBlockRe() {
|
|
2443
|
+
return new RegExp(
|
|
2444
|
+
`${escapeRe(MARKER_BEGIN)}(?:(?!block=)[^\\n])*?-->\\n[\\s\\S]*?${escapeRe(MARKER_END)}(?:(?!block=)[^\\n])*?-->\\n?`
|
|
2445
|
+
);
|
|
2446
|
+
}
|
|
2447
|
+
function parseAttrs(beginLine) {
|
|
2448
|
+
const attrs = {};
|
|
2449
|
+
for (const m of beginLine.matchAll(/(\w+)=(\S+)/g)) attrs[m[1]] = m[2];
|
|
2450
|
+
return attrs;
|
|
2451
|
+
}
|
|
2452
|
+
function innerBody(segment) {
|
|
2453
|
+
const firstNl = segment.indexOf("\n");
|
|
2454
|
+
const endIdx = segment.lastIndexOf(MARKER_END);
|
|
2455
|
+
return segment.slice(firstNl + 1, endIdx).replace(/\n$/, "");
|
|
2456
|
+
}
|
|
2457
|
+
function parseManagedBlock(content, block) {
|
|
2458
|
+
const keyed = content.match(keyedBlockRe(block));
|
|
2459
|
+
if (keyed) {
|
|
2460
|
+
const attrs = parseAttrs(keyed[0].slice(0, keyed[0].indexOf("\n")));
|
|
2461
|
+
return { block, source: attrs.source ?? null, sha256: attrs.sha256 ?? null, body: innerBody(keyed[0]) };
|
|
2462
|
+
}
|
|
2463
|
+
if (block === "role-template") {
|
|
2464
|
+
const legacy = content.match(legacyBlockRe());
|
|
2465
|
+
if (legacy) return { block, source: null, sha256: null, body: innerBody(legacy[0]) };
|
|
2466
|
+
}
|
|
2467
|
+
return null;
|
|
2468
|
+
}
|
|
2469
|
+
function ensureDir2(path) {
|
|
2470
|
+
mkdirSync4(dirname5(path), { recursive: true });
|
|
2471
|
+
}
|
|
2472
|
+
function readOr(path, fallback) {
|
|
2473
|
+
try {
|
|
2474
|
+
return readFileSync4(path, "utf8");
|
|
2475
|
+
} catch {
|
|
2476
|
+
return fallback;
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
function mergeMcpJson(path, snippet, dryRun) {
|
|
2480
|
+
const incoming = JSON.parse(snippet);
|
|
2481
|
+
const existed = existsSync5(path);
|
|
2482
|
+
let current = {};
|
|
2483
|
+
if (existed) {
|
|
2484
|
+
try {
|
|
2485
|
+
current = JSON.parse(readFileSync4(path, "utf8"));
|
|
2486
|
+
} catch {
|
|
2487
|
+
return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
|
|
1828
2488
|
}
|
|
1829
2489
|
}
|
|
1830
2490
|
current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
|
|
1831
2491
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
1832
2492
|
ensureDir2(path);
|
|
1833
|
-
|
|
2493
|
+
writeFileSync4(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
|
|
1834
2494
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
1835
2495
|
}
|
|
1836
2496
|
function mergeCodexToml(path, snippet, dryRun) {
|
|
1837
|
-
const existed =
|
|
2497
|
+
const existed = existsSync5(path);
|
|
1838
2498
|
let body = readOr(path, "");
|
|
1839
2499
|
body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
|
|
1840
2500
|
const trimmed = body.trim();
|
|
1841
2501
|
const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
|
|
1842
2502
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
1843
2503
|
ensureDir2(path);
|
|
1844
|
-
|
|
2504
|
+
writeFileSync4(path, next, { mode: 384 });
|
|
1845
2505
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
1846
2506
|
}
|
|
1847
|
-
function writeInstructionBlock(path,
|
|
1848
|
-
const
|
|
1849
|
-
|
|
1850
|
-
${BLOCK_END}
|
|
1851
|
-
`;
|
|
1852
|
-
const existed = existsSync2(path);
|
|
1853
|
-
const current = readOr(path, "");
|
|
1854
|
-
let next;
|
|
1855
|
-
const re = new RegExp(`${escapeRe(BLOCK_BEGIN)}[\\s\\S]*?${escapeRe(BLOCK_END)}\\n?`);
|
|
1856
|
-
if (re.test(current)) {
|
|
1857
|
-
next = current.replace(re, block);
|
|
1858
|
-
} else {
|
|
1859
|
-
next = current.trim().length > 0 ? `${current.trimEnd()}
|
|
1860
|
-
|
|
1861
|
-
${block}` : block;
|
|
1862
|
-
}
|
|
2507
|
+
function writeInstructionBlock(path, write, dryRun) {
|
|
2508
|
+
const existed = existsSync5(path);
|
|
2509
|
+
const next = computeBlockFile(readOr(path, ""), write);
|
|
1863
2510
|
if (dryRun) return { kind: "instruction", path, status: "dry-run" };
|
|
1864
2511
|
ensureDir2(path);
|
|
1865
|
-
|
|
2512
|
+
writeFileSync4(path, next);
|
|
1866
2513
|
return { kind: "instruction", path, status: existed ? "merged" : "created" };
|
|
1867
2514
|
}
|
|
2515
|
+
function computeBlockFile(current, write) {
|
|
2516
|
+
const rendered = renderBlock(write);
|
|
2517
|
+
const keyed = keyedBlockRe(write.block);
|
|
2518
|
+
if (keyed.test(current)) return current.replace(keyed, rendered);
|
|
2519
|
+
if (write.block === "role-template" && legacyBlockRe().test(current)) {
|
|
2520
|
+
return current.replace(legacyBlockRe(), rendered);
|
|
2521
|
+
}
|
|
2522
|
+
return current.trim().length > 0 ? `${current.trimEnd()}
|
|
2523
|
+
|
|
2524
|
+
${rendered}` : rendered;
|
|
2525
|
+
}
|
|
2526
|
+
function evaluateBlock(content, block, serverBody) {
|
|
2527
|
+
const onDisk = parseManagedBlock(content, block);
|
|
2528
|
+
if (!onDisk) return "absent";
|
|
2529
|
+
const actual = bodySha256(onDisk.body);
|
|
2530
|
+
if (onDisk.sha256 && actual !== onDisk.sha256) return "drift";
|
|
2531
|
+
return actual === bodySha256(serverBody) ? "current" : "stale";
|
|
2532
|
+
}
|
|
2533
|
+
function applyBlock(path, write, mode, dryRun) {
|
|
2534
|
+
const current = readOr(path, "");
|
|
2535
|
+
const state = evaluateBlock(current, write.block, write.body);
|
|
2536
|
+
if (mode === "check") {
|
|
2537
|
+
return {
|
|
2538
|
+
kind: "instruction",
|
|
2539
|
+
path,
|
|
2540
|
+
status: state === "current" ? "current" : "skipped",
|
|
2541
|
+
eval: state,
|
|
2542
|
+
note: state === "current" ? void 0 : `would ${state === "absent" ? "write" : "refresh"} (${state})`
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
if (state === "current") {
|
|
2546
|
+
return { kind: "instruction", path, status: "current", eval: "current" };
|
|
2547
|
+
}
|
|
2548
|
+
if (state === "drift" && mode !== "force") {
|
|
2549
|
+
const proposedPath = `${path}.proposed`;
|
|
2550
|
+
const next = computeBlockFile(current, write);
|
|
2551
|
+
if (!dryRun) {
|
|
2552
|
+
ensureDir2(proposedPath);
|
|
2553
|
+
writeFileSync4(proposedPath, next);
|
|
2554
|
+
}
|
|
2555
|
+
return {
|
|
2556
|
+
kind: "instruction",
|
|
2557
|
+
path,
|
|
2558
|
+
status: "skipped",
|
|
2559
|
+
eval: "drift",
|
|
2560
|
+
proposedPath,
|
|
2561
|
+
note: `local edits \u2014 wrote ${proposedPath} (original left untouched)`
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
const action = writeInstructionBlock(path, write, dryRun);
|
|
2565
|
+
const note = state === "stale" ? "refreshed" : state === "drift" ? "overwrote local edits" : void 0;
|
|
2566
|
+
return { ...action, eval: state, note: note ?? action.note };
|
|
2567
|
+
}
|
|
1868
2568
|
function escapeRe(s) {
|
|
1869
2569
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1870
2570
|
}
|
|
1871
2571
|
async function applyClient(cfg, setup, target, opts) {
|
|
1872
2572
|
const actions = [];
|
|
2573
|
+
const mode = opts.mode ?? "apply";
|
|
2574
|
+
const dryRun = opts.dryRun || mode === "check";
|
|
1873
2575
|
if (opts.mcp && target.mcp) {
|
|
1874
2576
|
const surface = findSurface(setup, target.mcp.surfaceKey);
|
|
1875
2577
|
const section = findSection(surface, target.mcp.sectionType);
|
|
@@ -1878,7 +2580,7 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
1878
2580
|
actions.push({ kind: "mcp", path: target.mcp.path, status: "skipped", note: `no ${target.mcp.sectionType} section on surface '${target.mcp.surfaceKey}'` });
|
|
1879
2581
|
} else {
|
|
1880
2582
|
actions.push(
|
|
1881
|
-
target.mcp.format === "toml" ? mergeCodexToml(target.mcp.path, snippet,
|
|
2583
|
+
target.mcp.format === "toml" ? mergeCodexToml(target.mcp.path, snippet, dryRun) : mergeMcpJson(target.mcp.path, snippet, dryRun)
|
|
1882
2584
|
);
|
|
1883
2585
|
}
|
|
1884
2586
|
}
|
|
@@ -1892,67 +2594,206 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
1892
2594
|
if (!resolved) {
|
|
1893
2595
|
actions.push({ kind: "instruction", path: target.instruction.path, status: "skipped", note: "no role template found in this tenant \u2014 install the SEM Starter bundle, then re-run `sechroom setup agent-files`" });
|
|
1894
2596
|
} else {
|
|
1895
|
-
const action =
|
|
1896
|
-
|
|
2597
|
+
const action = applyBlock(
|
|
2598
|
+
target.instruction.path,
|
|
2599
|
+
{ block: "role-template", body: resolved.body, source: resolved.sourceRef },
|
|
2600
|
+
mode,
|
|
2601
|
+
opts.dryRun
|
|
2602
|
+
);
|
|
2603
|
+
actions.push(resolved.source === "override" && action.status !== "current" ? { ...action, note: action.note ?? "your personal copy" } : action);
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
const conventionsSection = findSection(surface, SectionType.WorkspaceConventions);
|
|
2607
|
+
if (conventionsSection) {
|
|
2608
|
+
const conventions = await resolveWorkspaceConventions(cfg, conventionsSection);
|
|
2609
|
+
if (conventions) {
|
|
2610
|
+
const action = applyBlock(
|
|
2611
|
+
target.instruction.path,
|
|
2612
|
+
{ block: "workspace-conventions", body: conventions.body, source: `workspace:${cfg.workspaceId ?? ""}` },
|
|
2613
|
+
mode,
|
|
2614
|
+
opts.dryRun
|
|
2615
|
+
);
|
|
2616
|
+
actions.push(action.status === "current" ? action : { ...action, note: action.note ?? `workspace conventions (${conventions.refs.length})` });
|
|
1897
2617
|
}
|
|
1898
2618
|
}
|
|
1899
2619
|
}
|
|
1900
2620
|
return actions;
|
|
1901
2621
|
}
|
|
1902
2622
|
|
|
1903
|
-
// src/setup/
|
|
1904
|
-
import {
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
2623
|
+
// src/setup/hooks-offer.ts
|
|
2624
|
+
import { homedir as homedir4 } from "os";
|
|
2625
|
+
async function maybeOfferHooks(opts) {
|
|
2626
|
+
if (opts.dryRun) return;
|
|
2627
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
2628
|
+
const surfaces = detectHookSurfaces(cwd);
|
|
2629
|
+
if (surfaces.length === 0) return;
|
|
2630
|
+
const names = surfaces.map((s) => HOOK_SURFACE_LABEL[s]).join(" + ");
|
|
2631
|
+
process.stderr.write(
|
|
2632
|
+
`
|
|
2633
|
+
Sechroom can wire continuity lifecycle hooks into ${style.bold(names)} so your agent
|
|
2634
|
+
auto-resumes where you left off and checkpoints working state before compacting.
|
|
2635
|
+
`
|
|
2636
|
+
);
|
|
2637
|
+
const install = opts.yes ? true : canPrompt() ? await promptYesNo(`Install the continuity hooks for ${names}?`) : false;
|
|
2638
|
+
if (!install) return;
|
|
2639
|
+
try {
|
|
2640
|
+
const installed = installHookSurfaces(surfaces, { dryRun: false, cwd, home: homedir4() });
|
|
2641
|
+
let changed = false;
|
|
2642
|
+
for (const { surface, results } of installed) {
|
|
2643
|
+
for (const r of results) {
|
|
2644
|
+
if (r.status !== "current") changed = true;
|
|
2645
|
+
const verb = r.status === "current" ? "already configured" : r.status === "created" ? "created" : "updated";
|
|
2646
|
+
process.stderr.write(`${style.green("\u2713")} ${HOOK_SURFACE_LABEL[surface]}: ${r.path} (${verb})
|
|
2647
|
+
`);
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
if (changed) {
|
|
2651
|
+
process.stderr.write(`${style.dim("Restart (or reload) your agent for the hooks to take effect.")}
|
|
2652
|
+
`);
|
|
2653
|
+
}
|
|
2654
|
+
warnIfSechroomNotOnPath();
|
|
2655
|
+
} catch (err2) {
|
|
2656
|
+
process.stderr.write(`${style.dim(`(skipped hook install: ${err2.message})`)}
|
|
2657
|
+
`);
|
|
1915
2658
|
}
|
|
1916
2659
|
}
|
|
1917
|
-
|
|
1918
|
-
|
|
2660
|
+
|
|
2661
|
+
// src/setup/skills-offer.ts
|
|
2662
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
2663
|
+
import { homedir as homedir5 } from "os";
|
|
2664
|
+
import { join as join5 } from "path";
|
|
2665
|
+
|
|
2666
|
+
// src/setup/lane-pin.ts
|
|
2667
|
+
var CODE_LANE_PREFIX_BY_CLIENT = {
|
|
2668
|
+
"claude-code": "claude-code",
|
|
2669
|
+
"claude-desktop": "claude-code",
|
|
2670
|
+
cursor: "claude-code",
|
|
2671
|
+
codex: "codex"
|
|
2672
|
+
};
|
|
2673
|
+
var CLIENT_PRIORITY = ["claude-code", "claude-desktop", "cursor", "codex"];
|
|
2674
|
+
function handleFromDisplayName(name) {
|
|
2675
|
+
if (!name) return void 0;
|
|
2676
|
+
const localPart = name.trim().split("@")[0] ?? "";
|
|
2677
|
+
const first = localPart.split(/[\s._-]+/)[0]?.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
2678
|
+
return first || void 0;
|
|
2679
|
+
}
|
|
2680
|
+
function codeLanePrefix(clients) {
|
|
2681
|
+
for (const c of CLIENT_PRIORITY) if (clients.includes(c)) return CODE_LANE_PREFIX_BY_CLIENT[c];
|
|
2682
|
+
return "claude-code";
|
|
2683
|
+
}
|
|
2684
|
+
async function inferLanes(cfg, clients) {
|
|
2685
|
+
let wf;
|
|
2686
|
+
let profile;
|
|
2687
|
+
try {
|
|
2688
|
+
const client = await makeClient(cfg);
|
|
2689
|
+
[wf, profile] = await Promise.all([
|
|
2690
|
+
client.GET("/me/workflow-preferences", {}).then((r) => r.data).catch(() => void 0),
|
|
2691
|
+
client.GET("/me/profile", {}).then((r) => r.data).catch(() => void 0)
|
|
2692
|
+
]);
|
|
2693
|
+
} catch {
|
|
2694
|
+
}
|
|
2695
|
+
const handle = handleFromDisplayName(profile?.effectiveDisplayName);
|
|
2696
|
+
const prefix = codeLanePrefix(clients ?? ["claude-code"]);
|
|
1919
2697
|
return {
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
label: "Claude Code",
|
|
1923
|
-
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join2(cwd, ".mcp.json"), format: "json" },
|
|
1924
|
-
instruction: { surfaceKey: "claude-code", path: join2(cwd, "CLAUDE.md") }
|
|
1925
|
-
},
|
|
1926
|
-
"claude-desktop": {
|
|
1927
|
-
key: "claude-desktop",
|
|
1928
|
-
label: "Claude Desktop",
|
|
1929
|
-
mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
|
|
1930
|
-
instruction: { surfaceKey: "claude-desktop", path: join2(home, ".claude", "CLAUDE.md") }
|
|
1931
|
-
},
|
|
1932
|
-
codex: {
|
|
1933
|
-
key: "codex",
|
|
1934
|
-
label: "Codex CLI",
|
|
1935
|
-
mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join2(home, ".codex", "config.toml"), format: "toml" },
|
|
1936
|
-
instruction: { surfaceKey: "chatgpt", path: join2(cwd, "AGENTS.md") }
|
|
1937
|
-
},
|
|
1938
|
-
cursor: {
|
|
1939
|
-
key: "cursor",
|
|
1940
|
-
label: "Cursor",
|
|
1941
|
-
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join2(cwd, ".cursor", "mcp.json"), format: "json" },
|
|
1942
|
-
instruction: { surfaceKey: "chatgpt", path: join2(cwd, "AGENTS.md") }
|
|
1943
|
-
}
|
|
2698
|
+
code: process.env.SECHROOM_CODE_LANE ?? wf?.defaultCodeLane ?? (handle ? `${prefix}-${handle}` : void 0),
|
|
2699
|
+
design: process.env.SECHROOM_DESIGN_LANE ?? wf?.defaultDesignLane ?? (handle ? `claude-design-${handle}` : void 0)
|
|
1944
2700
|
};
|
|
1945
2701
|
}
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
2702
|
+
function writePin(code, design) {
|
|
2703
|
+
const values = {};
|
|
2704
|
+
if (code) values["code-lane"] = code;
|
|
2705
|
+
if (design) values["design-lane"] = design;
|
|
2706
|
+
if (Object.keys(values).length === 0) return;
|
|
2707
|
+
const target = writeSem(values);
|
|
2708
|
+
process.stderr.write(`${ok("\u2713")} lane pin written \u2192 ${target} ${style.dim("(./.sechroom/lane.json, git-ignored)")}
|
|
2709
|
+
`);
|
|
2710
|
+
}
|
|
2711
|
+
async function ensureLanePin(cfg, opts) {
|
|
2712
|
+
if (opts.dryRun) return;
|
|
2713
|
+
if (readSem()) return;
|
|
2714
|
+
const { code: codeGuess, design: designGuess } = await inferLanes(cfg, opts.clients);
|
|
2715
|
+
if (!canPrompt() || opts.yes) {
|
|
2716
|
+
if (opts.yes && (codeGuess || designGuess)) writePin(codeGuess, designGuess);
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2719
|
+
if (codeGuess || designGuess) {
|
|
2720
|
+
process.stderr.write(
|
|
2721
|
+
`
|
|
2722
|
+
I can pin this checkout's lane so operator skills + the continuity hook resolve your identity:
|
|
2723
|
+
`
|
|
2724
|
+
);
|
|
2725
|
+
if (codeGuess) process.stderr.write(` ${style.dim("code-lane")} = ${style.cyan(codeGuess)}
|
|
2726
|
+
`);
|
|
2727
|
+
if (designGuess) process.stderr.write(` ${style.dim("design-lane")} = ${style.cyan(designGuess)}
|
|
2728
|
+
`);
|
|
2729
|
+
if (await promptYesNo("Pin these?")) {
|
|
2730
|
+
writePin(codeGuess, designGuess);
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
const code = await promptText("Code-lane id (e.g. claude-code-you, blank to skip)?", codeGuess ?? "");
|
|
2735
|
+
const design = await promptText("Design-lane id (e.g. claude-design-you, blank to skip)?", designGuess ?? "");
|
|
2736
|
+
if (!code && !design) {
|
|
2737
|
+
process.stderr.write(
|
|
2738
|
+
` ${style.dim("skipped \u2014 set later with")} ${style.cyan("sechroom skills set-lane --code-lane \u2026 --design-lane \u2026")}
|
|
2739
|
+
`
|
|
2740
|
+
);
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
writePin(code || void 0, design || void 0);
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
// src/setup/skills-offer.ts
|
|
2747
|
+
var ROLE_TAG = "sechroom:role:skill-template";
|
|
2748
|
+
function tagValue(tags, prefix) {
|
|
2749
|
+
return tags.find((t) => t.startsWith(prefix))?.slice(prefix.length);
|
|
2750
|
+
}
|
|
2751
|
+
async function maybeOfferSkills(cfg, personalWorkspaceId, opts) {
|
|
2752
|
+
if (!personalWorkspaceId || opts.dryRun) return;
|
|
2753
|
+
const surface = opts.surface ?? "claude-code";
|
|
2754
|
+
let rows = [];
|
|
2755
|
+
try {
|
|
2756
|
+
const client = await makeClient(cfg);
|
|
2757
|
+
const feed = await client.GET("/workspaces/{workspaceId}/memories/feed", {
|
|
2758
|
+
params: {
|
|
2759
|
+
path: { workspaceId: personalWorkspaceId },
|
|
2760
|
+
query: { limit: 200, cascadeWorkspaces: true, includeText: true }
|
|
2761
|
+
}
|
|
2762
|
+
}).then((r) => r.data).catch(() => void 0);
|
|
2763
|
+
rows = feed?.results ?? feed?.Results ?? [];
|
|
2764
|
+
} catch {
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
const skills = rows.map((r) => r.item ?? r).filter((m) => {
|
|
2768
|
+
const tags = m.tags ?? m.Tags ?? [];
|
|
2769
|
+
return tags.includes(ROLE_TAG) && tagValue(tags, "target:") === surface;
|
|
2770
|
+
});
|
|
2771
|
+
if (skills.length === 0) return;
|
|
2772
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2773
|
+
for (const m of skills) {
|
|
2774
|
+
const name = tagValue(m.tags ?? m.Tags ?? [], "skill:");
|
|
2775
|
+
if (name) byName.set(name, m);
|
|
2776
|
+
}
|
|
2777
|
+
const names = [...byName.keys()].sort();
|
|
2778
|
+
if (names.length === 0) return;
|
|
2779
|
+
process.stderr.write(
|
|
2780
|
+
`
|
|
2781
|
+
Found ${style.bold(String(names.length))} operator skill(s) installed in your workspace: ${names.join(", ")}.
|
|
2782
|
+
`
|
|
2783
|
+
);
|
|
2784
|
+
const dir = join5(homedir5(), ".claude", "skills");
|
|
2785
|
+
const materialise = opts.yes ? true : canPrompt() ? await promptYesNo(`Write them to ${dir}/ so ${surface} can use them?`) : false;
|
|
2786
|
+
if (!materialise) return;
|
|
2787
|
+
const written = [];
|
|
2788
|
+
for (const [name, m] of byName) {
|
|
2789
|
+
const body = m.text ?? m.Text ?? "";
|
|
2790
|
+
mkdirSync5(join5(dir, name), { recursive: true });
|
|
2791
|
+
writeFileSync5(join5(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
|
|
2792
|
+
written.push(name);
|
|
2793
|
+
}
|
|
2794
|
+
process.stderr.write(`${style.green("\u2713")} wrote ${written.length} skill(s) to ${dir}
|
|
2795
|
+
`);
|
|
2796
|
+
await ensureLanePin(cfg, { yes: opts.yes, dryRun: opts.dryRun, clients: [surface] });
|
|
1956
2797
|
}
|
|
1957
2798
|
|
|
1958
2799
|
// src/commands/setup.ts
|
|
@@ -2041,6 +2882,12 @@ Examples:
|
|
|
2041
2882
|
result.push({ client: key, actions });
|
|
2042
2883
|
if (!json) printActions(target, actions);
|
|
2043
2884
|
}
|
|
2885
|
+
if (!json && !opts.dryRun && !opts.mcpOnly) {
|
|
2886
|
+
await maybeOfferSkills(cfg, personalWorkspaceId, { yes: false, dryRun: Boolean(opts.dryRun), surface: "claude-code" });
|
|
2887
|
+
}
|
|
2888
|
+
if (!json && !opts.dryRun && !opts.mcpOnly) {
|
|
2889
|
+
await maybeOfferHooks({ yes: false, dryRun: Boolean(opts.dryRun), cwd: process.cwd() });
|
|
2890
|
+
}
|
|
2044
2891
|
if (json) {
|
|
2045
2892
|
emit({ dryRun: Boolean(opts.dryRun), clients: result }, true);
|
|
2046
2893
|
return;
|
|
@@ -2060,36 +2907,147 @@ Next \u2014 verify: ${verify.description}
|
|
|
2060
2907
|
}
|
|
2061
2908
|
function registerSetup(program2) {
|
|
2062
2909
|
const setup = program2.command("setup").description("Granular onboarding steps (init runs these together)");
|
|
2063
|
-
setup.command("mcp <
|
|
2064
|
-
await
|
|
2910
|
+
setup.command("mcp <clients...>").description(`Write only the MCP config for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`).option("--dry-run", "print what would be written without writing", false).addHelpText("after", "\nExamples:\n $ sechroom setup mcp codex\n $ sechroom setup mcp claude-code codex\n $ sechroom setup mcp all").action(async (clients, opts, cmd) => {
|
|
2911
|
+
await runClients(clients, cmd, { dryRun: Boolean(opts.dryRun), mcp: true, agentFiles: false });
|
|
2065
2912
|
});
|
|
2066
|
-
setup.command("agent-files <
|
|
2067
|
-
await
|
|
2913
|
+
setup.command("agent-files <clients...>").description(`Write only the agent instruction file(s) for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`).option("--dry-run", "print what would be written without writing", false).option("--copy", "make a personal copy you can edit (default: prompt on a TTY, else skip)").addHelpText("after", "\nExamples:\n $ sechroom setup agent-files claude-code CLAUDE.md\n $ sechroom setup agent-files claude-code codex CLAUDE.md + AGENTS.md in one run\n $ sechroom setup agent-files all").action(async (clients, opts, cmd) => {
|
|
2914
|
+
await runClients(clients, cmd, { dryRun: Boolean(opts.dryRun), mcp: false, agentFiles: true, copy: opts.copy });
|
|
2068
2915
|
});
|
|
2069
2916
|
}
|
|
2070
|
-
async function
|
|
2071
|
-
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2072
|
-
const targets = clientTargets(process.cwd());
|
|
2073
|
-
const
|
|
2074
|
-
|
|
2075
|
-
const
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2917
|
+
async function runClients(clients, cmd, opts) {
|
|
2918
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2919
|
+
const targets = clientTargets(process.cwd());
|
|
2920
|
+
const keys = resolveClientKeys(clients.join(","));
|
|
2921
|
+
const setupData = await withSpinner("Fetching setup descriptors", () => fetchSetup(cfg));
|
|
2922
|
+
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
2923
|
+
if (opts.agentFiles && !opts.dryRun) {
|
|
2924
|
+
await maybeOfferCopies(cfg, setupData, targets, keys, personalWorkspaceId, copyChoice(opts));
|
|
2925
|
+
}
|
|
2926
|
+
const json = cmd.optsWithGlobals().json;
|
|
2927
|
+
const result = [];
|
|
2928
|
+
for (const key of keys) {
|
|
2929
|
+
const target = targets[key];
|
|
2930
|
+
const actions = await applyClient(cfg, setupData, target, {
|
|
2931
|
+
dryRun: opts.dryRun,
|
|
2932
|
+
mcp: opts.mcp,
|
|
2933
|
+
agentFiles: opts.agentFiles,
|
|
2934
|
+
personalWorkspaceId
|
|
2935
|
+
});
|
|
2936
|
+
result.push({ client: key, actions });
|
|
2937
|
+
if (!json) printActions(target, actions);
|
|
2938
|
+
}
|
|
2939
|
+
if (json) {
|
|
2940
|
+
emit({ dryRun: opts.dryRun, clients: result }, true);
|
|
2941
|
+
return;
|
|
2942
|
+
}
|
|
2943
|
+
process.stdout.write(opts.dryRun ? "\n(dry run \u2014 nothing written)\n" : "\nDone.\n");
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
// src/commands/onboard.ts
|
|
2947
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2948
|
+
import { join as join7 } from "path";
|
|
2949
|
+
|
|
2950
|
+
// src/commands/fanout.ts
|
|
2951
|
+
import { spawnSync } from "child_process";
|
|
2952
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync, statSync } from "fs";
|
|
2953
|
+
import { isAbsolute, join as join6, resolve } from "path";
|
|
2954
|
+
var ICON = {
|
|
2955
|
+
refresh: "\u21BB",
|
|
2956
|
+
bind: "+",
|
|
2957
|
+
"skip-missing": "\u2013",
|
|
2958
|
+
"skip-unbound": "\u26A0"
|
|
2959
|
+
};
|
|
2960
|
+
function resolveChildDir(path, root) {
|
|
2961
|
+
return isAbsolute(path) ? path : resolve(root, path);
|
|
2962
|
+
}
|
|
2963
|
+
function discoverChildren(root) {
|
|
2964
|
+
let names;
|
|
2965
|
+
try {
|
|
2966
|
+
names = readdirSync(root);
|
|
2967
|
+
} catch {
|
|
2968
|
+
return [];
|
|
2079
2969
|
}
|
|
2080
|
-
const
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2970
|
+
const out = [];
|
|
2971
|
+
for (const name of names.sort()) {
|
|
2972
|
+
if (name.startsWith(".") || name === "node_modules") continue;
|
|
2973
|
+
const dir = join6(root, name);
|
|
2974
|
+
try {
|
|
2975
|
+
if (!statSync(dir).isDirectory()) continue;
|
|
2976
|
+
} catch {
|
|
2977
|
+
continue;
|
|
2978
|
+
}
|
|
2979
|
+
if (existsSync6(join6(dir, ".git")) || committedBindingPath(dir)) out.push(name);
|
|
2980
|
+
}
|
|
2981
|
+
return out;
|
|
2982
|
+
}
|
|
2983
|
+
function readManifest(path) {
|
|
2984
|
+
if (!existsSync6(path)) return null;
|
|
2985
|
+
let parsed;
|
|
2986
|
+
try {
|
|
2987
|
+
parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
2988
|
+
} catch (err2) {
|
|
2989
|
+
throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
2990
|
+
}
|
|
2991
|
+
return Array.isArray(parsed.repos) ? parsed.repos.filter((r) => r && typeof r.path === "string") : [];
|
|
2992
|
+
}
|
|
2993
|
+
function passthroughGlobals(g) {
|
|
2994
|
+
const out = [];
|
|
2995
|
+
if (g.baseUrl) out.push("--base-url", g.baseUrl);
|
|
2996
|
+
if (g.tenant) out.push("--tenant", g.tenant);
|
|
2997
|
+
return out;
|
|
2998
|
+
}
|
|
2999
|
+
function runChildren(plans, o) {
|
|
3000
|
+
const { globals, dryRun, json } = o;
|
|
3001
|
+
const results = [];
|
|
3002
|
+
for (const plan of plans) {
|
|
3003
|
+
const runs = plan.argv.length > 0;
|
|
3004
|
+
const argv = runs ? [...globals, ...plan.argv] : [];
|
|
3005
|
+
if (!json) {
|
|
3006
|
+
process.stderr.write(` ${ICON[plan.disposition]} ${style.cyan(plan.label)} ${style.dim(plan.reason)}
|
|
3007
|
+
`);
|
|
3008
|
+
if (runs && dryRun) process.stderr.write(` ${style.dim(`would run: sechroom ${argv.join(" ")}`)}
|
|
3009
|
+
`);
|
|
3010
|
+
}
|
|
3011
|
+
let exitCode = runs ? 0 : null;
|
|
3012
|
+
if (runs && !dryRun) {
|
|
3013
|
+
const res = spawnSync(process.execPath, [process.argv[1], ...argv], {
|
|
3014
|
+
cwd: plan.dir,
|
|
3015
|
+
stdio: json ? "ignore" : "inherit"
|
|
3016
|
+
});
|
|
3017
|
+
exitCode = res.status;
|
|
3018
|
+
if (!json) {
|
|
3019
|
+
process.stderr.write(
|
|
3020
|
+
exitCode === 0 ? ` ${ok("\u2713")} ${style.dim("onboard ok")}
|
|
3021
|
+
` : ` ${warn("\u2717")} ${style.dim(`onboard exited ${exitCode ?? "signal"}`)}
|
|
3022
|
+
`
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
results.push({
|
|
3027
|
+
path: plan.label,
|
|
3028
|
+
dir: plan.dir,
|
|
3029
|
+
disposition: plan.disposition,
|
|
3030
|
+
ran: runs && !dryRun,
|
|
3031
|
+
exitCode,
|
|
3032
|
+
reason: plan.reason
|
|
3033
|
+
});
|
|
2092
3034
|
}
|
|
3035
|
+
return results;
|
|
3036
|
+
}
|
|
3037
|
+
function summarizeFanout(results, o) {
|
|
3038
|
+
const ran = results.filter((r) => r.ran);
|
|
3039
|
+
const failed = ran.filter((r) => r.exitCode !== 0);
|
|
3040
|
+
const skipped = results.filter((r) => r.disposition.startsWith("skip"));
|
|
3041
|
+
const wouldRun = results.filter((r) => !r.disposition.startsWith("skip"));
|
|
3042
|
+
const tally = (o.dryRun ? [wouldRun.length ? `${wouldRun.length} would onboard` : null, skipped.length ? `${skipped.length} would skip` : null] : [
|
|
3043
|
+
ran.length ? `${ran.length - failed.length}/${ran.length} onboarded` : null,
|
|
3044
|
+
skipped.length ? `${skipped.length} skipped` : null,
|
|
3045
|
+
failed.length ? `${failed.length} failed` : null
|
|
3046
|
+
]).filter(Boolean).join(", ");
|
|
3047
|
+
process.stderr.write(`
|
|
3048
|
+
${failed.length ? warn("\u26A0") : ok("\u2713")} ${tally || "nothing to do"}${o.dryRun ? style.dim(" (dry run)") : ""}
|
|
3049
|
+
`);
|
|
3050
|
+
if (failed.length) process.exit(1);
|
|
2093
3051
|
}
|
|
2094
3052
|
|
|
2095
3053
|
// src/commands/onboard.ts
|
|
@@ -2101,42 +3059,197 @@ function systemTimezone() {
|
|
|
2101
3059
|
return "UTC";
|
|
2102
3060
|
}
|
|
2103
3061
|
}
|
|
2104
|
-
|
|
3062
|
+
function editDistance(a, b) {
|
|
3063
|
+
const m = a.length;
|
|
3064
|
+
const n = b.length;
|
|
3065
|
+
if (m === 0) return n;
|
|
3066
|
+
if (n === 0) return m;
|
|
3067
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
3068
|
+
for (let i = 1; i <= m; i++) {
|
|
3069
|
+
const curr = [i];
|
|
3070
|
+
for (let j = 1; j <= n; j++) {
|
|
3071
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
3072
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
3073
|
+
}
|
|
3074
|
+
prev = curr;
|
|
3075
|
+
}
|
|
3076
|
+
return prev[n];
|
|
3077
|
+
}
|
|
3078
|
+
function namesCollide(a, b) {
|
|
3079
|
+
const x = a.trim().toLowerCase();
|
|
3080
|
+
const y = b.trim().toLowerCase();
|
|
3081
|
+
return x === y || editDistance(x, y) <= 1;
|
|
3082
|
+
}
|
|
3083
|
+
function workspacePath(ws, byId) {
|
|
3084
|
+
const parts = [];
|
|
3085
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3086
|
+
let cur = ws;
|
|
3087
|
+
while (cur && !seen.has(cur.id)) {
|
|
3088
|
+
seen.add(cur.id);
|
|
3089
|
+
parts.unshift(cur.name);
|
|
3090
|
+
cur = cur.parentId ? byId.get(cur.parentId) : void 0;
|
|
3091
|
+
}
|
|
3092
|
+
return parts.join(" / ");
|
|
3093
|
+
}
|
|
3094
|
+
function resolveBaseUrl(g) {
|
|
2105
3095
|
const persisted = readPersisted();
|
|
2106
3096
|
const local = readLocalConfig();
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
3097
|
+
const baseUrl = g.baseUrl ?? process.env.SECHROOM_BASE_URL ?? local.baseUrl ?? persisted.baseUrl ?? DEFAULT_BASE_URL2;
|
|
3098
|
+
return baseUrl.replace(/\/$/, "");
|
|
3099
|
+
}
|
|
3100
|
+
async function fetchWorkspaces(client) {
|
|
3101
|
+
const { data, error } = await client.GET("/workspaces", { params: { query: { includeArchived: false } } });
|
|
3102
|
+
if (error) throw new Error(`Couldn't list your workspaces: ${JSON.stringify(error)}`);
|
|
3103
|
+
const rows = data ?? [];
|
|
3104
|
+
return rows.map((r) => r.item ?? r).filter((w) => Boolean(w?.id && w?.name)).map((w) => ({ id: w.id, name: w.name, parentId: w.parentId ?? null }));
|
|
3105
|
+
}
|
|
3106
|
+
async function lookupWorkspace(client, id) {
|
|
3107
|
+
const { data, error } = await client.GET("/workspaces/{workspaceId}", { params: { path: { workspaceId: id } } });
|
|
3108
|
+
if (error) return null;
|
|
3109
|
+
const env = data;
|
|
3110
|
+
const w = env?.item ?? env;
|
|
3111
|
+
return w?.id ? { id: w.id, name: w.name ?? id, parentId: w.parentId ?? null } : null;
|
|
3112
|
+
}
|
|
3113
|
+
async function warnIfProjectStray(client, projectId, workspaceId, json) {
|
|
3114
|
+
const { data, error } = await client.GET("/projects/{projectId}", { params: { path: { projectId } } });
|
|
3115
|
+
if (error) return;
|
|
3116
|
+
const env = data;
|
|
3117
|
+
const owner = env?.item?.workspaceId;
|
|
3118
|
+
if (owner && owner !== workspaceId && !json) {
|
|
3119
|
+
process.stderr.write(
|
|
3120
|
+
`${warn("\u26A0")} defaultProjectId ${style.dim(projectId)} belongs to a different workspace (${style.dim(owner)}), not ${style.dim(workspaceId)} \u2014 leaving it as-is.
|
|
3121
|
+
`
|
|
3122
|
+
);
|
|
2112
3123
|
}
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
3124
|
+
}
|
|
3125
|
+
async function pickWorkspace(client, promptLabel = "Bind this directory to a workspace:") {
|
|
3126
|
+
const all = await withSpinner("Listing your workspaces", () => fetchWorkspaces(client));
|
|
3127
|
+
if (all.length === 0) {
|
|
3128
|
+
process.stderr.write(`no workspaces found \u2014 skipping workspace binding (you can set it later with \`sechroom config set --local workspaceId <id>\`)
|
|
3129
|
+
`);
|
|
3130
|
+
return void 0;
|
|
3131
|
+
}
|
|
3132
|
+
const byId = new Map(all.map((w) => [w.id, w]));
|
|
3133
|
+
let pool = all;
|
|
3134
|
+
if (all.length > 12) {
|
|
3135
|
+
const q = (await promptText(`Filter ${all.length} workspaces (substring, Enter to list all)?`, "")).trim().toLowerCase();
|
|
3136
|
+
if (q) {
|
|
3137
|
+
const hits = all.filter((w) => `${w.name} ${workspacePath(w, byId)}`.toLowerCase().includes(q));
|
|
3138
|
+
if (hits.length > 0) pool = hits;
|
|
3139
|
+
else process.stderr.write(`no match for "${q}" \u2014 listing all
|
|
3140
|
+
`);
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
const SKIP = "__skip__";
|
|
3144
|
+
const choices = [
|
|
3145
|
+
...pool.slice().sort((a, b) => workspacePath(a, byId).localeCompare(workspacePath(b, byId))).map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: w.id })),
|
|
3146
|
+
{ label: style.dim("skip \u2014 don't bind a workspace"), value: SKIP, hint: void 0 }
|
|
3147
|
+
];
|
|
3148
|
+
const chosen = await promptSelect(promptLabel, choices, SKIP);
|
|
3149
|
+
if (chosen === SKIP) return void 0;
|
|
3150
|
+
const picked = byId.get(chosen);
|
|
3151
|
+
const collisions = all.filter((w) => w.id !== picked.id && namesCollide(w.name, picked.name));
|
|
3152
|
+
if (collisions.length > 0) {
|
|
3153
|
+
process.stderr.write(
|
|
3154
|
+
`${warn("\u26A0")} ${collisions.length} other workspace(s) have a similar name to ${style.cyan(workspacePath(picked, byId))}:
|
|
3155
|
+
` + collisions.map((w) => ` ${style.dim(workspacePath(w, byId))} ${style.dim(`(${w.id})`)}`).join("\n") + `
|
|
3156
|
+
You picked ${style.dim(picked.id)} \u2014 re-run \`sechroom config set --local workspaceId <id>\` if that's wrong.
|
|
3157
|
+
`
|
|
2117
3158
|
);
|
|
2118
3159
|
}
|
|
3160
|
+
return chosen;
|
|
3161
|
+
}
|
|
3162
|
+
async function resolveWorkspaceBinding(client, existing, opts) {
|
|
3163
|
+
if (opts.workspace) {
|
|
3164
|
+
const found = await lookupWorkspace(client, opts.workspace);
|
|
3165
|
+
if (!found && !opts.json) {
|
|
3166
|
+
process.stderr.write(
|
|
3167
|
+
`${warn("\u26A0")} workspace ${style.dim(opts.workspace)} not found (or you lack access) \u2014 binding it anyway.
|
|
3168
|
+
`
|
|
3169
|
+
);
|
|
3170
|
+
}
|
|
3171
|
+
return opts.workspace;
|
|
3172
|
+
}
|
|
3173
|
+
if (existing) return existing;
|
|
3174
|
+
if (!canPrompt() || opts.yes) return void 0;
|
|
3175
|
+
return pickWorkspace(client);
|
|
3176
|
+
}
|
|
3177
|
+
async function ensureTenant(baseUrl, g, opts) {
|
|
3178
|
+
const persisted = readPersisted();
|
|
3179
|
+
const local = readLocalConfig();
|
|
3180
|
+
let tenant = g.tenant ?? process.env.SECHROOM_TENANT ?? local.tenant ?? persisted.tenant ?? "";
|
|
3181
|
+
if (!tenant) {
|
|
3182
|
+
const client = await makeClient({ baseUrl, tenant: "", clientId: persisted.clientId });
|
|
3183
|
+
const { data, error } = await client.GET("/auth/me/tenants", {});
|
|
3184
|
+
if (error) {
|
|
3185
|
+
fail(`Couldn't list your tenants: ${JSON.stringify(error)}. Pass --tenant <id> to skip this.`);
|
|
3186
|
+
}
|
|
3187
|
+
const tenants = data?.tenants ?? [];
|
|
3188
|
+
if (tenants.length === 0) {
|
|
3189
|
+
fail(
|
|
3190
|
+
"You're signed in, but your account isn't a member of any tenant yet. Ask an admin to add you (or create one in the app), then re-run \u2014 or pass --tenant <id>."
|
|
3191
|
+
);
|
|
3192
|
+
} else if (tenants.length === 1) {
|
|
3193
|
+
tenant = tenants[0].key;
|
|
3194
|
+
if (!opts.json) {
|
|
3195
|
+
process.stderr.write(
|
|
3196
|
+
`${ok("\u2713")} using your tenant ${style.cyan(tenants[0].label)} ${style.dim(`(${tenant})`)}
|
|
3197
|
+
`
|
|
3198
|
+
);
|
|
3199
|
+
}
|
|
3200
|
+
} else if (canPrompt() && !opts.yes) {
|
|
3201
|
+
tenant = await promptSelect(
|
|
3202
|
+
"You belong to several tenants \u2014 pick one:",
|
|
3203
|
+
tenants.map((t) => ({ label: t.label, value: t.key, hint: t.key })),
|
|
3204
|
+
data?.defaultTenantKey ?? tenants[0].key
|
|
3205
|
+
);
|
|
3206
|
+
} else {
|
|
3207
|
+
tenant = data?.defaultTenantKey ?? tenants[0].key;
|
|
3208
|
+
if (!opts.json) {
|
|
3209
|
+
process.stderr.write(
|
|
3210
|
+
`using tenant ${tenant} (${tenants.length} available \u2014 pass --tenant to choose another)
|
|
3211
|
+
`
|
|
3212
|
+
);
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
const existingWorkspace = local.workspaceId ?? persisted.workspaceId ?? void 0;
|
|
3217
|
+
const wsClient = await makeClient({ baseUrl, tenant, clientId: persisted.clientId });
|
|
3218
|
+
const workspaceId = await resolveWorkspaceBinding(wsClient, existingWorkspace, {
|
|
3219
|
+
yes: opts.yes,
|
|
3220
|
+
json: opts.json,
|
|
3221
|
+
workspace: opts.workspace
|
|
3222
|
+
});
|
|
3223
|
+
const defaultProjectId = local.defaultProjectId ?? persisted.defaultProjectId ?? void 0;
|
|
3224
|
+
if (defaultProjectId && workspaceId) await warnIfProjectStray(wsClient, defaultProjectId, workspaceId, opts.json);
|
|
2119
3225
|
let storeLocal = Boolean(opts.local);
|
|
2120
3226
|
if (!opts.local && canPrompt() && !opts.yes) {
|
|
2121
3227
|
storeLocal = await promptSelect(
|
|
2122
3228
|
"Where should this tenant + base URL be saved?",
|
|
2123
3229
|
[
|
|
2124
3230
|
{ label: "Globally", value: "global", hint: "all projects on this machine" },
|
|
2125
|
-
{ label: "This directory", value: "local", hint: ".sechroom.json \u2014 project + subdirs" }
|
|
3231
|
+
{ label: "This directory", value: "local", hint: ".sechroom.json \u2014 committed, project + subdirs" }
|
|
2126
3232
|
],
|
|
2127
3233
|
local.path ? "local" : "global"
|
|
2128
3234
|
) === "local";
|
|
2129
3235
|
}
|
|
2130
|
-
if (
|
|
2131
|
-
const
|
|
2132
|
-
if (
|
|
3236
|
+
if (opts.persist !== false) {
|
|
3237
|
+
const patch = { baseUrl, tenant, ...workspaceId ? { workspaceId } : {} };
|
|
3238
|
+
if (storeLocal) {
|
|
3239
|
+
const path = writeLocalConfig(patch);
|
|
3240
|
+
if (!opts.json) process.stderr.write(`${ok("\u2713")} config saved to ${path} (directory-local)
|
|
2133
3241
|
`);
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
3242
|
+
} else {
|
|
3243
|
+
writePersisted(patch);
|
|
3244
|
+
if (!opts.json) process.stderr.write(`${ok("\u2713")} config saved globally (~/.config/sechroom/config.json)
|
|
3245
|
+
`);
|
|
3246
|
+
}
|
|
3247
|
+
if (workspaceId && !existingWorkspace && !opts.json) {
|
|
3248
|
+
process.stderr.write(`${ok("\u2713")} bound to workspace ${style.dim(workspaceId)}
|
|
2137
3249
|
`);
|
|
3250
|
+
}
|
|
2138
3251
|
}
|
|
2139
|
-
return { baseUrl, tenant, clientId: persisted.clientId };
|
|
3252
|
+
return { baseUrl, tenant, workspaceId, defaultProjectId, clientId: persisted.clientId };
|
|
2140
3253
|
}
|
|
2141
3254
|
async function ensureAuth(cfg, yes) {
|
|
2142
3255
|
if (process.env.SECHROOM_TOKEN) return;
|
|
@@ -2190,25 +3303,144 @@ async function chooseClients(clientFlag, yes, cwd) {
|
|
|
2190
3303
|
);
|
|
2191
3304
|
return picks.length > 0 ? picks : preselected;
|
|
2192
3305
|
}
|
|
3306
|
+
async function planRecurseChild(entry, root, client, opts) {
|
|
3307
|
+
const dir = resolveChildDir(entry.path, root);
|
|
3308
|
+
if (!existsSync7(dir)) {
|
|
3309
|
+
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
3310
|
+
}
|
|
3311
|
+
if (existsSync7(join7(dir, ".sechroom.json"))) {
|
|
3312
|
+
return {
|
|
3313
|
+
label: entry.path,
|
|
3314
|
+
dir,
|
|
3315
|
+
disposition: "refresh",
|
|
3316
|
+
argv: ["onboard", "--refresh", "--yes"],
|
|
3317
|
+
reason: "bound (committed .sechroom.json) \u2014 refresh in place"
|
|
3318
|
+
};
|
|
3319
|
+
}
|
|
3320
|
+
if (entry.workspaceId) {
|
|
3321
|
+
return {
|
|
3322
|
+
label: entry.path,
|
|
3323
|
+
dir,
|
|
3324
|
+
disposition: "bind",
|
|
3325
|
+
argv: ["onboard", "--yes", "--local", "--workspace", entry.workspaceId],
|
|
3326
|
+
reason: `unbound \u2014 bind to ${entry.workspaceId}`
|
|
3327
|
+
};
|
|
3328
|
+
}
|
|
3329
|
+
if (opts.dryRun) {
|
|
3330
|
+
return { label: entry.path, dir, disposition: "bind", argv: ["onboard", "--yes", "--local", "--workspace", "<prompt>"], reason: "unbound \u2014 would prompt for a workspace" };
|
|
3331
|
+
}
|
|
3332
|
+
if (opts.yes || !canPrompt()) {
|
|
3333
|
+
return { label: entry.path, dir, disposition: "skip-unbound", argv: [], reason: "unbound + no workspace (run interactively, or add it to ./.sechroom/repos.json)" };
|
|
3334
|
+
}
|
|
3335
|
+
process.stderr.write(`
|
|
3336
|
+
${style.bold(entry.path)} ${style.dim("is not bound yet.")}
|
|
3337
|
+
`);
|
|
3338
|
+
const ws = await pickWorkspace(client, `Bind ${style.cyan(entry.path)} to a workspace:`);
|
|
3339
|
+
if (!ws) {
|
|
3340
|
+
return { label: entry.path, dir, disposition: "skip-unbound", argv: [], reason: "unbound \u2014 no workspace chosen (skipped)" };
|
|
3341
|
+
}
|
|
3342
|
+
return {
|
|
3343
|
+
label: entry.path,
|
|
3344
|
+
dir,
|
|
3345
|
+
disposition: "bind",
|
|
3346
|
+
argv: ["onboard", "--yes", "--local", "--workspace", ws],
|
|
3347
|
+
reason: `unbound \u2014 bind to ${ws}`
|
|
3348
|
+
};
|
|
3349
|
+
}
|
|
3350
|
+
async function resolveFanoutLane(cfg, opts) {
|
|
3351
|
+
let code = opts.lane ?? process.env.SECHROOM_CODE_LANE;
|
|
3352
|
+
let design = opts.designLane ?? process.env.SECHROOM_DESIGN_LANE;
|
|
3353
|
+
if (!code || !design) {
|
|
3354
|
+
const clients = detectInstalledClients(process.cwd());
|
|
3355
|
+
const inferred = await inferLanes(cfg, clients.length ? clients : void 0);
|
|
3356
|
+
code = code ?? inferred.code;
|
|
3357
|
+
design = design ?? inferred.design;
|
|
3358
|
+
}
|
|
3359
|
+
if (!opts.lane && !opts.yes && !opts.dryRun && canPrompt() && (code || design)) {
|
|
3360
|
+
process.stderr.write(`
|
|
3361
|
+
This fan-out will pin the same lane in every repo:
|
|
3362
|
+
`);
|
|
3363
|
+
if (code) process.stderr.write(` ${style.dim("code-lane")} = ${style.cyan(code)}
|
|
3364
|
+
`);
|
|
3365
|
+
if (design) process.stderr.write(` ${style.dim("design-lane")} = ${style.cyan(design)}
|
|
3366
|
+
`);
|
|
3367
|
+
if (!await promptYesNo("Use this lane for all repos?")) {
|
|
3368
|
+
code = await promptText("Code-lane id (blank = let each repo infer)?", code ?? "") || void 0;
|
|
3369
|
+
design = await promptText("Design-lane id (blank = skip)?", design ?? "") || void 0;
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
if (code) process.env.SECHROOM_CODE_LANE = code;
|
|
3373
|
+
else delete process.env.SECHROOM_CODE_LANE;
|
|
3374
|
+
if (design) process.env.SECHROOM_DESIGN_LANE = design;
|
|
3375
|
+
else delete process.env.SECHROOM_DESIGN_LANE;
|
|
3376
|
+
return { code, design };
|
|
3377
|
+
}
|
|
3378
|
+
async function runRecurse(cfg, g, opts) {
|
|
3379
|
+
const { yes, dryRun, json } = opts;
|
|
3380
|
+
const root = process.cwd();
|
|
3381
|
+
const manifestPath = join7(root, ".sechroom", "repos.json");
|
|
3382
|
+
const fromManifest = readManifest(manifestPath);
|
|
3383
|
+
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
3384
|
+
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
3385
|
+
if (entries.length === 0) {
|
|
3386
|
+
if (json) process.stdout.write(JSON.stringify({ recurse: true, root, repos: [] }) + "\n");
|
|
3387
|
+
else process.stderr.write(`${warn("\u26A0")} no child repos found ${fromManifest ? `in ${manifestPath}` : `under ${root}`} \u2014 nothing to do.
|
|
3388
|
+
`);
|
|
3389
|
+
return;
|
|
3390
|
+
}
|
|
3391
|
+
if (!json) {
|
|
3392
|
+
process.stderr.write(`${style.bold("onboard --recurse")} ${style.dim(`(${entries.length} repo${entries.length === 1 ? "" : "s"} from ${sourceLabel})`)}
|
|
3393
|
+
`);
|
|
3394
|
+
}
|
|
3395
|
+
const lane = await resolveFanoutLane(cfg, { lane: opts.lane, designLane: opts.designLane, yes, dryRun });
|
|
3396
|
+
if (!json && lane.code) process.stderr.write(`${ok("\u2713")} lane ${style.cyan(lane.code)}${lane.design ? ` ${style.dim(`/ ${lane.design}`)}` : ""} for every repo
|
|
3397
|
+
`);
|
|
3398
|
+
const client = await makeClient(cfg);
|
|
3399
|
+
const plans = [];
|
|
3400
|
+
for (const entry of entries) plans.push(await planRecurseChild(entry, root, client, { yes, dryRun }));
|
|
3401
|
+
const results = runChildren(plans, { globals: passthroughGlobals(g), dryRun, json });
|
|
3402
|
+
if (json) {
|
|
3403
|
+
process.stdout.write(JSON.stringify({ recurse: true, root, dryRun, repos: results }) + "\n");
|
|
3404
|
+
return;
|
|
3405
|
+
}
|
|
3406
|
+
summarizeFanout(results, { dryRun });
|
|
3407
|
+
}
|
|
2193
3408
|
function registerOnboard(program2) {
|
|
2194
|
-
program2.command("onboard").description("Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project").option("--client <list>", `comma-separated clients (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`).option("--local", "save tenant + base URL to a
|
|
3409
|
+
program2.command("onboard").description("Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project").option("--recurse", "orchestration-root mode: onboard every child repo under this dir (auto-discovered, or from ./.sechroom/repos.json) \u2014 refreshes bound repos, prompts a workspace per new one", false).option("--lane <id>", "set the code-lane (substrate source identity) explicitly instead of inferring it; with --recurse it's used for every child repo").option("--design-lane <id>", "set the design-lane explicitly (substrate-authoring identity); with --recurse applies to every child").option("--client <list>", `comma-separated clients (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`).option("--local", "save the binding (tenant + base URL + workspace) to a committed .sechroom.json in this repo instead of the global config", false).option("--workspace <id>", "bind this directory to a workspace (skips the interactive workspace pick)").option("--cli-only", "configure the CLI only \u2014 don't wire any AI client (no MCP config, no agent files)", false).option("--no-mcp", "skip the MCP server config (.mcp.json etc.); still write the agent instruction files").option("--copy", "make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)").option("--dry-run", "walk through without writing files or changing the profile", false).option("--refresh", "re-fetch descriptors and refresh any out-of-date managed blocks (local edits preserved to .proposed)", false).option("--force", "rewrite every managed block, overwriting local edits inside the markers (content outside untouched)", false).option("--check", "report whether anything would change and exit (0 = all current, 1 = stale/drift/absent); writes nothing", false).option("-y, --yes", "non-interactive: accept defaults (system timezone, detected clients, global config, full wire)", false).addHelpText(
|
|
2195
3410
|
"after",
|
|
2196
3411
|
`
|
|
2197
3412
|
Examples:
|
|
2198
3413
|
$ sechroom onboard guided, interactive (asks where to save config + how to wire)
|
|
2199
3414
|
$ sechroom onboard --cli-only just the CLI \u2014 no .mcp.json, no agent files
|
|
2200
3415
|
$ sechroom onboard --no-mcp agent instructions only, skip MCP config
|
|
2201
|
-
$ sechroom onboard --local save tenant + base URL to ./.sechroom.json
|
|
3416
|
+
$ sechroom onboard --local save tenant + base URL to a committed ./.sechroom.json
|
|
3417
|
+
$ sechroom onboard --workspace wsp_XX bind this directory to a workspace (no pick prompt)
|
|
3418
|
+
$ sechroom onboard --recurse orchestration root: onboard every child repo under this dir
|
|
3419
|
+
$ sechroom onboard --recurse --lane claude-code-you pin one lane across every repo in the tree
|
|
3420
|
+
$ sechroom onboard --refresh refresh out-of-date instruction blocks in place
|
|
3421
|
+
$ sechroom onboard --check CI/pre-commit: nonzero exit if instructions are out of date
|
|
2202
3422
|
$ sechroom onboard --yes non-interactive: defaults + global config + full wire
|
|
2203
3423
|
$ sechroom onboard --client all --dry-run preview wiring every client, write nothing`
|
|
2204
3424
|
).action(async (opts, cmd) => {
|
|
2205
3425
|
const g = cmd.optsWithGlobals();
|
|
2206
3426
|
const json = Boolean(g.json);
|
|
2207
|
-
const yes = Boolean(opts.yes);
|
|
2208
3427
|
const dryRun = Boolean(opts.dryRun);
|
|
2209
|
-
const
|
|
2210
|
-
|
|
2211
|
-
const
|
|
3428
|
+
const mode = opts.check ? "check" : opts.force ? "force" : "apply";
|
|
3429
|
+
const check = mode === "check";
|
|
3430
|
+
const yes = Boolean(opts.yes) || check;
|
|
3431
|
+
if (opts.lane) process.env.SECHROOM_CODE_LANE = opts.lane;
|
|
3432
|
+
if (opts.designLane) process.env.SECHROOM_DESIGN_LANE = opts.designLane;
|
|
3433
|
+
if (opts.recurse) {
|
|
3434
|
+
const baseUrl2 = resolveBaseUrl(g);
|
|
3435
|
+
await ensureAuth({ baseUrl: baseUrl2, tenant: "", clientId: readPersisted().clientId }, yes);
|
|
3436
|
+
const cfg2 = await ensureTenant(baseUrl2, g, { yes: true, json, persist: false });
|
|
3437
|
+
await runRecurse(cfg2, g, { yes, dryRun, json, lane: opts.lane, designLane: opts.designLane });
|
|
3438
|
+
return;
|
|
3439
|
+
}
|
|
3440
|
+
const baseUrl = resolveBaseUrl(g);
|
|
3441
|
+
await ensureAuth({ baseUrl, tenant: "", clientId: readPersisted().clientId }, yes);
|
|
3442
|
+
const cfg = await ensureTenant(baseUrl, g, { yes, json, local: Boolean(opts.local), workspace: opts.workspace, persist: !check });
|
|
3443
|
+
const tz = await ensureTimezone(cfg, { yes, dryRun: dryRun || check });
|
|
2212
3444
|
if (!json && tz.action !== "already-set") {
|
|
2213
3445
|
const line = tz.action === "set" ? `${ok("\u2713")} timezone set to ${tz.timezone}
|
|
2214
3446
|
` : tz.action === "dry-run" ? `(dry run \u2014 would set timezone to ${tz.timezone})
|
|
@@ -2219,22 +3451,27 @@ Examples:
|
|
|
2219
3451
|
const wire = await chooseWire(opts, yes);
|
|
2220
3452
|
if (wire === "cli-only") {
|
|
2221
3453
|
if (json) {
|
|
2222
|
-
emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, timezone: tz, wire, clients: [] }, true);
|
|
3454
|
+
emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, workspaceId: cfg.workspaceId ?? null, timezone: tz, wire, clients: [] }, true);
|
|
2223
3455
|
return;
|
|
2224
3456
|
}
|
|
3457
|
+
if (!dryRun) {
|
|
3458
|
+
await ensureLanePin(cfg, { yes, dryRun, clients: detectInstalledClients(process.cwd()) });
|
|
3459
|
+
await maybeOfferHooks({ yes, dryRun, cwd: process.cwd() });
|
|
3460
|
+
}
|
|
2225
3461
|
process.stdout.write(
|
|
2226
3462
|
`
|
|
2227
3463
|
${style.bold("Done.")} The CLI is configured for ${style.cyan(cfg.tenant)} \u2014 no AI-client files written.
|
|
2228
3464
|
Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom --help")}
|
|
2229
3465
|
`
|
|
2230
3466
|
);
|
|
3467
|
+
await printStarterPrompt("cli");
|
|
2231
3468
|
return;
|
|
2232
3469
|
}
|
|
2233
3470
|
const keys = await chooseClients(opts.client, yes, process.cwd());
|
|
2234
3471
|
const setup = await withSpinner("Fetching setup descriptors", () => fetchSetup(cfg));
|
|
2235
3472
|
const targets = clientTargets(process.cwd());
|
|
2236
3473
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
2237
|
-
if (!dryRun) {
|
|
3474
|
+
if (!dryRun && !check) {
|
|
2238
3475
|
await maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId, copyChoice(opts));
|
|
2239
3476
|
}
|
|
2240
3477
|
const writeMcp = wire === "full";
|
|
@@ -2245,22 +3482,68 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
2245
3482
|
dryRun,
|
|
2246
3483
|
mcp: writeMcp,
|
|
2247
3484
|
agentFiles: true,
|
|
2248
|
-
personalWorkspaceId
|
|
3485
|
+
personalWorkspaceId,
|
|
3486
|
+
mode
|
|
2249
3487
|
});
|
|
2250
3488
|
result.push({ client: key, actions });
|
|
2251
|
-
if (!json) printActions(target, actions);
|
|
3489
|
+
if (!json && !check) printActions(target, actions);
|
|
3490
|
+
}
|
|
3491
|
+
const evalCounts = { current: 0, stale: 0, drift: 0, absent: 0 };
|
|
3492
|
+
for (const { actions } of result) for (const a of actions) if (a.eval) evalCounts[a.eval]++;
|
|
3493
|
+
const wouldChange = evalCounts.stale + evalCounts.drift + evalCounts.absent;
|
|
3494
|
+
if (check) {
|
|
3495
|
+
if (json) {
|
|
3496
|
+
emit({ check: true, baseUrl: cfg.baseUrl, tenant: cfg.tenant, workspaceId: cfg.workspaceId ?? null, eval: evalCounts, wouldChange, clients: result }, true);
|
|
3497
|
+
} else if (wouldChange === 0) {
|
|
3498
|
+
process.stdout.write(`${ok("\u2713")} all instruction blocks are up to date.
|
|
3499
|
+
`);
|
|
3500
|
+
} else {
|
|
3501
|
+
const bits = [];
|
|
3502
|
+
if (evalCounts.stale) bits.push(`${evalCounts.stale} out of date`);
|
|
3503
|
+
if (evalCounts.drift) bits.push(`${evalCounts.drift} with local edits`);
|
|
3504
|
+
if (evalCounts.absent) bits.push(`${evalCounts.absent} not yet written`);
|
|
3505
|
+
process.stderr.write(
|
|
3506
|
+
`${warn("\u26A0")} ${wouldChange} instruction block(s) would change: ${bits.join(", ")}. Run ${style.cyan("sechroom onboard --refresh")}.
|
|
3507
|
+
`
|
|
3508
|
+
);
|
|
3509
|
+
}
|
|
3510
|
+
process.exit(wouldChange === 0 ? 0 : 1);
|
|
3511
|
+
}
|
|
3512
|
+
if (!json && !dryRun) {
|
|
3513
|
+
await ensureLanePin(cfg, { yes, dryRun, clients: keys });
|
|
3514
|
+
}
|
|
3515
|
+
if (!json && !dryRun) {
|
|
3516
|
+
await maybeOfferSkills(cfg, personalWorkspaceId, { yes, dryRun, surface: "claude-code" });
|
|
3517
|
+
}
|
|
3518
|
+
if (!json && !dryRun) {
|
|
3519
|
+
await maybeOfferHooks({ yes, dryRun, cwd: process.cwd() });
|
|
2252
3520
|
}
|
|
2253
3521
|
if (json) {
|
|
2254
|
-
emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, timezone: tz, wire, clients: result }, true);
|
|
3522
|
+
emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, workspaceId: cfg.workspaceId ?? null, timezone: tz, wire, eval: evalCounts, clients: result }, true);
|
|
2255
3523
|
return;
|
|
2256
3524
|
}
|
|
3525
|
+
if (!dryRun && evalCounts.stale) {
|
|
3526
|
+
process.stderr.write(`${style.cyan("\u21BB")} refreshed ${evalCounts.stale} section(s) the server had moved
|
|
3527
|
+
`);
|
|
3528
|
+
}
|
|
3529
|
+
if (!dryRun && evalCounts.drift) {
|
|
3530
|
+
process.stderr.write(
|
|
3531
|
+
mode === "force" ? `${warn("\u26A0")} overwrote ${evalCounts.drift} section(s) that had local edits (--force)
|
|
3532
|
+
` : `${warn("\u26A0")} ${evalCounts.drift} section(s) have local edits \u2014 wrote a .proposed file alongside (original untouched). Review + merge, or re-run with ${style.cyan("--force")}.
|
|
3533
|
+
`
|
|
3534
|
+
);
|
|
3535
|
+
}
|
|
3536
|
+
const wroteSomething = result.some(({ actions }) => actions.some((a) => a.status === "created" || a.status === "merged"));
|
|
2257
3537
|
process.stdout.write(
|
|
2258
|
-
dryRun ? "\n(dry run \u2014 nothing written)\n" :
|
|
3538
|
+
dryRun ? "\n(dry run \u2014 nothing written)\n" : !wroteSomething ? `
|
|
3539
|
+
${style.bold("Done.")} Everything's already up to date.
|
|
3540
|
+
` : writeMcp ? `
|
|
2259
3541
|
${style.bold("Done.")} Restart your AI client (or reload MCP) to pick up the new config.
|
|
2260
3542
|
` : `
|
|
2261
3543
|
${style.bold("Done.")} Agent instructions written (no MCP config).
|
|
2262
3544
|
`
|
|
2263
3545
|
);
|
|
3546
|
+
if (!dryRun) await printStarterPrompt("agent", cfg);
|
|
2264
3547
|
});
|
|
2265
3548
|
}
|
|
2266
3549
|
async function chooseWire(opts, yes) {
|
|
@@ -2278,12 +3561,437 @@ async function chooseWire(opts, yes) {
|
|
|
2278
3561
|
}
|
|
2279
3562
|
return opts.mcp === false ? "agent-only" : "full";
|
|
2280
3563
|
}
|
|
3564
|
+
var FALLBACK_AGENT_PROMPT = "Resume my sechroom continuity, summarise what I was last working on, then suggest the next step.";
|
|
3565
|
+
async function printStarterPrompt(mode, cfg) {
|
|
3566
|
+
if (mode === "cli") {
|
|
3567
|
+
process.stdout.write(
|
|
3568
|
+
`
|
|
3569
|
+
${style.bold("Next:")} pick up where you left off \u2014
|
|
3570
|
+
${style.cyan("sechroom continuity resume-me")}
|
|
3571
|
+
`
|
|
3572
|
+
);
|
|
3573
|
+
return;
|
|
3574
|
+
}
|
|
3575
|
+
let primary = FALLBACK_AGENT_PROMPT;
|
|
3576
|
+
if (cfg) {
|
|
3577
|
+
try {
|
|
3578
|
+
const client = await makeClient(cfg);
|
|
3579
|
+
const { data } = await client.GET("/me/onboarding/starter-prompt", {});
|
|
3580
|
+
if (data?.primary) primary = data.primary;
|
|
3581
|
+
} catch {
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
process.stdout.write(
|
|
3585
|
+
`
|
|
3586
|
+
${style.bold("Next:")} paste this into your AI agent to get going \u2014
|
|
3587
|
+
${style.cyan(`"${primary}"`)}
|
|
3588
|
+
`
|
|
3589
|
+
);
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
// src/commands/sweep.ts
|
|
3593
|
+
import { existsSync as existsSync8 } from "fs";
|
|
3594
|
+
import { dirname as dirname6, join as join8, resolve as resolve2 } from "path";
|
|
3595
|
+
var DEFAULT_MANIFEST = join8(".sechroom", "repos.json");
|
|
3596
|
+
function planEntry(entry, root) {
|
|
3597
|
+
const dir = resolveChildDir(entry.path, root);
|
|
3598
|
+
if (!existsSync8(dir)) {
|
|
3599
|
+
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
3600
|
+
}
|
|
3601
|
+
if (committedBindingPath(dir)) {
|
|
3602
|
+
return {
|
|
3603
|
+
label: entry.path,
|
|
3604
|
+
dir,
|
|
3605
|
+
disposition: "refresh",
|
|
3606
|
+
argv: ["onboard", "--refresh", "--yes"],
|
|
3607
|
+
reason: "bound (committed .sechroom.json) \u2014 refresh in place"
|
|
3608
|
+
};
|
|
3609
|
+
}
|
|
3610
|
+
if (entry.workspaceId) {
|
|
3611
|
+
return {
|
|
3612
|
+
label: entry.path,
|
|
3613
|
+
dir,
|
|
3614
|
+
disposition: "bind",
|
|
3615
|
+
argv: ["onboard", "--yes", "--local", "--workspace", entry.workspaceId],
|
|
3616
|
+
reason: `unbound \u2014 bind to ${entry.workspaceId} + commit .sechroom.json`
|
|
3617
|
+
};
|
|
3618
|
+
}
|
|
3619
|
+
return {
|
|
3620
|
+
label: entry.path,
|
|
3621
|
+
dir,
|
|
3622
|
+
disposition: "skip-unbound",
|
|
3623
|
+
argv: [],
|
|
3624
|
+
reason: "unbound + no workspaceId in the manifest \u2014 add one or run `sechroom onboard` there"
|
|
3625
|
+
};
|
|
3626
|
+
}
|
|
3627
|
+
function registerSweep(program2) {
|
|
3628
|
+
program2.command("sweep").description("Non-interactive fan-out from ./.sechroom/repos.json (headless sibling of `onboard --recurse`)").option("--manifest <path>", "path to the repos manifest", DEFAULT_MANIFEST).option("--dry-run", "print the plan (per-repo disposition + the onboard command) without running anything", false).addHelpText(
|
|
3629
|
+
"after",
|
|
3630
|
+
`
|
|
3631
|
+
For an interactive, no-manifest run use ${"`sechroom onboard --recurse`"} instead \u2014 it
|
|
3632
|
+
auto-discovers the child repos and prompts for a workspace per new one. ${"`sweep`"} is
|
|
3633
|
+
the deterministic manifest-driven form for scripts / CI.
|
|
3634
|
+
|
|
3635
|
+
Manifest \u2014 ./.sechroom/repos.json (per-operator, gitignored, alongside lane.json):
|
|
3636
|
+
{
|
|
3637
|
+
"repos": [
|
|
3638
|
+
{ "path": "sechroom", "workspaceId": "wsp_XXXX" },
|
|
3639
|
+
{ "path": "../other-repo", "workspaceId": "wsp_YYYY" },
|
|
3640
|
+
{ "path": "already-bound" }
|
|
3641
|
+
]
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
Per repo (paths resolve relative to the manifest's root):
|
|
3645
|
+
${ICON.refresh} bound committed .sechroom.json present \u2192 onboard --refresh (manifest workspace ignored)
|
|
3646
|
+
${ICON.bind} unbound bind to the manifest workspaceId \u2192 onboard --local --workspace <id>
|
|
3647
|
+
${ICON["skip-missing"]} missing directory does not exist \u2192 skipped
|
|
3648
|
+
${ICON["skip-unbound"]} no workspace unbound + no workspaceId in manifest \u2192 skipped (add one, or onboard manually)
|
|
3649
|
+
|
|
3650
|
+
Examples:
|
|
3651
|
+
$ sechroom sweep --dry-run preview every repo's disposition, run nothing
|
|
3652
|
+
$ sechroom sweep onboard the whole tree from the root
|
|
3653
|
+
$ sechroom --tenant ocd sweep force a tenant for every child (else each resolves its own)`
|
|
3654
|
+
).action((opts, cmd) => {
|
|
3655
|
+
const g = cmd.optsWithGlobals();
|
|
3656
|
+
const json = Boolean(g.json);
|
|
3657
|
+
const dryRun = Boolean(opts.dryRun);
|
|
3658
|
+
const manifestPath = resolve2(opts.manifest);
|
|
3659
|
+
let repos;
|
|
3660
|
+
try {
|
|
3661
|
+
repos = readManifest(manifestPath);
|
|
3662
|
+
} catch (err2) {
|
|
3663
|
+
fail(err2 instanceof Error ? err2.message : String(err2));
|
|
3664
|
+
}
|
|
3665
|
+
if (repos === null) {
|
|
3666
|
+
fail(`no manifest at ${manifestPath} \u2014 create ./.sechroom/repos.json, or use \`sechroom onboard --recurse\` to auto-discover (see \`sechroom sweep --help\`).`);
|
|
3667
|
+
}
|
|
3668
|
+
if (repos.length === 0) {
|
|
3669
|
+
if (json) process.stdout.write(JSON.stringify({ manifest: manifestPath, repos: [] }) + "\n");
|
|
3670
|
+
else process.stderr.write(`${warn("\u26A0")} ${manifestPath} lists no repos \u2014 nothing to do.
|
|
3671
|
+
`);
|
|
3672
|
+
return;
|
|
3673
|
+
}
|
|
3674
|
+
const root = dirname6(dirname6(manifestPath));
|
|
3675
|
+
const plans = repos.map((entry) => planEntry(entry, root));
|
|
3676
|
+
if (!json) {
|
|
3677
|
+
process.stderr.write(
|
|
3678
|
+
`${style.bold("sweep")} ${style.dim(`(${plans.length} repo${plans.length === 1 ? "" : "s"} from ${manifestPath})`)}
|
|
3679
|
+
`
|
|
3680
|
+
);
|
|
3681
|
+
}
|
|
3682
|
+
const results = runChildren(plans, { globals: passthroughGlobals(g), dryRun, json });
|
|
3683
|
+
if (json) {
|
|
3684
|
+
process.stdout.write(JSON.stringify({ manifest: manifestPath, dryRun, repos: results }) + "\n");
|
|
3685
|
+
return;
|
|
3686
|
+
}
|
|
3687
|
+
summarizeFanout(results, { dryRun });
|
|
3688
|
+
});
|
|
3689
|
+
}
|
|
3690
|
+
|
|
3691
|
+
// src/commands/skills.ts
|
|
3692
|
+
import { homedir as homedir6 } from "os";
|
|
3693
|
+
import { join as join9 } from "path";
|
|
3694
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6, rmSync as rmSync2, existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
|
|
3695
|
+
var DEFAULT_SLUG = "operator-skills";
|
|
3696
|
+
var ROLE_TAGS = ["sechroom:role:skill-template", "role:skill-template"];
|
|
3697
|
+
var LOCK = ".sechroom-skills.json";
|
|
3698
|
+
function skillsDir(global) {
|
|
3699
|
+
return global ? join9(homedir6(), ".claude", "skills") : join9(process.cwd(), ".claude", "skills");
|
|
3700
|
+
}
|
|
3701
|
+
function tagValue2(tags, prefix) {
|
|
3702
|
+
return (tags ?? []).find((t) => t.startsWith(prefix))?.slice(prefix.length);
|
|
3703
|
+
}
|
|
3704
|
+
function hasAny(tags, candidates) {
|
|
3705
|
+
return (tags ?? []).some((t) => candidates.includes(t));
|
|
3706
|
+
}
|
|
3707
|
+
function registerSkills(program2) {
|
|
3708
|
+
const skills = program2.command("skills").description("Install + manage operator skills from a bundle");
|
|
3709
|
+
skills.addHelpText(
|
|
3710
|
+
"after",
|
|
3711
|
+
`
|
|
3712
|
+
Examples:
|
|
3713
|
+
$ sechroom skills install --code-lane claude-code-chris --design-lane claude-design-chris
|
|
3714
|
+
$ sechroom skills install operator-skills --surface claude-code --local
|
|
3715
|
+
$ sechroom skills list
|
|
3716
|
+
$ sechroom skills set-lane --code-lane claude-code-chris --design-lane claude-design-chris
|
|
3717
|
+
$ sechroom skills lane
|
|
3718
|
+
$ sechroom skills clean`
|
|
3719
|
+
);
|
|
3720
|
+
skills.command("install [slug]").description(`Install a skills bundle (default ${DEFAULT_SLUG}) into your personal workspace + write SKILL.md files`).option("--version <v>", "bundle version (default: latest published in the catalogue)").option("--instance <name>", "install as a named, separate instance (install the same bundle more than once)").option("--code-lane <id>", "identity.code-lane binding (e.g. claude-code-chris)").option("--design-lane <id>", "identity.design-lane binding (e.g. claude-design-chris)").option("--surface <s>", "skill target surface to materialise", "claude-code").option("--local", "write to ./.claude/skills instead of ~/.claude/skills").option("--json", "machine output").action(async (slugArg, opts, cmd) => {
|
|
3721
|
+
const slug = slugArg || DEFAULT_SLUG;
|
|
3722
|
+
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
3723
|
+
const pw = await runApi("resolving personal workspace", () => client.GET("/me/personal-workspace", {}));
|
|
3724
|
+
const personalWsId = pw?.id || pw?.workspaceId || pw?.personalWorkspaceId || pw?.item?.id;
|
|
3725
|
+
if (!personalWsId) fail("Could not resolve your personal workspace.");
|
|
3726
|
+
let version = opts.version;
|
|
3727
|
+
if (!version) {
|
|
3728
|
+
const cat = await runApi("reading the bundle catalogue", () => client.GET("/me/bundles", {}));
|
|
3729
|
+
const item = (cat?.bundles ?? cat?.Bundles ?? []).find((b) => (b.slug ?? b.Slug) === slug);
|
|
3730
|
+
if (!item) fail(`Bundle '${slug}' is not in your self-serve catalogue (must be UserInstallable + Published).`);
|
|
3731
|
+
version = item.latestVersion ?? item.LatestVersion;
|
|
3732
|
+
if (!version) fail(`Bundle '${slug}' has no installable (Published) version.`);
|
|
3733
|
+
}
|
|
3734
|
+
const installOptions = {};
|
|
3735
|
+
if (opts.codeLane) installOptions["identity.code-lane"] = opts.codeLane;
|
|
3736
|
+
if (opts.designLane) installOptions["identity.design-lane"] = opts.designLane;
|
|
3737
|
+
const res = await runApi(
|
|
3738
|
+
`installing ${slug}@${version}${opts.instance ? ` (${opts.instance})` : ""}`,
|
|
3739
|
+
() => client.POST("/me/bundles/{slug}/versions/{version}/install", {
|
|
3740
|
+
params: { path: { slug, version } },
|
|
3741
|
+
// instance: null/absent = the default instance (reinstall updates in
|
|
3742
|
+
// place); a name installs a separate instance.
|
|
3743
|
+
body: { installOptions, instance: opts.instance ?? null }
|
|
3744
|
+
})
|
|
3745
|
+
);
|
|
3746
|
+
const status = String(res?.status ?? res?.Status ?? "");
|
|
3747
|
+
if (status && status.toLowerCase() !== "completed") {
|
|
3748
|
+
fail(`Install did not complete (status=${status}; ${res?.failureReason ?? res?.FailureReason ?? ""}).`);
|
|
3749
|
+
}
|
|
3750
|
+
const feed = await runApi(
|
|
3751
|
+
"materialising skill files",
|
|
3752
|
+
() => client.GET("/workspaces/{workspaceId}/memories/feed", {
|
|
3753
|
+
// cascadeWorkspaces: skills land in an "Operator Skills" SUB-workspace of
|
|
3754
|
+
// the personal workspace, so we recurse from the personal-ws root.
|
|
3755
|
+
// includeText: the feed omits bodies by default; we need them for SKILL.md.
|
|
3756
|
+
params: {
|
|
3757
|
+
path: { workspaceId: personalWsId },
|
|
3758
|
+
query: { limit: 200, cascadeWorkspaces: true, includeText: true }
|
|
3759
|
+
}
|
|
3760
|
+
})
|
|
3761
|
+
);
|
|
3762
|
+
const rows = feed?.results ?? feed?.Results ?? [];
|
|
3763
|
+
const dir = skillsDir(!opts.local);
|
|
3764
|
+
const wantInstance = opts.instance || "default";
|
|
3765
|
+
const written = [];
|
|
3766
|
+
const bundleTagPrefix = `sechroom:bundle:${slug}@`;
|
|
3767
|
+
for (const r of rows) {
|
|
3768
|
+
const m = r.item ?? r;
|
|
3769
|
+
const tags = m.tags ?? m.Tags ?? [];
|
|
3770
|
+
if (!hasAny(tags, ROLE_TAGS)) continue;
|
|
3771
|
+
if (tagValue2(tags, "target:") !== opts.surface) continue;
|
|
3772
|
+
if (!tags.some((t) => t.startsWith(bundleTagPrefix))) continue;
|
|
3773
|
+
if ((tagValue2(tags, "sechroom:skill-instance:") ?? "default") !== wantInstance) continue;
|
|
3774
|
+
const name = tagValue2(tags, "skill:");
|
|
3775
|
+
if (!name) continue;
|
|
3776
|
+
const body = m.text ?? m.Text ?? "";
|
|
3777
|
+
mkdirSync6(join9(dir, name), { recursive: true });
|
|
3778
|
+
writeFileSync6(join9(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
|
|
3779
|
+
written.push(name);
|
|
3780
|
+
}
|
|
3781
|
+
mkdirSync6(dir, { recursive: true });
|
|
3782
|
+
const lockPath = join9(dir, LOCK);
|
|
3783
|
+
const lock = existsSync9(lockPath) ? JSON.parse(readFileSync6(lockPath, "utf8")) : {};
|
|
3784
|
+
lock[slug] = { surface: opts.surface, version, instance: wantInstance, skills: written.sort() };
|
|
3785
|
+
writeFileSync6(lockPath, JSON.stringify(lock, null, 2) + "\n");
|
|
3786
|
+
if (opts.json) return emit({ slug, version, instance: wantInstance, surface: opts.surface, dir, installed: written }, true);
|
|
3787
|
+
const instanceNote = opts.instance ? ` (${opts.instance})` : "";
|
|
3788
|
+
console.log(style.green(`Installed ${slug}@${version}${instanceNote} \u2014 ${written.length} skill(s) \u2192 ${dir}`));
|
|
3789
|
+
written.forEach((n) => console.log(" " + style.dim("\u2022") + " " + n));
|
|
3790
|
+
if (written.length === 0) console.log(style.dim(` (no '${opts.surface}' skill bodies found; check --surface)`));
|
|
3791
|
+
});
|
|
3792
|
+
skills.command("list").description("List your installed bundles (GET /me/bundle-installs)").option("--json", "machine output").action(async (opts, cmd) => {
|
|
3793
|
+
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
3794
|
+
const data = await runApi("reading your installs", () => client.GET("/me/bundle-installs", {}));
|
|
3795
|
+
if (opts.json) return emit(data, true);
|
|
3796
|
+
const installs = data?.installs ?? data?.Installs ?? [];
|
|
3797
|
+
if (installs.length === 0) return console.log(style.dim("No bundles installed."));
|
|
3798
|
+
installs.forEach((i) => {
|
|
3799
|
+
const inst = i.instance ?? i.Instance ?? "";
|
|
3800
|
+
const tag = inst ? style.dim(` [${inst}]`) : "";
|
|
3801
|
+
console.log(` ${i.bundleSlug ?? i.BundleSlug}@${i.bundleVersion ?? i.BundleVersion ?? "?"}${tag}`);
|
|
3802
|
+
});
|
|
3803
|
+
});
|
|
3804
|
+
skills.command("clean [slug]").description(`Remove materialised skill files written by install (default ${DEFAULT_SLUG})`).option("--local", "clean ./.claude/skills instead of ~/.claude/skills").option("--json", "machine output").action(async (slugArg, opts) => {
|
|
3805
|
+
const slug = slugArg || DEFAULT_SLUG;
|
|
3806
|
+
const dir = skillsDir(!opts.local);
|
|
3807
|
+
const lockPath = join9(dir, LOCK);
|
|
3808
|
+
if (!existsSync9(lockPath)) fail(`No skills lockfile at ${lockPath}; nothing to clean.`);
|
|
3809
|
+
const lock = JSON.parse(readFileSync6(lockPath, "utf8"));
|
|
3810
|
+
const entry = lock[slug];
|
|
3811
|
+
if (!entry) fail(`No installed record for '${slug}' in ${lockPath}.`);
|
|
3812
|
+
const removed = [];
|
|
3813
|
+
for (const name of entry.skills) {
|
|
3814
|
+
const skillPath = join9(dir, name);
|
|
3815
|
+
if (existsSync9(skillPath)) {
|
|
3816
|
+
rmSync2(skillPath, { recursive: true, force: true });
|
|
3817
|
+
removed.push(name);
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
delete lock[slug];
|
|
3821
|
+
writeFileSync6(lockPath, JSON.stringify(lock, null, 2) + "\n");
|
|
3822
|
+
if (opts.json) return emit({ slug, removed, dir }, true);
|
|
3823
|
+
console.log(style.green(`Removed ${removed.length} skill(s) for ${slug} from ${dir}`));
|
|
3824
|
+
});
|
|
3825
|
+
skills.command("set-lane").description("Write this checkout's lane pin to a local ./.sechroom/lane.json file (read at runtime by skills)").option("--code-lane <id>", "code-surface lane id (e.g. claude-code-chris)").option("--design-lane <id>", "design / substrate-authoring lane id (e.g. claude-design-chris)").option("--json", "machine output").action((opts, cmd) => {
|
|
3826
|
+
if (!opts.codeLane && !opts.designLane) fail("Provide --code-lane and/or --design-lane.");
|
|
3827
|
+
const target = localSemPath();
|
|
3828
|
+
const values = readLocalSemValues();
|
|
3829
|
+
if (opts.codeLane) values["code-lane"] = opts.codeLane;
|
|
3830
|
+
if (opts.designLane) values["design-lane"] = opts.designLane;
|
|
3831
|
+
writeSem(values, target);
|
|
3832
|
+
if (cmd.optsWithGlobals().json) return emit({ path: target, values }, true);
|
|
3833
|
+
console.log(style.green(`Wrote lane pin \u2192 ${target} ${style.dim("(git-ignored)")}`));
|
|
3834
|
+
Object.entries(values).forEach(([k, v]) => console.log(" " + style.dim(k) + " = " + v));
|
|
3835
|
+
});
|
|
3836
|
+
skills.command("lane").description("Show the lane pin resolved from ./.sechroom/lane.json (nearest in this checkout; legacy ./.sem honoured)").option("--json", "machine output").action((opts, cmd) => {
|
|
3837
|
+
const json = cmd.optsWithGlobals().json;
|
|
3838
|
+
const found = readSem();
|
|
3839
|
+
if (!found) {
|
|
3840
|
+
if (json) return emit({ path: null, values: {} }, true);
|
|
3841
|
+
return console.log(style.dim(`No ./.sechroom/lane.json pin in this checkout. Run 'sechroom skills set-lane'.`));
|
|
3842
|
+
}
|
|
3843
|
+
if (json) return emit(found, true);
|
|
3844
|
+
console.log(style.dim(`from ${found.path}`));
|
|
3845
|
+
Object.entries(found.values).forEach(([k, v]) => console.log(" " + style.bold(k) + " = " + v));
|
|
3846
|
+
});
|
|
3847
|
+
skills.command("set-workflow").description("Set your per-operator workflow defaults (server-side; follows you across tenants)").option("--default-code-lane <id>", "personal default code lane (e.g. claude-code-chris)").option("--default-design-lane <id>", "personal default design lane (e.g. claude-design-chris)").option("--handover-recipient <id>", "your daily-handover counterparty (e.g. andy)").option("--json", "machine output").action(async (opts, cmd) => {
|
|
3848
|
+
if (!opts.defaultCodeLane && !opts.defaultDesignLane && !opts.handoverRecipient)
|
|
3849
|
+
fail("Provide at least one of --default-code-lane / --default-design-lane / --handover-recipient.");
|
|
3850
|
+
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
3851
|
+
const cur = await runApi(
|
|
3852
|
+
"reading workflow preferences",
|
|
3853
|
+
() => client.GET("/me/workflow-preferences", {})
|
|
3854
|
+
);
|
|
3855
|
+
const body = {
|
|
3856
|
+
defaultCodeLane: opts.defaultCodeLane ?? cur?.defaultCodeLane ?? null,
|
|
3857
|
+
defaultDesignLane: opts.defaultDesignLane ?? cur?.defaultDesignLane ?? null,
|
|
3858
|
+
handoverRecipient: opts.handoverRecipient ?? cur?.handoverRecipient ?? null
|
|
3859
|
+
};
|
|
3860
|
+
const res = await runApi(
|
|
3861
|
+
"saving workflow preferences",
|
|
3862
|
+
() => client.POST("/me/workflow-preferences", { body })
|
|
3863
|
+
);
|
|
3864
|
+
if (cmd.optsWithGlobals().json) return emit(res, true);
|
|
3865
|
+
console.log(style.green("Saved your workflow preferences"));
|
|
3866
|
+
console.log(" " + style.dim("default-code-lane") + " = " + (body.defaultCodeLane ?? "(unset)"));
|
|
3867
|
+
console.log(" " + style.dim("default-design-lane") + " = " + (body.defaultDesignLane ?? "(unset)"));
|
|
3868
|
+
console.log(" " + style.dim("handover-recipient") + " = " + (body.handoverRecipient ?? "(unset)"));
|
|
3869
|
+
});
|
|
3870
|
+
skills.command("workflow").description("Show your per-operator workflow defaults (GET /me/workflow-preferences)").option("--json", "machine output").action(async (opts, cmd) => {
|
|
3871
|
+
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
3872
|
+
const data = await runApi(
|
|
3873
|
+
"reading workflow preferences",
|
|
3874
|
+
() => client.GET("/me/workflow-preferences", {})
|
|
3875
|
+
);
|
|
3876
|
+
if (cmd.optsWithGlobals().json) return emit(data, true);
|
|
3877
|
+
console.log(" " + style.bold("default-code-lane") + " = " + (data?.defaultCodeLane ?? style.dim("(unset)")));
|
|
3878
|
+
console.log(" " + style.bold("default-design-lane") + " = " + (data?.defaultDesignLane ?? style.dim("(unset)")));
|
|
3879
|
+
console.log(" " + style.bold("handover-recipient") + " = " + (data?.handoverRecipient ?? style.dim("(unset)")));
|
|
3880
|
+
});
|
|
3881
|
+
skills.command("resolve").description("Resolve the effective ${identity.*} slot values (per-location .sem + per-operator workflow prefs)").option("--json", "machine output (a flat slot->value map + per-slot source)").action(async (opts, cmd) => {
|
|
3882
|
+
const local = readSem()?.values ?? {};
|
|
3883
|
+
let operator = {};
|
|
3884
|
+
try {
|
|
3885
|
+
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
3886
|
+
operator = await runApi(
|
|
3887
|
+
"reading workflow preferences",
|
|
3888
|
+
() => client.GET("/me/workflow-preferences", {})
|
|
3889
|
+
) ?? {};
|
|
3890
|
+
} catch {
|
|
3891
|
+
}
|
|
3892
|
+
const pick = (loc, op) => loc != null && loc !== "" ? { value: loc, source: "per-location" } : op != null && op !== "" ? { value: op, source: "per-operator" } : { value: null, source: "unset" };
|
|
3893
|
+
const slots = {
|
|
3894
|
+
"identity.code-lane": pick(local["code-lane"], operator?.defaultCodeLane),
|
|
3895
|
+
"identity.design-lane": pick(local["design-lane"], operator?.defaultDesignLane),
|
|
3896
|
+
"identity.handover-recipient": pick(void 0, operator?.handoverRecipient)
|
|
3897
|
+
};
|
|
3898
|
+
if (cmd.optsWithGlobals().json) {
|
|
3899
|
+
const values = Object.fromEntries(Object.entries(slots).map(([k, v]) => [k, v.value]));
|
|
3900
|
+
return emit({ values, sources: Object.fromEntries(Object.entries(slots).map(([k, v]) => [k, v.source])) }, true);
|
|
3901
|
+
}
|
|
3902
|
+
for (const [slot, { value, source }] of Object.entries(slots)) {
|
|
3903
|
+
const v = value == null ? style.dim("(unset)") : value;
|
|
3904
|
+
console.log(" " + style.bold(slot) + " = " + v + " " + style.dim(`[${source}]`));
|
|
3905
|
+
}
|
|
3906
|
+
});
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3909
|
+
// src/commands/reset.ts
|
|
3910
|
+
import { homedir as homedir7 } from "os";
|
|
3911
|
+
import { join as join10 } from "path";
|
|
3912
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
|
|
3913
|
+
var SKILLS_LOCK = ".sechroom-skills.json";
|
|
3914
|
+
var localSkillsDir = () => join10(process.cwd(), ".claude", "skills");
|
|
3915
|
+
var globalSkillsDir = () => join10(homedir7(), ".claude", "skills");
|
|
3916
|
+
function removeMaterialisedSkills(dir) {
|
|
3917
|
+
const removed = [];
|
|
3918
|
+
const lockPath = join10(dir, SKILLS_LOCK);
|
|
3919
|
+
if (!existsSync10(lockPath)) return removed;
|
|
3920
|
+
try {
|
|
3921
|
+
const lock = JSON.parse(readFileSync7(lockPath, "utf8"));
|
|
3922
|
+
for (const entry of Object.values(lock)) {
|
|
3923
|
+
for (const name of entry.skills ?? []) {
|
|
3924
|
+
const p = join10(dir, name);
|
|
3925
|
+
if (existsSync10(p)) {
|
|
3926
|
+
rmSync3(p, { recursive: true, force: true });
|
|
3927
|
+
removed.push(p);
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
} catch {
|
|
3932
|
+
}
|
|
3933
|
+
rmSync3(lockPath, { force: true });
|
|
3934
|
+
removed.push(lockPath);
|
|
3935
|
+
return removed;
|
|
3936
|
+
}
|
|
3937
|
+
function registerReset(program2) {
|
|
3938
|
+
program2.command("logout").description("Sign out \u2014 remove the cached (global) auth token").action((_opts, cmd) => {
|
|
3939
|
+
const removed = clearToken();
|
|
3940
|
+
if (cmd.optsWithGlobals().json) return emit({ removed: removed ? [removed] : [] }, true);
|
|
3941
|
+
console.log(
|
|
3942
|
+
removed ? style.green("Signed out \u2014 auth token removed.") : style.dim("Already signed out (no token).")
|
|
3943
|
+
);
|
|
3944
|
+
});
|
|
3945
|
+
program2.command("reset").description("Reset LOCAL CLI state for this directory (./.sechroom/, legacy ./.sechroom.json + ./.sem, ./.claude/skills); --global also wipes the machine-wide token + config + ~/.claude/skills").option("--global", "also remove the global auth token, config, and ~/.claude/skills").option("-y, --yes", "don't prompt for confirmation").option("--json", "machine output").action(async (opts, cmd) => {
|
|
3946
|
+
const json = cmd.optsWithGlobals().json;
|
|
3947
|
+
const global = Boolean(opts.global);
|
|
3948
|
+
if (!opts.yes && canPrompt()) {
|
|
3949
|
+
const scope = global ? "this directory's local state AND your global auth token + config + ~/.claude/skills" : "this directory's local state (./.sechroom/, legacy ./.sechroom.json + ./.sem, ./.claude/skills)";
|
|
3950
|
+
if (!await promptYesNo(`Remove ${scope}?`)) {
|
|
3951
|
+
if (!json) console.log(style.dim("Cancelled."));
|
|
3952
|
+
return;
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
const removed = [];
|
|
3956
|
+
const stateDir = join10(process.cwd(), ".sechroom");
|
|
3957
|
+
if (existsSync10(stateDir)) {
|
|
3958
|
+
rmSync3(stateDir, { recursive: true, force: true });
|
|
3959
|
+
removed.push(stateDir);
|
|
3960
|
+
}
|
|
3961
|
+
const legacyCfg = join10(process.cwd(), ".sechroom.json");
|
|
3962
|
+
if (existsSync10(legacyCfg)) {
|
|
3963
|
+
rmSync3(legacyCfg, { force: true });
|
|
3964
|
+
removed.push(legacyCfg);
|
|
3965
|
+
}
|
|
3966
|
+
const legacySem = join10(process.cwd(), ".sem");
|
|
3967
|
+
if (existsSync10(legacySem)) {
|
|
3968
|
+
rmSync3(legacySem, { force: true });
|
|
3969
|
+
removed.push(legacySem);
|
|
3970
|
+
}
|
|
3971
|
+
removed.push(...removeMaterialisedSkills(localSkillsDir()));
|
|
3972
|
+
if (global) {
|
|
3973
|
+
const tok = clearToken();
|
|
3974
|
+
if (tok) removed.push(tok);
|
|
3975
|
+
const cfg = clearPersisted();
|
|
3976
|
+
if (cfg) removed.push(cfg);
|
|
3977
|
+
removed.push(...removeMaterialisedSkills(globalSkillsDir()));
|
|
3978
|
+
}
|
|
3979
|
+
if (json) return emit({ global, removed }, true);
|
|
3980
|
+
if (removed.length === 0) {
|
|
3981
|
+
console.log(style.dim(global ? "Nothing to remove \u2014 already clean." : "No local CLI state in this directory."));
|
|
3982
|
+
return;
|
|
3983
|
+
}
|
|
3984
|
+
console.log(style.green(`Reset complete \u2014 removed ${removed.length} item(s):`));
|
|
3985
|
+
removed.forEach((p) => console.log(" " + style.dim("\u2022") + " " + p));
|
|
3986
|
+
if (global) console.log(style.dim("Run 'sechroom onboard' to set up again."));
|
|
3987
|
+
});
|
|
3988
|
+
}
|
|
2281
3989
|
|
|
2282
3990
|
// src/index.ts
|
|
2283
3991
|
function resolveVersion() {
|
|
2284
3992
|
try {
|
|
2285
3993
|
const pkg = JSON.parse(
|
|
2286
|
-
|
|
3994
|
+
readFileSync8(new URL("../package.json", import.meta.url), "utf8")
|
|
2287
3995
|
);
|
|
2288
3996
|
return pkg.version ?? "0.0.0";
|
|
2289
3997
|
} catch {
|
|
@@ -2299,7 +4007,7 @@ Examples:
|
|
|
2299
4007
|
$ sechroom onboard guided first-run: configure, sign in, wire this project
|
|
2300
4008
|
$ sechroom login sign in via browser (OAuth + PKCE)
|
|
2301
4009
|
$ sechroom config set tenant ocd set your tenant (global)
|
|
2302
|
-
$ sechroom config set --local tenant cli-smoke pin tenant for this directory (.sechroom.json)
|
|
4010
|
+
$ sechroom config set --local tenant cli-smoke pin tenant for this directory (committed .sechroom.json)
|
|
2303
4011
|
$ sechroom config show resolved config + which source won
|
|
2304
4012
|
|
|
2305
4013
|
$ sechroom memory create --text "a note" --title "Note" --tag idea
|
|
@@ -2311,7 +4019,7 @@ Examples:
|
|
|
2311
4019
|
$ sechroom --json memory search "auth" compact JSON for scripts and agents
|
|
2312
4020
|
$ SECHROOM_TOKEN=<bearer> sechroom --json memory get mem_XXXX headless
|
|
2313
4021
|
|
|
2314
|
-
Config precedence (high -> low): --flag > env (SECHROOM_*) > ./.sechroom.json > global > default.
|
|
4022
|
+
Config precedence (high -> low): --flag > env (SECHROOM_*) > directory-local (committed ./.sechroom.json, shadowed per-field by the gitignored ./.sechroom/config.json override) > global > default.
|
|
2315
4023
|
Run 'sechroom <command> --help' for command-specific examples.`
|
|
2316
4024
|
);
|
|
2317
4025
|
program.hook("preAction", (_thisCmd, actionCmd) => {
|
|
@@ -2337,14 +4045,14 @@ config.addHelpText(
|
|
|
2337
4045
|
Examples:
|
|
2338
4046
|
$ sechroom config set baseUrl https://app.sechroom.ai/api prod (staging: https://staging.app.sechroom.ai/api)
|
|
2339
4047
|
$ sechroom config set tenant ocd
|
|
2340
|
-
$ sechroom config set --local tenant cli-smoke this dir + subdirs (.sechroom.json)
|
|
4048
|
+
$ sechroom config set --local tenant cli-smoke this dir + subdirs (committed .sechroom.json)
|
|
2341
4049
|
$ sechroom config set clientId dyn-XXXX global-only escape hatch (no DCR endpoint)
|
|
2342
4050
|
$ sechroom config show --json`
|
|
2343
4051
|
);
|
|
2344
|
-
config.command("set <key> <value>").description("Set baseUrl | tenant | clientId (clientId is global-only)").option("--local", "Write
|
|
4052
|
+
config.command("set <key> <value>").description("Set baseUrl | tenant | workspaceId | defaultProjectId | clientId (clientId is global-only)").option("--local", "Write the committed directory-local .sechroom.json (nearest up the tree, else cwd) instead of the global config").action((key, value, opts) => {
|
|
2345
4053
|
if (opts.local) {
|
|
2346
|
-
if (!["baseUrl", "tenant"].includes(key)) {
|
|
2347
|
-
process.stderr.write(`--local supports only: baseUrl | tenant (clientId is global)
|
|
4054
|
+
if (!["baseUrl", "tenant", "workspaceId", "defaultProjectId"].includes(key)) {
|
|
4055
|
+
process.stderr.write(`--local supports only: baseUrl | tenant | workspaceId | defaultProjectId (clientId is global)
|
|
2348
4056
|
`);
|
|
2349
4057
|
process.exit(1);
|
|
2350
4058
|
}
|
|
@@ -2353,8 +4061,8 @@ config.command("set <key> <value>").description("Set baseUrl | tenant | clientId
|
|
|
2353
4061
|
`);
|
|
2354
4062
|
return;
|
|
2355
4063
|
}
|
|
2356
|
-
if (!["baseUrl", "tenant", "clientId"].includes(key)) {
|
|
2357
|
-
process.stderr.write(`unknown key: ${key} (expected baseUrl | tenant | clientId)
|
|
4064
|
+
if (!["baseUrl", "tenant", "clientId", "workspaceId", "defaultProjectId"].includes(key)) {
|
|
4065
|
+
process.stderr.write(`unknown key: ${key} (expected baseUrl | tenant | workspaceId | defaultProjectId | clientId)
|
|
2358
4066
|
`);
|
|
2359
4067
|
process.exit(1);
|
|
2360
4068
|
}
|
|
@@ -2368,7 +4076,7 @@ config.command("show").description("Print resolved config + sources (flag > env
|
|
|
2368
4076
|
if (g.json) {
|
|
2369
4077
|
process.stdout.write(
|
|
2370
4078
|
JSON.stringify({
|
|
2371
|
-
resolved: { baseUrl: d.baseUrl, tenant: d.tenant },
|
|
4079
|
+
resolved: { baseUrl: d.baseUrl, tenant: d.tenant, workspaceId: d.workspaceId },
|
|
2372
4080
|
global: readPersisted(),
|
|
2373
4081
|
local: readLocalConfig()
|
|
2374
4082
|
}) + "\n"
|
|
@@ -2376,8 +4084,9 @@ config.command("show").description("Print resolved config + sources (flag > env
|
|
|
2376
4084
|
return;
|
|
2377
4085
|
}
|
|
2378
4086
|
process.stdout.write(
|
|
2379
|
-
`baseUrl:
|
|
2380
|
-
tenant:
|
|
4087
|
+
`baseUrl: ${d.baseUrl.value} [${d.baseUrl.source}]
|
|
4088
|
+
tenant: ${d.tenant.value ?? "(unset)"} [${d.tenant.source}]
|
|
4089
|
+
workspaceId: ${d.workspaceId.value ?? "(unset)"} [${d.workspaceId.source}]
|
|
2381
4090
|
|
|
2382
4091
|
global: ${JSON.stringify(readPersisted())}
|
|
2383
4092
|
local: ${d.localPath ?? "(none)"} ${JSON.stringify(readLocalConfig())}
|
|
@@ -2392,12 +4101,16 @@ registerWorkspace(program);
|
|
|
2392
4101
|
registerProject(program);
|
|
2393
4102
|
registerFiling(program);
|
|
2394
4103
|
registerContinuity(program);
|
|
4104
|
+
registerHook(program);
|
|
2395
4105
|
registerId(program);
|
|
2396
4106
|
registerAccount(program);
|
|
2397
4107
|
registerChat(program);
|
|
2398
4108
|
registerInit(program);
|
|
2399
4109
|
registerSetup(program);
|
|
2400
4110
|
registerOnboard(program);
|
|
4111
|
+
registerSweep(program);
|
|
4112
|
+
registerSkills(program);
|
|
4113
|
+
registerReset(program);
|
|
2401
4114
|
program.parseAsync().catch((err2) => {
|
|
2402
4115
|
process.stderr.write(`error: ${err2 instanceof Error ? err2.message : String(err2)}
|
|
2403
4116
|
`);
|