@sechroom/cli 2026.8.2-rc.047ac2c6d → 2026.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1233 -177
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4366,9 +4366,19 @@ import { dirname as dirname5, join as join7 } from "path";
|
|
|
4366
4366
|
function claudeDesktopConfigPath(home) {
|
|
4367
4367
|
switch (process.platform) {
|
|
4368
4368
|
case "darwin":
|
|
4369
|
-
return join7(
|
|
4369
|
+
return join7(
|
|
4370
|
+
home,
|
|
4371
|
+
"Library",
|
|
4372
|
+
"Application Support",
|
|
4373
|
+
"Claude",
|
|
4374
|
+
"claude_desktop_config.json"
|
|
4375
|
+
);
|
|
4370
4376
|
case "win32":
|
|
4371
|
-
return join7(
|
|
4377
|
+
return join7(
|
|
4378
|
+
process.env.APPDATA ?? join7(home, "AppData", "Roaming"),
|
|
4379
|
+
"Claude",
|
|
4380
|
+
"claude_desktop_config.json"
|
|
4381
|
+
);
|
|
4372
4382
|
default:
|
|
4373
4383
|
return join7(home, ".config", "Claude", "claude_desktop_config.json");
|
|
4374
4384
|
}
|
|
@@ -4376,30 +4386,53 @@ function claudeDesktopConfigPath(home) {
|
|
|
4376
4386
|
function clientTargets(cwd, opts = {}) {
|
|
4377
4387
|
const home = homedir3();
|
|
4378
4388
|
const claudeDir = opts.claudeDir ?? join7(home, ".claude");
|
|
4379
|
-
const codexHome = opts.codexHome
|
|
4389
|
+
const codexHome = opts.codexHome === void 0 ? join7(home, ".codex") : opts.codexHome;
|
|
4380
4390
|
return {
|
|
4381
4391
|
"claude-code": {
|
|
4382
4392
|
key: "claude-code",
|
|
4383
4393
|
label: "Claude Code",
|
|
4384
|
-
mcp: {
|
|
4394
|
+
mcp: {
|
|
4395
|
+
surfaceKey: "claude-code",
|
|
4396
|
+
sectionType: SectionType.McpConfig,
|
|
4397
|
+
path: join7(cwd, ".mcp.json"),
|
|
4398
|
+
format: "json"
|
|
4399
|
+
},
|
|
4385
4400
|
instruction: { surfaceKey: "claude-code", path: join7(cwd, "CLAUDE.md") }
|
|
4386
4401
|
},
|
|
4387
4402
|
"claude-desktop": {
|
|
4388
4403
|
key: "claude-desktop",
|
|
4389
4404
|
label: "Claude Desktop",
|
|
4390
|
-
mcp: {
|
|
4391
|
-
|
|
4405
|
+
mcp: {
|
|
4406
|
+
surfaceKey: "claude-desktop",
|
|
4407
|
+
sectionType: SectionType.McpConfig,
|
|
4408
|
+
path: claudeDesktopConfigPath(home),
|
|
4409
|
+
format: "json"
|
|
4410
|
+
},
|
|
4411
|
+
instruction: {
|
|
4412
|
+
surfaceKey: "claude-desktop",
|
|
4413
|
+
path: join7(claudeDir, "CLAUDE.md")
|
|
4414
|
+
}
|
|
4392
4415
|
},
|
|
4393
4416
|
codex: {
|
|
4394
4417
|
key: "codex",
|
|
4395
4418
|
label: "Codex CLI",
|
|
4396
|
-
mcp:
|
|
4419
|
+
mcp: codexHome ? {
|
|
4420
|
+
surfaceKey: "chatgpt",
|
|
4421
|
+
sectionType: SectionType.McpConfigToml,
|
|
4422
|
+
path: join7(codexHome, "config.toml"),
|
|
4423
|
+
format: "toml"
|
|
4424
|
+
} : null,
|
|
4397
4425
|
instruction: { surfaceKey: "chatgpt", path: join7(cwd, "AGENTS.md") }
|
|
4398
4426
|
},
|
|
4399
4427
|
cursor: {
|
|
4400
4428
|
key: "cursor",
|
|
4401
4429
|
label: "Cursor",
|
|
4402
|
-
mcp: {
|
|
4430
|
+
mcp: {
|
|
4431
|
+
surfaceKey: "claude-code",
|
|
4432
|
+
sectionType: SectionType.McpConfig,
|
|
4433
|
+
path: join7(cwd, ".cursor", "mcp.json"),
|
|
4434
|
+
format: "json"
|
|
4435
|
+
},
|
|
4403
4436
|
instruction: { surfaceKey: "chatgpt", path: join7(cwd, "AGENTS.md") }
|
|
4404
4437
|
},
|
|
4405
4438
|
antigravity: {
|
|
@@ -4411,20 +4444,34 @@ function clientTargets(cwd, opts = {}) {
|
|
|
4411
4444
|
// `type` — comes from the `antigravity` server surface, so we don't
|
|
4412
4445
|
// hardcode it here. Instructions go in the project `AGENTS.md`
|
|
4413
4446
|
// (cross-tool, shared with Codex/Cursor).
|
|
4414
|
-
mcp: {
|
|
4447
|
+
mcp: {
|
|
4448
|
+
surfaceKey: "antigravity",
|
|
4449
|
+
sectionType: SectionType.McpConfig,
|
|
4450
|
+
path: join7(home, ".gemini", "config", "mcp_config.json"),
|
|
4451
|
+
format: "json"
|
|
4452
|
+
},
|
|
4415
4453
|
instruction: { surfaceKey: "antigravity", path: join7(cwd, "AGENTS.md") }
|
|
4416
4454
|
}
|
|
4417
4455
|
};
|
|
4418
4456
|
}
|
|
4419
|
-
var ALL_CLIENT_KEYS = [
|
|
4457
|
+
var ALL_CLIENT_KEYS = [
|
|
4458
|
+
"claude-code",
|
|
4459
|
+
"claude-desktop",
|
|
4460
|
+
"codex",
|
|
4461
|
+
"cursor",
|
|
4462
|
+
"antigravity"
|
|
4463
|
+
];
|
|
4420
4464
|
var DEFAULT_CLIENT_KEY = "claude-code";
|
|
4421
4465
|
function detectInstalledClients(cwd) {
|
|
4422
4466
|
const home = homedir3();
|
|
4423
4467
|
const detected = [];
|
|
4424
|
-
if (resolveClaudeTargets({}).some((t) => existsSync5(t.dir)))
|
|
4425
|
-
|
|
4468
|
+
if (resolveClaudeTargets({}).some((t) => existsSync5(t.dir)))
|
|
4469
|
+
detected.push("claude-code");
|
|
4470
|
+
if (existsSync5(dirname5(claudeDesktopConfigPath(home))))
|
|
4471
|
+
detected.push("claude-desktop");
|
|
4426
4472
|
if (resolveCodexHomes({}).some((d) => existsSync5(d))) detected.push("codex");
|
|
4427
|
-
if (existsSync5(join7(home, ".cursor")) || existsSync5(join7(cwd, ".cursor")))
|
|
4473
|
+
if (existsSync5(join7(home, ".cursor")) || existsSync5(join7(cwd, ".cursor")))
|
|
4474
|
+
detected.push("cursor");
|
|
4428
4475
|
if (existsSync5(join7(home, ".gemini"))) detected.push("antigravity");
|
|
4429
4476
|
return detected;
|
|
4430
4477
|
}
|
|
@@ -10117,6 +10164,41 @@ function resolveCreateBody(textOpt, fileOpt) {
|
|
|
10117
10164
|
);
|
|
10118
10165
|
return { text: text2, defaultTitle };
|
|
10119
10166
|
}
|
|
10167
|
+
async function fetchAllMemoryRelationships(cfg, memoryId) {
|
|
10168
|
+
const pages = [];
|
|
10169
|
+
let page = 1;
|
|
10170
|
+
while (true) {
|
|
10171
|
+
const current = await runApi(
|
|
10172
|
+
`Fetching relationships (page ${page})`,
|
|
10173
|
+
async () => {
|
|
10174
|
+
const client = await makeClient(cfg);
|
|
10175
|
+
return client.GET("/memories/{memoryId}/relationships", {
|
|
10176
|
+
params: { path: { memoryId }, query: { page } }
|
|
10177
|
+
});
|
|
10178
|
+
}
|
|
10179
|
+
);
|
|
10180
|
+
pages.push(current);
|
|
10181
|
+
const hasNextPage = current.hasNextPage ?? (current.pageCount !== void 0 ? current.page < current.pageCount : current.page * current.pageSize < current.count);
|
|
10182
|
+
if (!hasNextPage) break;
|
|
10183
|
+
page = current.page + 1;
|
|
10184
|
+
}
|
|
10185
|
+
const first = pages[0];
|
|
10186
|
+
if (!first || pages.length === 1) return first;
|
|
10187
|
+
const items = pages.flatMap((current) => current.items);
|
|
10188
|
+
return {
|
|
10189
|
+
...first,
|
|
10190
|
+
items,
|
|
10191
|
+
page: 1,
|
|
10192
|
+
pageSize: items.length,
|
|
10193
|
+
pageCount: 1,
|
|
10194
|
+
hasPreviousPage: false,
|
|
10195
|
+
hasNextPage: false,
|
|
10196
|
+
isFirstPage: true,
|
|
10197
|
+
isLastPage: true,
|
|
10198
|
+
firstItemOnPage: 1,
|
|
10199
|
+
lastItemOnPage: items.length
|
|
10200
|
+
};
|
|
10201
|
+
}
|
|
10120
10202
|
function registerMemory(program2) {
|
|
10121
10203
|
const memory = program2.command("memory").description("Create, read, and search memories");
|
|
10122
10204
|
memory.addHelpText(
|
|
@@ -10371,7 +10453,16 @@ ${plan.rows.map(
|
|
|
10371
10453
|
false
|
|
10372
10454
|
);
|
|
10373
10455
|
});
|
|
10374
|
-
memory.command("get <memoryId>").description("Fetch a memory by id (GET /memories/{memoryId})").
|
|
10456
|
+
const get = memory.command("get <memoryId>").description("Fetch a memory by id (GET /memories/{memoryId})").option(
|
|
10457
|
+
"--with-relationships",
|
|
10458
|
+
"Include the memory's relationships in the response",
|
|
10459
|
+
false
|
|
10460
|
+
).addHelpText(
|
|
10461
|
+
"after",
|
|
10462
|
+
`
|
|
10463
|
+
Note: CLI memory get omits relationships by default; MCP get_memory includes them. Use --with-relationships to read all relationship pages, or relationship list <memoryId> to inspect edges.`
|
|
10464
|
+
);
|
|
10465
|
+
get.action(async (memoryId, opts, cmd) => {
|
|
10375
10466
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
10376
10467
|
const data = await runApi("Fetching memory", async () => {
|
|
10377
10468
|
const client = await makeClient(cfg);
|
|
@@ -10379,6 +10470,11 @@ ${plan.rows.map(
|
|
|
10379
10470
|
params: { path: { memoryId } }
|
|
10380
10471
|
});
|
|
10381
10472
|
});
|
|
10473
|
+
if (opts.withRelationships) {
|
|
10474
|
+
const relationships = await fetchAllMemoryRelationships(cfg, memoryId);
|
|
10475
|
+
emit({ ...data, relationships }, cmd.optsWithGlobals().json);
|
|
10476
|
+
return;
|
|
10477
|
+
}
|
|
10382
10478
|
emit(data, cmd.optsWithGlobals().json);
|
|
10383
10479
|
});
|
|
10384
10480
|
memory.command("search <query>").description(
|
|
@@ -10726,8 +10822,18 @@ ${plan.rows.map(
|
|
|
10726
10822
|
}
|
|
10727
10823
|
|
|
10728
10824
|
// src/setup/apply.ts
|
|
10729
|
-
import { createHash as createHash5 } from "crypto";
|
|
10730
|
-
import {
|
|
10825
|
+
import { createHash as createHash5, randomUUID as randomUUID3 } from "crypto";
|
|
10826
|
+
import {
|
|
10827
|
+
chmodSync as chmodSync2,
|
|
10828
|
+
copyFileSync,
|
|
10829
|
+
existsSync as existsSync13,
|
|
10830
|
+
mkdirSync as mkdirSync16,
|
|
10831
|
+
readFileSync as readFileSync16,
|
|
10832
|
+
renameSync as renameSync4,
|
|
10833
|
+
rmSync as rmSync7,
|
|
10834
|
+
statSync as statSync5,
|
|
10835
|
+
writeFileSync as writeFileSync15
|
|
10836
|
+
} from "fs";
|
|
10731
10837
|
import { dirname as dirname15 } from "path";
|
|
10732
10838
|
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
10733
10839
|
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
@@ -10773,11 +10879,17 @@ function parseManagedBlock(content, block) {
|
|
|
10773
10879
|
const keyed = content.match(keyedBlockRe(block));
|
|
10774
10880
|
if (keyed) {
|
|
10775
10881
|
const attrs = parseAttrs(keyed[0].slice(0, keyed[0].indexOf("\n")));
|
|
10776
|
-
return {
|
|
10882
|
+
return {
|
|
10883
|
+
block,
|
|
10884
|
+
source: attrs.source ?? null,
|
|
10885
|
+
sha256: attrs.sha256 ?? null,
|
|
10886
|
+
body: innerBody(keyed[0])
|
|
10887
|
+
};
|
|
10777
10888
|
}
|
|
10778
10889
|
if (block === "role-template") {
|
|
10779
10890
|
const legacy = content.match(legacyBlockRe());
|
|
10780
|
-
if (legacy)
|
|
10891
|
+
if (legacy)
|
|
10892
|
+
return { block, source: null, sha256: null, body: innerBody(legacy[0]) };
|
|
10781
10893
|
}
|
|
10782
10894
|
return null;
|
|
10783
10895
|
}
|
|
@@ -10799,33 +10911,446 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
10799
10911
|
try {
|
|
10800
10912
|
current = JSON.parse(readFileSync16(path, "utf8"));
|
|
10801
10913
|
} catch {
|
|
10802
|
-
return {
|
|
10914
|
+
return {
|
|
10915
|
+
kind: "mcp",
|
|
10916
|
+
path,
|
|
10917
|
+
status: "skipped",
|
|
10918
|
+
note: "existing file isn't valid JSON \u2014 left untouched"
|
|
10919
|
+
};
|
|
10803
10920
|
}
|
|
10804
10921
|
}
|
|
10805
|
-
current.mcpServers = {
|
|
10922
|
+
current.mcpServers = {
|
|
10923
|
+
...current.mcpServers ?? {},
|
|
10924
|
+
...incoming.mcpServers ?? {}
|
|
10925
|
+
};
|
|
10806
10926
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
10807
10927
|
ensureDir2(path);
|
|
10808
10928
|
writeFileSync15(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
|
|
10809
10929
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
10810
10930
|
}
|
|
10811
|
-
|
|
10931
|
+
var CODEX_MCP_TABLE = "mcp_servers.sechroom";
|
|
10932
|
+
function decodeTomlBasicString(value) {
|
|
10933
|
+
let decoded = "";
|
|
10934
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
10935
|
+
const char = value[i];
|
|
10936
|
+
if (char !== "\\") {
|
|
10937
|
+
decoded += char;
|
|
10938
|
+
continue;
|
|
10939
|
+
}
|
|
10940
|
+
const escaped = value[++i];
|
|
10941
|
+
if (escaped === void 0) throw new Error("unterminated TOML escape");
|
|
10942
|
+
const simple = {
|
|
10943
|
+
b: "\b",
|
|
10944
|
+
f: "\f",
|
|
10945
|
+
n: "\n",
|
|
10946
|
+
r: "\r",
|
|
10947
|
+
t: " ",
|
|
10948
|
+
'"': '"',
|
|
10949
|
+
"\\": "\\"
|
|
10950
|
+
};
|
|
10951
|
+
if (escaped in simple) {
|
|
10952
|
+
decoded += simple[escaped];
|
|
10953
|
+
continue;
|
|
10954
|
+
}
|
|
10955
|
+
const digits = escaped === "u" ? 4 : escaped === "U" ? 8 : 0;
|
|
10956
|
+
if (digits === 0) throw new Error(`invalid TOML escape '\\\\${escaped}'`);
|
|
10957
|
+
const code = value.slice(i + 1, i + 1 + digits);
|
|
10958
|
+
if (!new RegExp(`^[0-9a-fA-F]{${digits}}$`).test(code))
|
|
10959
|
+
throw new Error(`invalid TOML Unicode escape '\\\\${escaped}${code}'`);
|
|
10960
|
+
decoded += String.fromCodePoint(Number.parseInt(code, 16));
|
|
10961
|
+
i += digits;
|
|
10962
|
+
}
|
|
10963
|
+
return decoded;
|
|
10964
|
+
}
|
|
10965
|
+
function normalizeTomlKey(raw) {
|
|
10966
|
+
const segments = [];
|
|
10967
|
+
let i = 0;
|
|
10968
|
+
while (i < raw.length) {
|
|
10969
|
+
while (/\s/.test(raw[i] ?? "")) i += 1;
|
|
10970
|
+
if (i >= raw.length) break;
|
|
10971
|
+
const quote = raw[i];
|
|
10972
|
+
let segment;
|
|
10973
|
+
if (quote === '"' || quote === "'") {
|
|
10974
|
+
i += 1;
|
|
10975
|
+
const start = i;
|
|
10976
|
+
let escaped = false;
|
|
10977
|
+
while (i < raw.length) {
|
|
10978
|
+
const char = raw[i];
|
|
10979
|
+
if (quote === '"' && escaped) {
|
|
10980
|
+
escaped = false;
|
|
10981
|
+
i += 1;
|
|
10982
|
+
continue;
|
|
10983
|
+
}
|
|
10984
|
+
if (quote === '"' && char === "\\") {
|
|
10985
|
+
escaped = true;
|
|
10986
|
+
i += 1;
|
|
10987
|
+
continue;
|
|
10988
|
+
}
|
|
10989
|
+
if (char === quote) break;
|
|
10990
|
+
i += 1;
|
|
10991
|
+
}
|
|
10992
|
+
if (raw[i] !== quote) throw new Error(`unterminated TOML key '${raw}'`);
|
|
10993
|
+
const encoded = raw.slice(start, i);
|
|
10994
|
+
segment = quote === '"' ? decodeTomlBasicString(encoded) : encoded;
|
|
10995
|
+
i += 1;
|
|
10996
|
+
} else {
|
|
10997
|
+
const start = i;
|
|
10998
|
+
while (i < raw.length && /[A-Za-z0-9_-]/.test(raw[i])) i += 1;
|
|
10999
|
+
segment = raw.slice(start, i);
|
|
11000
|
+
if (!segment) throw new Error(`invalid TOML key '${raw.trim()}'`);
|
|
11001
|
+
}
|
|
11002
|
+
segments.push(segment);
|
|
11003
|
+
while (/\s/.test(raw[i] ?? "")) i += 1;
|
|
11004
|
+
if (i >= raw.length) break;
|
|
11005
|
+
if (raw[i] !== ".") throw new Error(`invalid TOML key '${raw.trim()}'`);
|
|
11006
|
+
i += 1;
|
|
11007
|
+
}
|
|
11008
|
+
if (segments.length === 0) throw new Error("empty TOML key");
|
|
11009
|
+
return segments;
|
|
11010
|
+
}
|
|
11011
|
+
function sameTomlKey(left, right) {
|
|
11012
|
+
return left.length === right.length && left.every((segment, index) => segment === right[index]);
|
|
11013
|
+
}
|
|
11014
|
+
function isTomlKeyPrefix(prefix, candidate) {
|
|
11015
|
+
return prefix.length < candidate.length && prefix.every((segment, index) => segment === candidate[index]);
|
|
11016
|
+
}
|
|
11017
|
+
function tomlKeyLabel(key) {
|
|
11018
|
+
return key.join(".");
|
|
11019
|
+
}
|
|
11020
|
+
function stripTomlComment(line) {
|
|
11021
|
+
let quote = null;
|
|
11022
|
+
let escaped = false;
|
|
11023
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
11024
|
+
const rest = line.slice(i);
|
|
11025
|
+
const char = line[i];
|
|
11026
|
+
if (quote === "multiline-basic") {
|
|
11027
|
+
if (rest.startsWith('"""') && !escaped) {
|
|
11028
|
+
escaped = false;
|
|
11029
|
+
quote = null;
|
|
11030
|
+
i += 2;
|
|
11031
|
+
continue;
|
|
11032
|
+
}
|
|
11033
|
+
if (escaped) escaped = false;
|
|
11034
|
+
else if (char === "\\") escaped = true;
|
|
11035
|
+
continue;
|
|
11036
|
+
}
|
|
11037
|
+
if (quote === "multiline-literal") {
|
|
11038
|
+
if (rest.startsWith("'''")) {
|
|
11039
|
+
i += 2;
|
|
11040
|
+
quote = null;
|
|
11041
|
+
}
|
|
11042
|
+
continue;
|
|
11043
|
+
}
|
|
11044
|
+
if (quote === "basic") {
|
|
11045
|
+
if (escaped) escaped = false;
|
|
11046
|
+
else if (char === "\\") escaped = true;
|
|
11047
|
+
else if (char === '"') quote = null;
|
|
11048
|
+
continue;
|
|
11049
|
+
}
|
|
11050
|
+
if (quote === "literal") {
|
|
11051
|
+
if (char === "'") quote = null;
|
|
11052
|
+
continue;
|
|
11053
|
+
}
|
|
11054
|
+
if (rest.startsWith('"""')) {
|
|
11055
|
+
quote = "multiline-basic";
|
|
11056
|
+
i += 2;
|
|
11057
|
+
} else if (rest.startsWith("'''")) {
|
|
11058
|
+
quote = "multiline-literal";
|
|
11059
|
+
i += 2;
|
|
11060
|
+
} else if (char === '"') quote = "basic";
|
|
11061
|
+
else if (char === "'") quote = "literal";
|
|
11062
|
+
else if (char === "#") return line.slice(0, i);
|
|
11063
|
+
}
|
|
11064
|
+
return line;
|
|
11065
|
+
}
|
|
11066
|
+
function scanTomlValue(value, state) {
|
|
11067
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
11068
|
+
const rest = value.slice(i);
|
|
11069
|
+
const char = value[i];
|
|
11070
|
+
if (state.quote === "multiline-basic") {
|
|
11071
|
+
if (rest.startsWith('"""') && !state.escaped) {
|
|
11072
|
+
state.quote = null;
|
|
11073
|
+
i += 2;
|
|
11074
|
+
continue;
|
|
11075
|
+
}
|
|
11076
|
+
if (state.escaped) state.escaped = false;
|
|
11077
|
+
else if (char === "\\") state.escaped = true;
|
|
11078
|
+
continue;
|
|
11079
|
+
}
|
|
11080
|
+
if (state.quote === "multiline-literal") {
|
|
11081
|
+
if (rest.startsWith("'''")) {
|
|
11082
|
+
state.quote = null;
|
|
11083
|
+
i += 2;
|
|
11084
|
+
}
|
|
11085
|
+
continue;
|
|
11086
|
+
}
|
|
11087
|
+
if (state.quote === "basic") {
|
|
11088
|
+
if (state.escaped) state.escaped = false;
|
|
11089
|
+
else if (char === "\\") state.escaped = true;
|
|
11090
|
+
else if (char === '"') state.quote = null;
|
|
11091
|
+
continue;
|
|
11092
|
+
}
|
|
11093
|
+
if (state.quote === "literal") {
|
|
11094
|
+
if (char === "'") state.quote = null;
|
|
11095
|
+
continue;
|
|
11096
|
+
}
|
|
11097
|
+
if (char === "#") break;
|
|
11098
|
+
if (rest.startsWith('"""')) {
|
|
11099
|
+
state.quote = "multiline-basic";
|
|
11100
|
+
i += 2;
|
|
11101
|
+
} else if (rest.startsWith("'''")) {
|
|
11102
|
+
state.quote = "multiline-literal";
|
|
11103
|
+
i += 2;
|
|
11104
|
+
} else if (char === '"') state.quote = "basic";
|
|
11105
|
+
else if (char === "'") state.quote = "literal";
|
|
11106
|
+
else if (char === "[" || char === "{") state.containers.push(char);
|
|
11107
|
+
else if (char === "]" || char === "}") {
|
|
11108
|
+
const expected = char === "]" ? "[" : "{";
|
|
11109
|
+
if (state.containers.pop() !== expected)
|
|
11110
|
+
throw new Error(`unbalanced TOML delimiter '${char}'`);
|
|
11111
|
+
}
|
|
11112
|
+
}
|
|
11113
|
+
}
|
|
11114
|
+
function parseTomlHeader(rawLine) {
|
|
11115
|
+
const line = stripTomlComment(rawLine).trim();
|
|
11116
|
+
const array = line.startsWith("[[") && line.endsWith("]]");
|
|
11117
|
+
const table = !array && line.startsWith("[") && line.endsWith("]");
|
|
11118
|
+
if (!array && !table) return null;
|
|
11119
|
+
const prefix = array ? 2 : 1;
|
|
11120
|
+
const suffix = array ? 2 : 1;
|
|
11121
|
+
const rawName = line.slice(prefix, line.length - suffix).trim();
|
|
11122
|
+
if (!rawName) throw new Error("empty TOML table name");
|
|
11123
|
+
return { name: normalizeTomlKey(rawName), array };
|
|
11124
|
+
}
|
|
11125
|
+
function findTomlEquals(line) {
|
|
11126
|
+
let quote = null;
|
|
11127
|
+
let escaped = false;
|
|
11128
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
11129
|
+
const char = line[i];
|
|
11130
|
+
if (quote === "basic") {
|
|
11131
|
+
if (escaped) escaped = false;
|
|
11132
|
+
else if (char === "\\") escaped = true;
|
|
11133
|
+
else if (char === '"') quote = null;
|
|
11134
|
+
continue;
|
|
11135
|
+
}
|
|
11136
|
+
if (quote === "literal") {
|
|
11137
|
+
if (char === "'") quote = null;
|
|
11138
|
+
continue;
|
|
11139
|
+
}
|
|
11140
|
+
if (char === '"') quote = "basic";
|
|
11141
|
+
else if (char === "'") quote = "literal";
|
|
11142
|
+
else if (char === "=") return i;
|
|
11143
|
+
}
|
|
11144
|
+
return -1;
|
|
11145
|
+
}
|
|
11146
|
+
function arrayTableContext(name, arrays) {
|
|
11147
|
+
let best = null;
|
|
11148
|
+
for (let i = arrays.length - 1; i >= 0; i -= 1) {
|
|
11149
|
+
const candidate = arrays[i];
|
|
11150
|
+
if (isTomlKeyPrefix(candidate.key, name)) {
|
|
11151
|
+
if (!best || candidate.key.length > best.key.length) best = candidate;
|
|
11152
|
+
}
|
|
11153
|
+
}
|
|
11154
|
+
return best;
|
|
11155
|
+
}
|
|
11156
|
+
function tomlTableIdentity(key, context) {
|
|
11157
|
+
return `${context?.identity ?? "root"}:${JSON.stringify(key)}`;
|
|
11158
|
+
}
|
|
11159
|
+
function isTomlScalar(value) {
|
|
11160
|
+
const decimal = "[+-]?(?:0|[1-9](?:_?[0-9])*)(?:\\.[0-9](?:_?[0-9])*)?(?:[eE][+-]?[0-9](?:_?[0-9])*)?";
|
|
11161
|
+
const number = new RegExp(
|
|
11162
|
+
`^(?:${decimal}|[+-]?0x[0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[+-]?0o[0-7](?:_?[0-7])*|[+-]?0b[01](?:_?[01])*|[+-]?(?:inf|nan))$`
|
|
11163
|
+
);
|
|
11164
|
+
const datetime = /^(?:[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]+)?|[0-9]{4}-[0-9]{2}-[0-9]{2}(?:(?:[Tt ]|[Tt])[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]+)?(?:Z|[+-][0-9]{2}:[0-9]{2})?)?)$/;
|
|
11165
|
+
return number.test(value) || datetime.test(value) || /^(?:true|false)$/.test(value);
|
|
11166
|
+
}
|
|
11167
|
+
function validateTomlValueShape(value) {
|
|
11168
|
+
const token = stripTomlComment(value).trim();
|
|
11169
|
+
if (!token) throw new Error("empty TOML value");
|
|
11170
|
+
if (token.startsWith('"') || token.startsWith("'")) {
|
|
11171
|
+
const quote = token[0];
|
|
11172
|
+
if (quote && token.endsWith(quote)) return;
|
|
11173
|
+
}
|
|
11174
|
+
if (token.startsWith("[") && token.endsWith("]")) return;
|
|
11175
|
+
if (token.startsWith("{") && token.endsWith("}")) return;
|
|
11176
|
+
if (isTomlScalar(token)) return;
|
|
11177
|
+
throw new Error(`invalid TOML value '${token}'`);
|
|
11178
|
+
}
|
|
11179
|
+
function validateToml(content) {
|
|
11180
|
+
const tables = /* @__PURE__ */ new Set();
|
|
11181
|
+
const keys = /* @__PURE__ */ new Set();
|
|
11182
|
+
const arrayInstances = [];
|
|
11183
|
+
let currentTable = { identity: "root" };
|
|
11184
|
+
let valueState = {
|
|
11185
|
+
containers: [],
|
|
11186
|
+
quote: null,
|
|
11187
|
+
escaped: false
|
|
11188
|
+
};
|
|
11189
|
+
for (const rawLine of content.replace(/\r\n/g, "\n").split("\n")) {
|
|
11190
|
+
if (valueState.quote || valueState.containers.length > 0) {
|
|
11191
|
+
scanTomlValue(rawLine, valueState);
|
|
11192
|
+
continue;
|
|
11193
|
+
}
|
|
11194
|
+
const line = stripTomlComment(rawLine).trim();
|
|
11195
|
+
if (!line) continue;
|
|
11196
|
+
const header = parseTomlHeader(rawLine);
|
|
11197
|
+
if (header) {
|
|
11198
|
+
if (header.array) {
|
|
11199
|
+
const context = arrayTableContext(header.name, arrayInstances);
|
|
11200
|
+
const parentIdentity = context?.identity ?? null;
|
|
11201
|
+
const previous = [...arrayInstances].reverse().find(
|
|
11202
|
+
(candidate) => sameTomlKey(candidate.key, header.name) && candidate.parentIdentity === parentIdentity
|
|
11203
|
+
);
|
|
11204
|
+
const count = (previous?.count ?? 0) + 1;
|
|
11205
|
+
const instance = {
|
|
11206
|
+
key: header.name,
|
|
11207
|
+
parentIdentity,
|
|
11208
|
+
identity: `${parentIdentity ?? "root"}:${JSON.stringify(header.name)}#${count}`,
|
|
11209
|
+
count
|
|
11210
|
+
};
|
|
11211
|
+
arrayInstances.push(instance);
|
|
11212
|
+
currentTable = { identity: instance.identity };
|
|
11213
|
+
} else {
|
|
11214
|
+
const context = arrayTableContext(header.name, arrayInstances);
|
|
11215
|
+
currentTable = {
|
|
11216
|
+
identity: tomlTableIdentity(header.name, context)
|
|
11217
|
+
};
|
|
11218
|
+
if (tables.has(currentTable.identity))
|
|
11219
|
+
throw new Error(
|
|
11220
|
+
`duplicate TOML table '${tomlKeyLabel(header.name)}'`
|
|
11221
|
+
);
|
|
11222
|
+
tables.add(currentTable.identity);
|
|
11223
|
+
}
|
|
11224
|
+
continue;
|
|
11225
|
+
}
|
|
11226
|
+
const equals = findTomlEquals(line);
|
|
11227
|
+
if (equals <= 0) throw new Error(`invalid TOML line '${rawLine}'`);
|
|
11228
|
+
const key = normalizeTomlKey(line.slice(0, equals).trim());
|
|
11229
|
+
const value = line.slice(equals + 1).trim();
|
|
11230
|
+
if (!value || value.startsWith("#"))
|
|
11231
|
+
throw new Error(`empty TOML value for '${tomlKeyLabel(key)}'`);
|
|
11232
|
+
const fullKey = `${currentTable.identity}\0${JSON.stringify(key)}`;
|
|
11233
|
+
if (keys.has(fullKey))
|
|
11234
|
+
throw new Error(`duplicate TOML key '${tomlKeyLabel(key)}'`);
|
|
11235
|
+
keys.add(fullKey);
|
|
11236
|
+
scanTomlValue(line.slice(equals + 1), valueState);
|
|
11237
|
+
if (valueState.quote === "basic" || valueState.quote === "literal")
|
|
11238
|
+
throw new Error("unterminated TOML string");
|
|
11239
|
+
if (!valueState.quote && valueState.containers.length === 0)
|
|
11240
|
+
validateTomlValueShape(value);
|
|
11241
|
+
}
|
|
11242
|
+
if (valueState.quote) throw new Error("unterminated TOML string");
|
|
11243
|
+
if (valueState.containers.length > 0)
|
|
11244
|
+
throw new Error("unbalanced TOML value");
|
|
11245
|
+
}
|
|
11246
|
+
function isTomlTableHeader(line) {
|
|
11247
|
+
try {
|
|
11248
|
+
return parseTomlHeader(line) !== null;
|
|
11249
|
+
} catch {
|
|
11250
|
+
return false;
|
|
11251
|
+
}
|
|
11252
|
+
}
|
|
11253
|
+
function replaceTomlTable(content, tableName, replacement) {
|
|
11254
|
+
const target = normalizeTomlKey(tableName);
|
|
11255
|
+
const lines = content.replace(/\r\n/g, "\n").split("\n");
|
|
11256
|
+
const replacementLines = replacement.trim().split("\n");
|
|
11257
|
+
const kept = [];
|
|
11258
|
+
let i = 0;
|
|
11259
|
+
while (i < lines.length) {
|
|
11260
|
+
const header = parseTomlHeader(lines[i]);
|
|
11261
|
+
if (header && (sameTomlKey(header.name, target) || isTomlKeyPrefix(target, header.name))) {
|
|
11262
|
+
i += 1;
|
|
11263
|
+
while (i < lines.length && !isTomlTableHeader(lines[i])) i += 1;
|
|
11264
|
+
continue;
|
|
11265
|
+
}
|
|
11266
|
+
kept.push(lines[i]);
|
|
11267
|
+
i += 1;
|
|
11268
|
+
}
|
|
11269
|
+
const base = kept.join("\n").trim();
|
|
11270
|
+
return `${base.length > 0 ? `${base}
|
|
11271
|
+
|
|
11272
|
+
` : ""}${replacementLines.join("\n")}
|
|
11273
|
+
`;
|
|
11274
|
+
}
|
|
11275
|
+
var defaultTomlFileOperations = {
|
|
11276
|
+
copyFileSync,
|
|
11277
|
+
renameSync: renameSync4
|
|
11278
|
+
};
|
|
11279
|
+
function writeTomlAtomic(path, content, fileOperations = {}) {
|
|
11280
|
+
ensureDir2(path);
|
|
11281
|
+
const operations = { ...defaultTomlFileOperations, ...fileOperations };
|
|
11282
|
+
const temporary = `${path}.sechroom-${process.pid}-${randomUUID3()}.tmp`;
|
|
11283
|
+
const backup = `${path}.bak`;
|
|
11284
|
+
const backupTemporary = `${backup}.sechroom-${process.pid}-${randomUUID3()}.tmp`;
|
|
11285
|
+
const mode = existsSync13(path) ? statSync5(path).mode & 4095 : 384;
|
|
11286
|
+
try {
|
|
11287
|
+
writeFileSync15(temporary, content, { mode });
|
|
11288
|
+
chmodSync2(temporary, mode);
|
|
11289
|
+
validateToml(readFileSync16(temporary, "utf8"));
|
|
11290
|
+
if (existsSync13(path) && !existsSync13(backup)) {
|
|
11291
|
+
operations.copyFileSync(path, backupTemporary);
|
|
11292
|
+
chmodSync2(backupTemporary, mode);
|
|
11293
|
+
validateToml(readFileSync16(backupTemporary, "utf8"));
|
|
11294
|
+
operations.renameSync(backupTemporary, backup);
|
|
11295
|
+
}
|
|
11296
|
+
operations.renameSync(temporary, path);
|
|
11297
|
+
} finally {
|
|
11298
|
+
rmSync7(temporary, { force: true });
|
|
11299
|
+
rmSync7(backupTemporary, { force: true });
|
|
11300
|
+
}
|
|
11301
|
+
}
|
|
11302
|
+
function mergeCodexToml(path, snippet, dryRun, fileOperations = {}) {
|
|
10812
11303
|
const existed = existsSync13(path);
|
|
10813
|
-
|
|
10814
|
-
|
|
10815
|
-
|
|
10816
|
-
|
|
11304
|
+
const body = readOr(path, "");
|
|
11305
|
+
try {
|
|
11306
|
+
validateToml(body);
|
|
11307
|
+
} catch (error) {
|
|
11308
|
+
return {
|
|
11309
|
+
kind: "mcp",
|
|
11310
|
+
path,
|
|
11311
|
+
status: "skipped",
|
|
11312
|
+
note: `existing TOML is invalid \u2014 left untouched (${error.message})`
|
|
11313
|
+
};
|
|
11314
|
+
}
|
|
11315
|
+
const next = replaceTomlTable(body, CODEX_MCP_TABLE, snippet);
|
|
11316
|
+
try {
|
|
11317
|
+
validateToml(next);
|
|
11318
|
+
} catch (error) {
|
|
11319
|
+
return {
|
|
11320
|
+
kind: "mcp",
|
|
11321
|
+
path,
|
|
11322
|
+
status: "skipped",
|
|
11323
|
+
note: `generated TOML is invalid \u2014 left untouched (${error.message})`
|
|
11324
|
+
};
|
|
11325
|
+
}
|
|
10817
11326
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
10818
|
-
|
|
10819
|
-
|
|
11327
|
+
if (next === body)
|
|
11328
|
+
return { kind: "mcp", path, status: existed ? "current" : "created" };
|
|
11329
|
+
try {
|
|
11330
|
+
writeTomlAtomic(path, next, fileOperations);
|
|
11331
|
+
} catch (error) {
|
|
11332
|
+
return {
|
|
11333
|
+
kind: "mcp",
|
|
11334
|
+
path,
|
|
11335
|
+
status: "skipped",
|
|
11336
|
+
note: `could not replace TOML \u2014 left untouched (${error.message})`
|
|
11337
|
+
};
|
|
11338
|
+
}
|
|
10820
11339
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
10821
11340
|
}
|
|
10822
11341
|
function writeInstructionBlock(path, write, dryRun) {
|
|
10823
11342
|
const existed = existsSync13(path);
|
|
10824
11343
|
const next = computeBlockFile(readOr(path, ""), write);
|
|
10825
|
-
if (dryRun)
|
|
11344
|
+
if (dryRun)
|
|
11345
|
+
return { kind: "instruction", path, status: "dry-run", block: write.block };
|
|
10826
11346
|
ensureDir2(path);
|
|
10827
11347
|
writeFileSync15(path, next);
|
|
10828
|
-
return {
|
|
11348
|
+
return {
|
|
11349
|
+
kind: "instruction",
|
|
11350
|
+
path,
|
|
11351
|
+
status: existed ? "merged" : "created",
|
|
11352
|
+
block: write.block
|
|
11353
|
+
};
|
|
10829
11354
|
}
|
|
10830
11355
|
function computeBlockFile(current, write) {
|
|
10831
11356
|
const rendered = renderBlock(write);
|
|
@@ -10874,7 +11399,13 @@ function applyBlock(path, write, mode, dryRun) {
|
|
|
10874
11399
|
};
|
|
10875
11400
|
}
|
|
10876
11401
|
if (state === "current") {
|
|
10877
|
-
return {
|
|
11402
|
+
return {
|
|
11403
|
+
kind: "instruction",
|
|
11404
|
+
path,
|
|
11405
|
+
status: "current",
|
|
11406
|
+
eval: "current",
|
|
11407
|
+
block: write.block
|
|
11408
|
+
};
|
|
10878
11409
|
}
|
|
10879
11410
|
if (state === "drift" && mode !== "force") {
|
|
10880
11411
|
const proposedPath = `${path}.proposed`;
|
|
@@ -10909,7 +11440,12 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
10909
11440
|
const section = findSection(surface, target.mcp.sectionType);
|
|
10910
11441
|
const snippet = sectionSnippet(section);
|
|
10911
11442
|
if (!snippet) {
|
|
10912
|
-
actions.push({
|
|
11443
|
+
actions.push({
|
|
11444
|
+
kind: "mcp",
|
|
11445
|
+
path: target.mcp.path,
|
|
11446
|
+
status: "skipped",
|
|
11447
|
+
note: `no ${target.mcp.sectionType} section on surface '${target.mcp.surfaceKey}'`
|
|
11448
|
+
});
|
|
10913
11449
|
} else {
|
|
10914
11450
|
actions.push(
|
|
10915
11451
|
target.mcp.format === "toml" ? mergeCodexToml(target.mcp.path, snippet, dryRun) : mergeMcpJson(target.mcp.path, snippet, dryRun)
|
|
@@ -10920,25 +11456,44 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
10920
11456
|
const surface = findSurface(setup, target.instruction.surfaceKey);
|
|
10921
11457
|
const section = findSection(surface, SectionType.InstructionFile);
|
|
10922
11458
|
if (!section) {
|
|
10923
|
-
actions.push({
|
|
11459
|
+
actions.push({
|
|
11460
|
+
kind: "instruction",
|
|
11461
|
+
path: target.instruction.path,
|
|
11462
|
+
status: "skipped",
|
|
11463
|
+
note: `no instruction-file section on surface '${target.instruction.surfaceKey}'`
|
|
11464
|
+
});
|
|
10924
11465
|
} else {
|
|
10925
11466
|
const resolved = await withSpinner(
|
|
10926
11467
|
`Resolving ${target.label} agent instructions`,
|
|
10927
11468
|
() => resolveInstruction(cfg, section, opts.personalWorkspaceId)
|
|
10928
11469
|
);
|
|
10929
11470
|
if (!resolved) {
|
|
10930
|
-
actions.push({
|
|
11471
|
+
actions.push({
|
|
11472
|
+
kind: "instruction",
|
|
11473
|
+
path: target.instruction.path,
|
|
11474
|
+
status: "skipped",
|
|
11475
|
+
note: "no role template found in this tenant \u2014 install the SEM Starter bundle, then re-run `sechroom setup agent-files`"
|
|
11476
|
+
});
|
|
10931
11477
|
} else {
|
|
10932
11478
|
const action = applyBlock(
|
|
10933
11479
|
target.instruction.path,
|
|
10934
|
-
{
|
|
11480
|
+
{
|
|
11481
|
+
block: "role-template",
|
|
11482
|
+
body: resolved.body,
|
|
11483
|
+
source: resolved.sourceRef
|
|
11484
|
+
},
|
|
10935
11485
|
mode,
|
|
10936
11486
|
opts.dryRun
|
|
10937
11487
|
);
|
|
10938
|
-
actions.push(
|
|
11488
|
+
actions.push(
|
|
11489
|
+
resolved.source === "override" && action.status !== "current" ? { ...action, note: action.note ?? "your personal copy" } : action
|
|
11490
|
+
);
|
|
10939
11491
|
}
|
|
10940
11492
|
}
|
|
10941
|
-
const conventionsSection = findSection(
|
|
11493
|
+
const conventionsSection = findSection(
|
|
11494
|
+
surface,
|
|
11495
|
+
SectionType.WorkspaceConventions
|
|
11496
|
+
);
|
|
10942
11497
|
if (conventionsSection) {
|
|
10943
11498
|
const conventions = await withSpinner(
|
|
10944
11499
|
`Composing ${target.label} workspace conventions`,
|
|
@@ -10947,11 +11502,20 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
10947
11502
|
if (conventions) {
|
|
10948
11503
|
const action = applyBlock(
|
|
10949
11504
|
target.instruction.path,
|
|
10950
|
-
{
|
|
11505
|
+
{
|
|
11506
|
+
block: "workspace-conventions",
|
|
11507
|
+
body: conventions.body,
|
|
11508
|
+
source: `workspace:${cfg.workspaceId ?? ""}`
|
|
11509
|
+
},
|
|
10951
11510
|
mode,
|
|
10952
11511
|
opts.dryRun
|
|
10953
11512
|
);
|
|
10954
|
-
actions.push(
|
|
11513
|
+
actions.push(
|
|
11514
|
+
action.status === "current" ? action : {
|
|
11515
|
+
...action,
|
|
11516
|
+
note: action.note ?? `workspace conventions (${conventions.refs.length})`
|
|
11517
|
+
}
|
|
11518
|
+
);
|
|
10955
11519
|
}
|
|
10956
11520
|
}
|
|
10957
11521
|
}
|
|
@@ -11169,7 +11733,12 @@ function buildConventionDraft(title, rawKind, rawBody) {
|
|
|
11169
11733
|
|
|
11170
11734
|
${body}
|
|
11171
11735
|
`,
|
|
11172
|
-
tags: [
|
|
11736
|
+
tags: [
|
|
11737
|
+
"agent-setup-bundle",
|
|
11738
|
+
"scope:sechroom",
|
|
11739
|
+
`kind:${kind}`,
|
|
11740
|
+
"archetype:document"
|
|
11741
|
+
]
|
|
11173
11742
|
};
|
|
11174
11743
|
}
|
|
11175
11744
|
function copyChoice(opts) {
|
|
@@ -11182,9 +11751,16 @@ async function maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId,
|
|
|
11182
11751
|
const instr = targets[key]?.instruction;
|
|
11183
11752
|
if (!instr || seen.has(instr.surfaceKey)) continue;
|
|
11184
11753
|
seen.add(instr.surfaceKey);
|
|
11185
|
-
const section = findSection(
|
|
11754
|
+
const section = findSection(
|
|
11755
|
+
findSurface(setup, instr.surfaceKey),
|
|
11756
|
+
SectionType.InstructionFile
|
|
11757
|
+
);
|
|
11186
11758
|
if (!section) continue;
|
|
11187
|
-
const resolved = await resolveInstruction(
|
|
11759
|
+
const resolved = await resolveInstruction(
|
|
11760
|
+
cfg,
|
|
11761
|
+
section,
|
|
11762
|
+
personalWorkspaceId
|
|
11763
|
+
);
|
|
11188
11764
|
if (!resolved || resolved.source === "override") continue;
|
|
11189
11765
|
let make = choice === "yes";
|
|
11190
11766
|
if (choice === "ask") {
|
|
@@ -11199,8 +11775,10 @@ version, the shared template stays clean, and you can discard back anytime.
|
|
|
11199
11775
|
}
|
|
11200
11776
|
if (make) {
|
|
11201
11777
|
await createOverride(cfg, resolved, personalWorkspaceId);
|
|
11202
|
-
process.stderr.write(
|
|
11203
|
-
|
|
11778
|
+
process.stderr.write(
|
|
11779
|
+
`\u2713 personal copy created for ${instr.surfaceKey} \u2014 edit it on the Agent setup page or via the API.
|
|
11780
|
+
`
|
|
11781
|
+
);
|
|
11204
11782
|
}
|
|
11205
11783
|
}
|
|
11206
11784
|
}
|
|
@@ -11210,7 +11788,9 @@ function resolveClientKeys(raw) {
|
|
|
11210
11788
|
if (tokens.includes("all")) return [...ALL_CLIENT_KEYS];
|
|
11211
11789
|
for (const k of tokens) {
|
|
11212
11790
|
if (!targets[k]) {
|
|
11213
|
-
fail(
|
|
11791
|
+
fail(
|
|
11792
|
+
`unknown client '${k}'. Known: ${ALL_CLIENT_KEYS.join(", ")}, or 'all'.`
|
|
11793
|
+
);
|
|
11214
11794
|
}
|
|
11215
11795
|
}
|
|
11216
11796
|
return [...new Set(tokens)];
|
|
@@ -11221,8 +11801,10 @@ ${client.label} (${client.key}):
|
|
|
11221
11801
|
`);
|
|
11222
11802
|
for (const a of actions) {
|
|
11223
11803
|
const tag = a.status === "skipped" ? "skip" : a.status;
|
|
11224
|
-
process.stdout.write(
|
|
11225
|
-
`
|
|
11804
|
+
process.stdout.write(
|
|
11805
|
+
` [${tag}] ${a.kind}: ${a.path}${a.note ? ` \u2014 ${a.note}` : ""}
|
|
11806
|
+
`
|
|
11807
|
+
);
|
|
11226
11808
|
}
|
|
11227
11809
|
}
|
|
11228
11810
|
function resolveEvalMode(opts) {
|
|
@@ -11296,8 +11878,10 @@ function summarizeEval(result, mode, json, dryRun) {
|
|
|
11296
11878
|
const { eval: counts } = buildCheckReport(result);
|
|
11297
11879
|
if (json) return;
|
|
11298
11880
|
if (!dryRun && counts.stale) {
|
|
11299
|
-
process.stderr.write(
|
|
11300
|
-
|
|
11881
|
+
process.stderr.write(
|
|
11882
|
+
`\u21BB refreshed ${counts.stale} section(s) the server had moved
|
|
11883
|
+
`
|
|
11884
|
+
);
|
|
11301
11885
|
}
|
|
11302
11886
|
if (!dryRun && counts.drift) {
|
|
11303
11887
|
process.stderr.write(
|
|
@@ -11328,7 +11912,39 @@ async function resolveNamespaceChoice(cfg, flag) {
|
|
|
11328
11912
|
return picked === GLOBAL_NAMESPACE ? null : picked;
|
|
11329
11913
|
}
|
|
11330
11914
|
function registerInit(program2) {
|
|
11331
|
-
program2.command("init").description(
|
|
11915
|
+
program2.command("init").description(
|
|
11916
|
+
"Wire this project for sechroom: write MCP config + agent instruction files from the server's setup descriptors"
|
|
11917
|
+
).option(
|
|
11918
|
+
"--client <list...>",
|
|
11919
|
+
`clients to wire \u2014 space- or comma-separated (${ALL_CLIENT_KEYS.join(", ")}) or 'all'`,
|
|
11920
|
+
DEFAULT_CLIENT_KEY
|
|
11921
|
+
).option(
|
|
11922
|
+
"--scope <scope>",
|
|
11923
|
+
"install skills/agents/hooks 'global' (config dir / CLAUDE_CONFIG_DIR) or 'project' (<cwd>/.claude) \u2014 default global",
|
|
11924
|
+
"global"
|
|
11925
|
+
).option("--dry-run", "print what would be written without writing", false).option("--mcp-only", "only write MCP config (skip agent files)", false).option(
|
|
11926
|
+
"--agent-files-only",
|
|
11927
|
+
"only write agent instruction files (skip MCP config)",
|
|
11928
|
+
false
|
|
11929
|
+
).option(
|
|
11930
|
+
"--copy",
|
|
11931
|
+
"make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)"
|
|
11932
|
+
).option(
|
|
11933
|
+
"--namespace <slug>",
|
|
11934
|
+
"MCP namespace for the connection URL (interactive picker if omitted on a TTY; defaults to tenant-global)"
|
|
11935
|
+
).option(
|
|
11936
|
+
"--refresh",
|
|
11937
|
+
"refresh out-of-date agent-file blocks in place (local edits preserved to .proposed)",
|
|
11938
|
+
false
|
|
11939
|
+
).option(
|
|
11940
|
+
"--force",
|
|
11941
|
+
"rewrite agent-file managed blocks, overwriting local edits inside the markers",
|
|
11942
|
+
false
|
|
11943
|
+
).option(
|
|
11944
|
+
"--check",
|
|
11945
|
+
"report whether agent files would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing",
|
|
11946
|
+
false
|
|
11947
|
+
).addHelpText(
|
|
11332
11948
|
"after",
|
|
11333
11949
|
`
|
|
11334
11950
|
Examples:
|
|
@@ -11343,7 +11959,9 @@ Examples:
|
|
|
11343
11959
|
const mode = resolveEvalMode(opts);
|
|
11344
11960
|
const check = mode === "check";
|
|
11345
11961
|
if (check && opts.mcpOnly) {
|
|
11346
|
-
fail(
|
|
11962
|
+
fail(
|
|
11963
|
+
"--check inspects agent files and cannot be combined with --mcp-only."
|
|
11964
|
+
);
|
|
11347
11965
|
}
|
|
11348
11966
|
const namespaceSlug = await resolveNamespaceChoice(cfg, opts.namespace);
|
|
11349
11967
|
const setup = await withSpinner(
|
|
@@ -11357,14 +11975,28 @@ Examples:
|
|
|
11357
11975
|
} catch (err2) {
|
|
11358
11976
|
return fail(err2.message);
|
|
11359
11977
|
}
|
|
11360
|
-
const claudeTargets = resolveClaudeTargets({
|
|
11978
|
+
const claudeTargets = resolveClaudeTargets({
|
|
11979
|
+
override: g.claudeConfigDir,
|
|
11980
|
+
scope,
|
|
11981
|
+
cwd: process.cwd()
|
|
11982
|
+
});
|
|
11361
11983
|
const codexHomes = resolveCodexHomes({ override: g.codexHome, scope });
|
|
11362
|
-
const targets = clientTargets(process.cwd(), {
|
|
11984
|
+
const targets = clientTargets(process.cwd(), {
|
|
11985
|
+
claudeDir: claudeTargets[0]?.dir,
|
|
11986
|
+
codexHome: codexHomes[0] ?? null
|
|
11987
|
+
});
|
|
11363
11988
|
const keys = resolveClientKeys(opts.client);
|
|
11364
11989
|
const json = g.json;
|
|
11365
11990
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
11366
11991
|
if (!opts.dryRun && !opts.mcpOnly && !check) {
|
|
11367
|
-
await maybeOfferCopies(
|
|
11992
|
+
await maybeOfferCopies(
|
|
11993
|
+
cfg,
|
|
11994
|
+
setup,
|
|
11995
|
+
targets,
|
|
11996
|
+
keys,
|
|
11997
|
+
personalWorkspaceId,
|
|
11998
|
+
copyChoice(opts)
|
|
11999
|
+
);
|
|
11368
12000
|
}
|
|
11369
12001
|
const result = [];
|
|
11370
12002
|
for (const key of keys) {
|
|
@@ -11382,18 +12014,33 @@ Examples:
|
|
|
11382
12014
|
summarizeEval(result, mode, Boolean(json), Boolean(opts.dryRun));
|
|
11383
12015
|
if (!json && !opts.dryRun && !opts.mcpOnly && !check) {
|
|
11384
12016
|
for (const t of claudeTargets) {
|
|
11385
|
-
await maybeOfferSkills(cfg, personalWorkspaceId, {
|
|
12017
|
+
await maybeOfferSkills(cfg, personalWorkspaceId, {
|
|
12018
|
+
yes: false,
|
|
12019
|
+
dryRun: Boolean(opts.dryRun),
|
|
12020
|
+
surface: "claude-code",
|
|
12021
|
+
configDir: t.dir
|
|
12022
|
+
});
|
|
11386
12023
|
}
|
|
11387
12024
|
}
|
|
11388
12025
|
if (!json && !opts.dryRun && !opts.mcpOnly && !check) {
|
|
11389
|
-
await maybeOfferHooks({
|
|
12026
|
+
await maybeOfferHooks({
|
|
12027
|
+
yes: false,
|
|
12028
|
+
dryRun: Boolean(opts.dryRun),
|
|
12029
|
+
cwd: process.cwd(),
|
|
12030
|
+
scope,
|
|
12031
|
+
claudeConfigDir: g.claudeConfigDir,
|
|
12032
|
+
codexHome: g.codexHome
|
|
12033
|
+
});
|
|
11390
12034
|
}
|
|
11391
12035
|
if (json) {
|
|
11392
12036
|
emit({ dryRun: Boolean(opts.dryRun), clients: result }, true);
|
|
11393
12037
|
return;
|
|
11394
12038
|
}
|
|
11395
12039
|
const first = targets[keys[0]];
|
|
11396
|
-
const surface = findSurface(
|
|
12040
|
+
const surface = findSurface(
|
|
12041
|
+
setup,
|
|
12042
|
+
first.mcp?.surfaceKey ?? first.instruction?.surfaceKey ?? ""
|
|
12043
|
+
);
|
|
11397
12044
|
const verify = findSection(surface, SectionType.Verify);
|
|
11398
12045
|
if (verify?.description) {
|
|
11399
12046
|
process.stdout.write(`
|
|
@@ -11407,13 +12054,64 @@ Next \u2014 verify: ${verify.description}
|
|
|
11407
12054
|
}
|
|
11408
12055
|
function registerSetup(program2, deps = {}) {
|
|
11409
12056
|
const setup = program2.command("setup").description("Granular onboarding steps (init runs these together)");
|
|
11410
|
-
setup.command("mcp <clients...>").description(
|
|
11411
|
-
|
|
11412
|
-
|
|
11413
|
-
|
|
11414
|
-
|
|
12057
|
+
setup.command("mcp <clients...>").description(
|
|
12058
|
+
`Write only the MCP config for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`
|
|
12059
|
+
).option("--dry-run", "print what would be written without writing", false).option(
|
|
12060
|
+
"--namespace <slug>",
|
|
12061
|
+
"MCP namespace for the connection URL (interactive picker if omitted on a TTY; defaults to tenant-global)"
|
|
12062
|
+
).addHelpText(
|
|
12063
|
+
"after",
|
|
12064
|
+
"\nExamples:\n $ sechroom setup mcp codex\n $ sechroom setup mcp claude-code codex\n $ sechroom setup mcp all"
|
|
12065
|
+
).action(async (clients, opts, cmd) => {
|
|
12066
|
+
await runClients(clients, cmd, {
|
|
12067
|
+
dryRun: Boolean(opts.dryRun),
|
|
12068
|
+
mcp: true,
|
|
12069
|
+
agentFiles: false,
|
|
12070
|
+
namespace: opts.namespace
|
|
12071
|
+
});
|
|
12072
|
+
});
|
|
12073
|
+
setup.command("agent-files <clients...>").description(
|
|
12074
|
+
`Write only the agent instruction file(s) for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`
|
|
12075
|
+
).option("--dry-run", "print what would be written without writing", false).option(
|
|
12076
|
+
"--copy",
|
|
12077
|
+
"make a personal copy you can edit (default: prompt on a TTY, else skip)"
|
|
12078
|
+
).option(
|
|
12079
|
+
"--refresh",
|
|
12080
|
+
"refresh out-of-date blocks in place (local edits preserved to .proposed)",
|
|
12081
|
+
false
|
|
12082
|
+
).option(
|
|
12083
|
+
"--force",
|
|
12084
|
+
"rewrite managed blocks, overwriting local edits inside the markers",
|
|
12085
|
+
false
|
|
12086
|
+
).option(
|
|
12087
|
+
"--check",
|
|
12088
|
+
"report whether anything would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing",
|
|
12089
|
+
false
|
|
12090
|
+
).addHelpText(
|
|
12091
|
+
"after",
|
|
12092
|
+
"\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 --check CI gate: nonzero exit if out of date\n $ sechroom setup agent-files claude-code --force overwrite local edits in the managed block"
|
|
12093
|
+
).action(async (clients, opts, cmd) => {
|
|
12094
|
+
await runClients(clients, cmd, {
|
|
12095
|
+
dryRun: Boolean(opts.dryRun),
|
|
12096
|
+
mcp: false,
|
|
12097
|
+
agentFiles: true,
|
|
12098
|
+
copy: opts.copy,
|
|
12099
|
+
mode: resolveEvalMode(opts)
|
|
12100
|
+
});
|
|
11415
12101
|
});
|
|
11416
|
-
setup.command("new-convention <title...>").description(
|
|
12102
|
+
setup.command("new-convention <title...>").description(
|
|
12103
|
+
"Scaffold a workspace-conventions section: author a correctly-tagged memo (header as first body line) + regen the agent files"
|
|
12104
|
+
).option(
|
|
12105
|
+
"--kind <kind>",
|
|
12106
|
+
"reference | standard (orders the section; reference first)",
|
|
12107
|
+
"reference"
|
|
12108
|
+
).option(
|
|
12109
|
+
"--workspace <id>",
|
|
12110
|
+
"workspace to author in (default: the bound workspace)"
|
|
12111
|
+
).option(
|
|
12112
|
+
"--body <markdown>",
|
|
12113
|
+
"section body (default: a TODO scaffold to edit later)"
|
|
12114
|
+
).option("--no-regen", "skip the agent-files regen after authoring").option("--dry-run", "print what would be authored; write nothing", false).addHelpText(
|
|
11417
12115
|
"after",
|
|
11418
12116
|
`
|
|
11419
12117
|
The memo carries the two conventions a workspace-conventions section needs (FR-sechroom-236):
|
|
@@ -11429,10 +12127,15 @@ Examples:
|
|
|
11429
12127
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
11430
12128
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
11431
12129
|
const title = titleParts.join(" ").trim();
|
|
11432
|
-
if (!title)
|
|
12130
|
+
if (!title)
|
|
12131
|
+
fail(
|
|
12132
|
+
'a section title is required, e.g. `sechroom setup new-convention "Deploy runbook"`.'
|
|
12133
|
+
);
|
|
11433
12134
|
const workspaceId = opts.workspace ?? cfg.workspaceId;
|
|
11434
12135
|
if (!workspaceId)
|
|
11435
|
-
fail(
|
|
12136
|
+
fail(
|
|
12137
|
+
"no workspace \u2014 pass --workspace <id> or bind one (`sechroom config set --local workspaceId <id>`)."
|
|
12138
|
+
);
|
|
11436
12139
|
const draft = buildConventionDraft(title, opts.kind, opts.body);
|
|
11437
12140
|
if (opts.dryRun) {
|
|
11438
12141
|
emit(
|
|
@@ -11473,7 +12176,10 @@ Examples:
|
|
|
11473
12176
|
}
|
|
11474
12177
|
if (opts.regen === false) {
|
|
11475
12178
|
if (json) emit({ id: data.id, workspaceId, regen: false }, true);
|
|
11476
|
-
else
|
|
12179
|
+
else
|
|
12180
|
+
process.stdout.write(
|
|
12181
|
+
"Skipped regen (--no-regen). Run `sechroom setup agent-files all` to apply.\n"
|
|
12182
|
+
);
|
|
11477
12183
|
return;
|
|
11478
12184
|
}
|
|
11479
12185
|
const regenerate = deps.regenerateConvention ?? runClients;
|
|
@@ -11502,7 +12208,14 @@ async function runClients(clients, cmd, opts) {
|
|
|
11502
12208
|
);
|
|
11503
12209
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
11504
12210
|
if (opts.agentFiles && !opts.dryRun && !check) {
|
|
11505
|
-
await maybeOfferCopies(
|
|
12211
|
+
await maybeOfferCopies(
|
|
12212
|
+
cfg,
|
|
12213
|
+
setupData,
|
|
12214
|
+
targets,
|
|
12215
|
+
keys,
|
|
12216
|
+
personalWorkspaceId,
|
|
12217
|
+
copyChoice(opts)
|
|
12218
|
+
);
|
|
11506
12219
|
}
|
|
11507
12220
|
const json = g.json;
|
|
11508
12221
|
const result = [];
|
|
@@ -11523,7 +12236,9 @@ async function runClients(clients, cmd, opts) {
|
|
|
11523
12236
|
emit({ dryRun: opts.dryRun, clients: result }, true);
|
|
11524
12237
|
return;
|
|
11525
12238
|
}
|
|
11526
|
-
process.stdout.write(
|
|
12239
|
+
process.stdout.write(
|
|
12240
|
+
opts.dryRun ? "\n(dry run \u2014 nothing written)\n" : "\nDone.\n"
|
|
12241
|
+
);
|
|
11527
12242
|
}
|
|
11528
12243
|
|
|
11529
12244
|
// src/commands/namespace.ts
|
|
@@ -11599,7 +12314,7 @@ import { basename as basename5, join as join21 } from "path";
|
|
|
11599
12314
|
|
|
11600
12315
|
// src/commands/fanout.ts
|
|
11601
12316
|
import { spawnSync } from "child_process";
|
|
11602
|
-
import { existsSync as existsSync14, readFileSync as readFileSync17, readdirSync as readdirSync4, statSync as
|
|
12317
|
+
import { existsSync as existsSync14, readFileSync as readFileSync17, readdirSync as readdirSync4, statSync as statSync6 } from "fs";
|
|
11603
12318
|
import { isAbsolute as isAbsolute3, join as join20, resolve as resolve7 } from "path";
|
|
11604
12319
|
var ICON = {
|
|
11605
12320
|
refresh: "\u21BB",
|
|
@@ -11622,7 +12337,7 @@ function discoverChildren(root) {
|
|
|
11622
12337
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
11623
12338
|
const dir = join20(root, name);
|
|
11624
12339
|
try {
|
|
11625
|
-
if (!
|
|
12340
|
+
if (!statSync6(dir).isDirectory()) continue;
|
|
11626
12341
|
} catch {
|
|
11627
12342
|
continue;
|
|
11628
12343
|
}
|
|
@@ -11748,20 +12463,27 @@ function resolveBaseUrl(g) {
|
|
|
11748
12463
|
return baseUrl.replace(/\/$/, "");
|
|
11749
12464
|
}
|
|
11750
12465
|
async function fetchWorkspaces(client) {
|
|
11751
|
-
const { data, error } = await client.GET("/workspaces", {
|
|
11752
|
-
|
|
12466
|
+
const { data, error } = await client.GET("/workspaces", {
|
|
12467
|
+
params: { query: { includeArchived: false } }
|
|
12468
|
+
});
|
|
12469
|
+
if (error)
|
|
12470
|
+
throw new Error(`Couldn't list your workspaces: ${JSON.stringify(error)}`);
|
|
11753
12471
|
const rows = data ?? [];
|
|
11754
12472
|
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 }));
|
|
11755
12473
|
}
|
|
11756
12474
|
async function lookupWorkspace(client, id) {
|
|
11757
|
-
const { data, error } = await client.GET("/workspaces/{workspaceId}", {
|
|
12475
|
+
const { data, error } = await client.GET("/workspaces/{workspaceId}", {
|
|
12476
|
+
params: { path: { workspaceId: id } }
|
|
12477
|
+
});
|
|
11758
12478
|
if (error) return null;
|
|
11759
12479
|
const env = data;
|
|
11760
12480
|
const w = env?.item ?? env;
|
|
11761
12481
|
return w?.id ? { id: w.id, name: w.name ?? id, parentId: w.parentId ?? null } : null;
|
|
11762
12482
|
}
|
|
11763
12483
|
async function warnIfProjectStray(client, projectId, workspaceId, json) {
|
|
11764
|
-
const { data, error } = await client.GET("/projects/{projectId}", {
|
|
12484
|
+
const { data, error } = await client.GET("/projects/{projectId}", {
|
|
12485
|
+
params: { path: { projectId } }
|
|
12486
|
+
});
|
|
11765
12487
|
if (error) return;
|
|
11766
12488
|
const env = data;
|
|
11767
12489
|
const owner = env?.item?.workspaceId;
|
|
@@ -11805,10 +12527,15 @@ function personalSubtreeIds(personalId, all) {
|
|
|
11805
12527
|
async function pickWorkspace(client, opts = {}) {
|
|
11806
12528
|
const promptLabel = opts.promptLabel ?? "Bind this directory to a workspace:";
|
|
11807
12529
|
const dirName = opts.dirName ?? basename5(process.cwd());
|
|
11808
|
-
const all = await withSpinner(
|
|
12530
|
+
const all = await withSpinner(
|
|
12531
|
+
"Listing your workspaces",
|
|
12532
|
+
() => fetchWorkspaces(client)
|
|
12533
|
+
);
|
|
11809
12534
|
if (all.length === 0) {
|
|
11810
|
-
process.stderr.write(
|
|
11811
|
-
`)
|
|
12535
|
+
process.stderr.write(
|
|
12536
|
+
`no workspaces found \u2014 skipping workspace binding (you can set it later with \`sechroom config set --local workspaceId <id>\`)
|
|
12537
|
+
`
|
|
12538
|
+
);
|
|
11812
12539
|
return void 0;
|
|
11813
12540
|
}
|
|
11814
12541
|
const byId = new Map(all.map((w) => [w.id, w]));
|
|
@@ -11823,19 +12550,35 @@ async function pickWorkspace(client, opts = {}) {
|
|
|
11823
12550
|
const matched = candidates.filter(isMatch).sort(byPath);
|
|
11824
12551
|
const rest = candidates.filter((w) => !isMatch(w)).sort(byPath);
|
|
11825
12552
|
const choices = [
|
|
11826
|
-
...matched.map((w) => ({
|
|
11827
|
-
|
|
11828
|
-
|
|
12553
|
+
...matched.map((w) => ({
|
|
12554
|
+
label: workspacePath(w, byId),
|
|
12555
|
+
value: w.id,
|
|
12556
|
+
hint: style.dim(`matches "${dirName}"`)
|
|
12557
|
+
})),
|
|
12558
|
+
...rest.map((w) => ({
|
|
12559
|
+
label: workspacePath(w, byId),
|
|
12560
|
+
value: w.id,
|
|
12561
|
+
hint: w.id
|
|
12562
|
+
})),
|
|
12563
|
+
{
|
|
12564
|
+
label: style.dim("skip \u2014 don't bind a workspace"),
|
|
12565
|
+
value: SKIP,
|
|
12566
|
+
hint: void 0
|
|
12567
|
+
}
|
|
11829
12568
|
];
|
|
11830
12569
|
const defaultValue = matched.length === 1 ? matched[0].id : SKIP;
|
|
11831
12570
|
const chosen = candidates.length > 12 ? await promptAutocomplete(promptLabel, choices, defaultValue) : await promptSelect(promptLabel, choices, defaultValue);
|
|
11832
12571
|
if (chosen === SKIP) return void 0;
|
|
11833
12572
|
const picked = byId.get(chosen);
|
|
11834
|
-
const collisions = all.filter(
|
|
12573
|
+
const collisions = all.filter(
|
|
12574
|
+
(w) => w.id !== picked.id && namesCollide(w.name, picked.name)
|
|
12575
|
+
);
|
|
11835
12576
|
if (collisions.length > 0) {
|
|
11836
12577
|
process.stderr.write(
|
|
11837
12578
|
`${warn("\u26A0")} ${collisions.length} other workspace(s) have a similar name to ${style.cyan(workspacePath(picked, byId))}:
|
|
11838
|
-
` + collisions.map(
|
|
12579
|
+
` + collisions.map(
|
|
12580
|
+
(w) => ` ${style.dim(workspacePath(w, byId))} ${style.dim(`(${w.id})`)}`
|
|
12581
|
+
).join("\n") + `
|
|
11839
12582
|
You picked ${style.dim(picked.id)} \u2014 re-run \`sechroom config set --local workspaceId <id>\` if that's wrong.
|
|
11840
12583
|
`
|
|
11841
12584
|
);
|
|
@@ -11862,10 +12605,17 @@ async function ensureTenant(baseUrl, g, opts) {
|
|
|
11862
12605
|
const local = readLocalConfig();
|
|
11863
12606
|
let tenant = g.tenant ?? process.env.SECHROOM_TENANT ?? local.tenant ?? persisted.tenant ?? "";
|
|
11864
12607
|
if (!tenant) {
|
|
11865
|
-
const client = await makeClient({
|
|
12608
|
+
const client = await makeClient({
|
|
12609
|
+
baseUrl,
|
|
12610
|
+
tenant: "",
|
|
12611
|
+
account: resolveAccountAlias(),
|
|
12612
|
+
clientId: persisted.clientId
|
|
12613
|
+
});
|
|
11866
12614
|
const { data, error } = await client.GET("/auth/me/tenants", {});
|
|
11867
12615
|
if (error) {
|
|
11868
|
-
fail(
|
|
12616
|
+
fail(
|
|
12617
|
+
`Couldn't list your tenants: ${JSON.stringify(error)}. Pass --tenant <id> to skip this.`
|
|
12618
|
+
);
|
|
11869
12619
|
}
|
|
11870
12620
|
const tenants = data?.tenants ?? [];
|
|
11871
12621
|
if (tenants.length === 0) {
|
|
@@ -11897,21 +12647,44 @@ async function ensureTenant(baseUrl, g, opts) {
|
|
|
11897
12647
|
}
|
|
11898
12648
|
}
|
|
11899
12649
|
const existingWorkspace = local.workspaceId ?? persisted.workspaceId ?? void 0;
|
|
11900
|
-
const wsClient = await makeClient({
|
|
11901
|
-
|
|
11902
|
-
|
|
11903
|
-
|
|
11904
|
-
|
|
12650
|
+
const wsClient = await makeClient({
|
|
12651
|
+
baseUrl,
|
|
12652
|
+
tenant,
|
|
12653
|
+
account: resolveAccountAlias(),
|
|
12654
|
+
clientId: persisted.clientId
|
|
11905
12655
|
});
|
|
12656
|
+
const workspaceId = await resolveWorkspaceBinding(
|
|
12657
|
+
wsClient,
|
|
12658
|
+
existingWorkspace,
|
|
12659
|
+
{
|
|
12660
|
+
yes: opts.yes,
|
|
12661
|
+
json: opts.json,
|
|
12662
|
+
workspace: opts.workspace
|
|
12663
|
+
}
|
|
12664
|
+
);
|
|
11906
12665
|
const defaultProjectId = local.defaultProjectId ?? persisted.defaultProjectId ?? void 0;
|
|
11907
|
-
if (defaultProjectId && workspaceId)
|
|
12666
|
+
if (defaultProjectId && workspaceId)
|
|
12667
|
+
await warnIfProjectStray(
|
|
12668
|
+
wsClient,
|
|
12669
|
+
defaultProjectId,
|
|
12670
|
+
workspaceId,
|
|
12671
|
+
opts.json
|
|
12672
|
+
);
|
|
11908
12673
|
let storeLocal = Boolean(opts.local);
|
|
11909
12674
|
if (!opts.local && canPrompt() && !opts.yes) {
|
|
11910
12675
|
storeLocal = await promptSelect(
|
|
11911
12676
|
"Where should this tenant + base URL be saved?",
|
|
11912
12677
|
[
|
|
11913
|
-
{
|
|
11914
|
-
|
|
12678
|
+
{
|
|
12679
|
+
label: "Globally",
|
|
12680
|
+
value: "global",
|
|
12681
|
+
hint: "all projects on this machine"
|
|
12682
|
+
},
|
|
12683
|
+
{
|
|
12684
|
+
label: "This directory",
|
|
12685
|
+
value: "local",
|
|
12686
|
+
hint: ".sechroom.json \u2014 committed, project + subdirs"
|
|
12687
|
+
}
|
|
11915
12688
|
],
|
|
11916
12689
|
local.path ? "local" : "global"
|
|
11917
12690
|
) === "local";
|
|
@@ -11920,19 +12693,34 @@ async function ensureTenant(baseUrl, g, opts) {
|
|
|
11920
12693
|
const patch = { baseUrl, tenant, ...workspaceId ? { workspaceId } : {} };
|
|
11921
12694
|
if (storeLocal) {
|
|
11922
12695
|
const path = writeLocalConfig(patch, { here: Boolean(opts.here) });
|
|
11923
|
-
if (!opts.json)
|
|
11924
|
-
|
|
12696
|
+
if (!opts.json)
|
|
12697
|
+
process.stderr.write(
|
|
12698
|
+
`${ok("\u2713")} config saved to ${path} (directory-local)
|
|
12699
|
+
`
|
|
12700
|
+
);
|
|
11925
12701
|
} else {
|
|
11926
12702
|
writePersisted(patch);
|
|
11927
|
-
if (!opts.json)
|
|
11928
|
-
|
|
12703
|
+
if (!opts.json)
|
|
12704
|
+
process.stderr.write(
|
|
12705
|
+
`${ok("\u2713")} config saved globally (~/.config/sechroom/config.json)
|
|
12706
|
+
`
|
|
12707
|
+
);
|
|
11929
12708
|
}
|
|
11930
12709
|
if (workspaceId && !existingWorkspace && !opts.json) {
|
|
11931
|
-
process.stderr.write(
|
|
11932
|
-
|
|
12710
|
+
process.stderr.write(
|
|
12711
|
+
`${ok("\u2713")} bound to workspace ${style.dim(workspaceId)}
|
|
12712
|
+
`
|
|
12713
|
+
);
|
|
11933
12714
|
}
|
|
11934
12715
|
}
|
|
11935
|
-
return {
|
|
12716
|
+
return {
|
|
12717
|
+
baseUrl,
|
|
12718
|
+
tenant,
|
|
12719
|
+
account: resolveAccountAlias(),
|
|
12720
|
+
workspaceId,
|
|
12721
|
+
defaultProjectId,
|
|
12722
|
+
clientId: persisted.clientId
|
|
12723
|
+
};
|
|
11936
12724
|
}
|
|
11937
12725
|
async function ensureAuth(cfg, yes) {
|
|
11938
12726
|
if (process.env.SECHROOM_TOKEN) return;
|
|
@@ -11940,17 +12728,27 @@ async function ensureAuth(cfg, yes) {
|
|
|
11940
12728
|
const usable = Boolean(cached?.accessToken) && (cached.expiresAt === void 0 || Date.now() < cached.expiresAt - 6e4 || Boolean(cached.refreshToken));
|
|
11941
12729
|
if (usable) return;
|
|
11942
12730
|
if (!canPrompt() || yes) {
|
|
11943
|
-
fail(
|
|
12731
|
+
fail(
|
|
12732
|
+
"Not signed in. Run `sechroom login` first, or set SECHROOM_TOKEN for headless use."
|
|
12733
|
+
);
|
|
11944
12734
|
}
|
|
11945
|
-
process.stderr.write(
|
|
12735
|
+
process.stderr.write(
|
|
12736
|
+
"\nNot signed in \u2014 opening the browser to authenticate.\n"
|
|
12737
|
+
);
|
|
11946
12738
|
await login(cfg);
|
|
11947
12739
|
}
|
|
11948
12740
|
async function ensureTimezone(cfg, opts) {
|
|
11949
12741
|
const client = await makeClient(cfg);
|
|
11950
12742
|
const { data, error } = await client.GET("/me/profile", {});
|
|
11951
|
-
if (error)
|
|
12743
|
+
if (error)
|
|
12744
|
+
return {
|
|
12745
|
+
timezone: null,
|
|
12746
|
+
action: "skipped",
|
|
12747
|
+
note: "could not read profile"
|
|
12748
|
+
};
|
|
11952
12749
|
const current = data?.effectiveTimezone;
|
|
11953
|
-
if (current && current.trim().length > 0)
|
|
12750
|
+
if (current && current.trim().length > 0)
|
|
12751
|
+
return { timezone: current, action: "already-set" };
|
|
11954
12752
|
const system = systemTimezone();
|
|
11955
12753
|
let tz = system;
|
|
11956
12754
|
if (canPrompt() && !opts.yes) {
|
|
@@ -11962,12 +12760,18 @@ async function ensureTimezone(cfg, opts) {
|
|
|
11962
12760
|
note: "no timezone set \u2014 re-run interactively or pass --yes to adopt the system timezone"
|
|
11963
12761
|
};
|
|
11964
12762
|
}
|
|
11965
|
-
if (!tz)
|
|
12763
|
+
if (!tz)
|
|
12764
|
+
return { timezone: null, action: "skipped", note: "no timezone provided" };
|
|
11966
12765
|
if (opts.dryRun) return { timezone: tz, action: "dry-run" };
|
|
11967
12766
|
const { error: putErr } = await client.PUT("/me/profile", {
|
|
11968
12767
|
body: { displayName: null, photoUrl: null, bio: null, timezone: tz }
|
|
11969
12768
|
});
|
|
11970
|
-
if (putErr)
|
|
12769
|
+
if (putErr)
|
|
12770
|
+
return {
|
|
12771
|
+
timezone: tz,
|
|
12772
|
+
action: "skipped",
|
|
12773
|
+
note: `update failed: ${JSON.stringify(putErr)}`
|
|
12774
|
+
};
|
|
11971
12775
|
return { timezone: tz, action: "set" };
|
|
11972
12776
|
}
|
|
11973
12777
|
async function chooseClients(clientFlag, yes, cwd) {
|
|
@@ -11992,7 +12796,11 @@ async function chooseScope(scopeFlag, yes) {
|
|
|
11992
12796
|
return promptSelect(
|
|
11993
12797
|
"Install skills, agents, and hooks globally or just for this project?",
|
|
11994
12798
|
[
|
|
11995
|
-
{
|
|
12799
|
+
{
|
|
12800
|
+
label: "Globally",
|
|
12801
|
+
value: "global",
|
|
12802
|
+
hint: "~/.claude (or CLAUDE_CONFIG_DIR) \u2014 all projects"
|
|
12803
|
+
},
|
|
11996
12804
|
{ label: "This project", value: "project", hint: "<repo>/.claude" }
|
|
11997
12805
|
],
|
|
11998
12806
|
"global"
|
|
@@ -12001,7 +12809,13 @@ async function chooseScope(scopeFlag, yes) {
|
|
|
12001
12809
|
async function planRecurseChild(entry, root, client, opts) {
|
|
12002
12810
|
const dir = resolveChildDir(entry.path, root);
|
|
12003
12811
|
if (!existsSync15(dir)) {
|
|
12004
|
-
return {
|
|
12812
|
+
return {
|
|
12813
|
+
label: entry.path,
|
|
12814
|
+
dir,
|
|
12815
|
+
disposition: "skip-missing",
|
|
12816
|
+
argv: [],
|
|
12817
|
+
reason: "directory does not exist"
|
|
12818
|
+
};
|
|
12005
12819
|
}
|
|
12006
12820
|
if (existsSync15(join21(dir, ".sechroom.json"))) {
|
|
12007
12821
|
return {
|
|
@@ -12022,20 +12836,40 @@ async function planRecurseChild(entry, root, client, opts) {
|
|
|
12022
12836
|
};
|
|
12023
12837
|
}
|
|
12024
12838
|
if (opts.dryRun) {
|
|
12025
|
-
return {
|
|
12839
|
+
return {
|
|
12840
|
+
label: entry.path,
|
|
12841
|
+
dir,
|
|
12842
|
+
disposition: "bind",
|
|
12843
|
+
argv: ["onboard", "--yes", "--local", "--workspace", "<prompt>"],
|
|
12844
|
+
reason: "unbound \u2014 would prompt for a workspace"
|
|
12845
|
+
};
|
|
12026
12846
|
}
|
|
12027
12847
|
if (opts.yes || !canPrompt()) {
|
|
12028
|
-
return {
|
|
12848
|
+
return {
|
|
12849
|
+
label: entry.path,
|
|
12850
|
+
dir,
|
|
12851
|
+
disposition: "skip-unbound",
|
|
12852
|
+
argv: [],
|
|
12853
|
+
reason: "unbound + no workspace (run interactively, or add it to ./.sechroom/repos.json)"
|
|
12854
|
+
};
|
|
12029
12855
|
}
|
|
12030
|
-
process.stderr.write(
|
|
12856
|
+
process.stderr.write(
|
|
12857
|
+
`
|
|
12031
12858
|
${style.bold(entry.path)} ${style.dim("is not bound yet.")}
|
|
12032
|
-
`
|
|
12859
|
+
`
|
|
12860
|
+
);
|
|
12033
12861
|
const ws = await pickWorkspace(client, {
|
|
12034
12862
|
promptLabel: `Bind ${style.cyan(entry.path)} to a workspace:`,
|
|
12035
12863
|
dirName: basename5(entry.path)
|
|
12036
12864
|
});
|
|
12037
12865
|
if (!ws) {
|
|
12038
|
-
return {
|
|
12866
|
+
return {
|
|
12867
|
+
label: entry.path,
|
|
12868
|
+
dir,
|
|
12869
|
+
disposition: "skip-unbound",
|
|
12870
|
+
argv: [],
|
|
12871
|
+
reason: "unbound \u2014 no workspace chosen (skipped)"
|
|
12872
|
+
};
|
|
12039
12873
|
}
|
|
12040
12874
|
return {
|
|
12041
12875
|
label: entry.path,
|
|
@@ -12050,20 +12884,34 @@ async function resolveFanoutLane(cfg, opts) {
|
|
|
12050
12884
|
let design = opts.designLane ?? process.env.SECHROOM_DESIGN_LANE;
|
|
12051
12885
|
if (!code || !design) {
|
|
12052
12886
|
const clients = detectInstalledClients(process.cwd());
|
|
12053
|
-
const inferred = await inferLanes(
|
|
12887
|
+
const inferred = await inferLanes(
|
|
12888
|
+
cfg,
|
|
12889
|
+
clients.length ? clients : void 0
|
|
12890
|
+
);
|
|
12054
12891
|
code = code ?? inferred.code;
|
|
12055
12892
|
design = design ?? inferred.design;
|
|
12056
12893
|
}
|
|
12057
12894
|
if (!opts.lane && !opts.yes && !opts.dryRun && canPrompt() && (code || design)) {
|
|
12058
|
-
process.stderr.write(
|
|
12895
|
+
process.stderr.write(
|
|
12896
|
+
`
|
|
12059
12897
|
This fan-out will pin the same lane in every repo:
|
|
12060
|
-
`
|
|
12061
|
-
|
|
12062
|
-
|
|
12063
|
-
|
|
12064
|
-
`)
|
|
12898
|
+
`
|
|
12899
|
+
);
|
|
12900
|
+
if (code)
|
|
12901
|
+
process.stderr.write(
|
|
12902
|
+
` ${style.dim("code-lane")} = ${style.cyan(code)}
|
|
12903
|
+
`
|
|
12904
|
+
);
|
|
12905
|
+
if (design)
|
|
12906
|
+
process.stderr.write(
|
|
12907
|
+
` ${style.dim("design-lane")} = ${style.cyan(design)}
|
|
12908
|
+
`
|
|
12909
|
+
);
|
|
12065
12910
|
if (!await promptYesNo("Use this lane for all repos?")) {
|
|
12066
|
-
code = await promptText(
|
|
12911
|
+
code = await promptText(
|
|
12912
|
+
"Code-lane id (blank = let each repo infer)?",
|
|
12913
|
+
code ?? ""
|
|
12914
|
+
) || void 0;
|
|
12067
12915
|
design = await promptText("Design-lane id (blank = skip)?", design ?? "") || void 0;
|
|
12068
12916
|
}
|
|
12069
12917
|
}
|
|
@@ -12081,30 +12929,112 @@ async function runRecurse(cfg, g, opts) {
|
|
|
12081
12929
|
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
12082
12930
|
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
12083
12931
|
if (entries.length === 0) {
|
|
12084
|
-
if (json)
|
|
12085
|
-
|
|
12086
|
-
|
|
12932
|
+
if (json)
|
|
12933
|
+
process.stdout.write(
|
|
12934
|
+
JSON.stringify({ recurse: true, root, repos: [] }) + "\n"
|
|
12935
|
+
);
|
|
12936
|
+
else
|
|
12937
|
+
process.stderr.write(
|
|
12938
|
+
`${warn("\u26A0")} no child repos found ${fromManifest ? `in ${manifestPath}` : `under ${root}`} \u2014 nothing to do.
|
|
12939
|
+
`
|
|
12940
|
+
);
|
|
12087
12941
|
return;
|
|
12088
12942
|
}
|
|
12089
12943
|
if (!json) {
|
|
12090
|
-
process.stderr.write(
|
|
12091
|
-
`)
|
|
12944
|
+
process.stderr.write(
|
|
12945
|
+
`${style.bold("onboard --recurse")} ${style.dim(`(${entries.length} repo${entries.length === 1 ? "" : "s"} from ${sourceLabel})`)}
|
|
12946
|
+
`
|
|
12947
|
+
);
|
|
12092
12948
|
}
|
|
12093
|
-
const lane = await resolveFanoutLane(cfg, {
|
|
12094
|
-
|
|
12095
|
-
|
|
12949
|
+
const lane = await resolveFanoutLane(cfg, {
|
|
12950
|
+
lane: opts.lane,
|
|
12951
|
+
designLane: opts.designLane,
|
|
12952
|
+
yes,
|
|
12953
|
+
dryRun
|
|
12954
|
+
});
|
|
12955
|
+
if (!json && lane.code)
|
|
12956
|
+
process.stderr.write(
|
|
12957
|
+
`${ok("\u2713")} lane ${style.cyan(lane.code)}${lane.design ? ` ${style.dim(`/ ${lane.design}`)}` : ""} for every repo
|
|
12958
|
+
`
|
|
12959
|
+
);
|
|
12096
12960
|
const client = await makeClient(cfg);
|
|
12097
12961
|
const plans = [];
|
|
12098
|
-
for (const entry of entries)
|
|
12099
|
-
|
|
12962
|
+
for (const entry of entries)
|
|
12963
|
+
plans.push(await planRecurseChild(entry, root, client, { yes, dryRun }));
|
|
12964
|
+
const results = runChildren(plans, {
|
|
12965
|
+
globals: passthroughGlobals(g),
|
|
12966
|
+
dryRun,
|
|
12967
|
+
json
|
|
12968
|
+
});
|
|
12100
12969
|
if (json) {
|
|
12101
|
-
process.stdout.write(
|
|
12970
|
+
process.stdout.write(
|
|
12971
|
+
JSON.stringify({ recurse: true, root, dryRun, repos: results }) + "\n"
|
|
12972
|
+
);
|
|
12102
12973
|
return;
|
|
12103
12974
|
}
|
|
12104
12975
|
summarizeFanout(results, { dryRun });
|
|
12105
12976
|
}
|
|
12106
12977
|
function registerOnboard(program2) {
|
|
12107
|
-
program2.command("onboard").description(
|
|
12978
|
+
program2.command("onboard").description(
|
|
12979
|
+
"Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project"
|
|
12980
|
+
).option(
|
|
12981
|
+
"--recurse",
|
|
12982
|
+
"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",
|
|
12983
|
+
false
|
|
12984
|
+
).option(
|
|
12985
|
+
"--lane <id>",
|
|
12986
|
+
"set the code-lane (substrate source identity) explicitly instead of inferring it; with --recurse it's used for every child repo"
|
|
12987
|
+
).option(
|
|
12988
|
+
"--design-lane <id>",
|
|
12989
|
+
"set the design-lane explicitly (substrate-authoring identity); with --recurse applies to every child"
|
|
12990
|
+
).option(
|
|
12991
|
+
"--client <list...>",
|
|
12992
|
+
`clients to wire \u2014 space- or comma-separated (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`
|
|
12993
|
+
).option(
|
|
12994
|
+
"--scope <scope>",
|
|
12995
|
+
"install skills/agents/hooks 'global' (config dir / CLAUDE_CONFIG_DIR) or 'project' (<cwd>/.claude) \u2014 default: prompt, else global"
|
|
12996
|
+
).option(
|
|
12997
|
+
"--local",
|
|
12998
|
+
"save the binding (tenant + base URL + workspace) to a committed .sechroom.json in this repo instead of the global config",
|
|
12999
|
+
false
|
|
13000
|
+
).option(
|
|
13001
|
+
"--here",
|
|
13002
|
+
"with --local: write the binding at THIS directory even when a parent already carries one \u2014 binds a subtree (e.g. a monorepo's frontend/) to its own workspace",
|
|
13003
|
+
false
|
|
13004
|
+
).option(
|
|
13005
|
+
"--workspace <id>",
|
|
13006
|
+
"bind this directory to a workspace (skips the interactive workspace pick)"
|
|
13007
|
+
).option(
|
|
13008
|
+
"--cli-only",
|
|
13009
|
+
"configure the CLI only \u2014 don't wire any AI client (no MCP config, no agent files)",
|
|
13010
|
+
false
|
|
13011
|
+
).option(
|
|
13012
|
+
"--no-mcp",
|
|
13013
|
+
"skip the MCP server config (.mcp.json etc.); still write the agent instruction files"
|
|
13014
|
+
).option(
|
|
13015
|
+
"--copy",
|
|
13016
|
+
"make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)"
|
|
13017
|
+
).option(
|
|
13018
|
+
"--dry-run",
|
|
13019
|
+
"walk through without writing files or changing the profile",
|
|
13020
|
+
false
|
|
13021
|
+
).option(
|
|
13022
|
+
"--refresh",
|
|
13023
|
+
"re-fetch descriptors and refresh any out-of-date managed blocks (local edits preserved to .proposed)",
|
|
13024
|
+
false
|
|
13025
|
+
).option(
|
|
13026
|
+
"--force",
|
|
13027
|
+
"rewrite every managed block, overwriting local edits inside the markers (content outside untouched)",
|
|
13028
|
+
false
|
|
13029
|
+
).option(
|
|
13030
|
+
"--check",
|
|
13031
|
+
"report whether anything would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing",
|
|
13032
|
+
false
|
|
13033
|
+
).option(
|
|
13034
|
+
"-y, --yes",
|
|
13035
|
+
"non-interactive: accept defaults (system timezone, detected clients, global config, full wire)",
|
|
13036
|
+
false
|
|
13037
|
+
).addHelpText(
|
|
12108
13038
|
"after",
|
|
12109
13039
|
`
|
|
12110
13040
|
Examples:
|
|
@@ -12127,20 +13057,56 @@ Examples:
|
|
|
12127
13057
|
const check = mode === "check";
|
|
12128
13058
|
const yes = Boolean(opts.yes) || check;
|
|
12129
13059
|
if (check && (opts.recurse || opts.cliOnly)) {
|
|
12130
|
-
fail(
|
|
13060
|
+
fail(
|
|
13061
|
+
"--check inspects this project's agent files and cannot be combined with --recurse or --cli-only."
|
|
13062
|
+
);
|
|
12131
13063
|
}
|
|
12132
13064
|
if (opts.lane) process.env.SECHROOM_CODE_LANE = opts.lane;
|
|
12133
13065
|
if (opts.designLane) process.env.SECHROOM_DESIGN_LANE = opts.designLane;
|
|
12134
13066
|
if (opts.recurse) {
|
|
12135
13067
|
const baseUrl2 = resolveBaseUrl(g);
|
|
12136
|
-
await ensureAuth(
|
|
12137
|
-
|
|
12138
|
-
|
|
13068
|
+
await ensureAuth(
|
|
13069
|
+
{
|
|
13070
|
+
baseUrl: baseUrl2,
|
|
13071
|
+
tenant: "",
|
|
13072
|
+
account: resolveAccountAlias(),
|
|
13073
|
+
clientId: readPersisted().clientId
|
|
13074
|
+
},
|
|
13075
|
+
yes
|
|
13076
|
+
);
|
|
13077
|
+
const cfg2 = await ensureTenant(baseUrl2, g, {
|
|
13078
|
+
yes: true,
|
|
13079
|
+
json,
|
|
13080
|
+
persist: false
|
|
13081
|
+
});
|
|
13082
|
+
await runRecurse(cfg2, g, {
|
|
13083
|
+
yes,
|
|
13084
|
+
dryRun,
|
|
13085
|
+
json,
|
|
13086
|
+
lane: opts.lane,
|
|
13087
|
+
designLane: opts.designLane
|
|
13088
|
+
});
|
|
12139
13089
|
return;
|
|
12140
13090
|
}
|
|
12141
13091
|
const baseUrl = resolveBaseUrl(g);
|
|
12142
|
-
await ensureAuth(
|
|
12143
|
-
|
|
13092
|
+
await ensureAuth(
|
|
13093
|
+
{
|
|
13094
|
+
baseUrl,
|
|
13095
|
+
tenant: "",
|
|
13096
|
+
account: resolveAccountAlias(),
|
|
13097
|
+
clientId: readPersisted().clientId
|
|
13098
|
+
},
|
|
13099
|
+
yes
|
|
13100
|
+
);
|
|
13101
|
+
const scope = await chooseScope(opts.scope, yes);
|
|
13102
|
+
const cfg = await ensureTenant(baseUrl, g, {
|
|
13103
|
+
yes,
|
|
13104
|
+
json,
|
|
13105
|
+
local: Boolean(opts.local) || scope === "project",
|
|
13106
|
+
here: scope === "project" ? true : Boolean(opts.here),
|
|
13107
|
+
workspace: opts.workspace,
|
|
13108
|
+
persist: !check
|
|
13109
|
+
});
|
|
12144
13110
|
const tz = await ensureTimezone(cfg, { yes, dryRun: dryRun || check });
|
|
12145
13111
|
if (!json && tz.action !== "already-set") {
|
|
12146
13112
|
const line = tz.action === "set" ? `${ok("\u2713")} timezone set to ${tz.timezone}
|
|
@@ -12150,21 +13116,48 @@ Examples:
|
|
|
12150
13116
|
process.stderr.write(line);
|
|
12151
13117
|
}
|
|
12152
13118
|
const wire = await chooseWire(opts, yes);
|
|
12153
|
-
const
|
|
12154
|
-
|
|
13119
|
+
const claudeTargets = resolveClaudeTargets({
|
|
13120
|
+
override: g.claudeConfigDir,
|
|
13121
|
+
scope,
|
|
13122
|
+
cwd: process.cwd()
|
|
13123
|
+
});
|
|
12155
13124
|
const codexHomes = resolveCodexHomes({ override: g.codexHome, scope });
|
|
12156
13125
|
if (scope === "project" && g.claudeConfigDir && !json) {
|
|
12157
|
-
process.stderr.write(
|
|
12158
|
-
|
|
13126
|
+
process.stderr.write(
|
|
13127
|
+
`${style.dim("(--claude-config-dir is ignored for --scope project \u2014 project files are repo-relative)")}
|
|
13128
|
+
`
|
|
13129
|
+
);
|
|
12159
13130
|
}
|
|
12160
13131
|
if (wire === "cli-only") {
|
|
12161
13132
|
if (json) {
|
|
12162
|
-
emit(
|
|
13133
|
+
emit(
|
|
13134
|
+
{
|
|
13135
|
+
dryRun,
|
|
13136
|
+
baseUrl: cfg.baseUrl,
|
|
13137
|
+
tenant: cfg.tenant,
|
|
13138
|
+
workspaceId: cfg.workspaceId ?? null,
|
|
13139
|
+
timezone: tz,
|
|
13140
|
+
wire,
|
|
13141
|
+
clients: []
|
|
13142
|
+
},
|
|
13143
|
+
true
|
|
13144
|
+
);
|
|
12163
13145
|
return;
|
|
12164
13146
|
}
|
|
12165
13147
|
if (!dryRun) {
|
|
12166
|
-
await ensureLanePin(cfg, {
|
|
12167
|
-
|
|
13148
|
+
await ensureLanePin(cfg, {
|
|
13149
|
+
yes,
|
|
13150
|
+
dryRun,
|
|
13151
|
+
clients: detectInstalledClients(process.cwd())
|
|
13152
|
+
});
|
|
13153
|
+
await maybeOfferHooks({
|
|
13154
|
+
yes,
|
|
13155
|
+
dryRun,
|
|
13156
|
+
cwd: process.cwd(),
|
|
13157
|
+
scope,
|
|
13158
|
+
claudeConfigDir: g.claudeConfigDir,
|
|
13159
|
+
codexHome: g.codexHome
|
|
13160
|
+
});
|
|
12168
13161
|
}
|
|
12169
13162
|
process.stdout.write(
|
|
12170
13163
|
`
|
|
@@ -12175,12 +13168,36 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
12175
13168
|
await printStarterPrompt("cli");
|
|
12176
13169
|
return;
|
|
12177
13170
|
}
|
|
12178
|
-
const
|
|
12179
|
-
|
|
12180
|
-
|
|
13171
|
+
const requestedKeys = await chooseClients(
|
|
13172
|
+
opts.client,
|
|
13173
|
+
yes,
|
|
13174
|
+
process.cwd()
|
|
13175
|
+
);
|
|
13176
|
+
const keys = scope === "project" ? requestedKeys.filter((key) => key !== "codex") : requestedKeys;
|
|
13177
|
+
if (scope === "project" && requestedKeys.includes("codex") && !json) {
|
|
13178
|
+
process.stderr.write(
|
|
13179
|
+
`${style.dim("Codex has no project scope \u2014 skipped (use --scope global for Codex).")}
|
|
13180
|
+
`
|
|
13181
|
+
);
|
|
13182
|
+
}
|
|
13183
|
+
const setup = await withSpinner(
|
|
13184
|
+
"Fetching setup descriptors",
|
|
13185
|
+
() => fetchSetup(cfg)
|
|
13186
|
+
);
|
|
13187
|
+
const targets = clientTargets(process.cwd(), {
|
|
13188
|
+
claudeDir: claudeTargets[0]?.dir,
|
|
13189
|
+
codexHome: codexHomes[0] ?? null
|
|
13190
|
+
});
|
|
12181
13191
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
12182
13192
|
if (!dryRun && !check) {
|
|
12183
|
-
await maybeOfferCopies(
|
|
13193
|
+
await maybeOfferCopies(
|
|
13194
|
+
cfg,
|
|
13195
|
+
setup,
|
|
13196
|
+
targets,
|
|
13197
|
+
keys,
|
|
13198
|
+
personalWorkspaceId,
|
|
13199
|
+
copyChoice(opts)
|
|
13200
|
+
);
|
|
12184
13201
|
}
|
|
12185
13202
|
const writeMcp = wire === "full";
|
|
12186
13203
|
const result = [];
|
|
@@ -12197,16 +13214,11 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
12197
13214
|
if (!json && !check) printActions(target, actions);
|
|
12198
13215
|
}
|
|
12199
13216
|
if (check) {
|
|
12200
|
-
reportCheckAndExit(
|
|
12201
|
-
|
|
12202
|
-
|
|
12203
|
-
|
|
12204
|
-
|
|
12205
|
-
baseUrl: cfg.baseUrl,
|
|
12206
|
-
tenant: cfg.tenant,
|
|
12207
|
-
workspaceId: cfg.workspaceId ?? null
|
|
12208
|
-
}
|
|
12209
|
-
);
|
|
13217
|
+
reportCheckAndExit(result, json, "sechroom onboard --refresh", {
|
|
13218
|
+
baseUrl: cfg.baseUrl,
|
|
13219
|
+
tenant: cfg.tenant,
|
|
13220
|
+
workspaceId: cfg.workspaceId ?? null
|
|
13221
|
+
});
|
|
12210
13222
|
}
|
|
12211
13223
|
const evalCounts = buildCheckReport(result).eval;
|
|
12212
13224
|
if (!json && !dryRun) {
|
|
@@ -12214,19 +13226,45 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
12214
13226
|
}
|
|
12215
13227
|
if (!json && !dryRun) {
|
|
12216
13228
|
for (const t of claudeTargets) {
|
|
12217
|
-
await maybeOfferSkills(cfg, personalWorkspaceId, {
|
|
13229
|
+
await maybeOfferSkills(cfg, personalWorkspaceId, {
|
|
13230
|
+
yes,
|
|
13231
|
+
dryRun,
|
|
13232
|
+
surface: "claude-code",
|
|
13233
|
+
configDir: t.dir
|
|
13234
|
+
});
|
|
12218
13235
|
}
|
|
12219
13236
|
}
|
|
12220
13237
|
if (!json && !dryRun) {
|
|
12221
|
-
await maybeOfferHooks({
|
|
13238
|
+
await maybeOfferHooks({
|
|
13239
|
+
yes,
|
|
13240
|
+
dryRun,
|
|
13241
|
+
cwd: process.cwd(),
|
|
13242
|
+
scope,
|
|
13243
|
+
claudeConfigDir: g.claudeConfigDir,
|
|
13244
|
+
codexHome: g.codexHome
|
|
13245
|
+
});
|
|
12222
13246
|
}
|
|
12223
13247
|
if (json) {
|
|
12224
|
-
emit(
|
|
13248
|
+
emit(
|
|
13249
|
+
{
|
|
13250
|
+
dryRun,
|
|
13251
|
+
baseUrl: cfg.baseUrl,
|
|
13252
|
+
tenant: cfg.tenant,
|
|
13253
|
+
workspaceId: cfg.workspaceId ?? null,
|
|
13254
|
+
timezone: tz,
|
|
13255
|
+
wire,
|
|
13256
|
+
eval: evalCounts,
|
|
13257
|
+
clients: result
|
|
13258
|
+
},
|
|
13259
|
+
true
|
|
13260
|
+
);
|
|
12225
13261
|
return;
|
|
12226
13262
|
}
|
|
12227
13263
|
if (!dryRun && evalCounts.stale) {
|
|
12228
|
-
process.stderr.write(
|
|
12229
|
-
|
|
13264
|
+
process.stderr.write(
|
|
13265
|
+
`${style.cyan("\u21BB")} refreshed ${evalCounts.stale} section(s) the server had moved
|
|
13266
|
+
`
|
|
13267
|
+
);
|
|
12230
13268
|
}
|
|
12231
13269
|
if (!dryRun && evalCounts.drift) {
|
|
12232
13270
|
process.stderr.write(
|
|
@@ -12235,7 +13273,9 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
12235
13273
|
`
|
|
12236
13274
|
);
|
|
12237
13275
|
}
|
|
12238
|
-
const wroteSomething = result.some(
|
|
13276
|
+
const wroteSomething = result.some(
|
|
13277
|
+
({ actions }) => actions.some((a) => a.status === "created" || a.status === "merged")
|
|
13278
|
+
);
|
|
12239
13279
|
process.stdout.write(
|
|
12240
13280
|
dryRun ? "\n(dry run \u2014 nothing written)\n" : !wroteSomething ? `
|
|
12241
13281
|
${style.bold("Done.")} Everything's already up to date.
|
|
@@ -12254,9 +13294,21 @@ async function chooseWire(opts, yes) {
|
|
|
12254
13294
|
return promptSelect(
|
|
12255
13295
|
"How should I set up Sechroom in this project?",
|
|
12256
13296
|
[
|
|
12257
|
-
{
|
|
12258
|
-
|
|
12259
|
-
|
|
13297
|
+
{
|
|
13298
|
+
label: "Wire my AI client",
|
|
13299
|
+
value: "full",
|
|
13300
|
+
hint: "MCP server (.mcp.json) + agent instructions"
|
|
13301
|
+
},
|
|
13302
|
+
{
|
|
13303
|
+
label: "Agent instructions only",
|
|
13304
|
+
value: "agent-only",
|
|
13305
|
+
hint: "skip MCP config"
|
|
13306
|
+
},
|
|
13307
|
+
{
|
|
13308
|
+
label: "CLI only",
|
|
13309
|
+
value: "cli-only",
|
|
13310
|
+
hint: "don't write any AI-client files"
|
|
13311
|
+
}
|
|
12260
13312
|
],
|
|
12261
13313
|
"full"
|
|
12262
13314
|
);
|
|
@@ -12278,7 +13330,9 @@ ${rule}
|
|
|
12278
13330
|
}
|
|
12279
13331
|
async function printStarterPrompt(mode, cfg) {
|
|
12280
13332
|
if (mode === "cli") {
|
|
12281
|
-
printNextStepBlock("Next \u2014 pick up where you left off:", [
|
|
13333
|
+
printNextStepBlock("Next \u2014 pick up where you left off:", [
|
|
13334
|
+
style.cyan("sechroom continuity resume-me")
|
|
13335
|
+
]);
|
|
12282
13336
|
return;
|
|
12283
13337
|
}
|
|
12284
13338
|
let primary = FALLBACK_AGENT_PROMPT;
|
|
@@ -12290,7 +13344,9 @@ async function printStarterPrompt(mode, cfg) {
|
|
|
12290
13344
|
} catch {
|
|
12291
13345
|
}
|
|
12292
13346
|
}
|
|
12293
|
-
printNextStepBlock("Next \u2014 paste this into your AI agent to get going:", [
|
|
13347
|
+
printNextStepBlock("Next \u2014 paste this into your AI agent to get going:", [
|
|
13348
|
+
style.cyan(`"${primary}"`)
|
|
13349
|
+
]);
|
|
12294
13350
|
}
|
|
12295
13351
|
|
|
12296
13352
|
// src/commands/project.ts
|
|
@@ -12605,7 +13661,7 @@ Examples:
|
|
|
12605
13661
|
// src/commands/reset.ts
|
|
12606
13662
|
import { homedir as homedir6 } from "os";
|
|
12607
13663
|
import { join as join22 } from "path";
|
|
12608
|
-
import { existsSync as existsSync16, readFileSync as readFileSync18, rmSync as
|
|
13664
|
+
import { existsSync as existsSync16, readFileSync as readFileSync18, rmSync as rmSync8 } from "fs";
|
|
12609
13665
|
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
12610
13666
|
var localSkillsDir = () => join22(process.cwd(), ".claude", "skills");
|
|
12611
13667
|
var globalSkillsDir = () => join22(homedir6(), ".claude", "skills");
|
|
@@ -12621,14 +13677,14 @@ function removeMaterialisedSkills(dir) {
|
|
|
12621
13677
|
for (const name of entry.skills ?? []) {
|
|
12622
13678
|
const p = join22(dir, name);
|
|
12623
13679
|
if (existsSync16(p)) {
|
|
12624
|
-
|
|
13680
|
+
rmSync8(p, { recursive: true, force: true });
|
|
12625
13681
|
removed.push(p);
|
|
12626
13682
|
}
|
|
12627
13683
|
}
|
|
12628
13684
|
}
|
|
12629
13685
|
} catch {
|
|
12630
13686
|
}
|
|
12631
|
-
|
|
13687
|
+
rmSync8(lockPath, { force: true });
|
|
12632
13688
|
removed.push(lockPath);
|
|
12633
13689
|
return removed;
|
|
12634
13690
|
}
|
|
@@ -12667,17 +13723,17 @@ function registerReset(program2) {
|
|
|
12667
13723
|
const removed = [];
|
|
12668
13724
|
const stateDir = join22(process.cwd(), ".sechroom");
|
|
12669
13725
|
if (existsSync16(stateDir)) {
|
|
12670
|
-
|
|
13726
|
+
rmSync8(stateDir, { recursive: true, force: true });
|
|
12671
13727
|
removed.push(stateDir);
|
|
12672
13728
|
}
|
|
12673
13729
|
const legacyCfg = join22(process.cwd(), ".sechroom.json");
|
|
12674
13730
|
if (existsSync16(legacyCfg)) {
|
|
12675
|
-
|
|
13731
|
+
rmSync8(legacyCfg, { force: true });
|
|
12676
13732
|
removed.push(legacyCfg);
|
|
12677
13733
|
}
|
|
12678
13734
|
const legacySem = join22(process.cwd(), ".sem");
|
|
12679
13735
|
if (existsSync16(legacySem)) {
|
|
12680
|
-
|
|
13736
|
+
rmSync8(legacySem, { force: true });
|
|
12681
13737
|
removed.push(legacySem);
|
|
12682
13738
|
}
|
|
12683
13739
|
removed.push(...removeMaterialisedSkills(localSkillsDir()));
|
|
@@ -12702,7 +13758,7 @@ function registerReset(program2) {
|
|
|
12702
13758
|
}
|
|
12703
13759
|
|
|
12704
13760
|
// src/commands/skills.ts
|
|
12705
|
-
import { existsSync as existsSync17, mkdirSync as mkdirSync18, statSync as
|
|
13761
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync18, statSync as statSync7, writeFileSync as writeFileSync17 } from "fs";
|
|
12706
13762
|
import { join as join23 } from "path";
|
|
12707
13763
|
function filenameFromDisposition(header) {
|
|
12708
13764
|
if (!header) return void 0;
|
|
@@ -12712,7 +13768,7 @@ function filenameFromDisposition(header) {
|
|
|
12712
13768
|
function resolveOutputPath(output, serverFilename) {
|
|
12713
13769
|
const filename = serverFilename || "skills.zip";
|
|
12714
13770
|
if (!output) return join23(process.cwd(), filename);
|
|
12715
|
-
const looksLikeDir = output.endsWith("/") || existsSync17(output) &&
|
|
13771
|
+
const looksLikeDir = output.endsWith("/") || existsSync17(output) && statSync7(output).isDirectory();
|
|
12716
13772
|
if (looksLikeDir) {
|
|
12717
13773
|
mkdirSync18(output, { recursive: true });
|
|
12718
13774
|
return join23(output, filename);
|