@sechroom/cli 2026.8.4-rc.e38fd260b → 2026.8.4
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 +1836 -377
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -642,6 +642,17 @@ function emitAction(summary, data, json) {
|
|
|
642
642
|
process.stdout.write(`${ok("\u2713")} ${summary}
|
|
643
643
|
`);
|
|
644
644
|
}
|
|
645
|
+
var JSON_NUMBER = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
|
|
646
|
+
function confidenceWireValue(value) {
|
|
647
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
648
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
|
|
649
|
+
const raw = String(value).trim();
|
|
650
|
+
if (JSON_NUMBER.test(raw)) {
|
|
651
|
+
const numeric = Number(raw);
|
|
652
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
653
|
+
}
|
|
654
|
+
return String(value);
|
|
655
|
+
}
|
|
645
656
|
var GOVERNANCE_QUEUED_PROBLEM_TYPE = "https://sechroom.dev/problems/governance-review-queued";
|
|
646
657
|
function isGovernanceQueued(body) {
|
|
647
658
|
return typeof body === "object" && body !== null && "type" in body && body.type === GOVERNANCE_QUEUED_PROBLEM_TYPE;
|
|
@@ -688,23 +699,57 @@ function formatFailureMessage(error) {
|
|
|
688
699
|
if (error.cause instanceof Error && error.cause.message) {
|
|
689
700
|
msg += `: ${error.cause.message}`;
|
|
690
701
|
}
|
|
691
|
-
} else if (
|
|
702
|
+
} else if (isRecord(error)) {
|
|
692
703
|
const problem = error;
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
704
|
+
const title = typeof problem.title === "string" && problem.title.length > 0 ? problem.title : void 0;
|
|
705
|
+
const problemDetail = typeof problem.detail === "string" && problem.detail.length > 0 ? problem.detail : void 0;
|
|
706
|
+
const fieldErrors = formatProblemErrors(problem.errors);
|
|
707
|
+
const structuredErrors = fieldErrors.length === 0 ? formatStructuredViolations(problem.violations) : [];
|
|
708
|
+
const parts = [
|
|
709
|
+
...title ? [title] : [],
|
|
710
|
+
...problemDetail ? [problemDetail] : [],
|
|
711
|
+
...fieldErrors,
|
|
712
|
+
...structuredErrors
|
|
713
|
+
];
|
|
714
|
+
if (parts.length > 0) {
|
|
715
|
+
if (title && !problemDetail && fieldErrors.length === 0 && structuredErrors.length === 0) {
|
|
716
|
+
parts.push("No additional error detail was returned by the API.");
|
|
717
|
+
}
|
|
718
|
+
msg = parts.join("\n");
|
|
719
|
+
} else {
|
|
720
|
+
const serialized = JSON.stringify(error);
|
|
721
|
+
msg = serialized && serialized !== "{}" ? serialized : "The API returned an empty error response.";
|
|
700
722
|
}
|
|
701
|
-
} else if (typeof error === "object" && error !== null) {
|
|
702
|
-
msg = JSON.stringify(error);
|
|
703
723
|
} else {
|
|
704
724
|
msg = String(error);
|
|
705
725
|
}
|
|
706
726
|
return msg;
|
|
707
727
|
}
|
|
728
|
+
function isRecord(value) {
|
|
729
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
730
|
+
}
|
|
731
|
+
function formatProblemErrors(errors) {
|
|
732
|
+
if (!isRecord(errors)) return [];
|
|
733
|
+
return Object.entries(errors).flatMap(
|
|
734
|
+
([field, messages]) => (Array.isArray(messages) ? messages : [messages]).map(
|
|
735
|
+
(message) => ` ${field}: ${formatErrorValue(message)}`
|
|
736
|
+
)
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
function formatStructuredViolations(violations) {
|
|
740
|
+
if (!Array.isArray(violations)) return [];
|
|
741
|
+
return violations.flatMap((violation) => {
|
|
742
|
+
if (!isRecord(violation)) return [formatErrorValue(violation)];
|
|
743
|
+
const field = typeof violation.field === "string" ? violation.field : "validation";
|
|
744
|
+
const message = typeof violation.message === "string" ? violation.message : formatErrorValue(violation);
|
|
745
|
+
return [` ${field}: ${message}`];
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
function formatErrorValue(value) {
|
|
749
|
+
if (typeof value === "string") return value;
|
|
750
|
+
const serialized = JSON.stringify(value);
|
|
751
|
+
return serialized ?? String(value);
|
|
752
|
+
}
|
|
708
753
|
function fail(error) {
|
|
709
754
|
const msg = formatFailureMessage(error);
|
|
710
755
|
process.stderr.write(`error: ${msg}
|
|
@@ -1027,14 +1072,19 @@ async function fetchSetup(cfg, namespaceSlug) {
|
|
|
1027
1072
|
"/operator-surface/setup",
|
|
1028
1073
|
hasQuery ? { params: { query } } : {}
|
|
1029
1074
|
);
|
|
1030
|
-
if (error)
|
|
1075
|
+
if (error)
|
|
1076
|
+
throw new Error(
|
|
1077
|
+
`GET /operator-surface/setup failed: ${JSON.stringify(error)}`
|
|
1078
|
+
);
|
|
1031
1079
|
return data;
|
|
1032
1080
|
}
|
|
1033
1081
|
async function listNamespaces(cfg) {
|
|
1034
1082
|
const client = await makeClient(cfg);
|
|
1035
1083
|
const { data } = await client.GET("/mcp-aggregator/namespaces", {});
|
|
1036
1084
|
const rows = data ?? [];
|
|
1037
|
-
return rows.filter(
|
|
1085
|
+
return rows.filter(
|
|
1086
|
+
(r) => typeof r.slug === "string"
|
|
1087
|
+
).map((r) => ({ slug: r.slug, displayName: r.displayName ?? r.slug }));
|
|
1038
1088
|
}
|
|
1039
1089
|
function findSurface(setup, surfaceKey) {
|
|
1040
1090
|
return setup.surfaces.find((s) => s.surfaceKey === surfaceKey);
|
|
@@ -1050,10 +1100,15 @@ function sectionSnippet(section) {
|
|
|
1050
1100
|
}
|
|
1051
1101
|
return null;
|
|
1052
1102
|
}
|
|
1053
|
-
function
|
|
1103
|
+
function parseTagArtifactCandidates(id) {
|
|
1054
1104
|
if (!id.startsWith("tag:")) return null;
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1105
|
+
const candidates = id.slice("tag:".length).split("|").map(
|
|
1106
|
+
(set) => set.split(",").map((t) => t.trim()).filter((t) => t.length > 0)
|
|
1107
|
+
).filter((set) => set.length > 0);
|
|
1108
|
+
return candidates.length > 0 ? candidates : null;
|
|
1109
|
+
}
|
|
1110
|
+
function parseTagArtifactId(id) {
|
|
1111
|
+
return parseTagArtifactCandidates(id)?.[0] ?? null;
|
|
1057
1112
|
}
|
|
1058
1113
|
async function getPersonalWorkspaceId(cfg) {
|
|
1059
1114
|
const client = await makeClient(cfg);
|
|
@@ -1062,40 +1117,102 @@ async function getPersonalWorkspaceId(cfg) {
|
|
|
1062
1117
|
}
|
|
1063
1118
|
async function fetchMemoryFields(cfg, id) {
|
|
1064
1119
|
const client = await makeClient(cfg);
|
|
1065
|
-
const { data } = await client.GET("/memories/{memoryId}", {
|
|
1120
|
+
const { data } = await client.GET("/memories/{memoryId}", {
|
|
1121
|
+
params: { path: { memoryId: id } }
|
|
1122
|
+
});
|
|
1066
1123
|
const env = data;
|
|
1067
1124
|
const m = env?.item ?? env;
|
|
1068
1125
|
if (!m) return null;
|
|
1069
1126
|
const version = typeof m.currentVersion === "string" ? Number(m.currentVersion) : m.currentVersion;
|
|
1070
|
-
return {
|
|
1127
|
+
return {
|
|
1128
|
+
text: m.text,
|
|
1129
|
+
title: m.title,
|
|
1130
|
+
tags: m.tags,
|
|
1131
|
+
version: Number.isFinite(version) ? version : void 0
|
|
1132
|
+
};
|
|
1071
1133
|
}
|
|
1072
|
-
async function
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
const tags = parseTagArtifactId(artifact.id);
|
|
1076
|
-
if (!tags) continue;
|
|
1134
|
+
async function findTemplateByCandidates(client, candidates) {
|
|
1135
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
1136
|
+
const tags = candidates[i];
|
|
1077
1137
|
const { data } = await client.POST("/memories/search", {
|
|
1078
|
-
body: {
|
|
1138
|
+
body: {
|
|
1139
|
+
query: null,
|
|
1140
|
+
textQuery: null,
|
|
1141
|
+
semanticQuery: null,
|
|
1142
|
+
hybrid: false,
|
|
1143
|
+
limit: 1,
|
|
1144
|
+
includeArchived: false,
|
|
1145
|
+
includeSystem: false,
|
|
1146
|
+
tags
|
|
1147
|
+
}
|
|
1079
1148
|
});
|
|
1080
1149
|
const hits = data?.items ?? [];
|
|
1081
1150
|
if (hits.length === 0) continue;
|
|
1082
|
-
|
|
1151
|
+
return { id: hits[0].id, tags, via: i === 0 ? "preferred" : "fallback" };
|
|
1152
|
+
}
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1155
|
+
function attemptedTagSets(section) {
|
|
1156
|
+
return section.artifacts.flatMap(
|
|
1157
|
+
(artifact) => parseTagArtifactCandidates(artifact.id) ?? []
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
async function resolveInstruction(cfg, section, personalWorkspaceId) {
|
|
1161
|
+
const client = await makeClient(cfg);
|
|
1162
|
+
for (const artifact of section.artifacts) {
|
|
1163
|
+
const candidates = parseTagArtifactCandidates(artifact.id);
|
|
1164
|
+
if (!candidates) continue;
|
|
1165
|
+
const found = await findTemplateByCandidates(client, candidates);
|
|
1166
|
+
if (!found) continue;
|
|
1167
|
+
const templateId = found.id;
|
|
1083
1168
|
const template = await fetchMemoryFields(cfg, templateId);
|
|
1084
|
-
if (typeof template?.text !== "string" || template.text.length === 0)
|
|
1085
|
-
|
|
1169
|
+
if (typeof template?.text !== "string" || template.text.length === 0)
|
|
1170
|
+
continue;
|
|
1171
|
+
const templateTags = template.tags ?? found.tags;
|
|
1086
1172
|
if (personalWorkspaceId) {
|
|
1087
1173
|
const { data: ovr } = await client.POST("/memories/search", {
|
|
1088
|
-
body: {
|
|
1174
|
+
body: {
|
|
1175
|
+
query: null,
|
|
1176
|
+
textQuery: null,
|
|
1177
|
+
semanticQuery: null,
|
|
1178
|
+
hybrid: false,
|
|
1179
|
+
limit: 1,
|
|
1180
|
+
includeArchived: false,
|
|
1181
|
+
includeSystem: false,
|
|
1182
|
+
tags: [
|
|
1183
|
+
"sechroom:role:override",
|
|
1184
|
+
`sechroom:template-ref:${templateId}`
|
|
1185
|
+
],
|
|
1186
|
+
owner: { type: "Workspace", id: personalWorkspaceId }
|
|
1187
|
+
}
|
|
1089
1188
|
});
|
|
1090
1189
|
const ovrHits = ovr?.items ?? [];
|
|
1091
1190
|
if (ovrHits.length > 0) {
|
|
1092
1191
|
const override = await fetchMemoryFields(cfg, ovrHits[0].id);
|
|
1093
1192
|
if (typeof override?.text === "string" && override.text.length > 0) {
|
|
1094
|
-
return {
|
|
1193
|
+
return {
|
|
1194
|
+
title: override.title ?? template.title ?? artifact.title,
|
|
1195
|
+
body: override.text,
|
|
1196
|
+
source: "override",
|
|
1197
|
+
templateId,
|
|
1198
|
+
templateTags,
|
|
1199
|
+
resolvedVia: found.via,
|
|
1200
|
+
resolvedTags: found.tags,
|
|
1201
|
+
sourceRef: `${ovrHits[0].id}@v${override.version ?? 1}`
|
|
1202
|
+
};
|
|
1095
1203
|
}
|
|
1096
1204
|
}
|
|
1097
1205
|
}
|
|
1098
|
-
return {
|
|
1206
|
+
return {
|
|
1207
|
+
title: template.title ?? artifact.title,
|
|
1208
|
+
body: template.text,
|
|
1209
|
+
source: "template",
|
|
1210
|
+
templateId,
|
|
1211
|
+
templateTags,
|
|
1212
|
+
resolvedVia: found.via,
|
|
1213
|
+
resolvedTags: found.tags,
|
|
1214
|
+
sourceRef: `${templateId}@v${template.version ?? 1}`
|
|
1215
|
+
};
|
|
1099
1216
|
}
|
|
1100
1217
|
return null;
|
|
1101
1218
|
}
|
|
@@ -1107,8 +1224,10 @@ async function resolveWorkspaceConventions(cfg, section) {
|
|
|
1107
1224
|
const mem = await fetchMemoryFields(cfg, artifact.id);
|
|
1108
1225
|
if (typeof mem?.text === "string" && mem.text.trim().length > 0) {
|
|
1109
1226
|
const ref = `${artifact.id}@v${mem.version ?? 1}`;
|
|
1110
|
-
parts.push(
|
|
1111
|
-
|
|
1227
|
+
parts.push(
|
|
1228
|
+
`<!-- @sechroom/cli:section source=${ref} -->
|
|
1229
|
+
${mem.text.trim()}`
|
|
1230
|
+
);
|
|
1112
1231
|
refs.push(ref);
|
|
1113
1232
|
}
|
|
1114
1233
|
}
|
|
@@ -1120,7 +1239,10 @@ async function createOverride(cfg, template, personalWorkspaceId) {
|
|
|
1120
1239
|
const overrideTags = template.templateTags.filter(
|
|
1121
1240
|
(t) => t !== "sechroom:role:template" && !t.startsWith("sechroom:bundle:") && !t.startsWith("sechroom:template-ref:")
|
|
1122
1241
|
);
|
|
1123
|
-
overrideTags.push(
|
|
1242
|
+
overrideTags.push(
|
|
1243
|
+
"sechroom:role:override",
|
|
1244
|
+
`sechroom:template-ref:${template.templateId}`
|
|
1245
|
+
);
|
|
1124
1246
|
const { error } = await client.POST("/memories", {
|
|
1125
1247
|
body: {
|
|
1126
1248
|
text: template.body,
|
|
@@ -1134,7 +1256,8 @@ async function createOverride(cfg, template, personalWorkspaceId) {
|
|
|
1134
1256
|
owner: { type: "Workspace", id: personalWorkspaceId }
|
|
1135
1257
|
}
|
|
1136
1258
|
});
|
|
1137
|
-
if (error)
|
|
1259
|
+
if (error)
|
|
1260
|
+
throw new Error(`creating personal copy failed: ${JSON.stringify(error)}`);
|
|
1138
1261
|
}
|
|
1139
1262
|
|
|
1140
1263
|
// src/setup/skill-resolution.ts
|
|
@@ -4366,9 +4489,19 @@ import { dirname as dirname5, join as join7 } from "path";
|
|
|
4366
4489
|
function claudeDesktopConfigPath(home) {
|
|
4367
4490
|
switch (process.platform) {
|
|
4368
4491
|
case "darwin":
|
|
4369
|
-
return join7(
|
|
4492
|
+
return join7(
|
|
4493
|
+
home,
|
|
4494
|
+
"Library",
|
|
4495
|
+
"Application Support",
|
|
4496
|
+
"Claude",
|
|
4497
|
+
"claude_desktop_config.json"
|
|
4498
|
+
);
|
|
4370
4499
|
case "win32":
|
|
4371
|
-
return join7(
|
|
4500
|
+
return join7(
|
|
4501
|
+
process.env.APPDATA ?? join7(home, "AppData", "Roaming"),
|
|
4502
|
+
"Claude",
|
|
4503
|
+
"claude_desktop_config.json"
|
|
4504
|
+
);
|
|
4372
4505
|
default:
|
|
4373
4506
|
return join7(home, ".config", "Claude", "claude_desktop_config.json");
|
|
4374
4507
|
}
|
|
@@ -4376,30 +4509,53 @@ function claudeDesktopConfigPath(home) {
|
|
|
4376
4509
|
function clientTargets(cwd, opts = {}) {
|
|
4377
4510
|
const home = homedir3();
|
|
4378
4511
|
const claudeDir = opts.claudeDir ?? join7(home, ".claude");
|
|
4379
|
-
const codexHome = opts.codexHome
|
|
4512
|
+
const codexHome = opts.codexHome === void 0 ? join7(home, ".codex") : opts.codexHome;
|
|
4380
4513
|
return {
|
|
4381
4514
|
"claude-code": {
|
|
4382
4515
|
key: "claude-code",
|
|
4383
4516
|
label: "Claude Code",
|
|
4384
|
-
mcp: {
|
|
4517
|
+
mcp: {
|
|
4518
|
+
surfaceKey: "claude-code",
|
|
4519
|
+
sectionType: SectionType.McpConfig,
|
|
4520
|
+
path: join7(cwd, ".mcp.json"),
|
|
4521
|
+
format: "json"
|
|
4522
|
+
},
|
|
4385
4523
|
instruction: { surfaceKey: "claude-code", path: join7(cwd, "CLAUDE.md") }
|
|
4386
4524
|
},
|
|
4387
4525
|
"claude-desktop": {
|
|
4388
4526
|
key: "claude-desktop",
|
|
4389
4527
|
label: "Claude Desktop",
|
|
4390
|
-
mcp: {
|
|
4391
|
-
|
|
4528
|
+
mcp: {
|
|
4529
|
+
surfaceKey: "claude-desktop",
|
|
4530
|
+
sectionType: SectionType.McpConfig,
|
|
4531
|
+
path: claudeDesktopConfigPath(home),
|
|
4532
|
+
format: "json"
|
|
4533
|
+
},
|
|
4534
|
+
instruction: {
|
|
4535
|
+
surfaceKey: "claude-desktop",
|
|
4536
|
+
path: join7(claudeDir, "CLAUDE.md")
|
|
4537
|
+
}
|
|
4392
4538
|
},
|
|
4393
4539
|
codex: {
|
|
4394
4540
|
key: "codex",
|
|
4395
4541
|
label: "Codex CLI",
|
|
4396
|
-
mcp:
|
|
4542
|
+
mcp: codexHome ? {
|
|
4543
|
+
surfaceKey: "chatgpt",
|
|
4544
|
+
sectionType: SectionType.McpConfigToml,
|
|
4545
|
+
path: join7(codexHome, "config.toml"),
|
|
4546
|
+
format: "toml"
|
|
4547
|
+
} : null,
|
|
4397
4548
|
instruction: { surfaceKey: "chatgpt", path: join7(cwd, "AGENTS.md") }
|
|
4398
4549
|
},
|
|
4399
4550
|
cursor: {
|
|
4400
4551
|
key: "cursor",
|
|
4401
4552
|
label: "Cursor",
|
|
4402
|
-
mcp: {
|
|
4553
|
+
mcp: {
|
|
4554
|
+
surfaceKey: "claude-code",
|
|
4555
|
+
sectionType: SectionType.McpConfig,
|
|
4556
|
+
path: join7(cwd, ".cursor", "mcp.json"),
|
|
4557
|
+
format: "json"
|
|
4558
|
+
},
|
|
4403
4559
|
instruction: { surfaceKey: "chatgpt", path: join7(cwd, "AGENTS.md") }
|
|
4404
4560
|
},
|
|
4405
4561
|
antigravity: {
|
|
@@ -4411,20 +4567,34 @@ function clientTargets(cwd, opts = {}) {
|
|
|
4411
4567
|
// `type` — comes from the `antigravity` server surface, so we don't
|
|
4412
4568
|
// hardcode it here. Instructions go in the project `AGENTS.md`
|
|
4413
4569
|
// (cross-tool, shared with Codex/Cursor).
|
|
4414
|
-
mcp: {
|
|
4570
|
+
mcp: {
|
|
4571
|
+
surfaceKey: "antigravity",
|
|
4572
|
+
sectionType: SectionType.McpConfig,
|
|
4573
|
+
path: join7(home, ".gemini", "config", "mcp_config.json"),
|
|
4574
|
+
format: "json"
|
|
4575
|
+
},
|
|
4415
4576
|
instruction: { surfaceKey: "antigravity", path: join7(cwd, "AGENTS.md") }
|
|
4416
4577
|
}
|
|
4417
4578
|
};
|
|
4418
4579
|
}
|
|
4419
|
-
var ALL_CLIENT_KEYS = [
|
|
4580
|
+
var ALL_CLIENT_KEYS = [
|
|
4581
|
+
"claude-code",
|
|
4582
|
+
"claude-desktop",
|
|
4583
|
+
"codex",
|
|
4584
|
+
"cursor",
|
|
4585
|
+
"antigravity"
|
|
4586
|
+
];
|
|
4420
4587
|
var DEFAULT_CLIENT_KEY = "claude-code";
|
|
4421
4588
|
function detectInstalledClients(cwd) {
|
|
4422
4589
|
const home = homedir3();
|
|
4423
4590
|
const detected = [];
|
|
4424
|
-
if (resolveClaudeTargets({}).some((t) => existsSync5(t.dir)))
|
|
4425
|
-
|
|
4591
|
+
if (resolveClaudeTargets({}).some((t) => existsSync5(t.dir)))
|
|
4592
|
+
detected.push("claude-code");
|
|
4593
|
+
if (existsSync5(dirname5(claudeDesktopConfigPath(home))))
|
|
4594
|
+
detected.push("claude-desktop");
|
|
4426
4595
|
if (resolveCodexHomes({}).some((d) => existsSync5(d))) detected.push("codex");
|
|
4427
|
-
if (existsSync5(join7(home, ".cursor")) || existsSync5(join7(cwd, ".cursor")))
|
|
4596
|
+
if (existsSync5(join7(home, ".cursor")) || existsSync5(join7(cwd, ".cursor")))
|
|
4597
|
+
detected.push("cursor");
|
|
4428
4598
|
if (existsSync5(join7(home, ".gemini"))) detected.push("antigravity");
|
|
4429
4599
|
return detected;
|
|
4430
4600
|
}
|
|
@@ -5751,6 +5921,7 @@ Call sechroom_lifecycle_signal at each phase boundary (start/work/verify/closeou
|
|
|
5751
5921
|
}
|
|
5752
5922
|
|
|
5753
5923
|
// src/commands/executor.ts
|
|
5924
|
+
var DEFAULT_CLAIM_POLICY = "restricted";
|
|
5754
5925
|
function executorSubscriptionInput(name) {
|
|
5755
5926
|
return {
|
|
5756
5927
|
name,
|
|
@@ -5812,8 +5983,8 @@ function registerExecutor(program2) {
|
|
|
5812
5983
|
"Capability operation keys claimed by this instance"
|
|
5813
5984
|
).option(
|
|
5814
5985
|
"--claim-policy <policy>",
|
|
5815
|
-
"open | restricted (restricted only claims preferred/own-lane/allow-listed-tag work)",
|
|
5816
|
-
|
|
5986
|
+
"open | restricted (restricted only claims preferred/own-lane/allow-listed-tag work; default restricted)",
|
|
5987
|
+
DEFAULT_CLAIM_POLICY
|
|
5817
5988
|
).option(
|
|
5818
5989
|
"--claim-tag <tag...>",
|
|
5819
5990
|
"Task tag this instance accepts under --claim-policy restricted"
|
|
@@ -5828,7 +5999,12 @@ function registerExecutor(program2) {
|
|
|
5828
5999
|
"--subscription-name <name>",
|
|
5829
6000
|
"SignalR delivery binding name",
|
|
5830
6001
|
"executor-dispatch"
|
|
5831
|
-
).option(
|
|
6002
|
+
).option(
|
|
6003
|
+
"--ttl <seconds>",
|
|
6004
|
+
"Advertisement TTL (30 to tenant maximum)",
|
|
6005
|
+
parseInteger,
|
|
6006
|
+
600
|
|
6007
|
+
).option(
|
|
5832
6008
|
"--task-lease-ttl <seconds>",
|
|
5833
6009
|
"Task lease TTL (60-86400)",
|
|
5834
6010
|
parseInteger,
|
|
@@ -5915,7 +6091,7 @@ function registerExecutor(program2) {
|
|
|
5915
6091
|
taskLeaseTtlSeconds: opts.taskLeaseTtl,
|
|
5916
6092
|
modelId: opts.modelId,
|
|
5917
6093
|
effortLabel: opts.effortLabel,
|
|
5918
|
-
claimPolicy: (opts.claimPolicy
|
|
6094
|
+
claimPolicy: parseClaimPolicy(opts.claimPolicy) === "Restricted" ? "restricted" : "open",
|
|
5919
6095
|
claimTags: opts.claimTag ?? [],
|
|
5920
6096
|
excludeTags: opts.excludeTag ?? [],
|
|
5921
6097
|
relayId: opts.relay,
|
|
@@ -6036,8 +6212,8 @@ function registerExecutor(program2) {
|
|
|
6036
6212
|
"Capability operation keys claimed by this instance"
|
|
6037
6213
|
).option(
|
|
6038
6214
|
"--claim-policy <policy>",
|
|
6039
|
-
"open | restricted (restricted only claims preferred/own-lane/allow-listed-tag work)",
|
|
6040
|
-
|
|
6215
|
+
"open | restricted (restricted only claims preferred/own-lane/allow-listed-tag work; default restricted)",
|
|
6216
|
+
DEFAULT_CLAIM_POLICY
|
|
6041
6217
|
).option(
|
|
6042
6218
|
"--claim-tag <tag...>",
|
|
6043
6219
|
"Task tag this instance accepts under --claim-policy restricted"
|
|
@@ -6048,7 +6224,12 @@ function registerExecutor(program2) {
|
|
|
6048
6224
|
"--activation-mode <mode>",
|
|
6049
6225
|
"attached | detached \u2014 detached marks a fleet run as a service that outlives its shell (default attached)",
|
|
6050
6226
|
"attached"
|
|
6051
|
-
).option(
|
|
6227
|
+
).option(
|
|
6228
|
+
"--ttl <seconds>",
|
|
6229
|
+
"Advertisement TTL (30 to tenant maximum)",
|
|
6230
|
+
parseInteger,
|
|
6231
|
+
120
|
|
6232
|
+
).option(
|
|
6052
6233
|
"--task-lease-ttl <seconds>",
|
|
6053
6234
|
"Task lease TTL (60-86400)",
|
|
6054
6235
|
parseInteger,
|
|
@@ -6104,7 +6285,12 @@ function registerExecutor(program2) {
|
|
|
6104
6285
|
`)
|
|
6105
6286
|
);
|
|
6106
6287
|
});
|
|
6107
|
-
executor.command("refresh <id>").description("Refresh one advertisement lease once").option(
|
|
6288
|
+
executor.command("refresh <id>").description("Refresh one advertisement lease once").option(
|
|
6289
|
+
"--ttl <seconds>",
|
|
6290
|
+
"Advertisement TTL (30 to tenant maximum)",
|
|
6291
|
+
parseInteger,
|
|
6292
|
+
120
|
|
6293
|
+
).action(async (id, opts, cmd) => {
|
|
6108
6294
|
const data = await refreshExecutorInstance(
|
|
6109
6295
|
resolveConfig(cmd.optsWithGlobals()),
|
|
6110
6296
|
id,
|
|
@@ -6112,7 +6298,12 @@ function registerExecutor(program2) {
|
|
|
6112
6298
|
);
|
|
6113
6299
|
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
6114
6300
|
});
|
|
6115
|
-
executor.command("heartbeat <id>").description("Keep an advertisement alive until interrupted").option(
|
|
6301
|
+
executor.command("heartbeat <id>").description("Keep an advertisement alive until interrupted").option(
|
|
6302
|
+
"--ttl <seconds>",
|
|
6303
|
+
"Advertisement TTL (30 to tenant maximum)",
|
|
6304
|
+
parseInteger,
|
|
6305
|
+
120
|
|
6306
|
+
).option("--interval <seconds>", "Refresh interval", parseInteger, 40).action(async (id, opts, cmd) => {
|
|
6116
6307
|
if (opts.interval >= opts.ttl)
|
|
6117
6308
|
fail("heartbeat interval must be shorter than the TTL");
|
|
6118
6309
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
@@ -6274,12 +6465,12 @@ function parseRuntimeKind(value) {
|
|
|
6274
6465
|
}
|
|
6275
6466
|
}
|
|
6276
6467
|
function parseClaimPolicy(value) {
|
|
6277
|
-
switch ((value ??
|
|
6468
|
+
switch ((value ?? DEFAULT_CLAIM_POLICY).trim().toLowerCase()) {
|
|
6278
6469
|
case "":
|
|
6279
|
-
case "open":
|
|
6280
|
-
return "Open";
|
|
6281
6470
|
case "restricted":
|
|
6282
6471
|
return "Restricted";
|
|
6472
|
+
case "open":
|
|
6473
|
+
return "Open";
|
|
6283
6474
|
default:
|
|
6284
6475
|
return fail("claim-policy must be open or restricted");
|
|
6285
6476
|
}
|
|
@@ -7607,6 +7798,7 @@ function resolveLane(flagLane, cwd) {
|
|
|
7607
7798
|
return applyWorktreeLaneSuffix(base, start);
|
|
7608
7799
|
}
|
|
7609
7800
|
var INTENT_FILE = join15(".sechroom", "continuity.json");
|
|
7801
|
+
var LOCAL_DRY_RUN_VALIDATION_WARNING = "LOCAL-ONLY \u2014 NOT SERVER-VALIDATED";
|
|
7610
7802
|
function resolveIntentPath(start) {
|
|
7611
7803
|
let dir = start;
|
|
7612
7804
|
for (; ; ) {
|
|
@@ -7631,6 +7823,16 @@ function hasRequiredIntent(i) {
|
|
|
7631
7823
|
i.objective?.trim() && i.state?.trim() && i.lastAction?.trim() && i.nextAction?.trim() && i.resumeInstruction?.trim()
|
|
7632
7824
|
);
|
|
7633
7825
|
}
|
|
7826
|
+
function localDryRunMissingFields(i) {
|
|
7827
|
+
const required = [
|
|
7828
|
+
["objective", "--objective"],
|
|
7829
|
+
["state", "--state"],
|
|
7830
|
+
["lastAction", "--last-action"],
|
|
7831
|
+
["nextAction", "--next-action"],
|
|
7832
|
+
["resumeInstruction", "--resume-instruction"]
|
|
7833
|
+
];
|
|
7834
|
+
return required.filter(([key]) => !String(i[key] ?? "").trim()).map(([, flag]) => flag);
|
|
7835
|
+
}
|
|
7634
7836
|
async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScope, opts) {
|
|
7635
7837
|
const lane = resolveLane(laneFlag, cwd);
|
|
7636
7838
|
if (!lane) return false;
|
|
@@ -7652,7 +7854,9 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
|
|
|
7652
7854
|
openQuestions: intent.questions ?? null,
|
|
7653
7855
|
surfaceMarkers: intent.surfaceMarkers ?? null,
|
|
7654
7856
|
relevantArtifactIds: intent.artifacts ?? null,
|
|
7655
|
-
|
|
7857
|
+
// Preserve invalid JSON-string confidence tokens for the server's semantic
|
|
7858
|
+
// validator; never coerce them through Number/NaN/null.
|
|
7859
|
+
confidence: confidenceWireValue(intent.confidence),
|
|
7656
7860
|
// Frequent triggers (compaction, session-end) land within the FR-051 4h
|
|
7657
7861
|
// window; Acknowledge lets the checkpoint persist on the lane.
|
|
7658
7862
|
concurrentSessionPolicy: "Acknowledge"
|
|
@@ -7947,12 +8151,18 @@ Fail-soft: failures exit 0 and never block; session-context refresh failures ren
|
|
|
7947
8151
|
function registerCheckpoint(program2) {
|
|
7948
8152
|
program2.command("checkpoint").description(
|
|
7949
8153
|
"Checkpoint working state: create a continuity snapshot (server-validated) AND sync ./.sechroom/continuity.json in one step"
|
|
7950
|
-
).option("--lane <laneId>", "Lane id (else SECHROOM_LANE, else ./.sechroom/lane.json code-lane)").option("--scope <scope>", "Snapshot scope (else the file's scope, else 'session')").option("--objective <text>", "Current objective").option("--state <text>", "Current state").option("--last-action <text>", "Last meaningful action").option("--next-action <text>", "Next intended action").option("--resume-instruction <text>", "Resume instruction").option("--constraint <text...>", "Active constraints (repeatable)").option("--question <text...>", "Open questions (repeatable)").option("--surface-marker <text...>", "Surface markers (repeatable)").option("--artifact <id...>", "Relevant artifact ids (repeatable)").option("--confidence <n>", "Confidence 0..1").option(
|
|
8154
|
+
).option("--lane <laneId>", "Lane id (else SECHROOM_LANE, else ./.sechroom/lane.json code-lane)").option("--scope <scope>", "Snapshot scope (else the file's scope, else 'session')").option("--objective <text>", "Current objective").option("--state <text>", "Current state").option("--last-action <text>", "Last meaningful action").option("--next-action <text>", "Next intended action").option("--resume-instruction <text>", "Resume instruction").option("--constraint <text...>", "Active constraints (repeatable)").option("--question <text...>", "Open questions (repeatable)").option("--surface-marker <text...>", "Surface markers (repeatable)").option("--artifact <id...>", "Relevant artifact ids (repeatable)").option("--confidence <n>", "Confidence 0..1").option(
|
|
8155
|
+
"--dry-run",
|
|
8156
|
+
"run the local required-field check and print the payload (LOCAL-ONLY; NOT SERVER-VALIDATED)",
|
|
8157
|
+
false
|
|
8158
|
+
).addHelpText(
|
|
7951
8159
|
"after",
|
|
7952
8160
|
`
|
|
7953
8161
|
File-first: reads ./.sechroom/continuity.json (kept current as you work) as the base; any flag
|
|
7954
8162
|
overrides that field. The snapshot is created FIRST (server-validated), then the local file is
|
|
7955
8163
|
written/normalized with the returned snapshotId. Lane: --lane > SECHROOM_LANE > ./.sechroom/lane.json code-lane.
|
|
8164
|
+
--dry-run performs only the local required-field check and prints a payload. Its output is
|
|
8165
|
+
explicitly LOCAL-ONLY; NOT SERVER-VALIDATED, so it does not establish write-path parity.
|
|
7956
8166
|
|
|
7957
8167
|
Examples:
|
|
7958
8168
|
$ sechroom checkpoint snapshot from ./.sechroom/continuity.json, then sync it
|
|
@@ -7975,7 +8185,9 @@ Examples:
|
|
|
7975
8185
|
questions: opts.question ?? base.questions,
|
|
7976
8186
|
surfaceMarkers: opts.surfaceMarker ?? base.surfaceMarkers,
|
|
7977
8187
|
artifacts: opts.artifact ?? base.artifacts,
|
|
7978
|
-
|
|
8188
|
+
// Keep the raw token until the server's shared validator sees it. In particular,
|
|
8189
|
+
// Number("high") -> NaN -> JSON null would silently discard the operator's input.
|
|
8190
|
+
confidence: opts.confidence != null ? opts.confidence : base.confidence
|
|
7979
8191
|
};
|
|
7980
8192
|
const lane = resolveLane(opts.lane, cwd);
|
|
7981
8193
|
if (!lane) {
|
|
@@ -7983,19 +8195,6 @@ Examples:
|
|
|
7983
8195
|
"no lane resolved \u2014 pass --lane, set SECHROOM_LANE, or pin one in ./.sechroom/lane.json (code-lane). See `sechroom lane`."
|
|
7984
8196
|
);
|
|
7985
8197
|
}
|
|
7986
|
-
const required = [
|
|
7987
|
-
["objective", "--objective"],
|
|
7988
|
-
["state", "--state"],
|
|
7989
|
-
["lastAction", "--last-action"],
|
|
7990
|
-
["nextAction", "--next-action"],
|
|
7991
|
-
["resumeInstruction", "--resume-instruction"]
|
|
7992
|
-
];
|
|
7993
|
-
const missing = required.filter(([k]) => !String(merged[k] ?? "").trim()).map(([, flag]) => flag);
|
|
7994
|
-
if (missing.length > 0) {
|
|
7995
|
-
fail(
|
|
7996
|
-
`missing required field(s): ${missing.join(", ")} \u2014 supply via flag or in ./.sechroom/continuity.json`
|
|
7997
|
-
);
|
|
7998
|
-
}
|
|
7999
8198
|
const scope = merged.scope ?? "session";
|
|
8000
8199
|
const body = {
|
|
8001
8200
|
laneId: lane,
|
|
@@ -8009,13 +8208,33 @@ Examples:
|
|
|
8009
8208
|
openQuestions: merged.questions ?? null,
|
|
8010
8209
|
surfaceMarkers: merged.surfaceMarkers ?? null,
|
|
8011
8210
|
relevantArtifactIds: merged.artifacts ?? null,
|
|
8012
|
-
confidence: merged.confidence
|
|
8211
|
+
confidence: confidenceWireValue(merged.confidence),
|
|
8013
8212
|
// Explicit checkpoints are often within the FR-051 4h window; Acknowledge
|
|
8014
8213
|
// lets one land on the lane (matches `hook pre-compact`).
|
|
8015
8214
|
concurrentSessionPolicy: "Acknowledge"
|
|
8016
8215
|
};
|
|
8017
8216
|
if (opts.dryRun) {
|
|
8018
|
-
|
|
8217
|
+
const missing = localDryRunMissingFields(merged);
|
|
8218
|
+
if (missing.length > 0) {
|
|
8219
|
+
fail(
|
|
8220
|
+
`LOCAL-ONLY CHECK \u2014 NOT SERVER-VALIDATED: missing required field(s): ${missing.join(", ")} \u2014 supply via flag or in ./.sechroom/continuity.json`
|
|
8221
|
+
);
|
|
8222
|
+
}
|
|
8223
|
+
emit(
|
|
8224
|
+
{
|
|
8225
|
+
dryRun: true,
|
|
8226
|
+
validation: {
|
|
8227
|
+
mode: "local-only",
|
|
8228
|
+
serverValidated: false,
|
|
8229
|
+
checks: ["required-fields"],
|
|
8230
|
+
warning: LOCAL_DRY_RUN_VALIDATION_WARNING
|
|
8231
|
+
},
|
|
8232
|
+
lane,
|
|
8233
|
+
scope,
|
|
8234
|
+
wouldCreate: body
|
|
8235
|
+
},
|
|
8236
|
+
json
|
|
8237
|
+
);
|
|
8019
8238
|
return;
|
|
8020
8239
|
}
|
|
8021
8240
|
const data = await runApi("Creating snapshot", async () => {
|
|
@@ -8252,7 +8471,7 @@ Displaced snapshot recovery surfaces:
|
|
|
8252
8471
|
openQuestions: opts.question ?? null,
|
|
8253
8472
|
surfaceMarkers: opts.surfaceMarker ?? null,
|
|
8254
8473
|
relevantArtifactIds: opts.artifact ?? null,
|
|
8255
|
-
confidence:
|
|
8474
|
+
confidence: confidenceWireValue(opts.confidence)
|
|
8256
8475
|
}
|
|
8257
8476
|
});
|
|
8258
8477
|
});
|
|
@@ -8401,6 +8620,96 @@ function snapshotGetNotFoundHint(includeAll, status) {
|
|
|
8401
8620
|
|
|
8402
8621
|
// src/commands/work-plan.ts
|
|
8403
8622
|
import { readFile as readFile2 } from "fs/promises";
|
|
8623
|
+
|
|
8624
|
+
// src/paging.ts
|
|
8625
|
+
var MAX_AUTO_PAGES = 100;
|
|
8626
|
+
var asNumber = (value) => value === void 0 || value === "" ? void 0 : Number(value);
|
|
8627
|
+
function shouldAutoPage(opts) {
|
|
8628
|
+
if (opts.autoPage === false) return false;
|
|
8629
|
+
return asNumber(opts.page) === void 0;
|
|
8630
|
+
}
|
|
8631
|
+
function singlePageQuery(opts) {
|
|
8632
|
+
const page = asNumber(opts.page);
|
|
8633
|
+
const pageSize = asNumber(opts.pageSize);
|
|
8634
|
+
return {
|
|
8635
|
+
...page === void 0 ? {} : { page },
|
|
8636
|
+
...pageSize === void 0 ? {} : { pageSize }
|
|
8637
|
+
};
|
|
8638
|
+
}
|
|
8639
|
+
async function fetchAllPages(fetchPage, opts = {}, label = "results") {
|
|
8640
|
+
const pages = [];
|
|
8641
|
+
let page = 1;
|
|
8642
|
+
let truncated = false;
|
|
8643
|
+
for (; ; ) {
|
|
8644
|
+
const current = await fetchPage(page);
|
|
8645
|
+
pages.push(current);
|
|
8646
|
+
if (!hasNextPage(current)) break;
|
|
8647
|
+
if (pages.length >= MAX_AUTO_PAGES) {
|
|
8648
|
+
truncated = true;
|
|
8649
|
+
break;
|
|
8650
|
+
}
|
|
8651
|
+
page = current.page + 1;
|
|
8652
|
+
}
|
|
8653
|
+
const first = pages[0];
|
|
8654
|
+
if (!first) throw new Error("paged read returned no envelope at all");
|
|
8655
|
+
if (truncated) {
|
|
8656
|
+
if (!isQuiet()) {
|
|
8657
|
+
process.stderr.write(
|
|
8658
|
+
`${warn("!")} Stopped after ${MAX_AUTO_PAGES} pages \u2014 more ${label} remain. ${style.dim("Narrow the filters, or read a specific window with --page/--page-size.")}
|
|
8659
|
+
`
|
|
8660
|
+
);
|
|
8661
|
+
}
|
|
8662
|
+
const partial = pages.flatMap((p) => p.items);
|
|
8663
|
+
return {
|
|
8664
|
+
...first,
|
|
8665
|
+
items: partial,
|
|
8666
|
+
page: 1,
|
|
8667
|
+
pageSize: partial.length,
|
|
8668
|
+
// Pages OF THIS SIZE, so the number stays consistent with the pageSize just reported.
|
|
8669
|
+
pageCount: partial.length === 0 ? 1 : Math.ceil(first.count / partial.length),
|
|
8670
|
+
hasPreviousPage: false,
|
|
8671
|
+
hasNextPage: true,
|
|
8672
|
+
isFirstPage: true,
|
|
8673
|
+
isLastPage: false,
|
|
8674
|
+
firstItemOnPage: partial.length === 0 ? 0 : 1,
|
|
8675
|
+
lastItemOnPage: partial.length
|
|
8676
|
+
};
|
|
8677
|
+
}
|
|
8678
|
+
if (pages.length === 1) return first;
|
|
8679
|
+
const items = pages.flatMap((current) => current.items);
|
|
8680
|
+
return {
|
|
8681
|
+
...first,
|
|
8682
|
+
items,
|
|
8683
|
+
page: 1,
|
|
8684
|
+
pageSize: items.length,
|
|
8685
|
+
pageCount: 1,
|
|
8686
|
+
hasPreviousPage: false,
|
|
8687
|
+
hasNextPage: false,
|
|
8688
|
+
isFirstPage: true,
|
|
8689
|
+
isLastPage: true,
|
|
8690
|
+
firstItemOnPage: items.length === 0 ? 0 : 1,
|
|
8691
|
+
lastItemOnPage: items.length
|
|
8692
|
+
};
|
|
8693
|
+
}
|
|
8694
|
+
function hasNextPage(current) {
|
|
8695
|
+
if (current.hasNextPage !== void 0) return current.hasNextPage;
|
|
8696
|
+
if (current.pageCount !== void 0) return current.page < current.pageCount;
|
|
8697
|
+
return current.page * current.pageSize < current.count;
|
|
8698
|
+
}
|
|
8699
|
+
var PAGE_OPTION = [
|
|
8700
|
+
"--page <n>",
|
|
8701
|
+
"Page number (reads only that page instead of all)"
|
|
8702
|
+
];
|
|
8703
|
+
var PAGE_SIZE_OPTION = [
|
|
8704
|
+
"--page-size <n>",
|
|
8705
|
+
"Page size used while paging"
|
|
8706
|
+
];
|
|
8707
|
+
var NO_AUTO_PAGE_OPTION = [
|
|
8708
|
+
"--no-auto-page",
|
|
8709
|
+
"Read only the first page instead of walking every page"
|
|
8710
|
+
];
|
|
8711
|
+
|
|
8712
|
+
// src/commands/work-plan.ts
|
|
8404
8713
|
function registerWorkPlan(program2) {
|
|
8405
8714
|
const workPlan = program2.command("work-plan").description(
|
|
8406
8715
|
"Drive a work plan: create one from a brief, then execute / accept / reject"
|
|
@@ -8594,22 +8903,27 @@ Examples:
|
|
|
8594
8903
|
globals.json
|
|
8595
8904
|
);
|
|
8596
8905
|
});
|
|
8597
|
-
workPlan.command("list").description(
|
|
8906
|
+
workPlan.command("list").description(
|
|
8907
|
+
"List work plans, newest-first (GET /decompositions). Walks every page by default."
|
|
8908
|
+
).option("--status <status>", "Filter by decomposition status").option("--brief <briefId>", "Filter by work-brief memory id").option(...PAGE_OPTION).option(...PAGE_SIZE_OPTION).option(...NO_AUTO_PAGE_OPTION).action(async (opts, cmd) => {
|
|
8598
8909
|
const globals = cmd.optsWithGlobals();
|
|
8599
8910
|
const cfg = resolveConfig(globals);
|
|
8600
|
-
const
|
|
8911
|
+
const readPage = (query) => runApi("Listing work plans", async () => {
|
|
8601
8912
|
const client = await makeClient(cfg);
|
|
8602
8913
|
return client.GET("/decompositions", {
|
|
8603
8914
|
params: {
|
|
8604
|
-
query: {
|
|
8605
|
-
status: opts.status,
|
|
8606
|
-
briefId: opts.brief,
|
|
8607
|
-
page: opts.page,
|
|
8608
|
-
pageSize: opts.pageSize
|
|
8609
|
-
}
|
|
8915
|
+
query: { status: opts.status, briefId: opts.brief, ...query }
|
|
8610
8916
|
}
|
|
8611
8917
|
});
|
|
8612
8918
|
});
|
|
8919
|
+
const data = shouldAutoPage(opts) ? await fetchAllPages(
|
|
8920
|
+
(page) => readPage({
|
|
8921
|
+
page,
|
|
8922
|
+
...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
|
|
8923
|
+
}),
|
|
8924
|
+
opts,
|
|
8925
|
+
"work plans"
|
|
8926
|
+
) : await readPage(singlePageQuery(opts));
|
|
8613
8927
|
emitAction(
|
|
8614
8928
|
`listed ${style.bold(String(data.items.length))} of ${data.count} work plan(s)`,
|
|
8615
8929
|
data,
|
|
@@ -8697,35 +9011,53 @@ Examples:
|
|
|
8697
9011
|
$ sechroom filing reject fsg_XXXX --reason "wrong workspace"
|
|
8698
9012
|
$ sechroom filing edit-and-accept fsg_XXXX --target-kind Workspace --existing-target-id wsp_XXXX`
|
|
8699
9013
|
);
|
|
8700
|
-
filing.command("suggestions").description(
|
|
9014
|
+
filing.command("suggestions").description(
|
|
9015
|
+
"List filing suggestions (GET /filing/suggestions). Walks every page by default."
|
|
9016
|
+
).option("--memory-id <memoryId>", "Filter to a single memory's suggestions").option(
|
|
8701
9017
|
"--status <status>",
|
|
8702
9018
|
"Generating | Pending | Accepted | Rejected | EditedAndAccepted | Deferred | Invalidated"
|
|
8703
|
-
).option(
|
|
9019
|
+
).option(...PAGE_OPTION).option(...PAGE_SIZE_OPTION).option(...NO_AUTO_PAGE_OPTION).action(async (opts, cmd) => {
|
|
8704
9020
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
8705
|
-
const
|
|
9021
|
+
const filters = {
|
|
9022
|
+
...opts.memoryId ? { memoryId: opts.memoryId } : {},
|
|
9023
|
+
...opts.status ? {
|
|
9024
|
+
status: opts.status
|
|
9025
|
+
} : {}
|
|
9026
|
+
};
|
|
9027
|
+
const readPage = (query) => runApi("Listing filing suggestions", async () => {
|
|
8706
9028
|
const client = await makeClient(cfg);
|
|
8707
9029
|
return client.GET("/filing/suggestions", {
|
|
8708
|
-
params: {
|
|
8709
|
-
query: {
|
|
8710
|
-
...opts.memoryId ? { memoryId: opts.memoryId } : {},
|
|
8711
|
-
...opts.status ? { status: opts.status } : {},
|
|
8712
|
-
...opts.page ? { page: Number(opts.page) } : {},
|
|
8713
|
-
...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
|
|
8714
|
-
}
|
|
8715
|
-
}
|
|
9030
|
+
params: { query: { ...filters, ...query } }
|
|
8716
9031
|
});
|
|
8717
9032
|
});
|
|
9033
|
+
const data = shouldAutoPage(opts) ? await fetchAllPages(
|
|
9034
|
+
(page) => readPage({
|
|
9035
|
+
page,
|
|
9036
|
+
...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
|
|
9037
|
+
}),
|
|
9038
|
+
opts,
|
|
9039
|
+
"filing suggestions"
|
|
9040
|
+
) : await readPage(singlePageQuery(opts));
|
|
8718
9041
|
emit(data, cmd.optsWithGlobals().json);
|
|
8719
9042
|
});
|
|
8720
|
-
filing.command("get <id>").description(
|
|
9043
|
+
filing.command("get <id>").description(
|
|
9044
|
+
"Fetch a filing suggestion by id (GET /filing/suggestions/{id})"
|
|
9045
|
+
).action(async (id, _opts, cmd) => {
|
|
8721
9046
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
8722
9047
|
const data = await runApi("Fetching filing suggestion", async () => {
|
|
8723
9048
|
const client = await makeClient(cfg);
|
|
8724
|
-
return client.GET("/filing/suggestions/{id}", {
|
|
9049
|
+
return client.GET("/filing/suggestions/{id}", {
|
|
9050
|
+
params: { path: { id } }
|
|
9051
|
+
});
|
|
8725
9052
|
});
|
|
8726
9053
|
emit(data, cmd.optsWithGlobals().json);
|
|
8727
9054
|
});
|
|
8728
|
-
filing.command("preview").description(
|
|
9055
|
+
filing.command("preview").description(
|
|
9056
|
+
"Preview a filing suggestion for a memory id or ad-hoc shape (POST /filing/suggestions/preview)"
|
|
9057
|
+
).option("--memory-id <memoryId>", "Preview filing for an existing memory").option("--text <text>", "Ad-hoc memory body text (instead of --memory-id)").option("--title <title>", "Ad-hoc memory title").option("--tag <tag...>", "Ad-hoc memory tags (repeatable)").option("--type <type>", "Ad-hoc memory type", "reference").option(
|
|
9058
|
+
"--scope-workspace <workspaceId>",
|
|
9059
|
+
"Scope the preview to a workspace"
|
|
9060
|
+
).action(async (opts, cmd) => {
|
|
8729
9061
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
8730
9062
|
const memory = opts.text ? {
|
|
8731
9063
|
text: opts.text,
|
|
@@ -8745,15 +9077,26 @@ Examples:
|
|
|
8745
9077
|
});
|
|
8746
9078
|
emit(data, cmd.optsWithGlobals().json);
|
|
8747
9079
|
});
|
|
8748
|
-
filing.command("accept <id>").description(
|
|
9080
|
+
filing.command("accept <id>").description(
|
|
9081
|
+
"Accept a filing suggestion (POST /filing/suggestions/{id}/accept)"
|
|
9082
|
+
).action(async (id, _opts, cmd) => {
|
|
8749
9083
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
8750
9084
|
const data = await runApi("Accepting filing suggestion", async () => {
|
|
8751
9085
|
const client = await makeClient(cfg);
|
|
8752
|
-
return client.POST("/filing/suggestions/{id}/accept", {
|
|
9086
|
+
return client.POST("/filing/suggestions/{id}/accept", {
|
|
9087
|
+
params: { path: { id } },
|
|
9088
|
+
body: {}
|
|
9089
|
+
});
|
|
8753
9090
|
});
|
|
8754
|
-
emitAction(
|
|
9091
|
+
emitAction(
|
|
9092
|
+
`accepted filing suggestion ${style.bold(id)}`,
|
|
9093
|
+
data,
|
|
9094
|
+
cmd.optsWithGlobals().json
|
|
9095
|
+
);
|
|
8755
9096
|
});
|
|
8756
|
-
filing.command("reject <id>").description(
|
|
9097
|
+
filing.command("reject <id>").description(
|
|
9098
|
+
"Reject a filing suggestion (POST /filing/suggestions/{id}/reject)"
|
|
9099
|
+
).option("--reason <reason>", "Why the suggestion was rejected").option("--reason-code <code>", "Structured reason code").action(async (id, opts, cmd) => {
|
|
8757
9100
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
8758
9101
|
const data = await runApi("Rejecting filing suggestion", async () => {
|
|
8759
9102
|
const client = await makeClient(cfg);
|
|
@@ -8765,9 +9108,18 @@ Examples:
|
|
|
8765
9108
|
}
|
|
8766
9109
|
});
|
|
8767
9110
|
});
|
|
8768
|
-
emitAction(
|
|
9111
|
+
emitAction(
|
|
9112
|
+
`rejected filing suggestion ${style.bold(id)}`,
|
|
9113
|
+
data,
|
|
9114
|
+
cmd.optsWithGlobals().json
|
|
9115
|
+
);
|
|
8769
9116
|
});
|
|
8770
|
-
filing.command("defer <id>").description(
|
|
9117
|
+
filing.command("defer <id>").description(
|
|
9118
|
+
"Defer a filing suggestion (POST /filing/suggestions/{id}/defer)"
|
|
9119
|
+
).option(
|
|
9120
|
+
"--until <iso>",
|
|
9121
|
+
"Defer until an ISO-8601 timestamp (defaults to indefinite)"
|
|
9122
|
+
).action(async (id, opts, cmd) => {
|
|
8771
9123
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
8772
9124
|
const data = await runApi("Deferring filing suggestion", async () => {
|
|
8773
9125
|
const client = await makeClient(cfg);
|
|
@@ -8776,25 +9128,44 @@ Examples:
|
|
|
8776
9128
|
body: { until: opts.until ?? null }
|
|
8777
9129
|
});
|
|
8778
9130
|
});
|
|
8779
|
-
emitAction(
|
|
9131
|
+
emitAction(
|
|
9132
|
+
`deferred filing suggestion ${style.bold(id)}`,
|
|
9133
|
+
data,
|
|
9134
|
+
cmd.optsWithGlobals().json
|
|
9135
|
+
);
|
|
8780
9136
|
});
|
|
8781
|
-
filing.command("edit-and-accept <id>").description(
|
|
9137
|
+
filing.command("edit-and-accept <id>").description(
|
|
9138
|
+
"Override the target then accept (POST /filing/suggestions/{id}/edit-and-accept)"
|
|
9139
|
+
).option("--target-kind <kind>", "Workspace | Project").option(
|
|
9140
|
+
"--existing-target-id <id>",
|
|
9141
|
+
"File into an existing workspace/project"
|
|
9142
|
+
).option("--new-name <name>", "Create a new target with this name").option("--new-description <text>", "Description for the new target").option(
|
|
9143
|
+
"--new-parent-workspace <workspaceId>",
|
|
9144
|
+
"Parent workspace for a new project"
|
|
9145
|
+
).option("--memory-id <memoryId...>", "Override the memory set (repeatable)").action(async (id, opts, cmd) => {
|
|
8782
9146
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
8783
|
-
const data = await runApi(
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
9147
|
+
const data = await runApi(
|
|
9148
|
+
"Editing and accepting filing suggestion",
|
|
9149
|
+
async () => {
|
|
9150
|
+
const client = await makeClient(cfg);
|
|
9151
|
+
return client.POST("/filing/suggestions/{id}/edit-and-accept", {
|
|
9152
|
+
params: { path: { id } },
|
|
9153
|
+
body: {
|
|
9154
|
+
targetKind: opts.targetKind ?? null,
|
|
9155
|
+
existingTargetId: opts.existingTargetId ?? null,
|
|
9156
|
+
newName: opts.newName ?? null,
|
|
9157
|
+
newDescription: opts.newDescription ?? null,
|
|
9158
|
+
newParentWorkspaceId: opts.newParentWorkspace ?? null,
|
|
9159
|
+
overrideMemoryIds: opts.memoryId ?? null
|
|
9160
|
+
}
|
|
9161
|
+
});
|
|
9162
|
+
}
|
|
9163
|
+
);
|
|
9164
|
+
emitAction(
|
|
9165
|
+
`edited & accepted filing suggestion ${style.bold(id)}`,
|
|
9166
|
+
data,
|
|
9167
|
+
cmd.optsWithGlobals().json
|
|
9168
|
+
);
|
|
8798
9169
|
});
|
|
8799
9170
|
}
|
|
8800
9171
|
|
|
@@ -10117,6 +10488,18 @@ function resolveCreateBody(textOpt, fileOpt) {
|
|
|
10117
10488
|
);
|
|
10118
10489
|
return { text: text2, defaultTitle };
|
|
10119
10490
|
}
|
|
10491
|
+
async function fetchAllMemoryRelationships(cfg, memoryId) {
|
|
10492
|
+
return await fetchAllPages(
|
|
10493
|
+
(page) => runApi(`Fetching relationships (page ${page})`, async () => {
|
|
10494
|
+
const client = await makeClient(cfg);
|
|
10495
|
+
return client.GET("/memories/{memoryId}/relationships", {
|
|
10496
|
+
params: { path: { memoryId }, query: { page } }
|
|
10497
|
+
});
|
|
10498
|
+
}),
|
|
10499
|
+
{},
|
|
10500
|
+
"relationships"
|
|
10501
|
+
);
|
|
10502
|
+
}
|
|
10120
10503
|
function registerMemory(program2) {
|
|
10121
10504
|
const memory = program2.command("memory").description("Create, read, and search memories");
|
|
10122
10505
|
memory.addHelpText(
|
|
@@ -10371,7 +10754,16 @@ ${plan.rows.map(
|
|
|
10371
10754
|
false
|
|
10372
10755
|
);
|
|
10373
10756
|
});
|
|
10374
|
-
memory.command("get <memoryId>").description("Fetch a memory by id (GET /memories/{memoryId})").
|
|
10757
|
+
const get = memory.command("get <memoryId>").description("Fetch a memory by id (GET /memories/{memoryId})").option(
|
|
10758
|
+
"--with-relationships",
|
|
10759
|
+
"Include the memory's relationships in the response",
|
|
10760
|
+
false
|
|
10761
|
+
).addHelpText(
|
|
10762
|
+
"after",
|
|
10763
|
+
`
|
|
10764
|
+
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.`
|
|
10765
|
+
);
|
|
10766
|
+
get.action(async (memoryId, opts, cmd) => {
|
|
10375
10767
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
10376
10768
|
const data = await runApi("Fetching memory", async () => {
|
|
10377
10769
|
const client = await makeClient(cfg);
|
|
@@ -10379,6 +10771,11 @@ ${plan.rows.map(
|
|
|
10379
10771
|
params: { path: { memoryId } }
|
|
10380
10772
|
});
|
|
10381
10773
|
});
|
|
10774
|
+
if (opts.withRelationships) {
|
|
10775
|
+
const relationships = await fetchAllMemoryRelationships(cfg, memoryId);
|
|
10776
|
+
emit({ ...data, relationships }, cmd.optsWithGlobals().json);
|
|
10777
|
+
return;
|
|
10778
|
+
}
|
|
10382
10779
|
emit(data, cmd.optsWithGlobals().json);
|
|
10383
10780
|
});
|
|
10384
10781
|
memory.command("search <query>").description(
|
|
@@ -10601,21 +10998,28 @@ ${plan.rows.map(
|
|
|
10601
10998
|
cmd.optsWithGlobals().json
|
|
10602
10999
|
);
|
|
10603
11000
|
});
|
|
10604
|
-
memory.command("list-archived").description(
|
|
11001
|
+
memory.command("list-archived").description(
|
|
11002
|
+
"List archived memories (GET /memories/archived). Walks every page by default."
|
|
11003
|
+
).option("--workspace <workspaceId>", "Scope to a workspace").option("--project <projectId>", "Scope to a project").option(...PAGE_OPTION).option(...PAGE_SIZE_OPTION).option(...NO_AUTO_PAGE_OPTION).action(async (opts, cmd) => {
|
|
10605
11004
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
10606
|
-
const
|
|
11005
|
+
const filters = {
|
|
11006
|
+
...opts.workspace ? { workspaceId: opts.workspace } : {},
|
|
11007
|
+
...opts.project ? { projectId: opts.project } : {}
|
|
11008
|
+
};
|
|
11009
|
+
const readPage = (query) => runApi("Listing archived memories", async () => {
|
|
10607
11010
|
const client = await makeClient(cfg);
|
|
10608
11011
|
return client.GET("/memories/archived", {
|
|
10609
|
-
params: {
|
|
10610
|
-
query: {
|
|
10611
|
-
...opts.workspace ? { workspaceId: opts.workspace } : {},
|
|
10612
|
-
...opts.project ? { projectId: opts.project } : {},
|
|
10613
|
-
...opts.page ? { page: Number(opts.page) } : {},
|
|
10614
|
-
...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
|
|
10615
|
-
}
|
|
10616
|
-
}
|
|
11012
|
+
params: { query: { ...filters, ...query } }
|
|
10617
11013
|
});
|
|
10618
11014
|
});
|
|
11015
|
+
const data = shouldAutoPage(opts) ? await fetchAllPages(
|
|
11016
|
+
(page) => readPage({
|
|
11017
|
+
page,
|
|
11018
|
+
...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
|
|
11019
|
+
}),
|
|
11020
|
+
opts,
|
|
11021
|
+
"archived memories"
|
|
11022
|
+
) : await readPage(singlePageQuery(opts));
|
|
10619
11023
|
emit(data, cmd.optsWithGlobals().json);
|
|
10620
11024
|
});
|
|
10621
11025
|
memory.command("versions <memoryId>").description("List a memory's versions (GET /memories/{memoryId}/versions)").action(async (memoryId, _opts, cmd) => {
|
|
@@ -10726,8 +11130,18 @@ ${plan.rows.map(
|
|
|
10726
11130
|
}
|
|
10727
11131
|
|
|
10728
11132
|
// src/setup/apply.ts
|
|
10729
|
-
import { createHash as createHash5 } from "crypto";
|
|
10730
|
-
import {
|
|
11133
|
+
import { createHash as createHash5, randomUUID as randomUUID3 } from "crypto";
|
|
11134
|
+
import {
|
|
11135
|
+
chmodSync as chmodSync2,
|
|
11136
|
+
copyFileSync,
|
|
11137
|
+
existsSync as existsSync13,
|
|
11138
|
+
mkdirSync as mkdirSync16,
|
|
11139
|
+
readFileSync as readFileSync16,
|
|
11140
|
+
renameSync as renameSync4,
|
|
11141
|
+
rmSync as rmSync7,
|
|
11142
|
+
statSync as statSync5,
|
|
11143
|
+
writeFileSync as writeFileSync15
|
|
11144
|
+
} from "fs";
|
|
10731
11145
|
import { dirname as dirname15 } from "path";
|
|
10732
11146
|
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
10733
11147
|
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
@@ -10773,11 +11187,17 @@ function parseManagedBlock(content, block) {
|
|
|
10773
11187
|
const keyed = content.match(keyedBlockRe(block));
|
|
10774
11188
|
if (keyed) {
|
|
10775
11189
|
const attrs = parseAttrs(keyed[0].slice(0, keyed[0].indexOf("\n")));
|
|
10776
|
-
return {
|
|
11190
|
+
return {
|
|
11191
|
+
block,
|
|
11192
|
+
source: attrs.source ?? null,
|
|
11193
|
+
sha256: attrs.sha256 ?? null,
|
|
11194
|
+
body: innerBody(keyed[0])
|
|
11195
|
+
};
|
|
10777
11196
|
}
|
|
10778
11197
|
if (block === "role-template") {
|
|
10779
11198
|
const legacy = content.match(legacyBlockRe());
|
|
10780
|
-
if (legacy)
|
|
11199
|
+
if (legacy)
|
|
11200
|
+
return { block, source: null, sha256: null, body: innerBody(legacy[0]) };
|
|
10781
11201
|
}
|
|
10782
11202
|
return null;
|
|
10783
11203
|
}
|
|
@@ -10799,56 +11219,469 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
10799
11219
|
try {
|
|
10800
11220
|
current = JSON.parse(readFileSync16(path, "utf8"));
|
|
10801
11221
|
} catch {
|
|
10802
|
-
return {
|
|
11222
|
+
return {
|
|
11223
|
+
kind: "mcp",
|
|
11224
|
+
path,
|
|
11225
|
+
status: "skipped",
|
|
11226
|
+
note: "existing file isn't valid JSON \u2014 left untouched"
|
|
11227
|
+
};
|
|
10803
11228
|
}
|
|
10804
11229
|
}
|
|
10805
|
-
current.mcpServers = {
|
|
11230
|
+
current.mcpServers = {
|
|
11231
|
+
...current.mcpServers ?? {},
|
|
11232
|
+
...incoming.mcpServers ?? {}
|
|
11233
|
+
};
|
|
10806
11234
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
10807
11235
|
ensureDir2(path);
|
|
10808
11236
|
writeFileSync15(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
|
|
10809
11237
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
10810
11238
|
}
|
|
10811
|
-
|
|
10812
|
-
|
|
10813
|
-
let
|
|
10814
|
-
|
|
10815
|
-
|
|
10816
|
-
|
|
10817
|
-
|
|
10818
|
-
|
|
10819
|
-
|
|
10820
|
-
|
|
10821
|
-
|
|
10822
|
-
|
|
10823
|
-
|
|
10824
|
-
|
|
10825
|
-
|
|
10826
|
-
|
|
10827
|
-
|
|
10828
|
-
|
|
11239
|
+
var CODEX_MCP_TABLE = "mcp_servers.sechroom";
|
|
11240
|
+
function decodeTomlBasicString(value) {
|
|
11241
|
+
let decoded = "";
|
|
11242
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
11243
|
+
const char = value[i];
|
|
11244
|
+
if (char !== "\\") {
|
|
11245
|
+
decoded += char;
|
|
11246
|
+
continue;
|
|
11247
|
+
}
|
|
11248
|
+
const escaped = value[++i];
|
|
11249
|
+
if (escaped === void 0) throw new Error("unterminated TOML escape");
|
|
11250
|
+
const simple = {
|
|
11251
|
+
b: "\b",
|
|
11252
|
+
f: "\f",
|
|
11253
|
+
n: "\n",
|
|
11254
|
+
r: "\r",
|
|
11255
|
+
t: " ",
|
|
11256
|
+
'"': '"',
|
|
11257
|
+
"\\": "\\"
|
|
11258
|
+
};
|
|
11259
|
+
if (escaped in simple) {
|
|
11260
|
+
decoded += simple[escaped];
|
|
11261
|
+
continue;
|
|
11262
|
+
}
|
|
11263
|
+
const digits = escaped === "u" ? 4 : escaped === "U" ? 8 : 0;
|
|
11264
|
+
if (digits === 0) throw new Error(`invalid TOML escape '\\\\${escaped}'`);
|
|
11265
|
+
const code = value.slice(i + 1, i + 1 + digits);
|
|
11266
|
+
if (!new RegExp(`^[0-9a-fA-F]{${digits}}$`).test(code))
|
|
11267
|
+
throw new Error(`invalid TOML Unicode escape '\\\\${escaped}${code}'`);
|
|
11268
|
+
decoded += String.fromCodePoint(Number.parseInt(code, 16));
|
|
11269
|
+
i += digits;
|
|
11270
|
+
}
|
|
11271
|
+
return decoded;
|
|
10829
11272
|
}
|
|
10830
|
-
function
|
|
10831
|
-
const
|
|
10832
|
-
|
|
10833
|
-
|
|
10834
|
-
|
|
10835
|
-
|
|
11273
|
+
function normalizeTomlKey(raw) {
|
|
11274
|
+
const segments = [];
|
|
11275
|
+
let i = 0;
|
|
11276
|
+
while (i < raw.length) {
|
|
11277
|
+
while (/\s/.test(raw[i] ?? "")) i += 1;
|
|
11278
|
+
if (i >= raw.length) break;
|
|
11279
|
+
const quote = raw[i];
|
|
11280
|
+
let segment;
|
|
11281
|
+
if (quote === '"' || quote === "'") {
|
|
11282
|
+
i += 1;
|
|
11283
|
+
const start = i;
|
|
11284
|
+
let escaped = false;
|
|
11285
|
+
while (i < raw.length) {
|
|
11286
|
+
const char = raw[i];
|
|
11287
|
+
if (quote === '"' && escaped) {
|
|
11288
|
+
escaped = false;
|
|
11289
|
+
i += 1;
|
|
11290
|
+
continue;
|
|
11291
|
+
}
|
|
11292
|
+
if (quote === '"' && char === "\\") {
|
|
11293
|
+
escaped = true;
|
|
11294
|
+
i += 1;
|
|
11295
|
+
continue;
|
|
11296
|
+
}
|
|
11297
|
+
if (char === quote) break;
|
|
11298
|
+
i += 1;
|
|
11299
|
+
}
|
|
11300
|
+
if (raw[i] !== quote) throw new Error(`unterminated TOML key '${raw}'`);
|
|
11301
|
+
const encoded = raw.slice(start, i);
|
|
11302
|
+
segment = quote === '"' ? decodeTomlBasicString(encoded) : encoded;
|
|
11303
|
+
i += 1;
|
|
11304
|
+
} else {
|
|
11305
|
+
const start = i;
|
|
11306
|
+
while (i < raw.length && /[A-Za-z0-9_-]/.test(raw[i])) i += 1;
|
|
11307
|
+
segment = raw.slice(start, i);
|
|
11308
|
+
if (!segment) throw new Error(`invalid TOML key '${raw.trim()}'`);
|
|
11309
|
+
}
|
|
11310
|
+
segments.push(segment);
|
|
11311
|
+
while (/\s/.test(raw[i] ?? "")) i += 1;
|
|
11312
|
+
if (i >= raw.length) break;
|
|
11313
|
+
if (raw[i] !== ".") throw new Error(`invalid TOML key '${raw.trim()}'`);
|
|
11314
|
+
i += 1;
|
|
11315
|
+
}
|
|
11316
|
+
if (segments.length === 0) throw new Error("empty TOML key");
|
|
11317
|
+
return segments;
|
|
11318
|
+
}
|
|
11319
|
+
function sameTomlKey(left, right) {
|
|
11320
|
+
return left.length === right.length && left.every((segment, index) => segment === right[index]);
|
|
11321
|
+
}
|
|
11322
|
+
function isTomlKeyPrefix(prefix, candidate) {
|
|
11323
|
+
return prefix.length < candidate.length && prefix.every((segment, index) => segment === candidate[index]);
|
|
11324
|
+
}
|
|
11325
|
+
function tomlKeyLabel(key) {
|
|
11326
|
+
return key.join(".");
|
|
11327
|
+
}
|
|
11328
|
+
function stripTomlComment(line) {
|
|
11329
|
+
let quote = null;
|
|
11330
|
+
let escaped = false;
|
|
11331
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
11332
|
+
const rest = line.slice(i);
|
|
11333
|
+
const char = line[i];
|
|
11334
|
+
if (quote === "multiline-basic") {
|
|
11335
|
+
if (rest.startsWith('"""') && !escaped) {
|
|
11336
|
+
escaped = false;
|
|
11337
|
+
quote = null;
|
|
11338
|
+
i += 2;
|
|
11339
|
+
continue;
|
|
11340
|
+
}
|
|
11341
|
+
if (escaped) escaped = false;
|
|
11342
|
+
else if (char === "\\") escaped = true;
|
|
11343
|
+
continue;
|
|
11344
|
+
}
|
|
11345
|
+
if (quote === "multiline-literal") {
|
|
11346
|
+
if (rest.startsWith("'''")) {
|
|
11347
|
+
i += 2;
|
|
11348
|
+
quote = null;
|
|
11349
|
+
}
|
|
11350
|
+
continue;
|
|
11351
|
+
}
|
|
11352
|
+
if (quote === "basic") {
|
|
11353
|
+
if (escaped) escaped = false;
|
|
11354
|
+
else if (char === "\\") escaped = true;
|
|
11355
|
+
else if (char === '"') quote = null;
|
|
11356
|
+
continue;
|
|
11357
|
+
}
|
|
11358
|
+
if (quote === "literal") {
|
|
11359
|
+
if (char === "'") quote = null;
|
|
11360
|
+
continue;
|
|
11361
|
+
}
|
|
11362
|
+
if (rest.startsWith('"""')) {
|
|
11363
|
+
quote = "multiline-basic";
|
|
11364
|
+
i += 2;
|
|
11365
|
+
} else if (rest.startsWith("'''")) {
|
|
11366
|
+
quote = "multiline-literal";
|
|
11367
|
+
i += 2;
|
|
11368
|
+
} else if (char === '"') quote = "basic";
|
|
11369
|
+
else if (char === "'") quote = "literal";
|
|
11370
|
+
else if (char === "#") return line.slice(0, i);
|
|
11371
|
+
}
|
|
11372
|
+
return line;
|
|
11373
|
+
}
|
|
11374
|
+
function scanTomlValue(value, state) {
|
|
11375
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
11376
|
+
const rest = value.slice(i);
|
|
11377
|
+
const char = value[i];
|
|
11378
|
+
if (state.quote === "multiline-basic") {
|
|
11379
|
+
if (rest.startsWith('"""') && !state.escaped) {
|
|
11380
|
+
state.quote = null;
|
|
11381
|
+
i += 2;
|
|
11382
|
+
continue;
|
|
11383
|
+
}
|
|
11384
|
+
if (state.escaped) state.escaped = false;
|
|
11385
|
+
else if (char === "\\") state.escaped = true;
|
|
11386
|
+
continue;
|
|
11387
|
+
}
|
|
11388
|
+
if (state.quote === "multiline-literal") {
|
|
11389
|
+
if (rest.startsWith("'''")) {
|
|
11390
|
+
state.quote = null;
|
|
11391
|
+
i += 2;
|
|
11392
|
+
}
|
|
11393
|
+
continue;
|
|
11394
|
+
}
|
|
11395
|
+
if (state.quote === "basic") {
|
|
11396
|
+
if (state.escaped) state.escaped = false;
|
|
11397
|
+
else if (char === "\\") state.escaped = true;
|
|
11398
|
+
else if (char === '"') state.quote = null;
|
|
11399
|
+
continue;
|
|
11400
|
+
}
|
|
11401
|
+
if (state.quote === "literal") {
|
|
11402
|
+
if (char === "'") state.quote = null;
|
|
11403
|
+
continue;
|
|
11404
|
+
}
|
|
11405
|
+
if (char === "#") break;
|
|
11406
|
+
if (rest.startsWith('"""')) {
|
|
11407
|
+
state.quote = "multiline-basic";
|
|
11408
|
+
i += 2;
|
|
11409
|
+
} else if (rest.startsWith("'''")) {
|
|
11410
|
+
state.quote = "multiline-literal";
|
|
11411
|
+
i += 2;
|
|
11412
|
+
} else if (char === '"') state.quote = "basic";
|
|
11413
|
+
else if (char === "'") state.quote = "literal";
|
|
11414
|
+
else if (char === "[" || char === "{") state.containers.push(char);
|
|
11415
|
+
else if (char === "]" || char === "}") {
|
|
11416
|
+
const expected = char === "]" ? "[" : "{";
|
|
11417
|
+
if (state.containers.pop() !== expected)
|
|
11418
|
+
throw new Error(`unbalanced TOML delimiter '${char}'`);
|
|
11419
|
+
}
|
|
11420
|
+
}
|
|
11421
|
+
}
|
|
11422
|
+
function parseTomlHeader(rawLine) {
|
|
11423
|
+
const line = stripTomlComment(rawLine).trim();
|
|
11424
|
+
const array = line.startsWith("[[") && line.endsWith("]]");
|
|
11425
|
+
const table = !array && line.startsWith("[") && line.endsWith("]");
|
|
11426
|
+
if (!array && !table) return null;
|
|
11427
|
+
const prefix = array ? 2 : 1;
|
|
11428
|
+
const suffix = array ? 2 : 1;
|
|
11429
|
+
const rawName = line.slice(prefix, line.length - suffix).trim();
|
|
11430
|
+
if (!rawName) throw new Error("empty TOML table name");
|
|
11431
|
+
return { name: normalizeTomlKey(rawName), array };
|
|
11432
|
+
}
|
|
11433
|
+
function findTomlEquals(line) {
|
|
11434
|
+
let quote = null;
|
|
11435
|
+
let escaped = false;
|
|
11436
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
11437
|
+
const char = line[i];
|
|
11438
|
+
if (quote === "basic") {
|
|
11439
|
+
if (escaped) escaped = false;
|
|
11440
|
+
else if (char === "\\") escaped = true;
|
|
11441
|
+
else if (char === '"') quote = null;
|
|
11442
|
+
continue;
|
|
11443
|
+
}
|
|
11444
|
+
if (quote === "literal") {
|
|
11445
|
+
if (char === "'") quote = null;
|
|
11446
|
+
continue;
|
|
11447
|
+
}
|
|
11448
|
+
if (char === '"') quote = "basic";
|
|
11449
|
+
else if (char === "'") quote = "literal";
|
|
11450
|
+
else if (char === "=") return i;
|
|
10836
11451
|
}
|
|
10837
|
-
return
|
|
10838
|
-
|
|
10839
|
-
${rendered}` : rendered;
|
|
11452
|
+
return -1;
|
|
10840
11453
|
}
|
|
10841
|
-
function
|
|
10842
|
-
|
|
10843
|
-
|
|
10844
|
-
const
|
|
10845
|
-
if (
|
|
10846
|
-
|
|
10847
|
-
|
|
11454
|
+
function arrayTableContext(name, arrays) {
|
|
11455
|
+
let best = null;
|
|
11456
|
+
for (let i = arrays.length - 1; i >= 0; i -= 1) {
|
|
11457
|
+
const candidate = arrays[i];
|
|
11458
|
+
if (isTomlKeyPrefix(candidate.key, name)) {
|
|
11459
|
+
if (!best || candidate.key.length > best.key.length) best = candidate;
|
|
11460
|
+
}
|
|
10848
11461
|
}
|
|
10849
|
-
|
|
10850
|
-
|
|
10851
|
-
|
|
11462
|
+
return best;
|
|
11463
|
+
}
|
|
11464
|
+
function tomlTableIdentity(key, context) {
|
|
11465
|
+
return `${context?.identity ?? "root"}:${JSON.stringify(key)}`;
|
|
11466
|
+
}
|
|
11467
|
+
function isTomlScalar(value) {
|
|
11468
|
+
const decimal = "[+-]?(?:0|[1-9](?:_?[0-9])*)(?:\\.[0-9](?:_?[0-9])*)?(?:[eE][+-]?[0-9](?:_?[0-9])*)?";
|
|
11469
|
+
const number = new RegExp(
|
|
11470
|
+
`^(?:${decimal}|[+-]?0x[0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[+-]?0o[0-7](?:_?[0-7])*|[+-]?0b[01](?:_?[01])*|[+-]?(?:inf|nan))$`
|
|
11471
|
+
);
|
|
11472
|
+
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})?)?)$/;
|
|
11473
|
+
return number.test(value) || datetime.test(value) || /^(?:true|false)$/.test(value);
|
|
11474
|
+
}
|
|
11475
|
+
function validateTomlValueShape(value) {
|
|
11476
|
+
const token = stripTomlComment(value).trim();
|
|
11477
|
+
if (!token) throw new Error("empty TOML value");
|
|
11478
|
+
if (token.startsWith('"') || token.startsWith("'")) {
|
|
11479
|
+
const quote = token[0];
|
|
11480
|
+
if (quote && token.endsWith(quote)) return;
|
|
11481
|
+
}
|
|
11482
|
+
if (token.startsWith("[") && token.endsWith("]")) return;
|
|
11483
|
+
if (token.startsWith("{") && token.endsWith("}")) return;
|
|
11484
|
+
if (isTomlScalar(token)) return;
|
|
11485
|
+
throw new Error(`invalid TOML value '${token}'`);
|
|
11486
|
+
}
|
|
11487
|
+
function validateToml(content) {
|
|
11488
|
+
const tables = /* @__PURE__ */ new Set();
|
|
11489
|
+
const keys = /* @__PURE__ */ new Set();
|
|
11490
|
+
const arrayInstances = [];
|
|
11491
|
+
let currentTable = { identity: "root" };
|
|
11492
|
+
let valueState = {
|
|
11493
|
+
containers: [],
|
|
11494
|
+
quote: null,
|
|
11495
|
+
escaped: false
|
|
11496
|
+
};
|
|
11497
|
+
for (const rawLine of content.replace(/\r\n/g, "\n").split("\n")) {
|
|
11498
|
+
if (valueState.quote || valueState.containers.length > 0) {
|
|
11499
|
+
scanTomlValue(rawLine, valueState);
|
|
11500
|
+
continue;
|
|
11501
|
+
}
|
|
11502
|
+
const line = stripTomlComment(rawLine).trim();
|
|
11503
|
+
if (!line) continue;
|
|
11504
|
+
const header = parseTomlHeader(rawLine);
|
|
11505
|
+
if (header) {
|
|
11506
|
+
if (header.array) {
|
|
11507
|
+
const context = arrayTableContext(header.name, arrayInstances);
|
|
11508
|
+
const parentIdentity = context?.identity ?? null;
|
|
11509
|
+
const previous = [...arrayInstances].reverse().find(
|
|
11510
|
+
(candidate) => sameTomlKey(candidate.key, header.name) && candidate.parentIdentity === parentIdentity
|
|
11511
|
+
);
|
|
11512
|
+
const count = (previous?.count ?? 0) + 1;
|
|
11513
|
+
const instance = {
|
|
11514
|
+
key: header.name,
|
|
11515
|
+
parentIdentity,
|
|
11516
|
+
identity: `${parentIdentity ?? "root"}:${JSON.stringify(header.name)}#${count}`,
|
|
11517
|
+
count
|
|
11518
|
+
};
|
|
11519
|
+
arrayInstances.push(instance);
|
|
11520
|
+
currentTable = { identity: instance.identity };
|
|
11521
|
+
} else {
|
|
11522
|
+
const context = arrayTableContext(header.name, arrayInstances);
|
|
11523
|
+
currentTable = {
|
|
11524
|
+
identity: tomlTableIdentity(header.name, context)
|
|
11525
|
+
};
|
|
11526
|
+
if (tables.has(currentTable.identity))
|
|
11527
|
+
throw new Error(
|
|
11528
|
+
`duplicate TOML table '${tomlKeyLabel(header.name)}'`
|
|
11529
|
+
);
|
|
11530
|
+
tables.add(currentTable.identity);
|
|
11531
|
+
}
|
|
11532
|
+
continue;
|
|
11533
|
+
}
|
|
11534
|
+
const equals = findTomlEquals(line);
|
|
11535
|
+
if (equals <= 0) throw new Error(`invalid TOML line '${rawLine}'`);
|
|
11536
|
+
const key = normalizeTomlKey(line.slice(0, equals).trim());
|
|
11537
|
+
const value = line.slice(equals + 1).trim();
|
|
11538
|
+
if (!value || value.startsWith("#"))
|
|
11539
|
+
throw new Error(`empty TOML value for '${tomlKeyLabel(key)}'`);
|
|
11540
|
+
const fullKey = `${currentTable.identity}\0${JSON.stringify(key)}`;
|
|
11541
|
+
if (keys.has(fullKey))
|
|
11542
|
+
throw new Error(`duplicate TOML key '${tomlKeyLabel(key)}'`);
|
|
11543
|
+
keys.add(fullKey);
|
|
11544
|
+
scanTomlValue(line.slice(equals + 1), valueState);
|
|
11545
|
+
if (valueState.quote === "basic" || valueState.quote === "literal")
|
|
11546
|
+
throw new Error("unterminated TOML string");
|
|
11547
|
+
if (!valueState.quote && valueState.containers.length === 0)
|
|
11548
|
+
validateTomlValueShape(value);
|
|
11549
|
+
}
|
|
11550
|
+
if (valueState.quote) throw new Error("unterminated TOML string");
|
|
11551
|
+
if (valueState.containers.length > 0)
|
|
11552
|
+
throw new Error("unbalanced TOML value");
|
|
11553
|
+
}
|
|
11554
|
+
function isTomlTableHeader(line) {
|
|
11555
|
+
try {
|
|
11556
|
+
return parseTomlHeader(line) !== null;
|
|
11557
|
+
} catch {
|
|
11558
|
+
return false;
|
|
11559
|
+
}
|
|
11560
|
+
}
|
|
11561
|
+
function replaceTomlTable(content, tableName, replacement) {
|
|
11562
|
+
const target = normalizeTomlKey(tableName);
|
|
11563
|
+
const lines = content.replace(/\r\n/g, "\n").split("\n");
|
|
11564
|
+
const replacementLines = replacement.trim().split("\n");
|
|
11565
|
+
const kept = [];
|
|
11566
|
+
let i = 0;
|
|
11567
|
+
while (i < lines.length) {
|
|
11568
|
+
const header = parseTomlHeader(lines[i]);
|
|
11569
|
+
if (header && (sameTomlKey(header.name, target) || isTomlKeyPrefix(target, header.name))) {
|
|
11570
|
+
i += 1;
|
|
11571
|
+
while (i < lines.length && !isTomlTableHeader(lines[i])) i += 1;
|
|
11572
|
+
continue;
|
|
11573
|
+
}
|
|
11574
|
+
kept.push(lines[i]);
|
|
11575
|
+
i += 1;
|
|
11576
|
+
}
|
|
11577
|
+
const base = kept.join("\n").trim();
|
|
11578
|
+
return `${base.length > 0 ? `${base}
|
|
11579
|
+
|
|
11580
|
+
` : ""}${replacementLines.join("\n")}
|
|
11581
|
+
`;
|
|
11582
|
+
}
|
|
11583
|
+
var defaultTomlFileOperations = {
|
|
11584
|
+
copyFileSync,
|
|
11585
|
+
renameSync: renameSync4
|
|
11586
|
+
};
|
|
11587
|
+
function writeTomlAtomic(path, content, fileOperations = {}) {
|
|
11588
|
+
ensureDir2(path);
|
|
11589
|
+
const operations = { ...defaultTomlFileOperations, ...fileOperations };
|
|
11590
|
+
const temporary = `${path}.sechroom-${process.pid}-${randomUUID3()}.tmp`;
|
|
11591
|
+
const backup = `${path}.bak`;
|
|
11592
|
+
const backupTemporary = `${backup}.sechroom-${process.pid}-${randomUUID3()}.tmp`;
|
|
11593
|
+
const mode = existsSync13(path) ? statSync5(path).mode & 4095 : 384;
|
|
11594
|
+
try {
|
|
11595
|
+
writeFileSync15(temporary, content, { mode });
|
|
11596
|
+
chmodSync2(temporary, mode);
|
|
11597
|
+
validateToml(readFileSync16(temporary, "utf8"));
|
|
11598
|
+
if (existsSync13(path) && !existsSync13(backup)) {
|
|
11599
|
+
operations.copyFileSync(path, backupTemporary);
|
|
11600
|
+
chmodSync2(backupTemporary, mode);
|
|
11601
|
+
validateToml(readFileSync16(backupTemporary, "utf8"));
|
|
11602
|
+
operations.renameSync(backupTemporary, backup);
|
|
11603
|
+
}
|
|
11604
|
+
operations.renameSync(temporary, path);
|
|
11605
|
+
} finally {
|
|
11606
|
+
rmSync7(temporary, { force: true });
|
|
11607
|
+
rmSync7(backupTemporary, { force: true });
|
|
11608
|
+
}
|
|
11609
|
+
}
|
|
11610
|
+
function mergeCodexToml(path, snippet, dryRun, fileOperations = {}) {
|
|
11611
|
+
const existed = existsSync13(path);
|
|
11612
|
+
const body = readOr(path, "");
|
|
11613
|
+
try {
|
|
11614
|
+
validateToml(body);
|
|
11615
|
+
} catch (error) {
|
|
11616
|
+
return {
|
|
11617
|
+
kind: "mcp",
|
|
11618
|
+
path,
|
|
11619
|
+
status: "skipped",
|
|
11620
|
+
note: `existing TOML is invalid \u2014 left untouched (${error.message})`
|
|
11621
|
+
};
|
|
11622
|
+
}
|
|
11623
|
+
const next = replaceTomlTable(body, CODEX_MCP_TABLE, snippet);
|
|
11624
|
+
try {
|
|
11625
|
+
validateToml(next);
|
|
11626
|
+
} catch (error) {
|
|
11627
|
+
return {
|
|
11628
|
+
kind: "mcp",
|
|
11629
|
+
path,
|
|
11630
|
+
status: "skipped",
|
|
11631
|
+
note: `generated TOML is invalid \u2014 left untouched (${error.message})`
|
|
11632
|
+
};
|
|
11633
|
+
}
|
|
11634
|
+
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
11635
|
+
if (next === body)
|
|
11636
|
+
return { kind: "mcp", path, status: existed ? "current" : "created" };
|
|
11637
|
+
try {
|
|
11638
|
+
writeTomlAtomic(path, next, fileOperations);
|
|
11639
|
+
} catch (error) {
|
|
11640
|
+
return {
|
|
11641
|
+
kind: "mcp",
|
|
11642
|
+
path,
|
|
11643
|
+
status: "skipped",
|
|
11644
|
+
note: `could not replace TOML \u2014 left untouched (${error.message})`
|
|
11645
|
+
};
|
|
11646
|
+
}
|
|
11647
|
+
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
11648
|
+
}
|
|
11649
|
+
function writeInstructionBlock(path, write, dryRun) {
|
|
11650
|
+
const existed = existsSync13(path);
|
|
11651
|
+
const next = computeBlockFile(readOr(path, ""), write);
|
|
11652
|
+
if (dryRun)
|
|
11653
|
+
return { kind: "instruction", path, status: "dry-run", block: write.block };
|
|
11654
|
+
ensureDir2(path);
|
|
11655
|
+
writeFileSync15(path, next);
|
|
11656
|
+
return {
|
|
11657
|
+
kind: "instruction",
|
|
11658
|
+
path,
|
|
11659
|
+
status: existed ? "merged" : "created",
|
|
11660
|
+
block: write.block
|
|
11661
|
+
};
|
|
11662
|
+
}
|
|
11663
|
+
function computeBlockFile(current, write) {
|
|
11664
|
+
const rendered = renderBlock(write);
|
|
11665
|
+
const keyed = keyedBlockRe(write.block);
|
|
11666
|
+
if (keyed.test(current)) return current.replace(keyed, rendered);
|
|
11667
|
+
if (write.block === "role-template" && legacyBlockRe().test(current)) {
|
|
11668
|
+
return current.replace(legacyBlockRe(), rendered);
|
|
11669
|
+
}
|
|
11670
|
+
return current.trim().length > 0 ? `${current.trimEnd()}
|
|
11671
|
+
|
|
11672
|
+
${rendered}` : rendered;
|
|
11673
|
+
}
|
|
11674
|
+
function evaluateBlock(content, block, serverBody) {
|
|
11675
|
+
const onDisk = parseManagedBlock(content, block);
|
|
11676
|
+
if (!onDisk) {
|
|
11677
|
+
const hasManagedMarker = content.includes(MARKER_BEGIN) || content.includes(MARKER_END);
|
|
11678
|
+
if (!hasManagedMarker && content.includes(BOOTSTRAP_STUB_REFUSAL))
|
|
11679
|
+
return "stub";
|
|
11680
|
+
return "absent";
|
|
11681
|
+
}
|
|
11682
|
+
const actual = bodySha256(onDisk.body);
|
|
11683
|
+
if (onDisk.sha256 && actual !== onDisk.sha256) return "drift";
|
|
11684
|
+
return actual === bodySha256(serverBody) ? "current" : "stale";
|
|
10852
11685
|
}
|
|
10853
11686
|
function applyBlock(path, write, mode, dryRun) {
|
|
10854
11687
|
const current = readOr(path, "");
|
|
@@ -10874,7 +11707,13 @@ function applyBlock(path, write, mode, dryRun) {
|
|
|
10874
11707
|
};
|
|
10875
11708
|
}
|
|
10876
11709
|
if (state === "current") {
|
|
10877
|
-
return {
|
|
11710
|
+
return {
|
|
11711
|
+
kind: "instruction",
|
|
11712
|
+
path,
|
|
11713
|
+
status: "current",
|
|
11714
|
+
eval: "current",
|
|
11715
|
+
block: write.block
|
|
11716
|
+
};
|
|
10878
11717
|
}
|
|
10879
11718
|
if (state === "drift" && mode !== "force") {
|
|
10880
11719
|
const proposedPath = `${path}.proposed`;
|
|
@@ -10909,7 +11748,12 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
10909
11748
|
const section = findSection(surface, target.mcp.sectionType);
|
|
10910
11749
|
const snippet = sectionSnippet(section);
|
|
10911
11750
|
if (!snippet) {
|
|
10912
|
-
actions.push({
|
|
11751
|
+
actions.push({
|
|
11752
|
+
kind: "mcp",
|
|
11753
|
+
path: target.mcp.path,
|
|
11754
|
+
status: "skipped",
|
|
11755
|
+
note: `no ${target.mcp.sectionType} section on surface '${target.mcp.surfaceKey}'`
|
|
11756
|
+
});
|
|
10913
11757
|
} else {
|
|
10914
11758
|
actions.push(
|
|
10915
11759
|
target.mcp.format === "toml" ? mergeCodexToml(target.mcp.path, snippet, dryRun) : mergeMcpJson(target.mcp.path, snippet, dryRun)
|
|
@@ -10920,25 +11764,52 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
10920
11764
|
const surface = findSurface(setup, target.instruction.surfaceKey);
|
|
10921
11765
|
const section = findSection(surface, SectionType.InstructionFile);
|
|
10922
11766
|
if (!section) {
|
|
10923
|
-
actions.push({
|
|
11767
|
+
actions.push({
|
|
11768
|
+
kind: "instruction",
|
|
11769
|
+
path: target.instruction.path,
|
|
11770
|
+
status: "skipped",
|
|
11771
|
+
note: `no instruction-file section on surface '${target.instruction.surfaceKey}'`
|
|
11772
|
+
});
|
|
10924
11773
|
} else {
|
|
10925
11774
|
const resolved = await withSpinner(
|
|
10926
11775
|
`Resolving ${target.label} agent instructions`,
|
|
10927
11776
|
() => resolveInstruction(cfg, section, opts.personalWorkspaceId)
|
|
10928
11777
|
);
|
|
10929
11778
|
if (!resolved) {
|
|
10930
|
-
actions.push({
|
|
11779
|
+
actions.push({
|
|
11780
|
+
kind: "instruction",
|
|
11781
|
+
path: target.instruction.path,
|
|
11782
|
+
status: "skipped",
|
|
11783
|
+
// Name the block even when nothing resolved. Without it the skip reports a
|
|
11784
|
+
// path and no identity, so a consumer cannot tell WHICH managed block went
|
|
11785
|
+
// missing — absent rather than loud, which is the failure this guards.
|
|
11786
|
+
block: "role-template",
|
|
11787
|
+
// GUARD — an unresolved template must NAME what it looked for. A bare
|
|
11788
|
+
// "not found" reads identically whether the bundle is absent or the memo
|
|
11789
|
+
// was retagged out of every candidate family, and the second case is a
|
|
11790
|
+
// silent loss of a template the installation previously had.
|
|
11791
|
+
note: "no role template resolved \u2014 tried " + attemptedTagSets(section).map((set) => `[${set.join(", ")}]`).join(" then ") + " \u2014 install the SEM Starter bundle, or check the template's target tag, then re-run `sechroom setup agent-files`"
|
|
11792
|
+
});
|
|
10931
11793
|
} else {
|
|
10932
11794
|
const action = applyBlock(
|
|
10933
11795
|
target.instruction.path,
|
|
10934
|
-
{
|
|
11796
|
+
{
|
|
11797
|
+
block: "role-template",
|
|
11798
|
+
body: resolved.body,
|
|
11799
|
+
source: resolved.sourceRef
|
|
11800
|
+
},
|
|
10935
11801
|
mode,
|
|
10936
11802
|
opts.dryRun
|
|
10937
11803
|
);
|
|
10938
|
-
actions.push(
|
|
11804
|
+
actions.push(
|
|
11805
|
+
resolved.source === "override" && action.status !== "current" ? { ...action, note: action.note ?? "your personal copy" } : action
|
|
11806
|
+
);
|
|
10939
11807
|
}
|
|
10940
11808
|
}
|
|
10941
|
-
const conventionsSection = findSection(
|
|
11809
|
+
const conventionsSection = findSection(
|
|
11810
|
+
surface,
|
|
11811
|
+
SectionType.WorkspaceConventions
|
|
11812
|
+
);
|
|
10942
11813
|
if (conventionsSection) {
|
|
10943
11814
|
const conventions = await withSpinner(
|
|
10944
11815
|
`Composing ${target.label} workspace conventions`,
|
|
@@ -10947,11 +11818,20 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
10947
11818
|
if (conventions) {
|
|
10948
11819
|
const action = applyBlock(
|
|
10949
11820
|
target.instruction.path,
|
|
10950
|
-
{
|
|
11821
|
+
{
|
|
11822
|
+
block: "workspace-conventions",
|
|
11823
|
+
body: conventions.body,
|
|
11824
|
+
source: `workspace:${cfg.workspaceId ?? ""}`
|
|
11825
|
+
},
|
|
10951
11826
|
mode,
|
|
10952
11827
|
opts.dryRun
|
|
10953
11828
|
);
|
|
10954
|
-
actions.push(
|
|
11829
|
+
actions.push(
|
|
11830
|
+
action.status === "current" ? action : {
|
|
11831
|
+
...action,
|
|
11832
|
+
note: action.note ?? `workspace conventions (${conventions.refs.length})`
|
|
11833
|
+
}
|
|
11834
|
+
);
|
|
10955
11835
|
}
|
|
10956
11836
|
}
|
|
10957
11837
|
}
|
|
@@ -11169,7 +12049,12 @@ function buildConventionDraft(title, rawKind, rawBody) {
|
|
|
11169
12049
|
|
|
11170
12050
|
${body}
|
|
11171
12051
|
`,
|
|
11172
|
-
tags: [
|
|
12052
|
+
tags: [
|
|
12053
|
+
"agent-setup-bundle",
|
|
12054
|
+
"scope:sechroom",
|
|
12055
|
+
`kind:${kind}`,
|
|
12056
|
+
"archetype:document"
|
|
12057
|
+
]
|
|
11173
12058
|
};
|
|
11174
12059
|
}
|
|
11175
12060
|
function copyChoice(opts) {
|
|
@@ -11182,9 +12067,16 @@ async function maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId,
|
|
|
11182
12067
|
const instr = targets[key]?.instruction;
|
|
11183
12068
|
if (!instr || seen.has(instr.surfaceKey)) continue;
|
|
11184
12069
|
seen.add(instr.surfaceKey);
|
|
11185
|
-
const section = findSection(
|
|
12070
|
+
const section = findSection(
|
|
12071
|
+
findSurface(setup, instr.surfaceKey),
|
|
12072
|
+
SectionType.InstructionFile
|
|
12073
|
+
);
|
|
11186
12074
|
if (!section) continue;
|
|
11187
|
-
const resolved = await resolveInstruction(
|
|
12075
|
+
const resolved = await resolveInstruction(
|
|
12076
|
+
cfg,
|
|
12077
|
+
section,
|
|
12078
|
+
personalWorkspaceId
|
|
12079
|
+
);
|
|
11188
12080
|
if (!resolved || resolved.source === "override") continue;
|
|
11189
12081
|
let make = choice === "yes";
|
|
11190
12082
|
if (choice === "ask") {
|
|
@@ -11199,8 +12091,10 @@ version, the shared template stays clean, and you can discard back anytime.
|
|
|
11199
12091
|
}
|
|
11200
12092
|
if (make) {
|
|
11201
12093
|
await createOverride(cfg, resolved, personalWorkspaceId);
|
|
11202
|
-
process.stderr.write(
|
|
11203
|
-
|
|
12094
|
+
process.stderr.write(
|
|
12095
|
+
`\u2713 personal copy created for ${instr.surfaceKey} \u2014 edit it on the Agent setup page or via the API.
|
|
12096
|
+
`
|
|
12097
|
+
);
|
|
11204
12098
|
}
|
|
11205
12099
|
}
|
|
11206
12100
|
}
|
|
@@ -11210,7 +12104,9 @@ function resolveClientKeys(raw) {
|
|
|
11210
12104
|
if (tokens.includes("all")) return [...ALL_CLIENT_KEYS];
|
|
11211
12105
|
for (const k of tokens) {
|
|
11212
12106
|
if (!targets[k]) {
|
|
11213
|
-
fail(
|
|
12107
|
+
fail(
|
|
12108
|
+
`unknown client '${k}'. Known: ${ALL_CLIENT_KEYS.join(", ")}, or 'all'.`
|
|
12109
|
+
);
|
|
11214
12110
|
}
|
|
11215
12111
|
}
|
|
11216
12112
|
return [...new Set(tokens)];
|
|
@@ -11221,8 +12117,10 @@ ${client.label} (${client.key}):
|
|
|
11221
12117
|
`);
|
|
11222
12118
|
for (const a of actions) {
|
|
11223
12119
|
const tag = a.status === "skipped" ? "skip" : a.status;
|
|
11224
|
-
process.stdout.write(
|
|
11225
|
-
`
|
|
12120
|
+
process.stdout.write(
|
|
12121
|
+
` [${tag}] ${a.kind}: ${a.path}${a.note ? ` \u2014 ${a.note}` : ""}
|
|
12122
|
+
`
|
|
12123
|
+
);
|
|
11226
12124
|
}
|
|
11227
12125
|
}
|
|
11228
12126
|
function resolveEvalMode(opts) {
|
|
@@ -11237,8 +12135,17 @@ function buildCheckReport(result) {
|
|
|
11237
12135
|
stub: 0
|
|
11238
12136
|
};
|
|
11239
12137
|
const blocks = [];
|
|
12138
|
+
const unresolved = [];
|
|
11240
12139
|
for (const { client, actions } of result) {
|
|
11241
12140
|
for (const action of actions) {
|
|
12141
|
+
if (action.kind === "instruction" && action.status === "skipped" && !action.eval) {
|
|
12142
|
+
unresolved.push({
|
|
12143
|
+
client,
|
|
12144
|
+
path: action.path,
|
|
12145
|
+
block: action.block ?? "unknown",
|
|
12146
|
+
note: action.note
|
|
12147
|
+
});
|
|
12148
|
+
}
|
|
11242
12149
|
if (!action.eval) continue;
|
|
11243
12150
|
counts[action.eval]++;
|
|
11244
12151
|
blocks.push({
|
|
@@ -11252,7 +12159,8 @@ function buildCheckReport(result) {
|
|
|
11252
12159
|
return {
|
|
11253
12160
|
eval: counts,
|
|
11254
12161
|
wouldChange: counts.stale + counts.drift + counts.absent,
|
|
11255
|
-
blocks
|
|
12162
|
+
blocks,
|
|
12163
|
+
unresolved
|
|
11256
12164
|
};
|
|
11257
12165
|
}
|
|
11258
12166
|
function reportCheckAndExit(result, json, refreshCommand, jsonContext = {}) {
|
|
@@ -11267,7 +12175,7 @@ function reportCheckAndExit(result, json, refreshCommand, jsonContext = {}) {
|
|
|
11267
12175
|
},
|
|
11268
12176
|
true
|
|
11269
12177
|
);
|
|
11270
|
-
} else if (report.wouldChange === 0) {
|
|
12178
|
+
} else if (report.wouldChange === 0 && report.unresolved.length === 0) {
|
|
11271
12179
|
if (report.eval.stub) {
|
|
11272
12180
|
const files = report.eval.stub === 1 ? "file" : "files";
|
|
11273
12181
|
process.stdout.write(
|
|
@@ -11279,11 +12187,27 @@ function reportCheckAndExit(result, json, refreshCommand, jsonContext = {}) {
|
|
|
11279
12187
|
}
|
|
11280
12188
|
} else {
|
|
11281
12189
|
const bits = [];
|
|
11282
|
-
if (report.
|
|
11283
|
-
|
|
11284
|
-
|
|
12190
|
+
if (report.wouldChange > 0) {
|
|
12191
|
+
const changes = [];
|
|
12192
|
+
if (report.eval.stale) changes.push(`${report.eval.stale} out of date`);
|
|
12193
|
+
if (report.eval.drift)
|
|
12194
|
+
changes.push(`${report.eval.drift} with local edits`);
|
|
12195
|
+
if (report.eval.absent)
|
|
12196
|
+
changes.push(`${report.eval.absent} not yet written`);
|
|
12197
|
+
bits.push(
|
|
12198
|
+
`${report.wouldChange} instruction block(s) would change: ${changes.join(", ")}`
|
|
12199
|
+
);
|
|
12200
|
+
}
|
|
12201
|
+
if (report.unresolved.length > 0) {
|
|
12202
|
+
const details = report.unresolved.map(
|
|
12203
|
+
({ client, path, block, note }) => `${client} ${block} at ${path}${note ? ` \u2014 ${note}` : ""}`
|
|
12204
|
+
).join("; ");
|
|
12205
|
+
bits.push(
|
|
12206
|
+
`${report.unresolved.length} instruction template(s) skipped or unresolved: ${details}`
|
|
12207
|
+
);
|
|
12208
|
+
}
|
|
11285
12209
|
process.stderr.write(
|
|
11286
|
-
`\u26A0 ${
|
|
12210
|
+
`\u26A0 ${bits.join("; ")}. Run ${style.cyan(refreshCommand)}.
|
|
11287
12211
|
`
|
|
11288
12212
|
);
|
|
11289
12213
|
}
|
|
@@ -11296,8 +12220,10 @@ function summarizeEval(result, mode, json, dryRun) {
|
|
|
11296
12220
|
const { eval: counts } = buildCheckReport(result);
|
|
11297
12221
|
if (json) return;
|
|
11298
12222
|
if (!dryRun && counts.stale) {
|
|
11299
|
-
process.stderr.write(
|
|
11300
|
-
|
|
12223
|
+
process.stderr.write(
|
|
12224
|
+
`\u21BB refreshed ${counts.stale} section(s) the server had moved
|
|
12225
|
+
`
|
|
12226
|
+
);
|
|
11301
12227
|
}
|
|
11302
12228
|
if (!dryRun && counts.drift) {
|
|
11303
12229
|
process.stderr.write(
|
|
@@ -11328,7 +12254,39 @@ async function resolveNamespaceChoice(cfg, flag) {
|
|
|
11328
12254
|
return picked === GLOBAL_NAMESPACE ? null : picked;
|
|
11329
12255
|
}
|
|
11330
12256
|
function registerInit(program2) {
|
|
11331
|
-
program2.command("init").description(
|
|
12257
|
+
program2.command("init").description(
|
|
12258
|
+
"Wire this project for sechroom: write MCP config + agent instruction files from the server's setup descriptors"
|
|
12259
|
+
).option(
|
|
12260
|
+
"--client <list...>",
|
|
12261
|
+
`clients to wire \u2014 space- or comma-separated (${ALL_CLIENT_KEYS.join(", ")}) or 'all'`,
|
|
12262
|
+
DEFAULT_CLIENT_KEY
|
|
12263
|
+
).option(
|
|
12264
|
+
"--scope <scope>",
|
|
12265
|
+
"install skills/agents/hooks 'global' (config dir / CLAUDE_CONFIG_DIR) or 'project' (<cwd>/.claude) \u2014 default global",
|
|
12266
|
+
"global"
|
|
12267
|
+
).option("--dry-run", "print what would be written without writing", false).option("--mcp-only", "only write MCP config (skip agent files)", false).option(
|
|
12268
|
+
"--agent-files-only",
|
|
12269
|
+
"only write agent instruction files (skip MCP config)",
|
|
12270
|
+
false
|
|
12271
|
+
).option(
|
|
12272
|
+
"--copy",
|
|
12273
|
+
"make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)"
|
|
12274
|
+
).option(
|
|
12275
|
+
"--namespace <slug>",
|
|
12276
|
+
"MCP namespace for the connection URL (interactive picker if omitted on a TTY; defaults to tenant-global)"
|
|
12277
|
+
).option(
|
|
12278
|
+
"--refresh",
|
|
12279
|
+
"refresh out-of-date agent-file blocks in place (local edits preserved to .proposed)",
|
|
12280
|
+
false
|
|
12281
|
+
).option(
|
|
12282
|
+
"--force",
|
|
12283
|
+
"rewrite agent-file managed blocks, overwriting local edits inside the markers",
|
|
12284
|
+
false
|
|
12285
|
+
).option(
|
|
12286
|
+
"--check",
|
|
12287
|
+
"report whether agent files would change and exit (0 = current/stub or unresolved warning, 1 = stale/drift/absent); writes nothing",
|
|
12288
|
+
false
|
|
12289
|
+
).addHelpText(
|
|
11332
12290
|
"after",
|
|
11333
12291
|
`
|
|
11334
12292
|
Examples:
|
|
@@ -11343,7 +12301,9 @@ Examples:
|
|
|
11343
12301
|
const mode = resolveEvalMode(opts);
|
|
11344
12302
|
const check = mode === "check";
|
|
11345
12303
|
if (check && opts.mcpOnly) {
|
|
11346
|
-
fail(
|
|
12304
|
+
fail(
|
|
12305
|
+
"--check inspects agent files and cannot be combined with --mcp-only."
|
|
12306
|
+
);
|
|
11347
12307
|
}
|
|
11348
12308
|
const namespaceSlug = await resolveNamespaceChoice(cfg, opts.namespace);
|
|
11349
12309
|
const setup = await withSpinner(
|
|
@@ -11357,14 +12317,28 @@ Examples:
|
|
|
11357
12317
|
} catch (err2) {
|
|
11358
12318
|
return fail(err2.message);
|
|
11359
12319
|
}
|
|
11360
|
-
const claudeTargets = resolveClaudeTargets({
|
|
12320
|
+
const claudeTargets = resolveClaudeTargets({
|
|
12321
|
+
override: g.claudeConfigDir,
|
|
12322
|
+
scope,
|
|
12323
|
+
cwd: process.cwd()
|
|
12324
|
+
});
|
|
11361
12325
|
const codexHomes = resolveCodexHomes({ override: g.codexHome, scope });
|
|
11362
|
-
const targets = clientTargets(process.cwd(), {
|
|
12326
|
+
const targets = clientTargets(process.cwd(), {
|
|
12327
|
+
claudeDir: claudeTargets[0]?.dir,
|
|
12328
|
+
codexHome: codexHomes[0] ?? null
|
|
12329
|
+
});
|
|
11363
12330
|
const keys = resolveClientKeys(opts.client);
|
|
11364
12331
|
const json = g.json;
|
|
11365
12332
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
11366
12333
|
if (!opts.dryRun && !opts.mcpOnly && !check) {
|
|
11367
|
-
await maybeOfferCopies(
|
|
12334
|
+
await maybeOfferCopies(
|
|
12335
|
+
cfg,
|
|
12336
|
+
setup,
|
|
12337
|
+
targets,
|
|
12338
|
+
keys,
|
|
12339
|
+
personalWorkspaceId,
|
|
12340
|
+
copyChoice(opts)
|
|
12341
|
+
);
|
|
11368
12342
|
}
|
|
11369
12343
|
const result = [];
|
|
11370
12344
|
for (const key of keys) {
|
|
@@ -11382,18 +12356,33 @@ Examples:
|
|
|
11382
12356
|
summarizeEval(result, mode, Boolean(json), Boolean(opts.dryRun));
|
|
11383
12357
|
if (!json && !opts.dryRun && !opts.mcpOnly && !check) {
|
|
11384
12358
|
for (const t of claudeTargets) {
|
|
11385
|
-
await maybeOfferSkills(cfg, personalWorkspaceId, {
|
|
12359
|
+
await maybeOfferSkills(cfg, personalWorkspaceId, {
|
|
12360
|
+
yes: false,
|
|
12361
|
+
dryRun: Boolean(opts.dryRun),
|
|
12362
|
+
surface: "claude-code",
|
|
12363
|
+
configDir: t.dir
|
|
12364
|
+
});
|
|
11386
12365
|
}
|
|
11387
12366
|
}
|
|
11388
12367
|
if (!json && !opts.dryRun && !opts.mcpOnly && !check) {
|
|
11389
|
-
await maybeOfferHooks({
|
|
12368
|
+
await maybeOfferHooks({
|
|
12369
|
+
yes: false,
|
|
12370
|
+
dryRun: Boolean(opts.dryRun),
|
|
12371
|
+
cwd: process.cwd(),
|
|
12372
|
+
scope,
|
|
12373
|
+
claudeConfigDir: g.claudeConfigDir,
|
|
12374
|
+
codexHome: g.codexHome
|
|
12375
|
+
});
|
|
11390
12376
|
}
|
|
11391
12377
|
if (json) {
|
|
11392
12378
|
emit({ dryRun: Boolean(opts.dryRun), clients: result }, true);
|
|
11393
12379
|
return;
|
|
11394
12380
|
}
|
|
11395
12381
|
const first = targets[keys[0]];
|
|
11396
|
-
const surface = findSurface(
|
|
12382
|
+
const surface = findSurface(
|
|
12383
|
+
setup,
|
|
12384
|
+
first.mcp?.surfaceKey ?? first.instruction?.surfaceKey ?? ""
|
|
12385
|
+
);
|
|
11397
12386
|
const verify = findSection(surface, SectionType.Verify);
|
|
11398
12387
|
if (verify?.description) {
|
|
11399
12388
|
process.stdout.write(`
|
|
@@ -11407,13 +12396,64 @@ Next \u2014 verify: ${verify.description}
|
|
|
11407
12396
|
}
|
|
11408
12397
|
function registerSetup(program2, deps = {}) {
|
|
11409
12398
|
const setup = program2.command("setup").description("Granular onboarding steps (init runs these together)");
|
|
11410
|
-
setup.command("mcp <clients...>").description(
|
|
11411
|
-
|
|
11412
|
-
|
|
11413
|
-
|
|
11414
|
-
|
|
12399
|
+
setup.command("mcp <clients...>").description(
|
|
12400
|
+
`Write only the MCP config for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`
|
|
12401
|
+
).option("--dry-run", "print what would be written without writing", false).option(
|
|
12402
|
+
"--namespace <slug>",
|
|
12403
|
+
"MCP namespace for the connection URL (interactive picker if omitted on a TTY; defaults to tenant-global)"
|
|
12404
|
+
).addHelpText(
|
|
12405
|
+
"after",
|
|
12406
|
+
"\nExamples:\n $ sechroom setup mcp codex\n $ sechroom setup mcp claude-code codex\n $ sechroom setup mcp all"
|
|
12407
|
+
).action(async (clients, opts, cmd) => {
|
|
12408
|
+
await runClients(clients, cmd, {
|
|
12409
|
+
dryRun: Boolean(opts.dryRun),
|
|
12410
|
+
mcp: true,
|
|
12411
|
+
agentFiles: false,
|
|
12412
|
+
namespace: opts.namespace
|
|
12413
|
+
});
|
|
12414
|
+
});
|
|
12415
|
+
setup.command("agent-files <clients...>").description(
|
|
12416
|
+
`Write only the agent instruction file(s) for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`
|
|
12417
|
+
).option("--dry-run", "print what would be written without writing", false).option(
|
|
12418
|
+
"--copy",
|
|
12419
|
+
"make a personal copy you can edit (default: prompt on a TTY, else skip)"
|
|
12420
|
+
).option(
|
|
12421
|
+
"--refresh",
|
|
12422
|
+
"refresh out-of-date blocks in place (local edits preserved to .proposed)",
|
|
12423
|
+
false
|
|
12424
|
+
).option(
|
|
12425
|
+
"--force",
|
|
12426
|
+
"rewrite managed blocks, overwriting local edits inside the markers",
|
|
12427
|
+
false
|
|
12428
|
+
).option(
|
|
12429
|
+
"--check",
|
|
12430
|
+
"report whether anything would change and exit (0 = current/stub or unresolved warning, 1 = stale/drift/absent); writes nothing",
|
|
12431
|
+
false
|
|
12432
|
+
).addHelpText(
|
|
12433
|
+
"after",
|
|
12434
|
+
"\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"
|
|
12435
|
+
).action(async (clients, opts, cmd) => {
|
|
12436
|
+
await runClients(clients, cmd, {
|
|
12437
|
+
dryRun: Boolean(opts.dryRun),
|
|
12438
|
+
mcp: false,
|
|
12439
|
+
agentFiles: true,
|
|
12440
|
+
copy: opts.copy,
|
|
12441
|
+
mode: resolveEvalMode(opts)
|
|
12442
|
+
});
|
|
11415
12443
|
});
|
|
11416
|
-
setup.command("new-convention <title...>").description(
|
|
12444
|
+
setup.command("new-convention <title...>").description(
|
|
12445
|
+
"Scaffold a workspace-conventions section: author a correctly-tagged memo (header as first body line) + regen the agent files"
|
|
12446
|
+
).option(
|
|
12447
|
+
"--kind <kind>",
|
|
12448
|
+
"reference | standard (orders the section; reference first)",
|
|
12449
|
+
"reference"
|
|
12450
|
+
).option(
|
|
12451
|
+
"--workspace <id>",
|
|
12452
|
+
"workspace to author in (default: the bound workspace)"
|
|
12453
|
+
).option(
|
|
12454
|
+
"--body <markdown>",
|
|
12455
|
+
"section body (default: a TODO scaffold to edit later)"
|
|
12456
|
+
).option("--no-regen", "skip the agent-files regen after authoring").option("--dry-run", "print what would be authored; write nothing", false).addHelpText(
|
|
11417
12457
|
"after",
|
|
11418
12458
|
`
|
|
11419
12459
|
The memo carries the two conventions a workspace-conventions section needs (FR-sechroom-236):
|
|
@@ -11429,10 +12469,15 @@ Examples:
|
|
|
11429
12469
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
11430
12470
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
11431
12471
|
const title = titleParts.join(" ").trim();
|
|
11432
|
-
if (!title)
|
|
12472
|
+
if (!title)
|
|
12473
|
+
fail(
|
|
12474
|
+
'a section title is required, e.g. `sechroom setup new-convention "Deploy runbook"`.'
|
|
12475
|
+
);
|
|
11433
12476
|
const workspaceId = opts.workspace ?? cfg.workspaceId;
|
|
11434
12477
|
if (!workspaceId)
|
|
11435
|
-
fail(
|
|
12478
|
+
fail(
|
|
12479
|
+
"no workspace \u2014 pass --workspace <id> or bind one (`sechroom config set --local workspaceId <id>`)."
|
|
12480
|
+
);
|
|
11436
12481
|
const draft = buildConventionDraft(title, opts.kind, opts.body);
|
|
11437
12482
|
if (opts.dryRun) {
|
|
11438
12483
|
emit(
|
|
@@ -11473,7 +12518,10 @@ Examples:
|
|
|
11473
12518
|
}
|
|
11474
12519
|
if (opts.regen === false) {
|
|
11475
12520
|
if (json) emit({ id: data.id, workspaceId, regen: false }, true);
|
|
11476
|
-
else
|
|
12521
|
+
else
|
|
12522
|
+
process.stdout.write(
|
|
12523
|
+
"Skipped regen (--no-regen). Run `sechroom setup agent-files all` to apply.\n"
|
|
12524
|
+
);
|
|
11477
12525
|
return;
|
|
11478
12526
|
}
|
|
11479
12527
|
const regenerate = deps.regenerateConvention ?? runClients;
|
|
@@ -11502,7 +12550,14 @@ async function runClients(clients, cmd, opts) {
|
|
|
11502
12550
|
);
|
|
11503
12551
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
11504
12552
|
if (opts.agentFiles && !opts.dryRun && !check) {
|
|
11505
|
-
await maybeOfferCopies(
|
|
12553
|
+
await maybeOfferCopies(
|
|
12554
|
+
cfg,
|
|
12555
|
+
setupData,
|
|
12556
|
+
targets,
|
|
12557
|
+
keys,
|
|
12558
|
+
personalWorkspaceId,
|
|
12559
|
+
copyChoice(opts)
|
|
12560
|
+
);
|
|
11506
12561
|
}
|
|
11507
12562
|
const json = g.json;
|
|
11508
12563
|
const result = [];
|
|
@@ -11523,7 +12578,9 @@ async function runClients(clients, cmd, opts) {
|
|
|
11523
12578
|
emit({ dryRun: opts.dryRun, clients: result }, true);
|
|
11524
12579
|
return;
|
|
11525
12580
|
}
|
|
11526
|
-
process.stdout.write(
|
|
12581
|
+
process.stdout.write(
|
|
12582
|
+
opts.dryRun ? "\n(dry run \u2014 nothing written)\n" : "\nDone.\n"
|
|
12583
|
+
);
|
|
11527
12584
|
}
|
|
11528
12585
|
|
|
11529
12586
|
// src/commands/namespace.ts
|
|
@@ -11599,7 +12656,7 @@ import { basename as basename5, join as join21 } from "path";
|
|
|
11599
12656
|
|
|
11600
12657
|
// src/commands/fanout.ts
|
|
11601
12658
|
import { spawnSync } from "child_process";
|
|
11602
|
-
import { existsSync as existsSync14, readFileSync as readFileSync17, readdirSync as readdirSync4, statSync as
|
|
12659
|
+
import { existsSync as existsSync14, readFileSync as readFileSync17, readdirSync as readdirSync4, statSync as statSync6 } from "fs";
|
|
11603
12660
|
import { isAbsolute as isAbsolute3, join as join20, resolve as resolve7 } from "path";
|
|
11604
12661
|
var ICON = {
|
|
11605
12662
|
refresh: "\u21BB",
|
|
@@ -11622,7 +12679,7 @@ function discoverChildren(root) {
|
|
|
11622
12679
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
11623
12680
|
const dir = join20(root, name);
|
|
11624
12681
|
try {
|
|
11625
|
-
if (!
|
|
12682
|
+
if (!statSync6(dir).isDirectory()) continue;
|
|
11626
12683
|
} catch {
|
|
11627
12684
|
continue;
|
|
11628
12685
|
}
|
|
@@ -11748,20 +12805,27 @@ function resolveBaseUrl(g) {
|
|
|
11748
12805
|
return baseUrl.replace(/\/$/, "");
|
|
11749
12806
|
}
|
|
11750
12807
|
async function fetchWorkspaces(client) {
|
|
11751
|
-
const { data, error } = await client.GET("/workspaces", {
|
|
11752
|
-
|
|
12808
|
+
const { data, error } = await client.GET("/workspaces", {
|
|
12809
|
+
params: { query: { includeArchived: false } }
|
|
12810
|
+
});
|
|
12811
|
+
if (error)
|
|
12812
|
+
throw new Error(`Couldn't list your workspaces: ${JSON.stringify(error)}`);
|
|
11753
12813
|
const rows = data ?? [];
|
|
11754
12814
|
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
12815
|
}
|
|
11756
12816
|
async function lookupWorkspace(client, id) {
|
|
11757
|
-
const { data, error } = await client.GET("/workspaces/{workspaceId}", {
|
|
12817
|
+
const { data, error } = await client.GET("/workspaces/{workspaceId}", {
|
|
12818
|
+
params: { path: { workspaceId: id } }
|
|
12819
|
+
});
|
|
11758
12820
|
if (error) return null;
|
|
11759
12821
|
const env = data;
|
|
11760
12822
|
const w = env?.item ?? env;
|
|
11761
12823
|
return w?.id ? { id: w.id, name: w.name ?? id, parentId: w.parentId ?? null } : null;
|
|
11762
12824
|
}
|
|
11763
12825
|
async function warnIfProjectStray(client, projectId, workspaceId, json) {
|
|
11764
|
-
const { data, error } = await client.GET("/projects/{projectId}", {
|
|
12826
|
+
const { data, error } = await client.GET("/projects/{projectId}", {
|
|
12827
|
+
params: { path: { projectId } }
|
|
12828
|
+
});
|
|
11765
12829
|
if (error) return;
|
|
11766
12830
|
const env = data;
|
|
11767
12831
|
const owner = env?.item?.workspaceId;
|
|
@@ -11805,10 +12869,15 @@ function personalSubtreeIds(personalId, all) {
|
|
|
11805
12869
|
async function pickWorkspace(client, opts = {}) {
|
|
11806
12870
|
const promptLabel = opts.promptLabel ?? "Bind this directory to a workspace:";
|
|
11807
12871
|
const dirName = opts.dirName ?? basename5(process.cwd());
|
|
11808
|
-
const all = await withSpinner(
|
|
12872
|
+
const all = await withSpinner(
|
|
12873
|
+
"Listing your workspaces",
|
|
12874
|
+
() => fetchWorkspaces(client)
|
|
12875
|
+
);
|
|
11809
12876
|
if (all.length === 0) {
|
|
11810
|
-
process.stderr.write(
|
|
11811
|
-
`)
|
|
12877
|
+
process.stderr.write(
|
|
12878
|
+
`no workspaces found \u2014 skipping workspace binding (you can set it later with \`sechroom config set --local workspaceId <id>\`)
|
|
12879
|
+
`
|
|
12880
|
+
);
|
|
11812
12881
|
return void 0;
|
|
11813
12882
|
}
|
|
11814
12883
|
const byId = new Map(all.map((w) => [w.id, w]));
|
|
@@ -11823,19 +12892,35 @@ async function pickWorkspace(client, opts = {}) {
|
|
|
11823
12892
|
const matched = candidates.filter(isMatch).sort(byPath);
|
|
11824
12893
|
const rest = candidates.filter((w) => !isMatch(w)).sort(byPath);
|
|
11825
12894
|
const choices = [
|
|
11826
|
-
...matched.map((w) => ({
|
|
11827
|
-
|
|
11828
|
-
|
|
12895
|
+
...matched.map((w) => ({
|
|
12896
|
+
label: workspacePath(w, byId),
|
|
12897
|
+
value: w.id,
|
|
12898
|
+
hint: style.dim(`matches "${dirName}"`)
|
|
12899
|
+
})),
|
|
12900
|
+
...rest.map((w) => ({
|
|
12901
|
+
label: workspacePath(w, byId),
|
|
12902
|
+
value: w.id,
|
|
12903
|
+
hint: w.id
|
|
12904
|
+
})),
|
|
12905
|
+
{
|
|
12906
|
+
label: style.dim("skip \u2014 don't bind a workspace"),
|
|
12907
|
+
value: SKIP,
|
|
12908
|
+
hint: void 0
|
|
12909
|
+
}
|
|
11829
12910
|
];
|
|
11830
12911
|
const defaultValue = matched.length === 1 ? matched[0].id : SKIP;
|
|
11831
12912
|
const chosen = candidates.length > 12 ? await promptAutocomplete(promptLabel, choices, defaultValue) : await promptSelect(promptLabel, choices, defaultValue);
|
|
11832
12913
|
if (chosen === SKIP) return void 0;
|
|
11833
12914
|
const picked = byId.get(chosen);
|
|
11834
|
-
const collisions = all.filter(
|
|
12915
|
+
const collisions = all.filter(
|
|
12916
|
+
(w) => w.id !== picked.id && namesCollide(w.name, picked.name)
|
|
12917
|
+
);
|
|
11835
12918
|
if (collisions.length > 0) {
|
|
11836
12919
|
process.stderr.write(
|
|
11837
12920
|
`${warn("\u26A0")} ${collisions.length} other workspace(s) have a similar name to ${style.cyan(workspacePath(picked, byId))}:
|
|
11838
|
-
` + collisions.map(
|
|
12921
|
+
` + collisions.map(
|
|
12922
|
+
(w) => ` ${style.dim(workspacePath(w, byId))} ${style.dim(`(${w.id})`)}`
|
|
12923
|
+
).join("\n") + `
|
|
11839
12924
|
You picked ${style.dim(picked.id)} \u2014 re-run \`sechroom config set --local workspaceId <id>\` if that's wrong.
|
|
11840
12925
|
`
|
|
11841
12926
|
);
|
|
@@ -11862,10 +12947,17 @@ async function ensureTenant(baseUrl, g, opts) {
|
|
|
11862
12947
|
const local = readLocalConfig();
|
|
11863
12948
|
let tenant = g.tenant ?? process.env.SECHROOM_TENANT ?? local.tenant ?? persisted.tenant ?? "";
|
|
11864
12949
|
if (!tenant) {
|
|
11865
|
-
const client = await makeClient({
|
|
12950
|
+
const client = await makeClient({
|
|
12951
|
+
baseUrl,
|
|
12952
|
+
tenant: "",
|
|
12953
|
+
account: resolveAccountAlias(),
|
|
12954
|
+
clientId: persisted.clientId
|
|
12955
|
+
});
|
|
11866
12956
|
const { data, error } = await client.GET("/auth/me/tenants", {});
|
|
11867
12957
|
if (error) {
|
|
11868
|
-
fail(
|
|
12958
|
+
fail(
|
|
12959
|
+
`Couldn't list your tenants: ${JSON.stringify(error)}. Pass --tenant <id> to skip this.`
|
|
12960
|
+
);
|
|
11869
12961
|
}
|
|
11870
12962
|
const tenants = data?.tenants ?? [];
|
|
11871
12963
|
if (tenants.length === 0) {
|
|
@@ -11897,21 +12989,44 @@ async function ensureTenant(baseUrl, g, opts) {
|
|
|
11897
12989
|
}
|
|
11898
12990
|
}
|
|
11899
12991
|
const existingWorkspace = local.workspaceId ?? persisted.workspaceId ?? void 0;
|
|
11900
|
-
const wsClient = await makeClient({
|
|
11901
|
-
|
|
11902
|
-
|
|
11903
|
-
|
|
11904
|
-
|
|
12992
|
+
const wsClient = await makeClient({
|
|
12993
|
+
baseUrl,
|
|
12994
|
+
tenant,
|
|
12995
|
+
account: resolveAccountAlias(),
|
|
12996
|
+
clientId: persisted.clientId
|
|
11905
12997
|
});
|
|
12998
|
+
const workspaceId = await resolveWorkspaceBinding(
|
|
12999
|
+
wsClient,
|
|
13000
|
+
existingWorkspace,
|
|
13001
|
+
{
|
|
13002
|
+
yes: opts.yes,
|
|
13003
|
+
json: opts.json,
|
|
13004
|
+
workspace: opts.workspace
|
|
13005
|
+
}
|
|
13006
|
+
);
|
|
11906
13007
|
const defaultProjectId = local.defaultProjectId ?? persisted.defaultProjectId ?? void 0;
|
|
11907
|
-
if (defaultProjectId && workspaceId)
|
|
13008
|
+
if (defaultProjectId && workspaceId)
|
|
13009
|
+
await warnIfProjectStray(
|
|
13010
|
+
wsClient,
|
|
13011
|
+
defaultProjectId,
|
|
13012
|
+
workspaceId,
|
|
13013
|
+
opts.json
|
|
13014
|
+
);
|
|
11908
13015
|
let storeLocal = Boolean(opts.local);
|
|
11909
13016
|
if (!opts.local && canPrompt() && !opts.yes) {
|
|
11910
13017
|
storeLocal = await promptSelect(
|
|
11911
13018
|
"Where should this tenant + base URL be saved?",
|
|
11912
13019
|
[
|
|
11913
|
-
{
|
|
11914
|
-
|
|
13020
|
+
{
|
|
13021
|
+
label: "Globally",
|
|
13022
|
+
value: "global",
|
|
13023
|
+
hint: "all projects on this machine"
|
|
13024
|
+
},
|
|
13025
|
+
{
|
|
13026
|
+
label: "This directory",
|
|
13027
|
+
value: "local",
|
|
13028
|
+
hint: ".sechroom.json \u2014 committed, project + subdirs"
|
|
13029
|
+
}
|
|
11915
13030
|
],
|
|
11916
13031
|
local.path ? "local" : "global"
|
|
11917
13032
|
) === "local";
|
|
@@ -11920,19 +13035,34 @@ async function ensureTenant(baseUrl, g, opts) {
|
|
|
11920
13035
|
const patch = { baseUrl, tenant, ...workspaceId ? { workspaceId } : {} };
|
|
11921
13036
|
if (storeLocal) {
|
|
11922
13037
|
const path = writeLocalConfig(patch, { here: Boolean(opts.here) });
|
|
11923
|
-
if (!opts.json)
|
|
11924
|
-
|
|
13038
|
+
if (!opts.json)
|
|
13039
|
+
process.stderr.write(
|
|
13040
|
+
`${ok("\u2713")} config saved to ${path} (directory-local)
|
|
13041
|
+
`
|
|
13042
|
+
);
|
|
11925
13043
|
} else {
|
|
11926
13044
|
writePersisted(patch);
|
|
11927
|
-
if (!opts.json)
|
|
11928
|
-
|
|
13045
|
+
if (!opts.json)
|
|
13046
|
+
process.stderr.write(
|
|
13047
|
+
`${ok("\u2713")} config saved globally (~/.config/sechroom/config.json)
|
|
13048
|
+
`
|
|
13049
|
+
);
|
|
11929
13050
|
}
|
|
11930
13051
|
if (workspaceId && !existingWorkspace && !opts.json) {
|
|
11931
|
-
process.stderr.write(
|
|
11932
|
-
|
|
13052
|
+
process.stderr.write(
|
|
13053
|
+
`${ok("\u2713")} bound to workspace ${style.dim(workspaceId)}
|
|
13054
|
+
`
|
|
13055
|
+
);
|
|
11933
13056
|
}
|
|
11934
13057
|
}
|
|
11935
|
-
return {
|
|
13058
|
+
return {
|
|
13059
|
+
baseUrl,
|
|
13060
|
+
tenant,
|
|
13061
|
+
account: resolveAccountAlias(),
|
|
13062
|
+
workspaceId,
|
|
13063
|
+
defaultProjectId,
|
|
13064
|
+
clientId: persisted.clientId
|
|
13065
|
+
};
|
|
11936
13066
|
}
|
|
11937
13067
|
async function ensureAuth(cfg, yes) {
|
|
11938
13068
|
if (process.env.SECHROOM_TOKEN) return;
|
|
@@ -11940,17 +13070,27 @@ async function ensureAuth(cfg, yes) {
|
|
|
11940
13070
|
const usable = Boolean(cached?.accessToken) && (cached.expiresAt === void 0 || Date.now() < cached.expiresAt - 6e4 || Boolean(cached.refreshToken));
|
|
11941
13071
|
if (usable) return;
|
|
11942
13072
|
if (!canPrompt() || yes) {
|
|
11943
|
-
fail(
|
|
13073
|
+
fail(
|
|
13074
|
+
"Not signed in. Run `sechroom login` first, or set SECHROOM_TOKEN for headless use."
|
|
13075
|
+
);
|
|
11944
13076
|
}
|
|
11945
|
-
process.stderr.write(
|
|
13077
|
+
process.stderr.write(
|
|
13078
|
+
"\nNot signed in \u2014 opening the browser to authenticate.\n"
|
|
13079
|
+
);
|
|
11946
13080
|
await login(cfg);
|
|
11947
13081
|
}
|
|
11948
13082
|
async function ensureTimezone(cfg, opts) {
|
|
11949
13083
|
const client = await makeClient(cfg);
|
|
11950
13084
|
const { data, error } = await client.GET("/me/profile", {});
|
|
11951
|
-
if (error)
|
|
13085
|
+
if (error)
|
|
13086
|
+
return {
|
|
13087
|
+
timezone: null,
|
|
13088
|
+
action: "skipped",
|
|
13089
|
+
note: "could not read profile"
|
|
13090
|
+
};
|
|
11952
13091
|
const current = data?.effectiveTimezone;
|
|
11953
|
-
if (current && current.trim().length > 0)
|
|
13092
|
+
if (current && current.trim().length > 0)
|
|
13093
|
+
return { timezone: current, action: "already-set" };
|
|
11954
13094
|
const system = systemTimezone();
|
|
11955
13095
|
let tz = system;
|
|
11956
13096
|
if (canPrompt() && !opts.yes) {
|
|
@@ -11962,12 +13102,18 @@ async function ensureTimezone(cfg, opts) {
|
|
|
11962
13102
|
note: "no timezone set \u2014 re-run interactively or pass --yes to adopt the system timezone"
|
|
11963
13103
|
};
|
|
11964
13104
|
}
|
|
11965
|
-
if (!tz)
|
|
13105
|
+
if (!tz)
|
|
13106
|
+
return { timezone: null, action: "skipped", note: "no timezone provided" };
|
|
11966
13107
|
if (opts.dryRun) return { timezone: tz, action: "dry-run" };
|
|
11967
13108
|
const { error: putErr } = await client.PUT("/me/profile", {
|
|
11968
13109
|
body: { displayName: null, photoUrl: null, bio: null, timezone: tz }
|
|
11969
13110
|
});
|
|
11970
|
-
if (putErr)
|
|
13111
|
+
if (putErr)
|
|
13112
|
+
return {
|
|
13113
|
+
timezone: tz,
|
|
13114
|
+
action: "skipped",
|
|
13115
|
+
note: `update failed: ${JSON.stringify(putErr)}`
|
|
13116
|
+
};
|
|
11971
13117
|
return { timezone: tz, action: "set" };
|
|
11972
13118
|
}
|
|
11973
13119
|
async function chooseClients(clientFlag, yes, cwd) {
|
|
@@ -11992,7 +13138,11 @@ async function chooseScope(scopeFlag, yes) {
|
|
|
11992
13138
|
return promptSelect(
|
|
11993
13139
|
"Install skills, agents, and hooks globally or just for this project?",
|
|
11994
13140
|
[
|
|
11995
|
-
{
|
|
13141
|
+
{
|
|
13142
|
+
label: "Globally",
|
|
13143
|
+
value: "global",
|
|
13144
|
+
hint: "~/.claude (or CLAUDE_CONFIG_DIR) \u2014 all projects"
|
|
13145
|
+
},
|
|
11996
13146
|
{ label: "This project", value: "project", hint: "<repo>/.claude" }
|
|
11997
13147
|
],
|
|
11998
13148
|
"global"
|
|
@@ -12001,7 +13151,13 @@ async function chooseScope(scopeFlag, yes) {
|
|
|
12001
13151
|
async function planRecurseChild(entry, root, client, opts) {
|
|
12002
13152
|
const dir = resolveChildDir(entry.path, root);
|
|
12003
13153
|
if (!existsSync15(dir)) {
|
|
12004
|
-
return {
|
|
13154
|
+
return {
|
|
13155
|
+
label: entry.path,
|
|
13156
|
+
dir,
|
|
13157
|
+
disposition: "skip-missing",
|
|
13158
|
+
argv: [],
|
|
13159
|
+
reason: "directory does not exist"
|
|
13160
|
+
};
|
|
12005
13161
|
}
|
|
12006
13162
|
if (existsSync15(join21(dir, ".sechroom.json"))) {
|
|
12007
13163
|
return {
|
|
@@ -12022,20 +13178,40 @@ async function planRecurseChild(entry, root, client, opts) {
|
|
|
12022
13178
|
};
|
|
12023
13179
|
}
|
|
12024
13180
|
if (opts.dryRun) {
|
|
12025
|
-
return {
|
|
13181
|
+
return {
|
|
13182
|
+
label: entry.path,
|
|
13183
|
+
dir,
|
|
13184
|
+
disposition: "bind",
|
|
13185
|
+
argv: ["onboard", "--yes", "--local", "--workspace", "<prompt>"],
|
|
13186
|
+
reason: "unbound \u2014 would prompt for a workspace"
|
|
13187
|
+
};
|
|
12026
13188
|
}
|
|
12027
13189
|
if (opts.yes || !canPrompt()) {
|
|
12028
|
-
return {
|
|
13190
|
+
return {
|
|
13191
|
+
label: entry.path,
|
|
13192
|
+
dir,
|
|
13193
|
+
disposition: "skip-unbound",
|
|
13194
|
+
argv: [],
|
|
13195
|
+
reason: "unbound + no workspace (run interactively, or add it to ./.sechroom/repos.json)"
|
|
13196
|
+
};
|
|
12029
13197
|
}
|
|
12030
|
-
process.stderr.write(
|
|
13198
|
+
process.stderr.write(
|
|
13199
|
+
`
|
|
12031
13200
|
${style.bold(entry.path)} ${style.dim("is not bound yet.")}
|
|
12032
|
-
`
|
|
13201
|
+
`
|
|
13202
|
+
);
|
|
12033
13203
|
const ws = await pickWorkspace(client, {
|
|
12034
13204
|
promptLabel: `Bind ${style.cyan(entry.path)} to a workspace:`,
|
|
12035
13205
|
dirName: basename5(entry.path)
|
|
12036
13206
|
});
|
|
12037
13207
|
if (!ws) {
|
|
12038
|
-
return {
|
|
13208
|
+
return {
|
|
13209
|
+
label: entry.path,
|
|
13210
|
+
dir,
|
|
13211
|
+
disposition: "skip-unbound",
|
|
13212
|
+
argv: [],
|
|
13213
|
+
reason: "unbound \u2014 no workspace chosen (skipped)"
|
|
13214
|
+
};
|
|
12039
13215
|
}
|
|
12040
13216
|
return {
|
|
12041
13217
|
label: entry.path,
|
|
@@ -12050,20 +13226,34 @@ async function resolveFanoutLane(cfg, opts) {
|
|
|
12050
13226
|
let design = opts.designLane ?? process.env.SECHROOM_DESIGN_LANE;
|
|
12051
13227
|
if (!code || !design) {
|
|
12052
13228
|
const clients = detectInstalledClients(process.cwd());
|
|
12053
|
-
const inferred = await inferLanes(
|
|
13229
|
+
const inferred = await inferLanes(
|
|
13230
|
+
cfg,
|
|
13231
|
+
clients.length ? clients : void 0
|
|
13232
|
+
);
|
|
12054
13233
|
code = code ?? inferred.code;
|
|
12055
13234
|
design = design ?? inferred.design;
|
|
12056
13235
|
}
|
|
12057
13236
|
if (!opts.lane && !opts.yes && !opts.dryRun && canPrompt() && (code || design)) {
|
|
12058
|
-
process.stderr.write(
|
|
13237
|
+
process.stderr.write(
|
|
13238
|
+
`
|
|
12059
13239
|
This fan-out will pin the same lane in every repo:
|
|
12060
|
-
`
|
|
12061
|
-
|
|
12062
|
-
|
|
12063
|
-
|
|
12064
|
-
`)
|
|
13240
|
+
`
|
|
13241
|
+
);
|
|
13242
|
+
if (code)
|
|
13243
|
+
process.stderr.write(
|
|
13244
|
+
` ${style.dim("code-lane")} = ${style.cyan(code)}
|
|
13245
|
+
`
|
|
13246
|
+
);
|
|
13247
|
+
if (design)
|
|
13248
|
+
process.stderr.write(
|
|
13249
|
+
` ${style.dim("design-lane")} = ${style.cyan(design)}
|
|
13250
|
+
`
|
|
13251
|
+
);
|
|
12065
13252
|
if (!await promptYesNo("Use this lane for all repos?")) {
|
|
12066
|
-
code = await promptText(
|
|
13253
|
+
code = await promptText(
|
|
13254
|
+
"Code-lane id (blank = let each repo infer)?",
|
|
13255
|
+
code ?? ""
|
|
13256
|
+
) || void 0;
|
|
12067
13257
|
design = await promptText("Design-lane id (blank = skip)?", design ?? "") || void 0;
|
|
12068
13258
|
}
|
|
12069
13259
|
}
|
|
@@ -12081,30 +13271,112 @@ async function runRecurse(cfg, g, opts) {
|
|
|
12081
13271
|
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
12082
13272
|
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
12083
13273
|
if (entries.length === 0) {
|
|
12084
|
-
if (json)
|
|
12085
|
-
|
|
12086
|
-
|
|
13274
|
+
if (json)
|
|
13275
|
+
process.stdout.write(
|
|
13276
|
+
JSON.stringify({ recurse: true, root, repos: [] }) + "\n"
|
|
13277
|
+
);
|
|
13278
|
+
else
|
|
13279
|
+
process.stderr.write(
|
|
13280
|
+
`${warn("\u26A0")} no child repos found ${fromManifest ? `in ${manifestPath}` : `under ${root}`} \u2014 nothing to do.
|
|
13281
|
+
`
|
|
13282
|
+
);
|
|
12087
13283
|
return;
|
|
12088
13284
|
}
|
|
12089
13285
|
if (!json) {
|
|
12090
|
-
process.stderr.write(
|
|
12091
|
-
`)
|
|
13286
|
+
process.stderr.write(
|
|
13287
|
+
`${style.bold("onboard --recurse")} ${style.dim(`(${entries.length} repo${entries.length === 1 ? "" : "s"} from ${sourceLabel})`)}
|
|
13288
|
+
`
|
|
13289
|
+
);
|
|
12092
13290
|
}
|
|
12093
|
-
const lane = await resolveFanoutLane(cfg, {
|
|
12094
|
-
|
|
12095
|
-
|
|
13291
|
+
const lane = await resolveFanoutLane(cfg, {
|
|
13292
|
+
lane: opts.lane,
|
|
13293
|
+
designLane: opts.designLane,
|
|
13294
|
+
yes,
|
|
13295
|
+
dryRun
|
|
13296
|
+
});
|
|
13297
|
+
if (!json && lane.code)
|
|
13298
|
+
process.stderr.write(
|
|
13299
|
+
`${ok("\u2713")} lane ${style.cyan(lane.code)}${lane.design ? ` ${style.dim(`/ ${lane.design}`)}` : ""} for every repo
|
|
13300
|
+
`
|
|
13301
|
+
);
|
|
12096
13302
|
const client = await makeClient(cfg);
|
|
12097
13303
|
const plans = [];
|
|
12098
|
-
for (const entry of entries)
|
|
12099
|
-
|
|
13304
|
+
for (const entry of entries)
|
|
13305
|
+
plans.push(await planRecurseChild(entry, root, client, { yes, dryRun }));
|
|
13306
|
+
const results = runChildren(plans, {
|
|
13307
|
+
globals: passthroughGlobals(g),
|
|
13308
|
+
dryRun,
|
|
13309
|
+
json
|
|
13310
|
+
});
|
|
12100
13311
|
if (json) {
|
|
12101
|
-
process.stdout.write(
|
|
13312
|
+
process.stdout.write(
|
|
13313
|
+
JSON.stringify({ recurse: true, root, dryRun, repos: results }) + "\n"
|
|
13314
|
+
);
|
|
12102
13315
|
return;
|
|
12103
13316
|
}
|
|
12104
13317
|
summarizeFanout(results, { dryRun });
|
|
12105
13318
|
}
|
|
12106
13319
|
function registerOnboard(program2) {
|
|
12107
|
-
program2.command("onboard").description(
|
|
13320
|
+
program2.command("onboard").description(
|
|
13321
|
+
"Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project"
|
|
13322
|
+
).option(
|
|
13323
|
+
"--recurse",
|
|
13324
|
+
"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",
|
|
13325
|
+
false
|
|
13326
|
+
).option(
|
|
13327
|
+
"--lane <id>",
|
|
13328
|
+
"set the code-lane (substrate source identity) explicitly instead of inferring it; with --recurse it's used for every child repo"
|
|
13329
|
+
).option(
|
|
13330
|
+
"--design-lane <id>",
|
|
13331
|
+
"set the design-lane explicitly (substrate-authoring identity); with --recurse applies to every child"
|
|
13332
|
+
).option(
|
|
13333
|
+
"--client <list...>",
|
|
13334
|
+
`clients to wire \u2014 space- or comma-separated (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`
|
|
13335
|
+
).option(
|
|
13336
|
+
"--scope <scope>",
|
|
13337
|
+
"install skills/agents/hooks 'global' (config dir / CLAUDE_CONFIG_DIR) or 'project' (<cwd>/.claude) \u2014 default: prompt, else global"
|
|
13338
|
+
).option(
|
|
13339
|
+
"--local",
|
|
13340
|
+
"save the binding (tenant + base URL + workspace) to a committed .sechroom.json in this repo instead of the global config",
|
|
13341
|
+
false
|
|
13342
|
+
).option(
|
|
13343
|
+
"--here",
|
|
13344
|
+
"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",
|
|
13345
|
+
false
|
|
13346
|
+
).option(
|
|
13347
|
+
"--workspace <id>",
|
|
13348
|
+
"bind this directory to a workspace (skips the interactive workspace pick)"
|
|
13349
|
+
).option(
|
|
13350
|
+
"--cli-only",
|
|
13351
|
+
"configure the CLI only \u2014 don't wire any AI client (no MCP config, no agent files)",
|
|
13352
|
+
false
|
|
13353
|
+
).option(
|
|
13354
|
+
"--no-mcp",
|
|
13355
|
+
"skip the MCP server config (.mcp.json etc.); still write the agent instruction files"
|
|
13356
|
+
).option(
|
|
13357
|
+
"--copy",
|
|
13358
|
+
"make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)"
|
|
13359
|
+
).option(
|
|
13360
|
+
"--dry-run",
|
|
13361
|
+
"walk through without writing files or changing the profile",
|
|
13362
|
+
false
|
|
13363
|
+
).option(
|
|
13364
|
+
"--refresh",
|
|
13365
|
+
"re-fetch descriptors and refresh any out-of-date managed blocks (local edits preserved to .proposed)",
|
|
13366
|
+
false
|
|
13367
|
+
).option(
|
|
13368
|
+
"--force",
|
|
13369
|
+
"rewrite every managed block, overwriting local edits inside the markers (content outside untouched)",
|
|
13370
|
+
false
|
|
13371
|
+
).option(
|
|
13372
|
+
"--check",
|
|
13373
|
+
"report whether anything would change and exit (0 = current/stub or unresolved warning, 1 = stale/drift/absent); writes nothing",
|
|
13374
|
+
false
|
|
13375
|
+
).option(
|
|
13376
|
+
"-y, --yes",
|
|
13377
|
+
"non-interactive: accept defaults (system timezone, detected clients, global config, full wire)",
|
|
13378
|
+
false
|
|
13379
|
+
).addHelpText(
|
|
12108
13380
|
"after",
|
|
12109
13381
|
`
|
|
12110
13382
|
Examples:
|
|
@@ -12127,20 +13399,56 @@ Examples:
|
|
|
12127
13399
|
const check = mode === "check";
|
|
12128
13400
|
const yes = Boolean(opts.yes) || check;
|
|
12129
13401
|
if (check && (opts.recurse || opts.cliOnly)) {
|
|
12130
|
-
fail(
|
|
13402
|
+
fail(
|
|
13403
|
+
"--check inspects this project's agent files and cannot be combined with --recurse or --cli-only."
|
|
13404
|
+
);
|
|
12131
13405
|
}
|
|
12132
13406
|
if (opts.lane) process.env.SECHROOM_CODE_LANE = opts.lane;
|
|
12133
13407
|
if (opts.designLane) process.env.SECHROOM_DESIGN_LANE = opts.designLane;
|
|
12134
13408
|
if (opts.recurse) {
|
|
12135
13409
|
const baseUrl2 = resolveBaseUrl(g);
|
|
12136
|
-
await ensureAuth(
|
|
12137
|
-
|
|
12138
|
-
|
|
13410
|
+
await ensureAuth(
|
|
13411
|
+
{
|
|
13412
|
+
baseUrl: baseUrl2,
|
|
13413
|
+
tenant: "",
|
|
13414
|
+
account: resolveAccountAlias(),
|
|
13415
|
+
clientId: readPersisted().clientId
|
|
13416
|
+
},
|
|
13417
|
+
yes
|
|
13418
|
+
);
|
|
13419
|
+
const cfg2 = await ensureTenant(baseUrl2, g, {
|
|
13420
|
+
yes: true,
|
|
13421
|
+
json,
|
|
13422
|
+
persist: false
|
|
13423
|
+
});
|
|
13424
|
+
await runRecurse(cfg2, g, {
|
|
13425
|
+
yes,
|
|
13426
|
+
dryRun,
|
|
13427
|
+
json,
|
|
13428
|
+
lane: opts.lane,
|
|
13429
|
+
designLane: opts.designLane
|
|
13430
|
+
});
|
|
12139
13431
|
return;
|
|
12140
13432
|
}
|
|
12141
13433
|
const baseUrl = resolveBaseUrl(g);
|
|
12142
|
-
await ensureAuth(
|
|
12143
|
-
|
|
13434
|
+
await ensureAuth(
|
|
13435
|
+
{
|
|
13436
|
+
baseUrl,
|
|
13437
|
+
tenant: "",
|
|
13438
|
+
account: resolveAccountAlias(),
|
|
13439
|
+
clientId: readPersisted().clientId
|
|
13440
|
+
},
|
|
13441
|
+
yes
|
|
13442
|
+
);
|
|
13443
|
+
const scope = await chooseScope(opts.scope, yes);
|
|
13444
|
+
const cfg = await ensureTenant(baseUrl, g, {
|
|
13445
|
+
yes,
|
|
13446
|
+
json,
|
|
13447
|
+
local: Boolean(opts.local) || scope === "project",
|
|
13448
|
+
here: scope === "project" ? true : Boolean(opts.here),
|
|
13449
|
+
workspace: opts.workspace,
|
|
13450
|
+
persist: !check
|
|
13451
|
+
});
|
|
12144
13452
|
const tz = await ensureTimezone(cfg, { yes, dryRun: dryRun || check });
|
|
12145
13453
|
if (!json && tz.action !== "already-set") {
|
|
12146
13454
|
const line = tz.action === "set" ? `${ok("\u2713")} timezone set to ${tz.timezone}
|
|
@@ -12150,21 +13458,48 @@ Examples:
|
|
|
12150
13458
|
process.stderr.write(line);
|
|
12151
13459
|
}
|
|
12152
13460
|
const wire = await chooseWire(opts, yes);
|
|
12153
|
-
const
|
|
12154
|
-
|
|
13461
|
+
const claudeTargets = resolveClaudeTargets({
|
|
13462
|
+
override: g.claudeConfigDir,
|
|
13463
|
+
scope,
|
|
13464
|
+
cwd: process.cwd()
|
|
13465
|
+
});
|
|
12155
13466
|
const codexHomes = resolveCodexHomes({ override: g.codexHome, scope });
|
|
12156
13467
|
if (scope === "project" && g.claudeConfigDir && !json) {
|
|
12157
|
-
process.stderr.write(
|
|
12158
|
-
|
|
13468
|
+
process.stderr.write(
|
|
13469
|
+
`${style.dim("(--claude-config-dir is ignored for --scope project \u2014 project files are repo-relative)")}
|
|
13470
|
+
`
|
|
13471
|
+
);
|
|
12159
13472
|
}
|
|
12160
13473
|
if (wire === "cli-only") {
|
|
12161
13474
|
if (json) {
|
|
12162
|
-
emit(
|
|
13475
|
+
emit(
|
|
13476
|
+
{
|
|
13477
|
+
dryRun,
|
|
13478
|
+
baseUrl: cfg.baseUrl,
|
|
13479
|
+
tenant: cfg.tenant,
|
|
13480
|
+
workspaceId: cfg.workspaceId ?? null,
|
|
13481
|
+
timezone: tz,
|
|
13482
|
+
wire,
|
|
13483
|
+
clients: []
|
|
13484
|
+
},
|
|
13485
|
+
true
|
|
13486
|
+
);
|
|
12163
13487
|
return;
|
|
12164
13488
|
}
|
|
12165
13489
|
if (!dryRun) {
|
|
12166
|
-
await ensureLanePin(cfg, {
|
|
12167
|
-
|
|
13490
|
+
await ensureLanePin(cfg, {
|
|
13491
|
+
yes,
|
|
13492
|
+
dryRun,
|
|
13493
|
+
clients: detectInstalledClients(process.cwd())
|
|
13494
|
+
});
|
|
13495
|
+
await maybeOfferHooks({
|
|
13496
|
+
yes,
|
|
13497
|
+
dryRun,
|
|
13498
|
+
cwd: process.cwd(),
|
|
13499
|
+
scope,
|
|
13500
|
+
claudeConfigDir: g.claudeConfigDir,
|
|
13501
|
+
codexHome: g.codexHome
|
|
13502
|
+
});
|
|
12168
13503
|
}
|
|
12169
13504
|
process.stdout.write(
|
|
12170
13505
|
`
|
|
@@ -12175,12 +13510,36 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
12175
13510
|
await printStarterPrompt("cli");
|
|
12176
13511
|
return;
|
|
12177
13512
|
}
|
|
12178
|
-
const
|
|
12179
|
-
|
|
12180
|
-
|
|
13513
|
+
const requestedKeys = await chooseClients(
|
|
13514
|
+
opts.client,
|
|
13515
|
+
yes,
|
|
13516
|
+
process.cwd()
|
|
13517
|
+
);
|
|
13518
|
+
const keys = scope === "project" ? requestedKeys.filter((key) => key !== "codex") : requestedKeys;
|
|
13519
|
+
if (scope === "project" && requestedKeys.includes("codex") && !json) {
|
|
13520
|
+
process.stderr.write(
|
|
13521
|
+
`${style.dim("Codex has no project scope \u2014 skipped (use --scope global for Codex).")}
|
|
13522
|
+
`
|
|
13523
|
+
);
|
|
13524
|
+
}
|
|
13525
|
+
const setup = await withSpinner(
|
|
13526
|
+
"Fetching setup descriptors",
|
|
13527
|
+
() => fetchSetup(cfg)
|
|
13528
|
+
);
|
|
13529
|
+
const targets = clientTargets(process.cwd(), {
|
|
13530
|
+
claudeDir: claudeTargets[0]?.dir,
|
|
13531
|
+
codexHome: codexHomes[0] ?? null
|
|
13532
|
+
});
|
|
12181
13533
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
12182
13534
|
if (!dryRun && !check) {
|
|
12183
|
-
await maybeOfferCopies(
|
|
13535
|
+
await maybeOfferCopies(
|
|
13536
|
+
cfg,
|
|
13537
|
+
setup,
|
|
13538
|
+
targets,
|
|
13539
|
+
keys,
|
|
13540
|
+
personalWorkspaceId,
|
|
13541
|
+
copyChoice(opts)
|
|
13542
|
+
);
|
|
12184
13543
|
}
|
|
12185
13544
|
const writeMcp = wire === "full";
|
|
12186
13545
|
const result = [];
|
|
@@ -12197,16 +13556,11 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
12197
13556
|
if (!json && !check) printActions(target, actions);
|
|
12198
13557
|
}
|
|
12199
13558
|
if (check) {
|
|
12200
|
-
reportCheckAndExit(
|
|
12201
|
-
|
|
12202
|
-
|
|
12203
|
-
|
|
12204
|
-
|
|
12205
|
-
baseUrl: cfg.baseUrl,
|
|
12206
|
-
tenant: cfg.tenant,
|
|
12207
|
-
workspaceId: cfg.workspaceId ?? null
|
|
12208
|
-
}
|
|
12209
|
-
);
|
|
13559
|
+
reportCheckAndExit(result, json, "sechroom onboard --refresh", {
|
|
13560
|
+
baseUrl: cfg.baseUrl,
|
|
13561
|
+
tenant: cfg.tenant,
|
|
13562
|
+
workspaceId: cfg.workspaceId ?? null
|
|
13563
|
+
});
|
|
12210
13564
|
}
|
|
12211
13565
|
const evalCounts = buildCheckReport(result).eval;
|
|
12212
13566
|
if (!json && !dryRun) {
|
|
@@ -12214,19 +13568,45 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
12214
13568
|
}
|
|
12215
13569
|
if (!json && !dryRun) {
|
|
12216
13570
|
for (const t of claudeTargets) {
|
|
12217
|
-
await maybeOfferSkills(cfg, personalWorkspaceId, {
|
|
13571
|
+
await maybeOfferSkills(cfg, personalWorkspaceId, {
|
|
13572
|
+
yes,
|
|
13573
|
+
dryRun,
|
|
13574
|
+
surface: "claude-code",
|
|
13575
|
+
configDir: t.dir
|
|
13576
|
+
});
|
|
12218
13577
|
}
|
|
12219
13578
|
}
|
|
12220
13579
|
if (!json && !dryRun) {
|
|
12221
|
-
await maybeOfferHooks({
|
|
13580
|
+
await maybeOfferHooks({
|
|
13581
|
+
yes,
|
|
13582
|
+
dryRun,
|
|
13583
|
+
cwd: process.cwd(),
|
|
13584
|
+
scope,
|
|
13585
|
+
claudeConfigDir: g.claudeConfigDir,
|
|
13586
|
+
codexHome: g.codexHome
|
|
13587
|
+
});
|
|
12222
13588
|
}
|
|
12223
13589
|
if (json) {
|
|
12224
|
-
emit(
|
|
13590
|
+
emit(
|
|
13591
|
+
{
|
|
13592
|
+
dryRun,
|
|
13593
|
+
baseUrl: cfg.baseUrl,
|
|
13594
|
+
tenant: cfg.tenant,
|
|
13595
|
+
workspaceId: cfg.workspaceId ?? null,
|
|
13596
|
+
timezone: tz,
|
|
13597
|
+
wire,
|
|
13598
|
+
eval: evalCounts,
|
|
13599
|
+
clients: result
|
|
13600
|
+
},
|
|
13601
|
+
true
|
|
13602
|
+
);
|
|
12225
13603
|
return;
|
|
12226
13604
|
}
|
|
12227
13605
|
if (!dryRun && evalCounts.stale) {
|
|
12228
|
-
process.stderr.write(
|
|
12229
|
-
|
|
13606
|
+
process.stderr.write(
|
|
13607
|
+
`${style.cyan("\u21BB")} refreshed ${evalCounts.stale} section(s) the server had moved
|
|
13608
|
+
`
|
|
13609
|
+
);
|
|
12230
13610
|
}
|
|
12231
13611
|
if (!dryRun && evalCounts.drift) {
|
|
12232
13612
|
process.stderr.write(
|
|
@@ -12235,7 +13615,9 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
12235
13615
|
`
|
|
12236
13616
|
);
|
|
12237
13617
|
}
|
|
12238
|
-
const wroteSomething = result.some(
|
|
13618
|
+
const wroteSomething = result.some(
|
|
13619
|
+
({ actions }) => actions.some((a) => a.status === "created" || a.status === "merged")
|
|
13620
|
+
);
|
|
12239
13621
|
process.stdout.write(
|
|
12240
13622
|
dryRun ? "\n(dry run \u2014 nothing written)\n" : !wroteSomething ? `
|
|
12241
13623
|
${style.bold("Done.")} Everything's already up to date.
|
|
@@ -12254,9 +13636,21 @@ async function chooseWire(opts, yes) {
|
|
|
12254
13636
|
return promptSelect(
|
|
12255
13637
|
"How should I set up Sechroom in this project?",
|
|
12256
13638
|
[
|
|
12257
|
-
{
|
|
12258
|
-
|
|
12259
|
-
|
|
13639
|
+
{
|
|
13640
|
+
label: "Wire my AI client",
|
|
13641
|
+
value: "full",
|
|
13642
|
+
hint: "MCP server (.mcp.json) + agent instructions"
|
|
13643
|
+
},
|
|
13644
|
+
{
|
|
13645
|
+
label: "Agent instructions only",
|
|
13646
|
+
value: "agent-only",
|
|
13647
|
+
hint: "skip MCP config"
|
|
13648
|
+
},
|
|
13649
|
+
{
|
|
13650
|
+
label: "CLI only",
|
|
13651
|
+
value: "cli-only",
|
|
13652
|
+
hint: "don't write any AI-client files"
|
|
13653
|
+
}
|
|
12260
13654
|
],
|
|
12261
13655
|
"full"
|
|
12262
13656
|
);
|
|
@@ -12278,7 +13672,9 @@ ${rule}
|
|
|
12278
13672
|
}
|
|
12279
13673
|
async function printStarterPrompt(mode, cfg) {
|
|
12280
13674
|
if (mode === "cli") {
|
|
12281
|
-
printNextStepBlock("Next \u2014 pick up where you left off:", [
|
|
13675
|
+
printNextStepBlock("Next \u2014 pick up where you left off:", [
|
|
13676
|
+
style.cyan("sechroom continuity resume-me")
|
|
13677
|
+
]);
|
|
12282
13678
|
return;
|
|
12283
13679
|
}
|
|
12284
13680
|
let primary = FALLBACK_AGENT_PROMPT;
|
|
@@ -12290,7 +13686,9 @@ async function printStarterPrompt(mode, cfg) {
|
|
|
12290
13686
|
} catch {
|
|
12291
13687
|
}
|
|
12292
13688
|
}
|
|
12293
|
-
printNextStepBlock("Next \u2014 paste this into your AI agent to get going:", [
|
|
13689
|
+
printNextStepBlock("Next \u2014 paste this into your AI agent to get going:", [
|
|
13690
|
+
style.cyan(`"${primary}"`)
|
|
13691
|
+
]);
|
|
12294
13692
|
}
|
|
12295
13693
|
|
|
12296
13694
|
// src/commands/project.ts
|
|
@@ -12443,7 +13841,13 @@ Examples:
|
|
|
12443
13841
|
$ sechroom relationship suggestions --status Pending --memory mem_XXXX
|
|
12444
13842
|
$ sechroom relationship suggestion accept rsg_XXXX`
|
|
12445
13843
|
);
|
|
12446
|
-
relationship.command("create <fromMemoryId> <toMemoryId>").description(
|
|
13844
|
+
relationship.command("create <fromMemoryId> <toMemoryId>").description(
|
|
13845
|
+
"Create a relationship (POST /memories/{memoryId}/relationships)"
|
|
13846
|
+
).option(
|
|
13847
|
+
"--type <type>",
|
|
13848
|
+
"Relationship type (Reference, Related, Parent, Child, Follows, \u2026)",
|
|
13849
|
+
"Reference"
|
|
13850
|
+
).option(
|
|
12447
13851
|
"--to-version <number>",
|
|
12448
13852
|
"Version of the target memory to pin the edge to (defaults to the target's current version)"
|
|
12449
13853
|
).action(async (fromMemoryId, toMemoryId, opts, cmd) => {
|
|
@@ -12453,15 +13857,21 @@ Examples:
|
|
|
12453
13857
|
if (opts.toVersion !== void 0) {
|
|
12454
13858
|
toVersion = Number(opts.toVersion);
|
|
12455
13859
|
if (!Number.isInteger(toVersion) || toVersion < 1) {
|
|
12456
|
-
fail(
|
|
13860
|
+
fail(
|
|
13861
|
+
`--to-version must be a positive integer (got '${opts.toVersion}').`
|
|
13862
|
+
);
|
|
12457
13863
|
}
|
|
12458
13864
|
} else {
|
|
12459
13865
|
const target = await runApi(
|
|
12460
13866
|
"Resolving target version",
|
|
12461
|
-
async () => client.GET("/memories/{memoryId}", {
|
|
13867
|
+
async () => client.GET("/memories/{memoryId}", {
|
|
13868
|
+
params: { path: { memoryId: toMemoryId } }
|
|
13869
|
+
})
|
|
12462
13870
|
);
|
|
12463
13871
|
if (typeof target.item?.currentVersion !== "number") {
|
|
12464
|
-
fail(
|
|
13872
|
+
fail(
|
|
13873
|
+
`Could not resolve the current version of ${toMemoryId}; pass --to-version explicitly.`
|
|
13874
|
+
);
|
|
12465
13875
|
}
|
|
12466
13876
|
toVersion = target.item.currentVersion;
|
|
12467
13877
|
}
|
|
@@ -12485,22 +13895,28 @@ Examples:
|
|
|
12485
13895
|
cmd.optsWithGlobals().json
|
|
12486
13896
|
);
|
|
12487
13897
|
});
|
|
12488
|
-
relationship.command("list <memoryId>").description(
|
|
13898
|
+
relationship.command("list <memoryId>").description(
|
|
13899
|
+
"List a memory's relationships (GET /memories/{memoryId}/relationships). Walks every page by default."
|
|
13900
|
+
).option("--direction <direction>", "Both | Outbound | Inbound").option("--include-deleted", "Include deleted relationships", false).option(...PAGE_OPTION).option(...PAGE_SIZE_OPTION).option(...NO_AUTO_PAGE_OPTION).action(async (memoryId, opts, cmd) => {
|
|
12489
13901
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
12490
|
-
const
|
|
13902
|
+
const filters = {
|
|
13903
|
+
...opts.direction ? { direction: opts.direction } : {},
|
|
13904
|
+
...opts.includeDeleted ? { includeDeleted: true } : {}
|
|
13905
|
+
};
|
|
13906
|
+
const readPage = (query) => runApi("Listing relationships", async () => {
|
|
12491
13907
|
const client = await makeClient(cfg);
|
|
12492
13908
|
return client.GET("/memories/{memoryId}/relationships", {
|
|
12493
|
-
params: {
|
|
12494
|
-
path: { memoryId },
|
|
12495
|
-
query: {
|
|
12496
|
-
...opts.direction ? { direction: opts.direction } : {},
|
|
12497
|
-
...opts.includeDeleted ? { includeDeleted: true } : {},
|
|
12498
|
-
...opts.page ? { page: Number(opts.page) } : {},
|
|
12499
|
-
...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
|
|
12500
|
-
}
|
|
12501
|
-
}
|
|
13909
|
+
params: { path: { memoryId }, query: { ...filters, ...query } }
|
|
12502
13910
|
});
|
|
12503
13911
|
});
|
|
13912
|
+
const data = shouldAutoPage(opts) ? await fetchAllPages(
|
|
13913
|
+
(page) => readPage({
|
|
13914
|
+
page,
|
|
13915
|
+
...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
|
|
13916
|
+
}),
|
|
13917
|
+
opts,
|
|
13918
|
+
"relationships"
|
|
13919
|
+
) : await readPage(singlePageQuery(opts));
|
|
12504
13920
|
emit(data, cmd.optsWithGlobals().json);
|
|
12505
13921
|
});
|
|
12506
13922
|
relationship.command("delete <id>").description("Delete a relationship (DELETE /relationships/{id})").action(async (id, _opts, cmd) => {
|
|
@@ -12512,9 +13928,15 @@ Examples:
|
|
|
12512
13928
|
body: {}
|
|
12513
13929
|
});
|
|
12514
13930
|
});
|
|
12515
|
-
emitAction(
|
|
13931
|
+
emitAction(
|
|
13932
|
+
`deleted relationship ${style.bold(id)}`,
|
|
13933
|
+
data,
|
|
13934
|
+
cmd.optsWithGlobals().json
|
|
13935
|
+
);
|
|
12516
13936
|
});
|
|
12517
|
-
relationship.command("suggest <memoryId>").description(
|
|
13937
|
+
relationship.command("suggest <memoryId>").description(
|
|
13938
|
+
"Generate relationship suggestions for a memory (POST /memories/{memoryId}/suggest-relationships)"
|
|
13939
|
+
).option("--limit <n>", "Max suggestions to generate").action(async (memoryId, opts, cmd) => {
|
|
12518
13940
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
12519
13941
|
const data = await runApi("Suggesting relationships", async () => {
|
|
12520
13942
|
const client = await makeClient(cfg);
|
|
@@ -12531,38 +13953,51 @@ Examples:
|
|
|
12531
13953
|
cmd.optsWithGlobals().json
|
|
12532
13954
|
);
|
|
12533
13955
|
});
|
|
12534
|
-
relationship.command("suggestions").description(
|
|
13956
|
+
relationship.command("suggestions").description(
|
|
13957
|
+
"List relationship suggestions (GET /relationship-suggestions)"
|
|
13958
|
+
).option("--memory <memoryId>", "Filter to a memory").option(
|
|
12535
13959
|
"--status <status>",
|
|
12536
13960
|
"Pending | Accepted | EditedAndAccepted | Rejected | Superseded | Deferred | Invalidated"
|
|
12537
|
-
).option(
|
|
13961
|
+
).option(...PAGE_OPTION).option(...PAGE_SIZE_OPTION).option(...NO_AUTO_PAGE_OPTION).action(async (opts, cmd) => {
|
|
12538
13962
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
12539
|
-
const
|
|
13963
|
+
const filters = {
|
|
13964
|
+
...opts.memory ? { memoryId: opts.memory } : {},
|
|
13965
|
+
...opts.status ? {
|
|
13966
|
+
status: opts.status
|
|
13967
|
+
} : {}
|
|
13968
|
+
};
|
|
13969
|
+
const readPage = (query) => runApi("Listing suggestions", async () => {
|
|
12540
13970
|
const client = await makeClient(cfg);
|
|
12541
13971
|
return client.GET("/relationship-suggestions", {
|
|
12542
|
-
params: {
|
|
12543
|
-
query: {
|
|
12544
|
-
...opts.memory ? { memoryId: opts.memory } : {},
|
|
12545
|
-
...opts.status ? {
|
|
12546
|
-
status: opts.status
|
|
12547
|
-
} : {},
|
|
12548
|
-
...opts.page ? { page: Number(opts.page) } : {},
|
|
12549
|
-
...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
|
|
12550
|
-
}
|
|
12551
|
-
}
|
|
13972
|
+
params: { query: { ...filters, ...query } }
|
|
12552
13973
|
});
|
|
12553
13974
|
});
|
|
13975
|
+
const data = shouldAutoPage(opts) ? await fetchAllPages(
|
|
13976
|
+
(page) => readPage({
|
|
13977
|
+
page,
|
|
13978
|
+
...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
|
|
13979
|
+
}),
|
|
13980
|
+
opts,
|
|
13981
|
+
"suggestions"
|
|
13982
|
+
) : await readPage(singlePageQuery(opts));
|
|
12554
13983
|
emit(data, cmd.optsWithGlobals().json);
|
|
12555
13984
|
});
|
|
12556
13985
|
const suggestion = relationship.command("suggestion").description("Inspect and decide on a single relationship suggestion");
|
|
12557
|
-
suggestion.command("get <id>").description(
|
|
13986
|
+
suggestion.command("get <id>").description(
|
|
13987
|
+
"Fetch a suggestion by id (GET /relationship-suggestions/{id})"
|
|
13988
|
+
).action(async (id, _opts, cmd) => {
|
|
12558
13989
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
12559
13990
|
const data = await runApi("Fetching suggestion", async () => {
|
|
12560
13991
|
const client = await makeClient(cfg);
|
|
12561
|
-
return client.GET("/relationship-suggestions/{id}", {
|
|
13992
|
+
return client.GET("/relationship-suggestions/{id}", {
|
|
13993
|
+
params: { path: { id } }
|
|
13994
|
+
});
|
|
12562
13995
|
});
|
|
12563
13996
|
emit(data, cmd.optsWithGlobals().json);
|
|
12564
13997
|
});
|
|
12565
|
-
suggestion.command("accept <id>").description(
|
|
13998
|
+
suggestion.command("accept <id>").description(
|
|
13999
|
+
"Accept a suggestion (POST /relationship-suggestions/{instanceId}/accept)"
|
|
14000
|
+
).action(async (id, _opts, cmd) => {
|
|
12566
14001
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
12567
14002
|
const data = await runApi("Accepting suggestion", async () => {
|
|
12568
14003
|
const client = await makeClient(cfg);
|
|
@@ -12571,9 +14006,15 @@ Examples:
|
|
|
12571
14006
|
body: {}
|
|
12572
14007
|
});
|
|
12573
14008
|
});
|
|
12574
|
-
emitAction(
|
|
14009
|
+
emitAction(
|
|
14010
|
+
`accepted suggestion ${style.bold(id)}`,
|
|
14011
|
+
data,
|
|
14012
|
+
cmd.optsWithGlobals().json
|
|
14013
|
+
);
|
|
12575
14014
|
});
|
|
12576
|
-
suggestion.command("reject <id>").description(
|
|
14015
|
+
suggestion.command("reject <id>").description(
|
|
14016
|
+
"Reject a suggestion (POST /relationship-suggestions/{instanceId}/reject)"
|
|
14017
|
+
).option("--reason <reason>", "Why it's being rejected").option("--reason-code <code>", "Structured reason code").action(async (id, opts, cmd) => {
|
|
12577
14018
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
12578
14019
|
const data = await runApi("Rejecting suggestion", async () => {
|
|
12579
14020
|
const client = await makeClient(cfg);
|
|
@@ -12585,9 +14026,18 @@ Examples:
|
|
|
12585
14026
|
}
|
|
12586
14027
|
});
|
|
12587
14028
|
});
|
|
12588
|
-
emitAction(
|
|
14029
|
+
emitAction(
|
|
14030
|
+
`rejected suggestion ${style.bold(id)}`,
|
|
14031
|
+
data,
|
|
14032
|
+
cmd.optsWithGlobals().json
|
|
14033
|
+
);
|
|
12589
14034
|
});
|
|
12590
|
-
suggestion.command("defer <id>").description(
|
|
14035
|
+
suggestion.command("defer <id>").description(
|
|
14036
|
+
"Defer a suggestion (POST /relationship-suggestions/{id}/defer)"
|
|
14037
|
+
).option(
|
|
14038
|
+
"--until <iso>",
|
|
14039
|
+
"Defer until this ISO date-time (omit to defer indefinitely)"
|
|
14040
|
+
).action(async (id, opts, cmd) => {
|
|
12591
14041
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
12592
14042
|
const data = await runApi("Deferring suggestion", async () => {
|
|
12593
14043
|
const client = await makeClient(cfg);
|
|
@@ -12598,14 +14048,18 @@ Examples:
|
|
|
12598
14048
|
}
|
|
12599
14049
|
});
|
|
12600
14050
|
});
|
|
12601
|
-
emitAction(
|
|
14051
|
+
emitAction(
|
|
14052
|
+
`deferred suggestion ${style.bold(id)}`,
|
|
14053
|
+
data,
|
|
14054
|
+
cmd.optsWithGlobals().json
|
|
14055
|
+
);
|
|
12602
14056
|
});
|
|
12603
14057
|
}
|
|
12604
14058
|
|
|
12605
14059
|
// src/commands/reset.ts
|
|
12606
14060
|
import { homedir as homedir6 } from "os";
|
|
12607
14061
|
import { join as join22 } from "path";
|
|
12608
|
-
import { existsSync as existsSync16, readFileSync as readFileSync18, rmSync as
|
|
14062
|
+
import { existsSync as existsSync16, readFileSync as readFileSync18, rmSync as rmSync8 } from "fs";
|
|
12609
14063
|
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
12610
14064
|
var localSkillsDir = () => join22(process.cwd(), ".claude", "skills");
|
|
12611
14065
|
var globalSkillsDir = () => join22(homedir6(), ".claude", "skills");
|
|
@@ -12621,14 +14075,14 @@ function removeMaterialisedSkills(dir) {
|
|
|
12621
14075
|
for (const name of entry.skills ?? []) {
|
|
12622
14076
|
const p = join22(dir, name);
|
|
12623
14077
|
if (existsSync16(p)) {
|
|
12624
|
-
|
|
14078
|
+
rmSync8(p, { recursive: true, force: true });
|
|
12625
14079
|
removed.push(p);
|
|
12626
14080
|
}
|
|
12627
14081
|
}
|
|
12628
14082
|
}
|
|
12629
14083
|
} catch {
|
|
12630
14084
|
}
|
|
12631
|
-
|
|
14085
|
+
rmSync8(lockPath, { force: true });
|
|
12632
14086
|
removed.push(lockPath);
|
|
12633
14087
|
return removed;
|
|
12634
14088
|
}
|
|
@@ -12667,17 +14121,17 @@ function registerReset(program2) {
|
|
|
12667
14121
|
const removed = [];
|
|
12668
14122
|
const stateDir = join22(process.cwd(), ".sechroom");
|
|
12669
14123
|
if (existsSync16(stateDir)) {
|
|
12670
|
-
|
|
14124
|
+
rmSync8(stateDir, { recursive: true, force: true });
|
|
12671
14125
|
removed.push(stateDir);
|
|
12672
14126
|
}
|
|
12673
14127
|
const legacyCfg = join22(process.cwd(), ".sechroom.json");
|
|
12674
14128
|
if (existsSync16(legacyCfg)) {
|
|
12675
|
-
|
|
14129
|
+
rmSync8(legacyCfg, { force: true });
|
|
12676
14130
|
removed.push(legacyCfg);
|
|
12677
14131
|
}
|
|
12678
14132
|
const legacySem = join22(process.cwd(), ".sem");
|
|
12679
14133
|
if (existsSync16(legacySem)) {
|
|
12680
|
-
|
|
14134
|
+
rmSync8(legacySem, { force: true });
|
|
12681
14135
|
removed.push(legacySem);
|
|
12682
14136
|
}
|
|
12683
14137
|
removed.push(...removeMaterialisedSkills(localSkillsDir()));
|
|
@@ -12702,7 +14156,7 @@ function registerReset(program2) {
|
|
|
12702
14156
|
}
|
|
12703
14157
|
|
|
12704
14158
|
// src/commands/skills.ts
|
|
12705
|
-
import { existsSync as existsSync17, mkdirSync as mkdirSync18, statSync as
|
|
14159
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync18, statSync as statSync7, writeFileSync as writeFileSync17 } from "fs";
|
|
12706
14160
|
import { join as join23 } from "path";
|
|
12707
14161
|
function filenameFromDisposition(header) {
|
|
12708
14162
|
if (!header) return void 0;
|
|
@@ -12712,7 +14166,7 @@ function filenameFromDisposition(header) {
|
|
|
12712
14166
|
function resolveOutputPath(output, serverFilename) {
|
|
12713
14167
|
const filename = serverFilename || "skills.zip";
|
|
12714
14168
|
if (!output) return join23(process.cwd(), filename);
|
|
12715
|
-
const looksLikeDir = output.endsWith("/") || existsSync17(output) &&
|
|
14169
|
+
const looksLikeDir = output.endsWith("/") || existsSync17(output) && statSync7(output).isDirectory();
|
|
12716
14170
|
if (looksLikeDir) {
|
|
12717
14171
|
mkdirSync18(output, { recursive: true });
|
|
12718
14172
|
return join23(output, filename);
|
|
@@ -13311,17 +14765,15 @@ Examples:
|
|
|
13311
14765
|
$ sechroom work-task mark-no-residue mem_XXXX --decomposition wlp_XXXX
|
|
13312
14766
|
$ sechroom work-task residue-produce mem_XXXX --file residue.json`
|
|
13313
14767
|
);
|
|
13314
|
-
workTask.command("list").description(
|
|
14768
|
+
workTask.command("list").description(
|
|
14769
|
+
"List work tasks, newest-first (GET /work-tasks). Walks every page by default."
|
|
14770
|
+
).option("--shape <shape>", "Filter: bare | managed").option(
|
|
13315
14771
|
"--lane <lane>",
|
|
13316
14772
|
"Filter by dispatch-lane value, e.g. claude-code-chris"
|
|
13317
|
-
).option("--status <status>", "Filter by status value, e.g. in-progress").option(
|
|
13318
|
-
"--page-size <n>",
|
|
13319
|
-
"Page size (default 50, capped 200)",
|
|
13320
|
-
(v) => Number.parseInt(v, 10)
|
|
13321
|
-
).action(async (opts, cmd) => {
|
|
14773
|
+
).option("--status <status>", "Filter by status value, e.g. in-progress").option(...PAGE_OPTION).option(...PAGE_SIZE_OPTION).option(...NO_AUTO_PAGE_OPTION).action(async (opts, cmd) => {
|
|
13322
14774
|
const globals = cmd.optsWithGlobals();
|
|
13323
14775
|
const cfg = resolveConfig(globals);
|
|
13324
|
-
const
|
|
14776
|
+
const readPage = (query) => runApi("Listing work tasks", async () => {
|
|
13325
14777
|
const client = await makeClient(cfg);
|
|
13326
14778
|
return client.GET("/work-tasks", {
|
|
13327
14779
|
params: {
|
|
@@ -13329,12 +14781,19 @@ Examples:
|
|
|
13329
14781
|
shape: opts.shape,
|
|
13330
14782
|
lane: opts.lane,
|
|
13331
14783
|
status: opts.status,
|
|
13332
|
-
|
|
13333
|
-
pageSize: opts.pageSize
|
|
14784
|
+
...query
|
|
13334
14785
|
}
|
|
13335
14786
|
}
|
|
13336
14787
|
});
|
|
13337
14788
|
});
|
|
14789
|
+
const data = shouldAutoPage(opts) ? await fetchAllPages(
|
|
14790
|
+
(page) => readPage({
|
|
14791
|
+
page,
|
|
14792
|
+
...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
|
|
14793
|
+
}),
|
|
14794
|
+
opts,
|
|
14795
|
+
"tasks"
|
|
14796
|
+
) : await readPage(singlePageQuery(opts));
|
|
13338
14797
|
emitAction(
|
|
13339
14798
|
`listed ${style.bold(String(data.items.length))} of ${data.count} task(s)`,
|
|
13340
14799
|
data,
|