@sechroom/cli 2026.8.3-rc.fc3795ae7 → 2026.8.3
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 +1667 -320
- 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}
|
|
@@ -4366,9 +4411,19 @@ import { dirname as dirname5, join as join7 } from "path";
|
|
|
4366
4411
|
function claudeDesktopConfigPath(home) {
|
|
4367
4412
|
switch (process.platform) {
|
|
4368
4413
|
case "darwin":
|
|
4369
|
-
return join7(
|
|
4414
|
+
return join7(
|
|
4415
|
+
home,
|
|
4416
|
+
"Library",
|
|
4417
|
+
"Application Support",
|
|
4418
|
+
"Claude",
|
|
4419
|
+
"claude_desktop_config.json"
|
|
4420
|
+
);
|
|
4370
4421
|
case "win32":
|
|
4371
|
-
return join7(
|
|
4422
|
+
return join7(
|
|
4423
|
+
process.env.APPDATA ?? join7(home, "AppData", "Roaming"),
|
|
4424
|
+
"Claude",
|
|
4425
|
+
"claude_desktop_config.json"
|
|
4426
|
+
);
|
|
4372
4427
|
default:
|
|
4373
4428
|
return join7(home, ".config", "Claude", "claude_desktop_config.json");
|
|
4374
4429
|
}
|
|
@@ -4376,30 +4431,53 @@ function claudeDesktopConfigPath(home) {
|
|
|
4376
4431
|
function clientTargets(cwd, opts = {}) {
|
|
4377
4432
|
const home = homedir3();
|
|
4378
4433
|
const claudeDir = opts.claudeDir ?? join7(home, ".claude");
|
|
4379
|
-
const codexHome = opts.codexHome
|
|
4434
|
+
const codexHome = opts.codexHome === void 0 ? join7(home, ".codex") : opts.codexHome;
|
|
4380
4435
|
return {
|
|
4381
4436
|
"claude-code": {
|
|
4382
4437
|
key: "claude-code",
|
|
4383
4438
|
label: "Claude Code",
|
|
4384
|
-
mcp: {
|
|
4439
|
+
mcp: {
|
|
4440
|
+
surfaceKey: "claude-code",
|
|
4441
|
+
sectionType: SectionType.McpConfig,
|
|
4442
|
+
path: join7(cwd, ".mcp.json"),
|
|
4443
|
+
format: "json"
|
|
4444
|
+
},
|
|
4385
4445
|
instruction: { surfaceKey: "claude-code", path: join7(cwd, "CLAUDE.md") }
|
|
4386
4446
|
},
|
|
4387
4447
|
"claude-desktop": {
|
|
4388
4448
|
key: "claude-desktop",
|
|
4389
4449
|
label: "Claude Desktop",
|
|
4390
|
-
mcp: {
|
|
4391
|
-
|
|
4450
|
+
mcp: {
|
|
4451
|
+
surfaceKey: "claude-desktop",
|
|
4452
|
+
sectionType: SectionType.McpConfig,
|
|
4453
|
+
path: claudeDesktopConfigPath(home),
|
|
4454
|
+
format: "json"
|
|
4455
|
+
},
|
|
4456
|
+
instruction: {
|
|
4457
|
+
surfaceKey: "claude-desktop",
|
|
4458
|
+
path: join7(claudeDir, "CLAUDE.md")
|
|
4459
|
+
}
|
|
4392
4460
|
},
|
|
4393
4461
|
codex: {
|
|
4394
4462
|
key: "codex",
|
|
4395
4463
|
label: "Codex CLI",
|
|
4396
|
-
mcp:
|
|
4464
|
+
mcp: codexHome ? {
|
|
4465
|
+
surfaceKey: "chatgpt",
|
|
4466
|
+
sectionType: SectionType.McpConfigToml,
|
|
4467
|
+
path: join7(codexHome, "config.toml"),
|
|
4468
|
+
format: "toml"
|
|
4469
|
+
} : null,
|
|
4397
4470
|
instruction: { surfaceKey: "chatgpt", path: join7(cwd, "AGENTS.md") }
|
|
4398
4471
|
},
|
|
4399
4472
|
cursor: {
|
|
4400
4473
|
key: "cursor",
|
|
4401
4474
|
label: "Cursor",
|
|
4402
|
-
mcp: {
|
|
4475
|
+
mcp: {
|
|
4476
|
+
surfaceKey: "claude-code",
|
|
4477
|
+
sectionType: SectionType.McpConfig,
|
|
4478
|
+
path: join7(cwd, ".cursor", "mcp.json"),
|
|
4479
|
+
format: "json"
|
|
4480
|
+
},
|
|
4403
4481
|
instruction: { surfaceKey: "chatgpt", path: join7(cwd, "AGENTS.md") }
|
|
4404
4482
|
},
|
|
4405
4483
|
antigravity: {
|
|
@@ -4411,20 +4489,34 @@ function clientTargets(cwd, opts = {}) {
|
|
|
4411
4489
|
// `type` — comes from the `antigravity` server surface, so we don't
|
|
4412
4490
|
// hardcode it here. Instructions go in the project `AGENTS.md`
|
|
4413
4491
|
// (cross-tool, shared with Codex/Cursor).
|
|
4414
|
-
mcp: {
|
|
4492
|
+
mcp: {
|
|
4493
|
+
surfaceKey: "antigravity",
|
|
4494
|
+
sectionType: SectionType.McpConfig,
|
|
4495
|
+
path: join7(home, ".gemini", "config", "mcp_config.json"),
|
|
4496
|
+
format: "json"
|
|
4497
|
+
},
|
|
4415
4498
|
instruction: { surfaceKey: "antigravity", path: join7(cwd, "AGENTS.md") }
|
|
4416
4499
|
}
|
|
4417
4500
|
};
|
|
4418
4501
|
}
|
|
4419
|
-
var ALL_CLIENT_KEYS = [
|
|
4502
|
+
var ALL_CLIENT_KEYS = [
|
|
4503
|
+
"claude-code",
|
|
4504
|
+
"claude-desktop",
|
|
4505
|
+
"codex",
|
|
4506
|
+
"cursor",
|
|
4507
|
+
"antigravity"
|
|
4508
|
+
];
|
|
4420
4509
|
var DEFAULT_CLIENT_KEY = "claude-code";
|
|
4421
4510
|
function detectInstalledClients(cwd) {
|
|
4422
4511
|
const home = homedir3();
|
|
4423
4512
|
const detected = [];
|
|
4424
|
-
if (resolveClaudeTargets({}).some((t) => existsSync5(t.dir)))
|
|
4425
|
-
|
|
4513
|
+
if (resolveClaudeTargets({}).some((t) => existsSync5(t.dir)))
|
|
4514
|
+
detected.push("claude-code");
|
|
4515
|
+
if (existsSync5(dirname5(claudeDesktopConfigPath(home))))
|
|
4516
|
+
detected.push("claude-desktop");
|
|
4426
4517
|
if (resolveCodexHomes({}).some((d) => existsSync5(d))) detected.push("codex");
|
|
4427
|
-
if (existsSync5(join7(home, ".cursor")) || existsSync5(join7(cwd, ".cursor")))
|
|
4518
|
+
if (existsSync5(join7(home, ".cursor")) || existsSync5(join7(cwd, ".cursor")))
|
|
4519
|
+
detected.push("cursor");
|
|
4428
4520
|
if (existsSync5(join7(home, ".gemini"))) detected.push("antigravity");
|
|
4429
4521
|
return detected;
|
|
4430
4522
|
}
|
|
@@ -5751,6 +5843,7 @@ Call sechroom_lifecycle_signal at each phase boundary (start/work/verify/closeou
|
|
|
5751
5843
|
}
|
|
5752
5844
|
|
|
5753
5845
|
// src/commands/executor.ts
|
|
5846
|
+
var DEFAULT_CLAIM_POLICY = "restricted";
|
|
5754
5847
|
function executorSubscriptionInput(name) {
|
|
5755
5848
|
return {
|
|
5756
5849
|
name,
|
|
@@ -5812,8 +5905,8 @@ function registerExecutor(program2) {
|
|
|
5812
5905
|
"Capability operation keys claimed by this instance"
|
|
5813
5906
|
).option(
|
|
5814
5907
|
"--claim-policy <policy>",
|
|
5815
|
-
"open | restricted (restricted only claims preferred/own-lane/allow-listed-tag work)",
|
|
5816
|
-
|
|
5908
|
+
"open | restricted (restricted only claims preferred/own-lane/allow-listed-tag work; default restricted)",
|
|
5909
|
+
DEFAULT_CLAIM_POLICY
|
|
5817
5910
|
).option(
|
|
5818
5911
|
"--claim-tag <tag...>",
|
|
5819
5912
|
"Task tag this instance accepts under --claim-policy restricted"
|
|
@@ -5828,7 +5921,12 @@ function registerExecutor(program2) {
|
|
|
5828
5921
|
"--subscription-name <name>",
|
|
5829
5922
|
"SignalR delivery binding name",
|
|
5830
5923
|
"executor-dispatch"
|
|
5831
|
-
).option(
|
|
5924
|
+
).option(
|
|
5925
|
+
"--ttl <seconds>",
|
|
5926
|
+
"Advertisement TTL (30 to tenant maximum)",
|
|
5927
|
+
parseInteger,
|
|
5928
|
+
600
|
|
5929
|
+
).option(
|
|
5832
5930
|
"--task-lease-ttl <seconds>",
|
|
5833
5931
|
"Task lease TTL (60-86400)",
|
|
5834
5932
|
parseInteger,
|
|
@@ -5915,7 +6013,7 @@ function registerExecutor(program2) {
|
|
|
5915
6013
|
taskLeaseTtlSeconds: opts.taskLeaseTtl,
|
|
5916
6014
|
modelId: opts.modelId,
|
|
5917
6015
|
effortLabel: opts.effortLabel,
|
|
5918
|
-
claimPolicy: (opts.claimPolicy
|
|
6016
|
+
claimPolicy: parseClaimPolicy(opts.claimPolicy) === "Restricted" ? "restricted" : "open",
|
|
5919
6017
|
claimTags: opts.claimTag ?? [],
|
|
5920
6018
|
excludeTags: opts.excludeTag ?? [],
|
|
5921
6019
|
relayId: opts.relay,
|
|
@@ -6036,8 +6134,8 @@ function registerExecutor(program2) {
|
|
|
6036
6134
|
"Capability operation keys claimed by this instance"
|
|
6037
6135
|
).option(
|
|
6038
6136
|
"--claim-policy <policy>",
|
|
6039
|
-
"open | restricted (restricted only claims preferred/own-lane/allow-listed-tag work)",
|
|
6040
|
-
|
|
6137
|
+
"open | restricted (restricted only claims preferred/own-lane/allow-listed-tag work; default restricted)",
|
|
6138
|
+
DEFAULT_CLAIM_POLICY
|
|
6041
6139
|
).option(
|
|
6042
6140
|
"--claim-tag <tag...>",
|
|
6043
6141
|
"Task tag this instance accepts under --claim-policy restricted"
|
|
@@ -6048,7 +6146,12 @@ function registerExecutor(program2) {
|
|
|
6048
6146
|
"--activation-mode <mode>",
|
|
6049
6147
|
"attached | detached \u2014 detached marks a fleet run as a service that outlives its shell (default attached)",
|
|
6050
6148
|
"attached"
|
|
6051
|
-
).option(
|
|
6149
|
+
).option(
|
|
6150
|
+
"--ttl <seconds>",
|
|
6151
|
+
"Advertisement TTL (30 to tenant maximum)",
|
|
6152
|
+
parseInteger,
|
|
6153
|
+
120
|
|
6154
|
+
).option(
|
|
6052
6155
|
"--task-lease-ttl <seconds>",
|
|
6053
6156
|
"Task lease TTL (60-86400)",
|
|
6054
6157
|
parseInteger,
|
|
@@ -6104,7 +6207,12 @@ function registerExecutor(program2) {
|
|
|
6104
6207
|
`)
|
|
6105
6208
|
);
|
|
6106
6209
|
});
|
|
6107
|
-
executor.command("refresh <id>").description("Refresh one advertisement lease once").option(
|
|
6210
|
+
executor.command("refresh <id>").description("Refresh one advertisement lease once").option(
|
|
6211
|
+
"--ttl <seconds>",
|
|
6212
|
+
"Advertisement TTL (30 to tenant maximum)",
|
|
6213
|
+
parseInteger,
|
|
6214
|
+
120
|
|
6215
|
+
).action(async (id, opts, cmd) => {
|
|
6108
6216
|
const data = await refreshExecutorInstance(
|
|
6109
6217
|
resolveConfig(cmd.optsWithGlobals()),
|
|
6110
6218
|
id,
|
|
@@ -6112,7 +6220,12 @@ function registerExecutor(program2) {
|
|
|
6112
6220
|
);
|
|
6113
6221
|
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
6114
6222
|
});
|
|
6115
|
-
executor.command("heartbeat <id>").description("Keep an advertisement alive until interrupted").option(
|
|
6223
|
+
executor.command("heartbeat <id>").description("Keep an advertisement alive until interrupted").option(
|
|
6224
|
+
"--ttl <seconds>",
|
|
6225
|
+
"Advertisement TTL (30 to tenant maximum)",
|
|
6226
|
+
parseInteger,
|
|
6227
|
+
120
|
|
6228
|
+
).option("--interval <seconds>", "Refresh interval", parseInteger, 40).action(async (id, opts, cmd) => {
|
|
6116
6229
|
if (opts.interval >= opts.ttl)
|
|
6117
6230
|
fail("heartbeat interval must be shorter than the TTL");
|
|
6118
6231
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
@@ -6274,12 +6387,12 @@ function parseRuntimeKind(value) {
|
|
|
6274
6387
|
}
|
|
6275
6388
|
}
|
|
6276
6389
|
function parseClaimPolicy(value) {
|
|
6277
|
-
switch ((value ??
|
|
6390
|
+
switch ((value ?? DEFAULT_CLAIM_POLICY).trim().toLowerCase()) {
|
|
6278
6391
|
case "":
|
|
6279
|
-
case "open":
|
|
6280
|
-
return "Open";
|
|
6281
6392
|
case "restricted":
|
|
6282
6393
|
return "Restricted";
|
|
6394
|
+
case "open":
|
|
6395
|
+
return "Open";
|
|
6283
6396
|
default:
|
|
6284
6397
|
return fail("claim-policy must be open or restricted");
|
|
6285
6398
|
}
|
|
@@ -7607,6 +7720,7 @@ function resolveLane(flagLane, cwd) {
|
|
|
7607
7720
|
return applyWorktreeLaneSuffix(base, start);
|
|
7608
7721
|
}
|
|
7609
7722
|
var INTENT_FILE = join15(".sechroom", "continuity.json");
|
|
7723
|
+
var LOCAL_DRY_RUN_VALIDATION_WARNING = "LOCAL-ONLY \u2014 NOT SERVER-VALIDATED";
|
|
7610
7724
|
function resolveIntentPath(start) {
|
|
7611
7725
|
let dir = start;
|
|
7612
7726
|
for (; ; ) {
|
|
@@ -7631,6 +7745,16 @@ function hasRequiredIntent(i) {
|
|
|
7631
7745
|
i.objective?.trim() && i.state?.trim() && i.lastAction?.trim() && i.nextAction?.trim() && i.resumeInstruction?.trim()
|
|
7632
7746
|
);
|
|
7633
7747
|
}
|
|
7748
|
+
function localDryRunMissingFields(i) {
|
|
7749
|
+
const required = [
|
|
7750
|
+
["objective", "--objective"],
|
|
7751
|
+
["state", "--state"],
|
|
7752
|
+
["lastAction", "--last-action"],
|
|
7753
|
+
["nextAction", "--next-action"],
|
|
7754
|
+
["resumeInstruction", "--resume-instruction"]
|
|
7755
|
+
];
|
|
7756
|
+
return required.filter(([key]) => !String(i[key] ?? "").trim()).map(([, flag]) => flag);
|
|
7757
|
+
}
|
|
7634
7758
|
async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScope, opts) {
|
|
7635
7759
|
const lane = resolveLane(laneFlag, cwd);
|
|
7636
7760
|
if (!lane) return false;
|
|
@@ -7652,7 +7776,9 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
|
|
|
7652
7776
|
openQuestions: intent.questions ?? null,
|
|
7653
7777
|
surfaceMarkers: intent.surfaceMarkers ?? null,
|
|
7654
7778
|
relevantArtifactIds: intent.artifacts ?? null,
|
|
7655
|
-
|
|
7779
|
+
// Preserve invalid JSON-string confidence tokens for the server's semantic
|
|
7780
|
+
// validator; never coerce them through Number/NaN/null.
|
|
7781
|
+
confidence: confidenceWireValue(intent.confidence),
|
|
7656
7782
|
// Frequent triggers (compaction, session-end) land within the FR-051 4h
|
|
7657
7783
|
// window; Acknowledge lets the checkpoint persist on the lane.
|
|
7658
7784
|
concurrentSessionPolicy: "Acknowledge"
|
|
@@ -7947,12 +8073,18 @@ Fail-soft: failures exit 0 and never block; session-context refresh failures ren
|
|
|
7947
8073
|
function registerCheckpoint(program2) {
|
|
7948
8074
|
program2.command("checkpoint").description(
|
|
7949
8075
|
"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(
|
|
8076
|
+
).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(
|
|
8077
|
+
"--dry-run",
|
|
8078
|
+
"run the local required-field check and print the payload (LOCAL-ONLY; NOT SERVER-VALIDATED)",
|
|
8079
|
+
false
|
|
8080
|
+
).addHelpText(
|
|
7951
8081
|
"after",
|
|
7952
8082
|
`
|
|
7953
8083
|
File-first: reads ./.sechroom/continuity.json (kept current as you work) as the base; any flag
|
|
7954
8084
|
overrides that field. The snapshot is created FIRST (server-validated), then the local file is
|
|
7955
8085
|
written/normalized with the returned snapshotId. Lane: --lane > SECHROOM_LANE > ./.sechroom/lane.json code-lane.
|
|
8086
|
+
--dry-run performs only the local required-field check and prints a payload. Its output is
|
|
8087
|
+
explicitly LOCAL-ONLY; NOT SERVER-VALIDATED, so it does not establish write-path parity.
|
|
7956
8088
|
|
|
7957
8089
|
Examples:
|
|
7958
8090
|
$ sechroom checkpoint snapshot from ./.sechroom/continuity.json, then sync it
|
|
@@ -7975,7 +8107,9 @@ Examples:
|
|
|
7975
8107
|
questions: opts.question ?? base.questions,
|
|
7976
8108
|
surfaceMarkers: opts.surfaceMarker ?? base.surfaceMarkers,
|
|
7977
8109
|
artifacts: opts.artifact ?? base.artifacts,
|
|
7978
|
-
|
|
8110
|
+
// Keep the raw token until the server's shared validator sees it. In particular,
|
|
8111
|
+
// Number("high") -> NaN -> JSON null would silently discard the operator's input.
|
|
8112
|
+
confidence: opts.confidence != null ? opts.confidence : base.confidence
|
|
7979
8113
|
};
|
|
7980
8114
|
const lane = resolveLane(opts.lane, cwd);
|
|
7981
8115
|
if (!lane) {
|
|
@@ -7983,19 +8117,6 @@ Examples:
|
|
|
7983
8117
|
"no lane resolved \u2014 pass --lane, set SECHROOM_LANE, or pin one in ./.sechroom/lane.json (code-lane). See `sechroom lane`."
|
|
7984
8118
|
);
|
|
7985
8119
|
}
|
|
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
8120
|
const scope = merged.scope ?? "session";
|
|
8000
8121
|
const body = {
|
|
8001
8122
|
laneId: lane,
|
|
@@ -8009,13 +8130,33 @@ Examples:
|
|
|
8009
8130
|
openQuestions: merged.questions ?? null,
|
|
8010
8131
|
surfaceMarkers: merged.surfaceMarkers ?? null,
|
|
8011
8132
|
relevantArtifactIds: merged.artifacts ?? null,
|
|
8012
|
-
confidence: merged.confidence
|
|
8133
|
+
confidence: confidenceWireValue(merged.confidence),
|
|
8013
8134
|
// Explicit checkpoints are often within the FR-051 4h window; Acknowledge
|
|
8014
8135
|
// lets one land on the lane (matches `hook pre-compact`).
|
|
8015
8136
|
concurrentSessionPolicy: "Acknowledge"
|
|
8016
8137
|
};
|
|
8017
8138
|
if (opts.dryRun) {
|
|
8018
|
-
|
|
8139
|
+
const missing = localDryRunMissingFields(merged);
|
|
8140
|
+
if (missing.length > 0) {
|
|
8141
|
+
fail(
|
|
8142
|
+
`LOCAL-ONLY CHECK \u2014 NOT SERVER-VALIDATED: missing required field(s): ${missing.join(", ")} \u2014 supply via flag or in ./.sechroom/continuity.json`
|
|
8143
|
+
);
|
|
8144
|
+
}
|
|
8145
|
+
emit(
|
|
8146
|
+
{
|
|
8147
|
+
dryRun: true,
|
|
8148
|
+
validation: {
|
|
8149
|
+
mode: "local-only",
|
|
8150
|
+
serverValidated: false,
|
|
8151
|
+
checks: ["required-fields"],
|
|
8152
|
+
warning: LOCAL_DRY_RUN_VALIDATION_WARNING
|
|
8153
|
+
},
|
|
8154
|
+
lane,
|
|
8155
|
+
scope,
|
|
8156
|
+
wouldCreate: body
|
|
8157
|
+
},
|
|
8158
|
+
json
|
|
8159
|
+
);
|
|
8019
8160
|
return;
|
|
8020
8161
|
}
|
|
8021
8162
|
const data = await runApi("Creating snapshot", async () => {
|
|
@@ -8252,7 +8393,7 @@ Displaced snapshot recovery surfaces:
|
|
|
8252
8393
|
openQuestions: opts.question ?? null,
|
|
8253
8394
|
surfaceMarkers: opts.surfaceMarker ?? null,
|
|
8254
8395
|
relevantArtifactIds: opts.artifact ?? null,
|
|
8255
|
-
confidence:
|
|
8396
|
+
confidence: confidenceWireValue(opts.confidence)
|
|
8256
8397
|
}
|
|
8257
8398
|
});
|
|
8258
8399
|
});
|
|
@@ -8401,6 +8542,96 @@ function snapshotGetNotFoundHint(includeAll, status) {
|
|
|
8401
8542
|
|
|
8402
8543
|
// src/commands/work-plan.ts
|
|
8403
8544
|
import { readFile as readFile2 } from "fs/promises";
|
|
8545
|
+
|
|
8546
|
+
// src/paging.ts
|
|
8547
|
+
var MAX_AUTO_PAGES = 100;
|
|
8548
|
+
var asNumber = (value) => value === void 0 || value === "" ? void 0 : Number(value);
|
|
8549
|
+
function shouldAutoPage(opts) {
|
|
8550
|
+
if (opts.autoPage === false) return false;
|
|
8551
|
+
return asNumber(opts.page) === void 0;
|
|
8552
|
+
}
|
|
8553
|
+
function singlePageQuery(opts) {
|
|
8554
|
+
const page = asNumber(opts.page);
|
|
8555
|
+
const pageSize = asNumber(opts.pageSize);
|
|
8556
|
+
return {
|
|
8557
|
+
...page === void 0 ? {} : { page },
|
|
8558
|
+
...pageSize === void 0 ? {} : { pageSize }
|
|
8559
|
+
};
|
|
8560
|
+
}
|
|
8561
|
+
async function fetchAllPages(fetchPage, opts = {}, label = "results") {
|
|
8562
|
+
const pages = [];
|
|
8563
|
+
let page = 1;
|
|
8564
|
+
let truncated = false;
|
|
8565
|
+
for (; ; ) {
|
|
8566
|
+
const current = await fetchPage(page);
|
|
8567
|
+
pages.push(current);
|
|
8568
|
+
if (!hasNextPage(current)) break;
|
|
8569
|
+
if (pages.length >= MAX_AUTO_PAGES) {
|
|
8570
|
+
truncated = true;
|
|
8571
|
+
break;
|
|
8572
|
+
}
|
|
8573
|
+
page = current.page + 1;
|
|
8574
|
+
}
|
|
8575
|
+
const first = pages[0];
|
|
8576
|
+
if (!first) throw new Error("paged read returned no envelope at all");
|
|
8577
|
+
if (truncated) {
|
|
8578
|
+
if (!isQuiet()) {
|
|
8579
|
+
process.stderr.write(
|
|
8580
|
+
`${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.")}
|
|
8581
|
+
`
|
|
8582
|
+
);
|
|
8583
|
+
}
|
|
8584
|
+
const partial = pages.flatMap((p) => p.items);
|
|
8585
|
+
return {
|
|
8586
|
+
...first,
|
|
8587
|
+
items: partial,
|
|
8588
|
+
page: 1,
|
|
8589
|
+
pageSize: partial.length,
|
|
8590
|
+
// Pages OF THIS SIZE, so the number stays consistent with the pageSize just reported.
|
|
8591
|
+
pageCount: partial.length === 0 ? 1 : Math.ceil(first.count / partial.length),
|
|
8592
|
+
hasPreviousPage: false,
|
|
8593
|
+
hasNextPage: true,
|
|
8594
|
+
isFirstPage: true,
|
|
8595
|
+
isLastPage: false,
|
|
8596
|
+
firstItemOnPage: partial.length === 0 ? 0 : 1,
|
|
8597
|
+
lastItemOnPage: partial.length
|
|
8598
|
+
};
|
|
8599
|
+
}
|
|
8600
|
+
if (pages.length === 1) return first;
|
|
8601
|
+
const items = pages.flatMap((current) => current.items);
|
|
8602
|
+
return {
|
|
8603
|
+
...first,
|
|
8604
|
+
items,
|
|
8605
|
+
page: 1,
|
|
8606
|
+
pageSize: items.length,
|
|
8607
|
+
pageCount: 1,
|
|
8608
|
+
hasPreviousPage: false,
|
|
8609
|
+
hasNextPage: false,
|
|
8610
|
+
isFirstPage: true,
|
|
8611
|
+
isLastPage: true,
|
|
8612
|
+
firstItemOnPage: items.length === 0 ? 0 : 1,
|
|
8613
|
+
lastItemOnPage: items.length
|
|
8614
|
+
};
|
|
8615
|
+
}
|
|
8616
|
+
function hasNextPage(current) {
|
|
8617
|
+
if (current.hasNextPage !== void 0) return current.hasNextPage;
|
|
8618
|
+
if (current.pageCount !== void 0) return current.page < current.pageCount;
|
|
8619
|
+
return current.page * current.pageSize < current.count;
|
|
8620
|
+
}
|
|
8621
|
+
var PAGE_OPTION = [
|
|
8622
|
+
"--page <n>",
|
|
8623
|
+
"Page number (reads only that page instead of all)"
|
|
8624
|
+
];
|
|
8625
|
+
var PAGE_SIZE_OPTION = [
|
|
8626
|
+
"--page-size <n>",
|
|
8627
|
+
"Page size used while paging"
|
|
8628
|
+
];
|
|
8629
|
+
var NO_AUTO_PAGE_OPTION = [
|
|
8630
|
+
"--no-auto-page",
|
|
8631
|
+
"Read only the first page instead of walking every page"
|
|
8632
|
+
];
|
|
8633
|
+
|
|
8634
|
+
// src/commands/work-plan.ts
|
|
8404
8635
|
function registerWorkPlan(program2) {
|
|
8405
8636
|
const workPlan = program2.command("work-plan").description(
|
|
8406
8637
|
"Drive a work plan: create one from a brief, then execute / accept / reject"
|
|
@@ -8594,22 +8825,27 @@ Examples:
|
|
|
8594
8825
|
globals.json
|
|
8595
8826
|
);
|
|
8596
8827
|
});
|
|
8597
|
-
workPlan.command("list").description(
|
|
8828
|
+
workPlan.command("list").description(
|
|
8829
|
+
"List work plans, newest-first (GET /decompositions). Walks every page by default."
|
|
8830
|
+
).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
8831
|
const globals = cmd.optsWithGlobals();
|
|
8599
8832
|
const cfg = resolveConfig(globals);
|
|
8600
|
-
const
|
|
8833
|
+
const readPage = (query) => runApi("Listing work plans", async () => {
|
|
8601
8834
|
const client = await makeClient(cfg);
|
|
8602
8835
|
return client.GET("/decompositions", {
|
|
8603
8836
|
params: {
|
|
8604
|
-
query: {
|
|
8605
|
-
status: opts.status,
|
|
8606
|
-
briefId: opts.brief,
|
|
8607
|
-
page: opts.page,
|
|
8608
|
-
pageSize: opts.pageSize
|
|
8609
|
-
}
|
|
8837
|
+
query: { status: opts.status, briefId: opts.brief, ...query }
|
|
8610
8838
|
}
|
|
8611
8839
|
});
|
|
8612
8840
|
});
|
|
8841
|
+
const data = shouldAutoPage(opts) ? await fetchAllPages(
|
|
8842
|
+
(page) => readPage({
|
|
8843
|
+
page,
|
|
8844
|
+
...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
|
|
8845
|
+
}),
|
|
8846
|
+
opts,
|
|
8847
|
+
"work plans"
|
|
8848
|
+
) : await readPage(singlePageQuery(opts));
|
|
8613
8849
|
emitAction(
|
|
8614
8850
|
`listed ${style.bold(String(data.items.length))} of ${data.count} work plan(s)`,
|
|
8615
8851
|
data,
|
|
@@ -8697,35 +8933,53 @@ Examples:
|
|
|
8697
8933
|
$ sechroom filing reject fsg_XXXX --reason "wrong workspace"
|
|
8698
8934
|
$ sechroom filing edit-and-accept fsg_XXXX --target-kind Workspace --existing-target-id wsp_XXXX`
|
|
8699
8935
|
);
|
|
8700
|
-
filing.command("suggestions").description(
|
|
8936
|
+
filing.command("suggestions").description(
|
|
8937
|
+
"List filing suggestions (GET /filing/suggestions). Walks every page by default."
|
|
8938
|
+
).option("--memory-id <memoryId>", "Filter to a single memory's suggestions").option(
|
|
8701
8939
|
"--status <status>",
|
|
8702
8940
|
"Generating | Pending | Accepted | Rejected | EditedAndAccepted | Deferred | Invalidated"
|
|
8703
|
-
).option(
|
|
8941
|
+
).option(...PAGE_OPTION).option(...PAGE_SIZE_OPTION).option(...NO_AUTO_PAGE_OPTION).action(async (opts, cmd) => {
|
|
8704
8942
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
8705
|
-
const
|
|
8943
|
+
const filters = {
|
|
8944
|
+
...opts.memoryId ? { memoryId: opts.memoryId } : {},
|
|
8945
|
+
...opts.status ? {
|
|
8946
|
+
status: opts.status
|
|
8947
|
+
} : {}
|
|
8948
|
+
};
|
|
8949
|
+
const readPage = (query) => runApi("Listing filing suggestions", async () => {
|
|
8706
8950
|
const client = await makeClient(cfg);
|
|
8707
8951
|
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
|
-
}
|
|
8952
|
+
params: { query: { ...filters, ...query } }
|
|
8716
8953
|
});
|
|
8717
8954
|
});
|
|
8955
|
+
const data = shouldAutoPage(opts) ? await fetchAllPages(
|
|
8956
|
+
(page) => readPage({
|
|
8957
|
+
page,
|
|
8958
|
+
...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
|
|
8959
|
+
}),
|
|
8960
|
+
opts,
|
|
8961
|
+
"filing suggestions"
|
|
8962
|
+
) : await readPage(singlePageQuery(opts));
|
|
8718
8963
|
emit(data, cmd.optsWithGlobals().json);
|
|
8719
8964
|
});
|
|
8720
|
-
filing.command("get <id>").description(
|
|
8965
|
+
filing.command("get <id>").description(
|
|
8966
|
+
"Fetch a filing suggestion by id (GET /filing/suggestions/{id})"
|
|
8967
|
+
).action(async (id, _opts, cmd) => {
|
|
8721
8968
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
8722
8969
|
const data = await runApi("Fetching filing suggestion", async () => {
|
|
8723
8970
|
const client = await makeClient(cfg);
|
|
8724
|
-
return client.GET("/filing/suggestions/{id}", {
|
|
8971
|
+
return client.GET("/filing/suggestions/{id}", {
|
|
8972
|
+
params: { path: { id } }
|
|
8973
|
+
});
|
|
8725
8974
|
});
|
|
8726
8975
|
emit(data, cmd.optsWithGlobals().json);
|
|
8727
8976
|
});
|
|
8728
|
-
filing.command("preview").description(
|
|
8977
|
+
filing.command("preview").description(
|
|
8978
|
+
"Preview a filing suggestion for a memory id or ad-hoc shape (POST /filing/suggestions/preview)"
|
|
8979
|
+
).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(
|
|
8980
|
+
"--scope-workspace <workspaceId>",
|
|
8981
|
+
"Scope the preview to a workspace"
|
|
8982
|
+
).action(async (opts, cmd) => {
|
|
8729
8983
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
8730
8984
|
const memory = opts.text ? {
|
|
8731
8985
|
text: opts.text,
|
|
@@ -8745,15 +8999,26 @@ Examples:
|
|
|
8745
8999
|
});
|
|
8746
9000
|
emit(data, cmd.optsWithGlobals().json);
|
|
8747
9001
|
});
|
|
8748
|
-
filing.command("accept <id>").description(
|
|
9002
|
+
filing.command("accept <id>").description(
|
|
9003
|
+
"Accept a filing suggestion (POST /filing/suggestions/{id}/accept)"
|
|
9004
|
+
).action(async (id, _opts, cmd) => {
|
|
8749
9005
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
8750
9006
|
const data = await runApi("Accepting filing suggestion", async () => {
|
|
8751
9007
|
const client = await makeClient(cfg);
|
|
8752
|
-
return client.POST("/filing/suggestions/{id}/accept", {
|
|
9008
|
+
return client.POST("/filing/suggestions/{id}/accept", {
|
|
9009
|
+
params: { path: { id } },
|
|
9010
|
+
body: {}
|
|
9011
|
+
});
|
|
8753
9012
|
});
|
|
8754
|
-
emitAction(
|
|
9013
|
+
emitAction(
|
|
9014
|
+
`accepted filing suggestion ${style.bold(id)}`,
|
|
9015
|
+
data,
|
|
9016
|
+
cmd.optsWithGlobals().json
|
|
9017
|
+
);
|
|
8755
9018
|
});
|
|
8756
|
-
filing.command("reject <id>").description(
|
|
9019
|
+
filing.command("reject <id>").description(
|
|
9020
|
+
"Reject a filing suggestion (POST /filing/suggestions/{id}/reject)"
|
|
9021
|
+
).option("--reason <reason>", "Why the suggestion was rejected").option("--reason-code <code>", "Structured reason code").action(async (id, opts, cmd) => {
|
|
8757
9022
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
8758
9023
|
const data = await runApi("Rejecting filing suggestion", async () => {
|
|
8759
9024
|
const client = await makeClient(cfg);
|
|
@@ -8765,9 +9030,18 @@ Examples:
|
|
|
8765
9030
|
}
|
|
8766
9031
|
});
|
|
8767
9032
|
});
|
|
8768
|
-
emitAction(
|
|
9033
|
+
emitAction(
|
|
9034
|
+
`rejected filing suggestion ${style.bold(id)}`,
|
|
9035
|
+
data,
|
|
9036
|
+
cmd.optsWithGlobals().json
|
|
9037
|
+
);
|
|
8769
9038
|
});
|
|
8770
|
-
filing.command("defer <id>").description(
|
|
9039
|
+
filing.command("defer <id>").description(
|
|
9040
|
+
"Defer a filing suggestion (POST /filing/suggestions/{id}/defer)"
|
|
9041
|
+
).option(
|
|
9042
|
+
"--until <iso>",
|
|
9043
|
+
"Defer until an ISO-8601 timestamp (defaults to indefinite)"
|
|
9044
|
+
).action(async (id, opts, cmd) => {
|
|
8771
9045
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
8772
9046
|
const data = await runApi("Deferring filing suggestion", async () => {
|
|
8773
9047
|
const client = await makeClient(cfg);
|
|
@@ -8776,25 +9050,44 @@ Examples:
|
|
|
8776
9050
|
body: { until: opts.until ?? null }
|
|
8777
9051
|
});
|
|
8778
9052
|
});
|
|
8779
|
-
emitAction(
|
|
9053
|
+
emitAction(
|
|
9054
|
+
`deferred filing suggestion ${style.bold(id)}`,
|
|
9055
|
+
data,
|
|
9056
|
+
cmd.optsWithGlobals().json
|
|
9057
|
+
);
|
|
8780
9058
|
});
|
|
8781
|
-
filing.command("edit-and-accept <id>").description(
|
|
9059
|
+
filing.command("edit-and-accept <id>").description(
|
|
9060
|
+
"Override the target then accept (POST /filing/suggestions/{id}/edit-and-accept)"
|
|
9061
|
+
).option("--target-kind <kind>", "Workspace | Project").option(
|
|
9062
|
+
"--existing-target-id <id>",
|
|
9063
|
+
"File into an existing workspace/project"
|
|
9064
|
+
).option("--new-name <name>", "Create a new target with this name").option("--new-description <text>", "Description for the new target").option(
|
|
9065
|
+
"--new-parent-workspace <workspaceId>",
|
|
9066
|
+
"Parent workspace for a new project"
|
|
9067
|
+
).option("--memory-id <memoryId...>", "Override the memory set (repeatable)").action(async (id, opts, cmd) => {
|
|
8782
9068
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
8783
|
-
const data = await runApi(
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
9069
|
+
const data = await runApi(
|
|
9070
|
+
"Editing and accepting filing suggestion",
|
|
9071
|
+
async () => {
|
|
9072
|
+
const client = await makeClient(cfg);
|
|
9073
|
+
return client.POST("/filing/suggestions/{id}/edit-and-accept", {
|
|
9074
|
+
params: { path: { id } },
|
|
9075
|
+
body: {
|
|
9076
|
+
targetKind: opts.targetKind ?? null,
|
|
9077
|
+
existingTargetId: opts.existingTargetId ?? null,
|
|
9078
|
+
newName: opts.newName ?? null,
|
|
9079
|
+
newDescription: opts.newDescription ?? null,
|
|
9080
|
+
newParentWorkspaceId: opts.newParentWorkspace ?? null,
|
|
9081
|
+
overrideMemoryIds: opts.memoryId ?? null
|
|
9082
|
+
}
|
|
9083
|
+
});
|
|
9084
|
+
}
|
|
9085
|
+
);
|
|
9086
|
+
emitAction(
|
|
9087
|
+
`edited & accepted filing suggestion ${style.bold(id)}`,
|
|
9088
|
+
data,
|
|
9089
|
+
cmd.optsWithGlobals().json
|
|
9090
|
+
);
|
|
8798
9091
|
});
|
|
8799
9092
|
}
|
|
8800
9093
|
|
|
@@ -10117,6 +10410,18 @@ function resolveCreateBody(textOpt, fileOpt) {
|
|
|
10117
10410
|
);
|
|
10118
10411
|
return { text: text2, defaultTitle };
|
|
10119
10412
|
}
|
|
10413
|
+
async function fetchAllMemoryRelationships(cfg, memoryId) {
|
|
10414
|
+
return await fetchAllPages(
|
|
10415
|
+
(page) => runApi(`Fetching relationships (page ${page})`, async () => {
|
|
10416
|
+
const client = await makeClient(cfg);
|
|
10417
|
+
return client.GET("/memories/{memoryId}/relationships", {
|
|
10418
|
+
params: { path: { memoryId }, query: { page } }
|
|
10419
|
+
});
|
|
10420
|
+
}),
|
|
10421
|
+
{},
|
|
10422
|
+
"relationships"
|
|
10423
|
+
);
|
|
10424
|
+
}
|
|
10120
10425
|
function registerMemory(program2) {
|
|
10121
10426
|
const memory = program2.command("memory").description("Create, read, and search memories");
|
|
10122
10427
|
memory.addHelpText(
|
|
@@ -10371,7 +10676,16 @@ ${plan.rows.map(
|
|
|
10371
10676
|
false
|
|
10372
10677
|
);
|
|
10373
10678
|
});
|
|
10374
|
-
memory.command("get <memoryId>").description("Fetch a memory by id (GET /memories/{memoryId})").
|
|
10679
|
+
const get = memory.command("get <memoryId>").description("Fetch a memory by id (GET /memories/{memoryId})").option(
|
|
10680
|
+
"--with-relationships",
|
|
10681
|
+
"Include the memory's relationships in the response",
|
|
10682
|
+
false
|
|
10683
|
+
).addHelpText(
|
|
10684
|
+
"after",
|
|
10685
|
+
`
|
|
10686
|
+
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.`
|
|
10687
|
+
);
|
|
10688
|
+
get.action(async (memoryId, opts, cmd) => {
|
|
10375
10689
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
10376
10690
|
const data = await runApi("Fetching memory", async () => {
|
|
10377
10691
|
const client = await makeClient(cfg);
|
|
@@ -10379,6 +10693,11 @@ ${plan.rows.map(
|
|
|
10379
10693
|
params: { path: { memoryId } }
|
|
10380
10694
|
});
|
|
10381
10695
|
});
|
|
10696
|
+
if (opts.withRelationships) {
|
|
10697
|
+
const relationships = await fetchAllMemoryRelationships(cfg, memoryId);
|
|
10698
|
+
emit({ ...data, relationships }, cmd.optsWithGlobals().json);
|
|
10699
|
+
return;
|
|
10700
|
+
}
|
|
10382
10701
|
emit(data, cmd.optsWithGlobals().json);
|
|
10383
10702
|
});
|
|
10384
10703
|
memory.command("search <query>").description(
|
|
@@ -10601,21 +10920,28 @@ ${plan.rows.map(
|
|
|
10601
10920
|
cmd.optsWithGlobals().json
|
|
10602
10921
|
);
|
|
10603
10922
|
});
|
|
10604
|
-
memory.command("list-archived").description(
|
|
10923
|
+
memory.command("list-archived").description(
|
|
10924
|
+
"List archived memories (GET /memories/archived). Walks every page by default."
|
|
10925
|
+
).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
10926
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
10606
|
-
const
|
|
10927
|
+
const filters = {
|
|
10928
|
+
...opts.workspace ? { workspaceId: opts.workspace } : {},
|
|
10929
|
+
...opts.project ? { projectId: opts.project } : {}
|
|
10930
|
+
};
|
|
10931
|
+
const readPage = (query) => runApi("Listing archived memories", async () => {
|
|
10607
10932
|
const client = await makeClient(cfg);
|
|
10608
10933
|
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
|
-
}
|
|
10934
|
+
params: { query: { ...filters, ...query } }
|
|
10617
10935
|
});
|
|
10618
10936
|
});
|
|
10937
|
+
const data = shouldAutoPage(opts) ? await fetchAllPages(
|
|
10938
|
+
(page) => readPage({
|
|
10939
|
+
page,
|
|
10940
|
+
...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
|
|
10941
|
+
}),
|
|
10942
|
+
opts,
|
|
10943
|
+
"archived memories"
|
|
10944
|
+
) : await readPage(singlePageQuery(opts));
|
|
10619
10945
|
emit(data, cmd.optsWithGlobals().json);
|
|
10620
10946
|
});
|
|
10621
10947
|
memory.command("versions <memoryId>").description("List a memory's versions (GET /memories/{memoryId}/versions)").action(async (memoryId, _opts, cmd) => {
|
|
@@ -10726,8 +11052,18 @@ ${plan.rows.map(
|
|
|
10726
11052
|
}
|
|
10727
11053
|
|
|
10728
11054
|
// src/setup/apply.ts
|
|
10729
|
-
import { createHash as createHash5 } from "crypto";
|
|
10730
|
-
import {
|
|
11055
|
+
import { createHash as createHash5, randomUUID as randomUUID3 } from "crypto";
|
|
11056
|
+
import {
|
|
11057
|
+
chmodSync as chmodSync2,
|
|
11058
|
+
copyFileSync,
|
|
11059
|
+
existsSync as existsSync13,
|
|
11060
|
+
mkdirSync as mkdirSync16,
|
|
11061
|
+
readFileSync as readFileSync16,
|
|
11062
|
+
renameSync as renameSync4,
|
|
11063
|
+
rmSync as rmSync7,
|
|
11064
|
+
statSync as statSync5,
|
|
11065
|
+
writeFileSync as writeFileSync15
|
|
11066
|
+
} from "fs";
|
|
10731
11067
|
import { dirname as dirname15 } from "path";
|
|
10732
11068
|
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
10733
11069
|
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
@@ -10773,11 +11109,17 @@ function parseManagedBlock(content, block) {
|
|
|
10773
11109
|
const keyed = content.match(keyedBlockRe(block));
|
|
10774
11110
|
if (keyed) {
|
|
10775
11111
|
const attrs = parseAttrs(keyed[0].slice(0, keyed[0].indexOf("\n")));
|
|
10776
|
-
return {
|
|
11112
|
+
return {
|
|
11113
|
+
block,
|
|
11114
|
+
source: attrs.source ?? null,
|
|
11115
|
+
sha256: attrs.sha256 ?? null,
|
|
11116
|
+
body: innerBody(keyed[0])
|
|
11117
|
+
};
|
|
10777
11118
|
}
|
|
10778
11119
|
if (block === "role-template") {
|
|
10779
11120
|
const legacy = content.match(legacyBlockRe());
|
|
10780
|
-
if (legacy)
|
|
11121
|
+
if (legacy)
|
|
11122
|
+
return { block, source: null, sha256: null, body: innerBody(legacy[0]) };
|
|
10781
11123
|
}
|
|
10782
11124
|
return null;
|
|
10783
11125
|
}
|
|
@@ -10799,33 +11141,446 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
10799
11141
|
try {
|
|
10800
11142
|
current = JSON.parse(readFileSync16(path, "utf8"));
|
|
10801
11143
|
} catch {
|
|
10802
|
-
return {
|
|
11144
|
+
return {
|
|
11145
|
+
kind: "mcp",
|
|
11146
|
+
path,
|
|
11147
|
+
status: "skipped",
|
|
11148
|
+
note: "existing file isn't valid JSON \u2014 left untouched"
|
|
11149
|
+
};
|
|
10803
11150
|
}
|
|
10804
11151
|
}
|
|
10805
|
-
current.mcpServers = {
|
|
11152
|
+
current.mcpServers = {
|
|
11153
|
+
...current.mcpServers ?? {},
|
|
11154
|
+
...incoming.mcpServers ?? {}
|
|
11155
|
+
};
|
|
10806
11156
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
10807
11157
|
ensureDir2(path);
|
|
10808
11158
|
writeFileSync15(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
|
|
10809
11159
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
10810
11160
|
}
|
|
10811
|
-
|
|
11161
|
+
var CODEX_MCP_TABLE = "mcp_servers.sechroom";
|
|
11162
|
+
function decodeTomlBasicString(value) {
|
|
11163
|
+
let decoded = "";
|
|
11164
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
11165
|
+
const char = value[i];
|
|
11166
|
+
if (char !== "\\") {
|
|
11167
|
+
decoded += char;
|
|
11168
|
+
continue;
|
|
11169
|
+
}
|
|
11170
|
+
const escaped = value[++i];
|
|
11171
|
+
if (escaped === void 0) throw new Error("unterminated TOML escape");
|
|
11172
|
+
const simple = {
|
|
11173
|
+
b: "\b",
|
|
11174
|
+
f: "\f",
|
|
11175
|
+
n: "\n",
|
|
11176
|
+
r: "\r",
|
|
11177
|
+
t: " ",
|
|
11178
|
+
'"': '"',
|
|
11179
|
+
"\\": "\\"
|
|
11180
|
+
};
|
|
11181
|
+
if (escaped in simple) {
|
|
11182
|
+
decoded += simple[escaped];
|
|
11183
|
+
continue;
|
|
11184
|
+
}
|
|
11185
|
+
const digits = escaped === "u" ? 4 : escaped === "U" ? 8 : 0;
|
|
11186
|
+
if (digits === 0) throw new Error(`invalid TOML escape '\\\\${escaped}'`);
|
|
11187
|
+
const code = value.slice(i + 1, i + 1 + digits);
|
|
11188
|
+
if (!new RegExp(`^[0-9a-fA-F]{${digits}}$`).test(code))
|
|
11189
|
+
throw new Error(`invalid TOML Unicode escape '\\\\${escaped}${code}'`);
|
|
11190
|
+
decoded += String.fromCodePoint(Number.parseInt(code, 16));
|
|
11191
|
+
i += digits;
|
|
11192
|
+
}
|
|
11193
|
+
return decoded;
|
|
11194
|
+
}
|
|
11195
|
+
function normalizeTomlKey(raw) {
|
|
11196
|
+
const segments = [];
|
|
11197
|
+
let i = 0;
|
|
11198
|
+
while (i < raw.length) {
|
|
11199
|
+
while (/\s/.test(raw[i] ?? "")) i += 1;
|
|
11200
|
+
if (i >= raw.length) break;
|
|
11201
|
+
const quote = raw[i];
|
|
11202
|
+
let segment;
|
|
11203
|
+
if (quote === '"' || quote === "'") {
|
|
11204
|
+
i += 1;
|
|
11205
|
+
const start = i;
|
|
11206
|
+
let escaped = false;
|
|
11207
|
+
while (i < raw.length) {
|
|
11208
|
+
const char = raw[i];
|
|
11209
|
+
if (quote === '"' && escaped) {
|
|
11210
|
+
escaped = false;
|
|
11211
|
+
i += 1;
|
|
11212
|
+
continue;
|
|
11213
|
+
}
|
|
11214
|
+
if (quote === '"' && char === "\\") {
|
|
11215
|
+
escaped = true;
|
|
11216
|
+
i += 1;
|
|
11217
|
+
continue;
|
|
11218
|
+
}
|
|
11219
|
+
if (char === quote) break;
|
|
11220
|
+
i += 1;
|
|
11221
|
+
}
|
|
11222
|
+
if (raw[i] !== quote) throw new Error(`unterminated TOML key '${raw}'`);
|
|
11223
|
+
const encoded = raw.slice(start, i);
|
|
11224
|
+
segment = quote === '"' ? decodeTomlBasicString(encoded) : encoded;
|
|
11225
|
+
i += 1;
|
|
11226
|
+
} else {
|
|
11227
|
+
const start = i;
|
|
11228
|
+
while (i < raw.length && /[A-Za-z0-9_-]/.test(raw[i])) i += 1;
|
|
11229
|
+
segment = raw.slice(start, i);
|
|
11230
|
+
if (!segment) throw new Error(`invalid TOML key '${raw.trim()}'`);
|
|
11231
|
+
}
|
|
11232
|
+
segments.push(segment);
|
|
11233
|
+
while (/\s/.test(raw[i] ?? "")) i += 1;
|
|
11234
|
+
if (i >= raw.length) break;
|
|
11235
|
+
if (raw[i] !== ".") throw new Error(`invalid TOML key '${raw.trim()}'`);
|
|
11236
|
+
i += 1;
|
|
11237
|
+
}
|
|
11238
|
+
if (segments.length === 0) throw new Error("empty TOML key");
|
|
11239
|
+
return segments;
|
|
11240
|
+
}
|
|
11241
|
+
function sameTomlKey(left, right) {
|
|
11242
|
+
return left.length === right.length && left.every((segment, index) => segment === right[index]);
|
|
11243
|
+
}
|
|
11244
|
+
function isTomlKeyPrefix(prefix, candidate) {
|
|
11245
|
+
return prefix.length < candidate.length && prefix.every((segment, index) => segment === candidate[index]);
|
|
11246
|
+
}
|
|
11247
|
+
function tomlKeyLabel(key) {
|
|
11248
|
+
return key.join(".");
|
|
11249
|
+
}
|
|
11250
|
+
function stripTomlComment(line) {
|
|
11251
|
+
let quote = null;
|
|
11252
|
+
let escaped = false;
|
|
11253
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
11254
|
+
const rest = line.slice(i);
|
|
11255
|
+
const char = line[i];
|
|
11256
|
+
if (quote === "multiline-basic") {
|
|
11257
|
+
if (rest.startsWith('"""') && !escaped) {
|
|
11258
|
+
escaped = false;
|
|
11259
|
+
quote = null;
|
|
11260
|
+
i += 2;
|
|
11261
|
+
continue;
|
|
11262
|
+
}
|
|
11263
|
+
if (escaped) escaped = false;
|
|
11264
|
+
else if (char === "\\") escaped = true;
|
|
11265
|
+
continue;
|
|
11266
|
+
}
|
|
11267
|
+
if (quote === "multiline-literal") {
|
|
11268
|
+
if (rest.startsWith("'''")) {
|
|
11269
|
+
i += 2;
|
|
11270
|
+
quote = null;
|
|
11271
|
+
}
|
|
11272
|
+
continue;
|
|
11273
|
+
}
|
|
11274
|
+
if (quote === "basic") {
|
|
11275
|
+
if (escaped) escaped = false;
|
|
11276
|
+
else if (char === "\\") escaped = true;
|
|
11277
|
+
else if (char === '"') quote = null;
|
|
11278
|
+
continue;
|
|
11279
|
+
}
|
|
11280
|
+
if (quote === "literal") {
|
|
11281
|
+
if (char === "'") quote = null;
|
|
11282
|
+
continue;
|
|
11283
|
+
}
|
|
11284
|
+
if (rest.startsWith('"""')) {
|
|
11285
|
+
quote = "multiline-basic";
|
|
11286
|
+
i += 2;
|
|
11287
|
+
} else if (rest.startsWith("'''")) {
|
|
11288
|
+
quote = "multiline-literal";
|
|
11289
|
+
i += 2;
|
|
11290
|
+
} else if (char === '"') quote = "basic";
|
|
11291
|
+
else if (char === "'") quote = "literal";
|
|
11292
|
+
else if (char === "#") return line.slice(0, i);
|
|
11293
|
+
}
|
|
11294
|
+
return line;
|
|
11295
|
+
}
|
|
11296
|
+
function scanTomlValue(value, state) {
|
|
11297
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
11298
|
+
const rest = value.slice(i);
|
|
11299
|
+
const char = value[i];
|
|
11300
|
+
if (state.quote === "multiline-basic") {
|
|
11301
|
+
if (rest.startsWith('"""') && !state.escaped) {
|
|
11302
|
+
state.quote = null;
|
|
11303
|
+
i += 2;
|
|
11304
|
+
continue;
|
|
11305
|
+
}
|
|
11306
|
+
if (state.escaped) state.escaped = false;
|
|
11307
|
+
else if (char === "\\") state.escaped = true;
|
|
11308
|
+
continue;
|
|
11309
|
+
}
|
|
11310
|
+
if (state.quote === "multiline-literal") {
|
|
11311
|
+
if (rest.startsWith("'''")) {
|
|
11312
|
+
state.quote = null;
|
|
11313
|
+
i += 2;
|
|
11314
|
+
}
|
|
11315
|
+
continue;
|
|
11316
|
+
}
|
|
11317
|
+
if (state.quote === "basic") {
|
|
11318
|
+
if (state.escaped) state.escaped = false;
|
|
11319
|
+
else if (char === "\\") state.escaped = true;
|
|
11320
|
+
else if (char === '"') state.quote = null;
|
|
11321
|
+
continue;
|
|
11322
|
+
}
|
|
11323
|
+
if (state.quote === "literal") {
|
|
11324
|
+
if (char === "'") state.quote = null;
|
|
11325
|
+
continue;
|
|
11326
|
+
}
|
|
11327
|
+
if (char === "#") break;
|
|
11328
|
+
if (rest.startsWith('"""')) {
|
|
11329
|
+
state.quote = "multiline-basic";
|
|
11330
|
+
i += 2;
|
|
11331
|
+
} else if (rest.startsWith("'''")) {
|
|
11332
|
+
state.quote = "multiline-literal";
|
|
11333
|
+
i += 2;
|
|
11334
|
+
} else if (char === '"') state.quote = "basic";
|
|
11335
|
+
else if (char === "'") state.quote = "literal";
|
|
11336
|
+
else if (char === "[" || char === "{") state.containers.push(char);
|
|
11337
|
+
else if (char === "]" || char === "}") {
|
|
11338
|
+
const expected = char === "]" ? "[" : "{";
|
|
11339
|
+
if (state.containers.pop() !== expected)
|
|
11340
|
+
throw new Error(`unbalanced TOML delimiter '${char}'`);
|
|
11341
|
+
}
|
|
11342
|
+
}
|
|
11343
|
+
}
|
|
11344
|
+
function parseTomlHeader(rawLine) {
|
|
11345
|
+
const line = stripTomlComment(rawLine).trim();
|
|
11346
|
+
const array = line.startsWith("[[") && line.endsWith("]]");
|
|
11347
|
+
const table = !array && line.startsWith("[") && line.endsWith("]");
|
|
11348
|
+
if (!array && !table) return null;
|
|
11349
|
+
const prefix = array ? 2 : 1;
|
|
11350
|
+
const suffix = array ? 2 : 1;
|
|
11351
|
+
const rawName = line.slice(prefix, line.length - suffix).trim();
|
|
11352
|
+
if (!rawName) throw new Error("empty TOML table name");
|
|
11353
|
+
return { name: normalizeTomlKey(rawName), array };
|
|
11354
|
+
}
|
|
11355
|
+
function findTomlEquals(line) {
|
|
11356
|
+
let quote = null;
|
|
11357
|
+
let escaped = false;
|
|
11358
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
11359
|
+
const char = line[i];
|
|
11360
|
+
if (quote === "basic") {
|
|
11361
|
+
if (escaped) escaped = false;
|
|
11362
|
+
else if (char === "\\") escaped = true;
|
|
11363
|
+
else if (char === '"') quote = null;
|
|
11364
|
+
continue;
|
|
11365
|
+
}
|
|
11366
|
+
if (quote === "literal") {
|
|
11367
|
+
if (char === "'") quote = null;
|
|
11368
|
+
continue;
|
|
11369
|
+
}
|
|
11370
|
+
if (char === '"') quote = "basic";
|
|
11371
|
+
else if (char === "'") quote = "literal";
|
|
11372
|
+
else if (char === "=") return i;
|
|
11373
|
+
}
|
|
11374
|
+
return -1;
|
|
11375
|
+
}
|
|
11376
|
+
function arrayTableContext(name, arrays) {
|
|
11377
|
+
let best = null;
|
|
11378
|
+
for (let i = arrays.length - 1; i >= 0; i -= 1) {
|
|
11379
|
+
const candidate = arrays[i];
|
|
11380
|
+
if (isTomlKeyPrefix(candidate.key, name)) {
|
|
11381
|
+
if (!best || candidate.key.length > best.key.length) best = candidate;
|
|
11382
|
+
}
|
|
11383
|
+
}
|
|
11384
|
+
return best;
|
|
11385
|
+
}
|
|
11386
|
+
function tomlTableIdentity(key, context) {
|
|
11387
|
+
return `${context?.identity ?? "root"}:${JSON.stringify(key)}`;
|
|
11388
|
+
}
|
|
11389
|
+
function isTomlScalar(value) {
|
|
11390
|
+
const decimal = "[+-]?(?:0|[1-9](?:_?[0-9])*)(?:\\.[0-9](?:_?[0-9])*)?(?:[eE][+-]?[0-9](?:_?[0-9])*)?";
|
|
11391
|
+
const number = new RegExp(
|
|
11392
|
+
`^(?:${decimal}|[+-]?0x[0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[+-]?0o[0-7](?:_?[0-7])*|[+-]?0b[01](?:_?[01])*|[+-]?(?:inf|nan))$`
|
|
11393
|
+
);
|
|
11394
|
+
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})?)?)$/;
|
|
11395
|
+
return number.test(value) || datetime.test(value) || /^(?:true|false)$/.test(value);
|
|
11396
|
+
}
|
|
11397
|
+
function validateTomlValueShape(value) {
|
|
11398
|
+
const token = stripTomlComment(value).trim();
|
|
11399
|
+
if (!token) throw new Error("empty TOML value");
|
|
11400
|
+
if (token.startsWith('"') || token.startsWith("'")) {
|
|
11401
|
+
const quote = token[0];
|
|
11402
|
+
if (quote && token.endsWith(quote)) return;
|
|
11403
|
+
}
|
|
11404
|
+
if (token.startsWith("[") && token.endsWith("]")) return;
|
|
11405
|
+
if (token.startsWith("{") && token.endsWith("}")) return;
|
|
11406
|
+
if (isTomlScalar(token)) return;
|
|
11407
|
+
throw new Error(`invalid TOML value '${token}'`);
|
|
11408
|
+
}
|
|
11409
|
+
function validateToml(content) {
|
|
11410
|
+
const tables = /* @__PURE__ */ new Set();
|
|
11411
|
+
const keys = /* @__PURE__ */ new Set();
|
|
11412
|
+
const arrayInstances = [];
|
|
11413
|
+
let currentTable = { identity: "root" };
|
|
11414
|
+
let valueState = {
|
|
11415
|
+
containers: [],
|
|
11416
|
+
quote: null,
|
|
11417
|
+
escaped: false
|
|
11418
|
+
};
|
|
11419
|
+
for (const rawLine of content.replace(/\r\n/g, "\n").split("\n")) {
|
|
11420
|
+
if (valueState.quote || valueState.containers.length > 0) {
|
|
11421
|
+
scanTomlValue(rawLine, valueState);
|
|
11422
|
+
continue;
|
|
11423
|
+
}
|
|
11424
|
+
const line = stripTomlComment(rawLine).trim();
|
|
11425
|
+
if (!line) continue;
|
|
11426
|
+
const header = parseTomlHeader(rawLine);
|
|
11427
|
+
if (header) {
|
|
11428
|
+
if (header.array) {
|
|
11429
|
+
const context = arrayTableContext(header.name, arrayInstances);
|
|
11430
|
+
const parentIdentity = context?.identity ?? null;
|
|
11431
|
+
const previous = [...arrayInstances].reverse().find(
|
|
11432
|
+
(candidate) => sameTomlKey(candidate.key, header.name) && candidate.parentIdentity === parentIdentity
|
|
11433
|
+
);
|
|
11434
|
+
const count = (previous?.count ?? 0) + 1;
|
|
11435
|
+
const instance = {
|
|
11436
|
+
key: header.name,
|
|
11437
|
+
parentIdentity,
|
|
11438
|
+
identity: `${parentIdentity ?? "root"}:${JSON.stringify(header.name)}#${count}`,
|
|
11439
|
+
count
|
|
11440
|
+
};
|
|
11441
|
+
arrayInstances.push(instance);
|
|
11442
|
+
currentTable = { identity: instance.identity };
|
|
11443
|
+
} else {
|
|
11444
|
+
const context = arrayTableContext(header.name, arrayInstances);
|
|
11445
|
+
currentTable = {
|
|
11446
|
+
identity: tomlTableIdentity(header.name, context)
|
|
11447
|
+
};
|
|
11448
|
+
if (tables.has(currentTable.identity))
|
|
11449
|
+
throw new Error(
|
|
11450
|
+
`duplicate TOML table '${tomlKeyLabel(header.name)}'`
|
|
11451
|
+
);
|
|
11452
|
+
tables.add(currentTable.identity);
|
|
11453
|
+
}
|
|
11454
|
+
continue;
|
|
11455
|
+
}
|
|
11456
|
+
const equals = findTomlEquals(line);
|
|
11457
|
+
if (equals <= 0) throw new Error(`invalid TOML line '${rawLine}'`);
|
|
11458
|
+
const key = normalizeTomlKey(line.slice(0, equals).trim());
|
|
11459
|
+
const value = line.slice(equals + 1).trim();
|
|
11460
|
+
if (!value || value.startsWith("#"))
|
|
11461
|
+
throw new Error(`empty TOML value for '${tomlKeyLabel(key)}'`);
|
|
11462
|
+
const fullKey = `${currentTable.identity}\0${JSON.stringify(key)}`;
|
|
11463
|
+
if (keys.has(fullKey))
|
|
11464
|
+
throw new Error(`duplicate TOML key '${tomlKeyLabel(key)}'`);
|
|
11465
|
+
keys.add(fullKey);
|
|
11466
|
+
scanTomlValue(line.slice(equals + 1), valueState);
|
|
11467
|
+
if (valueState.quote === "basic" || valueState.quote === "literal")
|
|
11468
|
+
throw new Error("unterminated TOML string");
|
|
11469
|
+
if (!valueState.quote && valueState.containers.length === 0)
|
|
11470
|
+
validateTomlValueShape(value);
|
|
11471
|
+
}
|
|
11472
|
+
if (valueState.quote) throw new Error("unterminated TOML string");
|
|
11473
|
+
if (valueState.containers.length > 0)
|
|
11474
|
+
throw new Error("unbalanced TOML value");
|
|
11475
|
+
}
|
|
11476
|
+
function isTomlTableHeader(line) {
|
|
11477
|
+
try {
|
|
11478
|
+
return parseTomlHeader(line) !== null;
|
|
11479
|
+
} catch {
|
|
11480
|
+
return false;
|
|
11481
|
+
}
|
|
11482
|
+
}
|
|
11483
|
+
function replaceTomlTable(content, tableName, replacement) {
|
|
11484
|
+
const target = normalizeTomlKey(tableName);
|
|
11485
|
+
const lines = content.replace(/\r\n/g, "\n").split("\n");
|
|
11486
|
+
const replacementLines = replacement.trim().split("\n");
|
|
11487
|
+
const kept = [];
|
|
11488
|
+
let i = 0;
|
|
11489
|
+
while (i < lines.length) {
|
|
11490
|
+
const header = parseTomlHeader(lines[i]);
|
|
11491
|
+
if (header && (sameTomlKey(header.name, target) || isTomlKeyPrefix(target, header.name))) {
|
|
11492
|
+
i += 1;
|
|
11493
|
+
while (i < lines.length && !isTomlTableHeader(lines[i])) i += 1;
|
|
11494
|
+
continue;
|
|
11495
|
+
}
|
|
11496
|
+
kept.push(lines[i]);
|
|
11497
|
+
i += 1;
|
|
11498
|
+
}
|
|
11499
|
+
const base = kept.join("\n").trim();
|
|
11500
|
+
return `${base.length > 0 ? `${base}
|
|
11501
|
+
|
|
11502
|
+
` : ""}${replacementLines.join("\n")}
|
|
11503
|
+
`;
|
|
11504
|
+
}
|
|
11505
|
+
var defaultTomlFileOperations = {
|
|
11506
|
+
copyFileSync,
|
|
11507
|
+
renameSync: renameSync4
|
|
11508
|
+
};
|
|
11509
|
+
function writeTomlAtomic(path, content, fileOperations = {}) {
|
|
11510
|
+
ensureDir2(path);
|
|
11511
|
+
const operations = { ...defaultTomlFileOperations, ...fileOperations };
|
|
11512
|
+
const temporary = `${path}.sechroom-${process.pid}-${randomUUID3()}.tmp`;
|
|
11513
|
+
const backup = `${path}.bak`;
|
|
11514
|
+
const backupTemporary = `${backup}.sechroom-${process.pid}-${randomUUID3()}.tmp`;
|
|
11515
|
+
const mode = existsSync13(path) ? statSync5(path).mode & 4095 : 384;
|
|
11516
|
+
try {
|
|
11517
|
+
writeFileSync15(temporary, content, { mode });
|
|
11518
|
+
chmodSync2(temporary, mode);
|
|
11519
|
+
validateToml(readFileSync16(temporary, "utf8"));
|
|
11520
|
+
if (existsSync13(path) && !existsSync13(backup)) {
|
|
11521
|
+
operations.copyFileSync(path, backupTemporary);
|
|
11522
|
+
chmodSync2(backupTemporary, mode);
|
|
11523
|
+
validateToml(readFileSync16(backupTemporary, "utf8"));
|
|
11524
|
+
operations.renameSync(backupTemporary, backup);
|
|
11525
|
+
}
|
|
11526
|
+
operations.renameSync(temporary, path);
|
|
11527
|
+
} finally {
|
|
11528
|
+
rmSync7(temporary, { force: true });
|
|
11529
|
+
rmSync7(backupTemporary, { force: true });
|
|
11530
|
+
}
|
|
11531
|
+
}
|
|
11532
|
+
function mergeCodexToml(path, snippet, dryRun, fileOperations = {}) {
|
|
10812
11533
|
const existed = existsSync13(path);
|
|
10813
|
-
|
|
10814
|
-
|
|
10815
|
-
|
|
10816
|
-
|
|
11534
|
+
const body = readOr(path, "");
|
|
11535
|
+
try {
|
|
11536
|
+
validateToml(body);
|
|
11537
|
+
} catch (error) {
|
|
11538
|
+
return {
|
|
11539
|
+
kind: "mcp",
|
|
11540
|
+
path,
|
|
11541
|
+
status: "skipped",
|
|
11542
|
+
note: `existing TOML is invalid \u2014 left untouched (${error.message})`
|
|
11543
|
+
};
|
|
11544
|
+
}
|
|
11545
|
+
const next = replaceTomlTable(body, CODEX_MCP_TABLE, snippet);
|
|
11546
|
+
try {
|
|
11547
|
+
validateToml(next);
|
|
11548
|
+
} catch (error) {
|
|
11549
|
+
return {
|
|
11550
|
+
kind: "mcp",
|
|
11551
|
+
path,
|
|
11552
|
+
status: "skipped",
|
|
11553
|
+
note: `generated TOML is invalid \u2014 left untouched (${error.message})`
|
|
11554
|
+
};
|
|
11555
|
+
}
|
|
10817
11556
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
10818
|
-
|
|
10819
|
-
|
|
11557
|
+
if (next === body)
|
|
11558
|
+
return { kind: "mcp", path, status: existed ? "current" : "created" };
|
|
11559
|
+
try {
|
|
11560
|
+
writeTomlAtomic(path, next, fileOperations);
|
|
11561
|
+
} catch (error) {
|
|
11562
|
+
return {
|
|
11563
|
+
kind: "mcp",
|
|
11564
|
+
path,
|
|
11565
|
+
status: "skipped",
|
|
11566
|
+
note: `could not replace TOML \u2014 left untouched (${error.message})`
|
|
11567
|
+
};
|
|
11568
|
+
}
|
|
10820
11569
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
10821
11570
|
}
|
|
10822
11571
|
function writeInstructionBlock(path, write, dryRun) {
|
|
10823
11572
|
const existed = existsSync13(path);
|
|
10824
11573
|
const next = computeBlockFile(readOr(path, ""), write);
|
|
10825
|
-
if (dryRun)
|
|
11574
|
+
if (dryRun)
|
|
11575
|
+
return { kind: "instruction", path, status: "dry-run", block: write.block };
|
|
10826
11576
|
ensureDir2(path);
|
|
10827
11577
|
writeFileSync15(path, next);
|
|
10828
|
-
return {
|
|
11578
|
+
return {
|
|
11579
|
+
kind: "instruction",
|
|
11580
|
+
path,
|
|
11581
|
+
status: existed ? "merged" : "created",
|
|
11582
|
+
block: write.block
|
|
11583
|
+
};
|
|
10829
11584
|
}
|
|
10830
11585
|
function computeBlockFile(current, write) {
|
|
10831
11586
|
const rendered = renderBlock(write);
|
|
@@ -10874,7 +11629,13 @@ function applyBlock(path, write, mode, dryRun) {
|
|
|
10874
11629
|
};
|
|
10875
11630
|
}
|
|
10876
11631
|
if (state === "current") {
|
|
10877
|
-
return {
|
|
11632
|
+
return {
|
|
11633
|
+
kind: "instruction",
|
|
11634
|
+
path,
|
|
11635
|
+
status: "current",
|
|
11636
|
+
eval: "current",
|
|
11637
|
+
block: write.block
|
|
11638
|
+
};
|
|
10878
11639
|
}
|
|
10879
11640
|
if (state === "drift" && mode !== "force") {
|
|
10880
11641
|
const proposedPath = `${path}.proposed`;
|
|
@@ -10909,7 +11670,12 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
10909
11670
|
const section = findSection(surface, target.mcp.sectionType);
|
|
10910
11671
|
const snippet = sectionSnippet(section);
|
|
10911
11672
|
if (!snippet) {
|
|
10912
|
-
actions.push({
|
|
11673
|
+
actions.push({
|
|
11674
|
+
kind: "mcp",
|
|
11675
|
+
path: target.mcp.path,
|
|
11676
|
+
status: "skipped",
|
|
11677
|
+
note: `no ${target.mcp.sectionType} section on surface '${target.mcp.surfaceKey}'`
|
|
11678
|
+
});
|
|
10913
11679
|
} else {
|
|
10914
11680
|
actions.push(
|
|
10915
11681
|
target.mcp.format === "toml" ? mergeCodexToml(target.mcp.path, snippet, dryRun) : mergeMcpJson(target.mcp.path, snippet, dryRun)
|
|
@@ -10920,25 +11686,44 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
10920
11686
|
const surface = findSurface(setup, target.instruction.surfaceKey);
|
|
10921
11687
|
const section = findSection(surface, SectionType.InstructionFile);
|
|
10922
11688
|
if (!section) {
|
|
10923
|
-
actions.push({
|
|
11689
|
+
actions.push({
|
|
11690
|
+
kind: "instruction",
|
|
11691
|
+
path: target.instruction.path,
|
|
11692
|
+
status: "skipped",
|
|
11693
|
+
note: `no instruction-file section on surface '${target.instruction.surfaceKey}'`
|
|
11694
|
+
});
|
|
10924
11695
|
} else {
|
|
10925
11696
|
const resolved = await withSpinner(
|
|
10926
11697
|
`Resolving ${target.label} agent instructions`,
|
|
10927
11698
|
() => resolveInstruction(cfg, section, opts.personalWorkspaceId)
|
|
10928
11699
|
);
|
|
10929
11700
|
if (!resolved) {
|
|
10930
|
-
actions.push({
|
|
11701
|
+
actions.push({
|
|
11702
|
+
kind: "instruction",
|
|
11703
|
+
path: target.instruction.path,
|
|
11704
|
+
status: "skipped",
|
|
11705
|
+
note: "no role template found in this tenant \u2014 install the SEM Starter bundle, then re-run `sechroom setup agent-files`"
|
|
11706
|
+
});
|
|
10931
11707
|
} else {
|
|
10932
11708
|
const action = applyBlock(
|
|
10933
11709
|
target.instruction.path,
|
|
10934
|
-
{
|
|
11710
|
+
{
|
|
11711
|
+
block: "role-template",
|
|
11712
|
+
body: resolved.body,
|
|
11713
|
+
source: resolved.sourceRef
|
|
11714
|
+
},
|
|
10935
11715
|
mode,
|
|
10936
11716
|
opts.dryRun
|
|
10937
11717
|
);
|
|
10938
|
-
actions.push(
|
|
11718
|
+
actions.push(
|
|
11719
|
+
resolved.source === "override" && action.status !== "current" ? { ...action, note: action.note ?? "your personal copy" } : action
|
|
11720
|
+
);
|
|
10939
11721
|
}
|
|
10940
11722
|
}
|
|
10941
|
-
const conventionsSection = findSection(
|
|
11723
|
+
const conventionsSection = findSection(
|
|
11724
|
+
surface,
|
|
11725
|
+
SectionType.WorkspaceConventions
|
|
11726
|
+
);
|
|
10942
11727
|
if (conventionsSection) {
|
|
10943
11728
|
const conventions = await withSpinner(
|
|
10944
11729
|
`Composing ${target.label} workspace conventions`,
|
|
@@ -10947,11 +11732,20 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
10947
11732
|
if (conventions) {
|
|
10948
11733
|
const action = applyBlock(
|
|
10949
11734
|
target.instruction.path,
|
|
10950
|
-
{
|
|
11735
|
+
{
|
|
11736
|
+
block: "workspace-conventions",
|
|
11737
|
+
body: conventions.body,
|
|
11738
|
+
source: `workspace:${cfg.workspaceId ?? ""}`
|
|
11739
|
+
},
|
|
10951
11740
|
mode,
|
|
10952
11741
|
opts.dryRun
|
|
10953
11742
|
);
|
|
10954
|
-
actions.push(
|
|
11743
|
+
actions.push(
|
|
11744
|
+
action.status === "current" ? action : {
|
|
11745
|
+
...action,
|
|
11746
|
+
note: action.note ?? `workspace conventions (${conventions.refs.length})`
|
|
11747
|
+
}
|
|
11748
|
+
);
|
|
10955
11749
|
}
|
|
10956
11750
|
}
|
|
10957
11751
|
}
|
|
@@ -11169,7 +11963,12 @@ function buildConventionDraft(title, rawKind, rawBody) {
|
|
|
11169
11963
|
|
|
11170
11964
|
${body}
|
|
11171
11965
|
`,
|
|
11172
|
-
tags: [
|
|
11966
|
+
tags: [
|
|
11967
|
+
"agent-setup-bundle",
|
|
11968
|
+
"scope:sechroom",
|
|
11969
|
+
`kind:${kind}`,
|
|
11970
|
+
"archetype:document"
|
|
11971
|
+
]
|
|
11173
11972
|
};
|
|
11174
11973
|
}
|
|
11175
11974
|
function copyChoice(opts) {
|
|
@@ -11182,9 +11981,16 @@ async function maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId,
|
|
|
11182
11981
|
const instr = targets[key]?.instruction;
|
|
11183
11982
|
if (!instr || seen.has(instr.surfaceKey)) continue;
|
|
11184
11983
|
seen.add(instr.surfaceKey);
|
|
11185
|
-
const section = findSection(
|
|
11984
|
+
const section = findSection(
|
|
11985
|
+
findSurface(setup, instr.surfaceKey),
|
|
11986
|
+
SectionType.InstructionFile
|
|
11987
|
+
);
|
|
11186
11988
|
if (!section) continue;
|
|
11187
|
-
const resolved = await resolveInstruction(
|
|
11989
|
+
const resolved = await resolveInstruction(
|
|
11990
|
+
cfg,
|
|
11991
|
+
section,
|
|
11992
|
+
personalWorkspaceId
|
|
11993
|
+
);
|
|
11188
11994
|
if (!resolved || resolved.source === "override") continue;
|
|
11189
11995
|
let make = choice === "yes";
|
|
11190
11996
|
if (choice === "ask") {
|
|
@@ -11199,8 +12005,10 @@ version, the shared template stays clean, and you can discard back anytime.
|
|
|
11199
12005
|
}
|
|
11200
12006
|
if (make) {
|
|
11201
12007
|
await createOverride(cfg, resolved, personalWorkspaceId);
|
|
11202
|
-
process.stderr.write(
|
|
11203
|
-
|
|
12008
|
+
process.stderr.write(
|
|
12009
|
+
`\u2713 personal copy created for ${instr.surfaceKey} \u2014 edit it on the Agent setup page or via the API.
|
|
12010
|
+
`
|
|
12011
|
+
);
|
|
11204
12012
|
}
|
|
11205
12013
|
}
|
|
11206
12014
|
}
|
|
@@ -11210,7 +12018,9 @@ function resolveClientKeys(raw) {
|
|
|
11210
12018
|
if (tokens.includes("all")) return [...ALL_CLIENT_KEYS];
|
|
11211
12019
|
for (const k of tokens) {
|
|
11212
12020
|
if (!targets[k]) {
|
|
11213
|
-
fail(
|
|
12021
|
+
fail(
|
|
12022
|
+
`unknown client '${k}'. Known: ${ALL_CLIENT_KEYS.join(", ")}, or 'all'.`
|
|
12023
|
+
);
|
|
11214
12024
|
}
|
|
11215
12025
|
}
|
|
11216
12026
|
return [...new Set(tokens)];
|
|
@@ -11221,8 +12031,10 @@ ${client.label} (${client.key}):
|
|
|
11221
12031
|
`);
|
|
11222
12032
|
for (const a of actions) {
|
|
11223
12033
|
const tag = a.status === "skipped" ? "skip" : a.status;
|
|
11224
|
-
process.stdout.write(
|
|
11225
|
-
`
|
|
12034
|
+
process.stdout.write(
|
|
12035
|
+
` [${tag}] ${a.kind}: ${a.path}${a.note ? ` \u2014 ${a.note}` : ""}
|
|
12036
|
+
`
|
|
12037
|
+
);
|
|
11226
12038
|
}
|
|
11227
12039
|
}
|
|
11228
12040
|
function resolveEvalMode(opts) {
|
|
@@ -11296,8 +12108,10 @@ function summarizeEval(result, mode, json, dryRun) {
|
|
|
11296
12108
|
const { eval: counts } = buildCheckReport(result);
|
|
11297
12109
|
if (json) return;
|
|
11298
12110
|
if (!dryRun && counts.stale) {
|
|
11299
|
-
process.stderr.write(
|
|
11300
|
-
|
|
12111
|
+
process.stderr.write(
|
|
12112
|
+
`\u21BB refreshed ${counts.stale} section(s) the server had moved
|
|
12113
|
+
`
|
|
12114
|
+
);
|
|
11301
12115
|
}
|
|
11302
12116
|
if (!dryRun && counts.drift) {
|
|
11303
12117
|
process.stderr.write(
|
|
@@ -11328,7 +12142,39 @@ async function resolveNamespaceChoice(cfg, flag) {
|
|
|
11328
12142
|
return picked === GLOBAL_NAMESPACE ? null : picked;
|
|
11329
12143
|
}
|
|
11330
12144
|
function registerInit(program2) {
|
|
11331
|
-
program2.command("init").description(
|
|
12145
|
+
program2.command("init").description(
|
|
12146
|
+
"Wire this project for sechroom: write MCP config + agent instruction files from the server's setup descriptors"
|
|
12147
|
+
).option(
|
|
12148
|
+
"--client <list...>",
|
|
12149
|
+
`clients to wire \u2014 space- or comma-separated (${ALL_CLIENT_KEYS.join(", ")}) or 'all'`,
|
|
12150
|
+
DEFAULT_CLIENT_KEY
|
|
12151
|
+
).option(
|
|
12152
|
+
"--scope <scope>",
|
|
12153
|
+
"install skills/agents/hooks 'global' (config dir / CLAUDE_CONFIG_DIR) or 'project' (<cwd>/.claude) \u2014 default global",
|
|
12154
|
+
"global"
|
|
12155
|
+
).option("--dry-run", "print what would be written without writing", false).option("--mcp-only", "only write MCP config (skip agent files)", false).option(
|
|
12156
|
+
"--agent-files-only",
|
|
12157
|
+
"only write agent instruction files (skip MCP config)",
|
|
12158
|
+
false
|
|
12159
|
+
).option(
|
|
12160
|
+
"--copy",
|
|
12161
|
+
"make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)"
|
|
12162
|
+
).option(
|
|
12163
|
+
"--namespace <slug>",
|
|
12164
|
+
"MCP namespace for the connection URL (interactive picker if omitted on a TTY; defaults to tenant-global)"
|
|
12165
|
+
).option(
|
|
12166
|
+
"--refresh",
|
|
12167
|
+
"refresh out-of-date agent-file blocks in place (local edits preserved to .proposed)",
|
|
12168
|
+
false
|
|
12169
|
+
).option(
|
|
12170
|
+
"--force",
|
|
12171
|
+
"rewrite agent-file managed blocks, overwriting local edits inside the markers",
|
|
12172
|
+
false
|
|
12173
|
+
).option(
|
|
12174
|
+
"--check",
|
|
12175
|
+
"report whether agent files would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing",
|
|
12176
|
+
false
|
|
12177
|
+
).addHelpText(
|
|
11332
12178
|
"after",
|
|
11333
12179
|
`
|
|
11334
12180
|
Examples:
|
|
@@ -11343,7 +12189,9 @@ Examples:
|
|
|
11343
12189
|
const mode = resolveEvalMode(opts);
|
|
11344
12190
|
const check = mode === "check";
|
|
11345
12191
|
if (check && opts.mcpOnly) {
|
|
11346
|
-
fail(
|
|
12192
|
+
fail(
|
|
12193
|
+
"--check inspects agent files and cannot be combined with --mcp-only."
|
|
12194
|
+
);
|
|
11347
12195
|
}
|
|
11348
12196
|
const namespaceSlug = await resolveNamespaceChoice(cfg, opts.namespace);
|
|
11349
12197
|
const setup = await withSpinner(
|
|
@@ -11357,14 +12205,28 @@ Examples:
|
|
|
11357
12205
|
} catch (err2) {
|
|
11358
12206
|
return fail(err2.message);
|
|
11359
12207
|
}
|
|
11360
|
-
const claudeTargets = resolveClaudeTargets({
|
|
12208
|
+
const claudeTargets = resolveClaudeTargets({
|
|
12209
|
+
override: g.claudeConfigDir,
|
|
12210
|
+
scope,
|
|
12211
|
+
cwd: process.cwd()
|
|
12212
|
+
});
|
|
11361
12213
|
const codexHomes = resolveCodexHomes({ override: g.codexHome, scope });
|
|
11362
|
-
const targets = clientTargets(process.cwd(), {
|
|
12214
|
+
const targets = clientTargets(process.cwd(), {
|
|
12215
|
+
claudeDir: claudeTargets[0]?.dir,
|
|
12216
|
+
codexHome: codexHomes[0] ?? null
|
|
12217
|
+
});
|
|
11363
12218
|
const keys = resolveClientKeys(opts.client);
|
|
11364
12219
|
const json = g.json;
|
|
11365
12220
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
11366
12221
|
if (!opts.dryRun && !opts.mcpOnly && !check) {
|
|
11367
|
-
await maybeOfferCopies(
|
|
12222
|
+
await maybeOfferCopies(
|
|
12223
|
+
cfg,
|
|
12224
|
+
setup,
|
|
12225
|
+
targets,
|
|
12226
|
+
keys,
|
|
12227
|
+
personalWorkspaceId,
|
|
12228
|
+
copyChoice(opts)
|
|
12229
|
+
);
|
|
11368
12230
|
}
|
|
11369
12231
|
const result = [];
|
|
11370
12232
|
for (const key of keys) {
|
|
@@ -11382,18 +12244,33 @@ Examples:
|
|
|
11382
12244
|
summarizeEval(result, mode, Boolean(json), Boolean(opts.dryRun));
|
|
11383
12245
|
if (!json && !opts.dryRun && !opts.mcpOnly && !check) {
|
|
11384
12246
|
for (const t of claudeTargets) {
|
|
11385
|
-
await maybeOfferSkills(cfg, personalWorkspaceId, {
|
|
12247
|
+
await maybeOfferSkills(cfg, personalWorkspaceId, {
|
|
12248
|
+
yes: false,
|
|
12249
|
+
dryRun: Boolean(opts.dryRun),
|
|
12250
|
+
surface: "claude-code",
|
|
12251
|
+
configDir: t.dir
|
|
12252
|
+
});
|
|
11386
12253
|
}
|
|
11387
12254
|
}
|
|
11388
12255
|
if (!json && !opts.dryRun && !opts.mcpOnly && !check) {
|
|
11389
|
-
await maybeOfferHooks({
|
|
12256
|
+
await maybeOfferHooks({
|
|
12257
|
+
yes: false,
|
|
12258
|
+
dryRun: Boolean(opts.dryRun),
|
|
12259
|
+
cwd: process.cwd(),
|
|
12260
|
+
scope,
|
|
12261
|
+
claudeConfigDir: g.claudeConfigDir,
|
|
12262
|
+
codexHome: g.codexHome
|
|
12263
|
+
});
|
|
11390
12264
|
}
|
|
11391
12265
|
if (json) {
|
|
11392
12266
|
emit({ dryRun: Boolean(opts.dryRun), clients: result }, true);
|
|
11393
12267
|
return;
|
|
11394
12268
|
}
|
|
11395
12269
|
const first = targets[keys[0]];
|
|
11396
|
-
const surface = findSurface(
|
|
12270
|
+
const surface = findSurface(
|
|
12271
|
+
setup,
|
|
12272
|
+
first.mcp?.surfaceKey ?? first.instruction?.surfaceKey ?? ""
|
|
12273
|
+
);
|
|
11397
12274
|
const verify = findSection(surface, SectionType.Verify);
|
|
11398
12275
|
if (verify?.description) {
|
|
11399
12276
|
process.stdout.write(`
|
|
@@ -11407,13 +12284,64 @@ Next \u2014 verify: ${verify.description}
|
|
|
11407
12284
|
}
|
|
11408
12285
|
function registerSetup(program2, deps = {}) {
|
|
11409
12286
|
const setup = program2.command("setup").description("Granular onboarding steps (init runs these together)");
|
|
11410
|
-
setup.command("mcp <clients...>").description(
|
|
11411
|
-
|
|
11412
|
-
|
|
11413
|
-
|
|
11414
|
-
|
|
12287
|
+
setup.command("mcp <clients...>").description(
|
|
12288
|
+
`Write only the MCP config for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`
|
|
12289
|
+
).option("--dry-run", "print what would be written without writing", false).option(
|
|
12290
|
+
"--namespace <slug>",
|
|
12291
|
+
"MCP namespace for the connection URL (interactive picker if omitted on a TTY; defaults to tenant-global)"
|
|
12292
|
+
).addHelpText(
|
|
12293
|
+
"after",
|
|
12294
|
+
"\nExamples:\n $ sechroom setup mcp codex\n $ sechroom setup mcp claude-code codex\n $ sechroom setup mcp all"
|
|
12295
|
+
).action(async (clients, opts, cmd) => {
|
|
12296
|
+
await runClients(clients, cmd, {
|
|
12297
|
+
dryRun: Boolean(opts.dryRun),
|
|
12298
|
+
mcp: true,
|
|
12299
|
+
agentFiles: false,
|
|
12300
|
+
namespace: opts.namespace
|
|
12301
|
+
});
|
|
12302
|
+
});
|
|
12303
|
+
setup.command("agent-files <clients...>").description(
|
|
12304
|
+
`Write only the agent instruction file(s) for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`
|
|
12305
|
+
).option("--dry-run", "print what would be written without writing", false).option(
|
|
12306
|
+
"--copy",
|
|
12307
|
+
"make a personal copy you can edit (default: prompt on a TTY, else skip)"
|
|
12308
|
+
).option(
|
|
12309
|
+
"--refresh",
|
|
12310
|
+
"refresh out-of-date blocks in place (local edits preserved to .proposed)",
|
|
12311
|
+
false
|
|
12312
|
+
).option(
|
|
12313
|
+
"--force",
|
|
12314
|
+
"rewrite managed blocks, overwriting local edits inside the markers",
|
|
12315
|
+
false
|
|
12316
|
+
).option(
|
|
12317
|
+
"--check",
|
|
12318
|
+
"report whether anything would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing",
|
|
12319
|
+
false
|
|
12320
|
+
).addHelpText(
|
|
12321
|
+
"after",
|
|
12322
|
+
"\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"
|
|
12323
|
+
).action(async (clients, opts, cmd) => {
|
|
12324
|
+
await runClients(clients, cmd, {
|
|
12325
|
+
dryRun: Boolean(opts.dryRun),
|
|
12326
|
+
mcp: false,
|
|
12327
|
+
agentFiles: true,
|
|
12328
|
+
copy: opts.copy,
|
|
12329
|
+
mode: resolveEvalMode(opts)
|
|
12330
|
+
});
|
|
11415
12331
|
});
|
|
11416
|
-
setup.command("new-convention <title...>").description(
|
|
12332
|
+
setup.command("new-convention <title...>").description(
|
|
12333
|
+
"Scaffold a workspace-conventions section: author a correctly-tagged memo (header as first body line) + regen the agent files"
|
|
12334
|
+
).option(
|
|
12335
|
+
"--kind <kind>",
|
|
12336
|
+
"reference | standard (orders the section; reference first)",
|
|
12337
|
+
"reference"
|
|
12338
|
+
).option(
|
|
12339
|
+
"--workspace <id>",
|
|
12340
|
+
"workspace to author in (default: the bound workspace)"
|
|
12341
|
+
).option(
|
|
12342
|
+
"--body <markdown>",
|
|
12343
|
+
"section body (default: a TODO scaffold to edit later)"
|
|
12344
|
+
).option("--no-regen", "skip the agent-files regen after authoring").option("--dry-run", "print what would be authored; write nothing", false).addHelpText(
|
|
11417
12345
|
"after",
|
|
11418
12346
|
`
|
|
11419
12347
|
The memo carries the two conventions a workspace-conventions section needs (FR-sechroom-236):
|
|
@@ -11429,10 +12357,15 @@ Examples:
|
|
|
11429
12357
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
11430
12358
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
11431
12359
|
const title = titleParts.join(" ").trim();
|
|
11432
|
-
if (!title)
|
|
12360
|
+
if (!title)
|
|
12361
|
+
fail(
|
|
12362
|
+
'a section title is required, e.g. `sechroom setup new-convention "Deploy runbook"`.'
|
|
12363
|
+
);
|
|
11433
12364
|
const workspaceId = opts.workspace ?? cfg.workspaceId;
|
|
11434
12365
|
if (!workspaceId)
|
|
11435
|
-
fail(
|
|
12366
|
+
fail(
|
|
12367
|
+
"no workspace \u2014 pass --workspace <id> or bind one (`sechroom config set --local workspaceId <id>`)."
|
|
12368
|
+
);
|
|
11436
12369
|
const draft = buildConventionDraft(title, opts.kind, opts.body);
|
|
11437
12370
|
if (opts.dryRun) {
|
|
11438
12371
|
emit(
|
|
@@ -11473,7 +12406,10 @@ Examples:
|
|
|
11473
12406
|
}
|
|
11474
12407
|
if (opts.regen === false) {
|
|
11475
12408
|
if (json) emit({ id: data.id, workspaceId, regen: false }, true);
|
|
11476
|
-
else
|
|
12409
|
+
else
|
|
12410
|
+
process.stdout.write(
|
|
12411
|
+
"Skipped regen (--no-regen). Run `sechroom setup agent-files all` to apply.\n"
|
|
12412
|
+
);
|
|
11477
12413
|
return;
|
|
11478
12414
|
}
|
|
11479
12415
|
const regenerate = deps.regenerateConvention ?? runClients;
|
|
@@ -11502,7 +12438,14 @@ async function runClients(clients, cmd, opts) {
|
|
|
11502
12438
|
);
|
|
11503
12439
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
11504
12440
|
if (opts.agentFiles && !opts.dryRun && !check) {
|
|
11505
|
-
await maybeOfferCopies(
|
|
12441
|
+
await maybeOfferCopies(
|
|
12442
|
+
cfg,
|
|
12443
|
+
setupData,
|
|
12444
|
+
targets,
|
|
12445
|
+
keys,
|
|
12446
|
+
personalWorkspaceId,
|
|
12447
|
+
copyChoice(opts)
|
|
12448
|
+
);
|
|
11506
12449
|
}
|
|
11507
12450
|
const json = g.json;
|
|
11508
12451
|
const result = [];
|
|
@@ -11523,7 +12466,9 @@ async function runClients(clients, cmd, opts) {
|
|
|
11523
12466
|
emit({ dryRun: opts.dryRun, clients: result }, true);
|
|
11524
12467
|
return;
|
|
11525
12468
|
}
|
|
11526
|
-
process.stdout.write(
|
|
12469
|
+
process.stdout.write(
|
|
12470
|
+
opts.dryRun ? "\n(dry run \u2014 nothing written)\n" : "\nDone.\n"
|
|
12471
|
+
);
|
|
11527
12472
|
}
|
|
11528
12473
|
|
|
11529
12474
|
// src/commands/namespace.ts
|
|
@@ -11599,7 +12544,7 @@ import { basename as basename5, join as join21 } from "path";
|
|
|
11599
12544
|
|
|
11600
12545
|
// src/commands/fanout.ts
|
|
11601
12546
|
import { spawnSync } from "child_process";
|
|
11602
|
-
import { existsSync as existsSync14, readFileSync as readFileSync17, readdirSync as readdirSync4, statSync as
|
|
12547
|
+
import { existsSync as existsSync14, readFileSync as readFileSync17, readdirSync as readdirSync4, statSync as statSync6 } from "fs";
|
|
11603
12548
|
import { isAbsolute as isAbsolute3, join as join20, resolve as resolve7 } from "path";
|
|
11604
12549
|
var ICON = {
|
|
11605
12550
|
refresh: "\u21BB",
|
|
@@ -11622,7 +12567,7 @@ function discoverChildren(root) {
|
|
|
11622
12567
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
11623
12568
|
const dir = join20(root, name);
|
|
11624
12569
|
try {
|
|
11625
|
-
if (!
|
|
12570
|
+
if (!statSync6(dir).isDirectory()) continue;
|
|
11626
12571
|
} catch {
|
|
11627
12572
|
continue;
|
|
11628
12573
|
}
|
|
@@ -11748,20 +12693,27 @@ function resolveBaseUrl(g) {
|
|
|
11748
12693
|
return baseUrl.replace(/\/$/, "");
|
|
11749
12694
|
}
|
|
11750
12695
|
async function fetchWorkspaces(client) {
|
|
11751
|
-
const { data, error } = await client.GET("/workspaces", {
|
|
11752
|
-
|
|
12696
|
+
const { data, error } = await client.GET("/workspaces", {
|
|
12697
|
+
params: { query: { includeArchived: false } }
|
|
12698
|
+
});
|
|
12699
|
+
if (error)
|
|
12700
|
+
throw new Error(`Couldn't list your workspaces: ${JSON.stringify(error)}`);
|
|
11753
12701
|
const rows = data ?? [];
|
|
11754
12702
|
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
12703
|
}
|
|
11756
12704
|
async function lookupWorkspace(client, id) {
|
|
11757
|
-
const { data, error } = await client.GET("/workspaces/{workspaceId}", {
|
|
12705
|
+
const { data, error } = await client.GET("/workspaces/{workspaceId}", {
|
|
12706
|
+
params: { path: { workspaceId: id } }
|
|
12707
|
+
});
|
|
11758
12708
|
if (error) return null;
|
|
11759
12709
|
const env = data;
|
|
11760
12710
|
const w = env?.item ?? env;
|
|
11761
12711
|
return w?.id ? { id: w.id, name: w.name ?? id, parentId: w.parentId ?? null } : null;
|
|
11762
12712
|
}
|
|
11763
12713
|
async function warnIfProjectStray(client, projectId, workspaceId, json) {
|
|
11764
|
-
const { data, error } = await client.GET("/projects/{projectId}", {
|
|
12714
|
+
const { data, error } = await client.GET("/projects/{projectId}", {
|
|
12715
|
+
params: { path: { projectId } }
|
|
12716
|
+
});
|
|
11765
12717
|
if (error) return;
|
|
11766
12718
|
const env = data;
|
|
11767
12719
|
const owner = env?.item?.workspaceId;
|
|
@@ -11805,10 +12757,15 @@ function personalSubtreeIds(personalId, all) {
|
|
|
11805
12757
|
async function pickWorkspace(client, opts = {}) {
|
|
11806
12758
|
const promptLabel = opts.promptLabel ?? "Bind this directory to a workspace:";
|
|
11807
12759
|
const dirName = opts.dirName ?? basename5(process.cwd());
|
|
11808
|
-
const all = await withSpinner(
|
|
12760
|
+
const all = await withSpinner(
|
|
12761
|
+
"Listing your workspaces",
|
|
12762
|
+
() => fetchWorkspaces(client)
|
|
12763
|
+
);
|
|
11809
12764
|
if (all.length === 0) {
|
|
11810
|
-
process.stderr.write(
|
|
11811
|
-
`)
|
|
12765
|
+
process.stderr.write(
|
|
12766
|
+
`no workspaces found \u2014 skipping workspace binding (you can set it later with \`sechroom config set --local workspaceId <id>\`)
|
|
12767
|
+
`
|
|
12768
|
+
);
|
|
11812
12769
|
return void 0;
|
|
11813
12770
|
}
|
|
11814
12771
|
const byId = new Map(all.map((w) => [w.id, w]));
|
|
@@ -11823,19 +12780,35 @@ async function pickWorkspace(client, opts = {}) {
|
|
|
11823
12780
|
const matched = candidates.filter(isMatch).sort(byPath);
|
|
11824
12781
|
const rest = candidates.filter((w) => !isMatch(w)).sort(byPath);
|
|
11825
12782
|
const choices = [
|
|
11826
|
-
...matched.map((w) => ({
|
|
11827
|
-
|
|
11828
|
-
|
|
12783
|
+
...matched.map((w) => ({
|
|
12784
|
+
label: workspacePath(w, byId),
|
|
12785
|
+
value: w.id,
|
|
12786
|
+
hint: style.dim(`matches "${dirName}"`)
|
|
12787
|
+
})),
|
|
12788
|
+
...rest.map((w) => ({
|
|
12789
|
+
label: workspacePath(w, byId),
|
|
12790
|
+
value: w.id,
|
|
12791
|
+
hint: w.id
|
|
12792
|
+
})),
|
|
12793
|
+
{
|
|
12794
|
+
label: style.dim("skip \u2014 don't bind a workspace"),
|
|
12795
|
+
value: SKIP,
|
|
12796
|
+
hint: void 0
|
|
12797
|
+
}
|
|
11829
12798
|
];
|
|
11830
12799
|
const defaultValue = matched.length === 1 ? matched[0].id : SKIP;
|
|
11831
12800
|
const chosen = candidates.length > 12 ? await promptAutocomplete(promptLabel, choices, defaultValue) : await promptSelect(promptLabel, choices, defaultValue);
|
|
11832
12801
|
if (chosen === SKIP) return void 0;
|
|
11833
12802
|
const picked = byId.get(chosen);
|
|
11834
|
-
const collisions = all.filter(
|
|
12803
|
+
const collisions = all.filter(
|
|
12804
|
+
(w) => w.id !== picked.id && namesCollide(w.name, picked.name)
|
|
12805
|
+
);
|
|
11835
12806
|
if (collisions.length > 0) {
|
|
11836
12807
|
process.stderr.write(
|
|
11837
12808
|
`${warn("\u26A0")} ${collisions.length} other workspace(s) have a similar name to ${style.cyan(workspacePath(picked, byId))}:
|
|
11838
|
-
` + collisions.map(
|
|
12809
|
+
` + collisions.map(
|
|
12810
|
+
(w) => ` ${style.dim(workspacePath(w, byId))} ${style.dim(`(${w.id})`)}`
|
|
12811
|
+
).join("\n") + `
|
|
11839
12812
|
You picked ${style.dim(picked.id)} \u2014 re-run \`sechroom config set --local workspaceId <id>\` if that's wrong.
|
|
11840
12813
|
`
|
|
11841
12814
|
);
|
|
@@ -11862,10 +12835,17 @@ async function ensureTenant(baseUrl, g, opts) {
|
|
|
11862
12835
|
const local = readLocalConfig();
|
|
11863
12836
|
let tenant = g.tenant ?? process.env.SECHROOM_TENANT ?? local.tenant ?? persisted.tenant ?? "";
|
|
11864
12837
|
if (!tenant) {
|
|
11865
|
-
const client = await makeClient({
|
|
12838
|
+
const client = await makeClient({
|
|
12839
|
+
baseUrl,
|
|
12840
|
+
tenant: "",
|
|
12841
|
+
account: resolveAccountAlias(),
|
|
12842
|
+
clientId: persisted.clientId
|
|
12843
|
+
});
|
|
11866
12844
|
const { data, error } = await client.GET("/auth/me/tenants", {});
|
|
11867
12845
|
if (error) {
|
|
11868
|
-
fail(
|
|
12846
|
+
fail(
|
|
12847
|
+
`Couldn't list your tenants: ${JSON.stringify(error)}. Pass --tenant <id> to skip this.`
|
|
12848
|
+
);
|
|
11869
12849
|
}
|
|
11870
12850
|
const tenants = data?.tenants ?? [];
|
|
11871
12851
|
if (tenants.length === 0) {
|
|
@@ -11897,21 +12877,44 @@ async function ensureTenant(baseUrl, g, opts) {
|
|
|
11897
12877
|
}
|
|
11898
12878
|
}
|
|
11899
12879
|
const existingWorkspace = local.workspaceId ?? persisted.workspaceId ?? void 0;
|
|
11900
|
-
const wsClient = await makeClient({
|
|
11901
|
-
|
|
11902
|
-
|
|
11903
|
-
|
|
11904
|
-
|
|
12880
|
+
const wsClient = await makeClient({
|
|
12881
|
+
baseUrl,
|
|
12882
|
+
tenant,
|
|
12883
|
+
account: resolveAccountAlias(),
|
|
12884
|
+
clientId: persisted.clientId
|
|
11905
12885
|
});
|
|
12886
|
+
const workspaceId = await resolveWorkspaceBinding(
|
|
12887
|
+
wsClient,
|
|
12888
|
+
existingWorkspace,
|
|
12889
|
+
{
|
|
12890
|
+
yes: opts.yes,
|
|
12891
|
+
json: opts.json,
|
|
12892
|
+
workspace: opts.workspace
|
|
12893
|
+
}
|
|
12894
|
+
);
|
|
11906
12895
|
const defaultProjectId = local.defaultProjectId ?? persisted.defaultProjectId ?? void 0;
|
|
11907
|
-
if (defaultProjectId && workspaceId)
|
|
12896
|
+
if (defaultProjectId && workspaceId)
|
|
12897
|
+
await warnIfProjectStray(
|
|
12898
|
+
wsClient,
|
|
12899
|
+
defaultProjectId,
|
|
12900
|
+
workspaceId,
|
|
12901
|
+
opts.json
|
|
12902
|
+
);
|
|
11908
12903
|
let storeLocal = Boolean(opts.local);
|
|
11909
12904
|
if (!opts.local && canPrompt() && !opts.yes) {
|
|
11910
12905
|
storeLocal = await promptSelect(
|
|
11911
12906
|
"Where should this tenant + base URL be saved?",
|
|
11912
12907
|
[
|
|
11913
|
-
{
|
|
11914
|
-
|
|
12908
|
+
{
|
|
12909
|
+
label: "Globally",
|
|
12910
|
+
value: "global",
|
|
12911
|
+
hint: "all projects on this machine"
|
|
12912
|
+
},
|
|
12913
|
+
{
|
|
12914
|
+
label: "This directory",
|
|
12915
|
+
value: "local",
|
|
12916
|
+
hint: ".sechroom.json \u2014 committed, project + subdirs"
|
|
12917
|
+
}
|
|
11915
12918
|
],
|
|
11916
12919
|
local.path ? "local" : "global"
|
|
11917
12920
|
) === "local";
|
|
@@ -11920,19 +12923,34 @@ async function ensureTenant(baseUrl, g, opts) {
|
|
|
11920
12923
|
const patch = { baseUrl, tenant, ...workspaceId ? { workspaceId } : {} };
|
|
11921
12924
|
if (storeLocal) {
|
|
11922
12925
|
const path = writeLocalConfig(patch, { here: Boolean(opts.here) });
|
|
11923
|
-
if (!opts.json)
|
|
11924
|
-
|
|
12926
|
+
if (!opts.json)
|
|
12927
|
+
process.stderr.write(
|
|
12928
|
+
`${ok("\u2713")} config saved to ${path} (directory-local)
|
|
12929
|
+
`
|
|
12930
|
+
);
|
|
11925
12931
|
} else {
|
|
11926
12932
|
writePersisted(patch);
|
|
11927
|
-
if (!opts.json)
|
|
11928
|
-
|
|
12933
|
+
if (!opts.json)
|
|
12934
|
+
process.stderr.write(
|
|
12935
|
+
`${ok("\u2713")} config saved globally (~/.config/sechroom/config.json)
|
|
12936
|
+
`
|
|
12937
|
+
);
|
|
11929
12938
|
}
|
|
11930
12939
|
if (workspaceId && !existingWorkspace && !opts.json) {
|
|
11931
|
-
process.stderr.write(
|
|
11932
|
-
|
|
12940
|
+
process.stderr.write(
|
|
12941
|
+
`${ok("\u2713")} bound to workspace ${style.dim(workspaceId)}
|
|
12942
|
+
`
|
|
12943
|
+
);
|
|
11933
12944
|
}
|
|
11934
12945
|
}
|
|
11935
|
-
return {
|
|
12946
|
+
return {
|
|
12947
|
+
baseUrl,
|
|
12948
|
+
tenant,
|
|
12949
|
+
account: resolveAccountAlias(),
|
|
12950
|
+
workspaceId,
|
|
12951
|
+
defaultProjectId,
|
|
12952
|
+
clientId: persisted.clientId
|
|
12953
|
+
};
|
|
11936
12954
|
}
|
|
11937
12955
|
async function ensureAuth(cfg, yes) {
|
|
11938
12956
|
if (process.env.SECHROOM_TOKEN) return;
|
|
@@ -11940,17 +12958,27 @@ async function ensureAuth(cfg, yes) {
|
|
|
11940
12958
|
const usable = Boolean(cached?.accessToken) && (cached.expiresAt === void 0 || Date.now() < cached.expiresAt - 6e4 || Boolean(cached.refreshToken));
|
|
11941
12959
|
if (usable) return;
|
|
11942
12960
|
if (!canPrompt() || yes) {
|
|
11943
|
-
fail(
|
|
12961
|
+
fail(
|
|
12962
|
+
"Not signed in. Run `sechroom login` first, or set SECHROOM_TOKEN for headless use."
|
|
12963
|
+
);
|
|
11944
12964
|
}
|
|
11945
|
-
process.stderr.write(
|
|
12965
|
+
process.stderr.write(
|
|
12966
|
+
"\nNot signed in \u2014 opening the browser to authenticate.\n"
|
|
12967
|
+
);
|
|
11946
12968
|
await login(cfg);
|
|
11947
12969
|
}
|
|
11948
12970
|
async function ensureTimezone(cfg, opts) {
|
|
11949
12971
|
const client = await makeClient(cfg);
|
|
11950
12972
|
const { data, error } = await client.GET("/me/profile", {});
|
|
11951
|
-
if (error)
|
|
12973
|
+
if (error)
|
|
12974
|
+
return {
|
|
12975
|
+
timezone: null,
|
|
12976
|
+
action: "skipped",
|
|
12977
|
+
note: "could not read profile"
|
|
12978
|
+
};
|
|
11952
12979
|
const current = data?.effectiveTimezone;
|
|
11953
|
-
if (current && current.trim().length > 0)
|
|
12980
|
+
if (current && current.trim().length > 0)
|
|
12981
|
+
return { timezone: current, action: "already-set" };
|
|
11954
12982
|
const system = systemTimezone();
|
|
11955
12983
|
let tz = system;
|
|
11956
12984
|
if (canPrompt() && !opts.yes) {
|
|
@@ -11962,12 +12990,18 @@ async function ensureTimezone(cfg, opts) {
|
|
|
11962
12990
|
note: "no timezone set \u2014 re-run interactively or pass --yes to adopt the system timezone"
|
|
11963
12991
|
};
|
|
11964
12992
|
}
|
|
11965
|
-
if (!tz)
|
|
12993
|
+
if (!tz)
|
|
12994
|
+
return { timezone: null, action: "skipped", note: "no timezone provided" };
|
|
11966
12995
|
if (opts.dryRun) return { timezone: tz, action: "dry-run" };
|
|
11967
12996
|
const { error: putErr } = await client.PUT("/me/profile", {
|
|
11968
12997
|
body: { displayName: null, photoUrl: null, bio: null, timezone: tz }
|
|
11969
12998
|
});
|
|
11970
|
-
if (putErr)
|
|
12999
|
+
if (putErr)
|
|
13000
|
+
return {
|
|
13001
|
+
timezone: tz,
|
|
13002
|
+
action: "skipped",
|
|
13003
|
+
note: `update failed: ${JSON.stringify(putErr)}`
|
|
13004
|
+
};
|
|
11971
13005
|
return { timezone: tz, action: "set" };
|
|
11972
13006
|
}
|
|
11973
13007
|
async function chooseClients(clientFlag, yes, cwd) {
|
|
@@ -11992,7 +13026,11 @@ async function chooseScope(scopeFlag, yes) {
|
|
|
11992
13026
|
return promptSelect(
|
|
11993
13027
|
"Install skills, agents, and hooks globally or just for this project?",
|
|
11994
13028
|
[
|
|
11995
|
-
{
|
|
13029
|
+
{
|
|
13030
|
+
label: "Globally",
|
|
13031
|
+
value: "global",
|
|
13032
|
+
hint: "~/.claude (or CLAUDE_CONFIG_DIR) \u2014 all projects"
|
|
13033
|
+
},
|
|
11996
13034
|
{ label: "This project", value: "project", hint: "<repo>/.claude" }
|
|
11997
13035
|
],
|
|
11998
13036
|
"global"
|
|
@@ -12001,7 +13039,13 @@ async function chooseScope(scopeFlag, yes) {
|
|
|
12001
13039
|
async function planRecurseChild(entry, root, client, opts) {
|
|
12002
13040
|
const dir = resolveChildDir(entry.path, root);
|
|
12003
13041
|
if (!existsSync15(dir)) {
|
|
12004
|
-
return {
|
|
13042
|
+
return {
|
|
13043
|
+
label: entry.path,
|
|
13044
|
+
dir,
|
|
13045
|
+
disposition: "skip-missing",
|
|
13046
|
+
argv: [],
|
|
13047
|
+
reason: "directory does not exist"
|
|
13048
|
+
};
|
|
12005
13049
|
}
|
|
12006
13050
|
if (existsSync15(join21(dir, ".sechroom.json"))) {
|
|
12007
13051
|
return {
|
|
@@ -12022,20 +13066,40 @@ async function planRecurseChild(entry, root, client, opts) {
|
|
|
12022
13066
|
};
|
|
12023
13067
|
}
|
|
12024
13068
|
if (opts.dryRun) {
|
|
12025
|
-
return {
|
|
13069
|
+
return {
|
|
13070
|
+
label: entry.path,
|
|
13071
|
+
dir,
|
|
13072
|
+
disposition: "bind",
|
|
13073
|
+
argv: ["onboard", "--yes", "--local", "--workspace", "<prompt>"],
|
|
13074
|
+
reason: "unbound \u2014 would prompt for a workspace"
|
|
13075
|
+
};
|
|
12026
13076
|
}
|
|
12027
13077
|
if (opts.yes || !canPrompt()) {
|
|
12028
|
-
return {
|
|
13078
|
+
return {
|
|
13079
|
+
label: entry.path,
|
|
13080
|
+
dir,
|
|
13081
|
+
disposition: "skip-unbound",
|
|
13082
|
+
argv: [],
|
|
13083
|
+
reason: "unbound + no workspace (run interactively, or add it to ./.sechroom/repos.json)"
|
|
13084
|
+
};
|
|
12029
13085
|
}
|
|
12030
|
-
process.stderr.write(
|
|
13086
|
+
process.stderr.write(
|
|
13087
|
+
`
|
|
12031
13088
|
${style.bold(entry.path)} ${style.dim("is not bound yet.")}
|
|
12032
|
-
`
|
|
13089
|
+
`
|
|
13090
|
+
);
|
|
12033
13091
|
const ws = await pickWorkspace(client, {
|
|
12034
13092
|
promptLabel: `Bind ${style.cyan(entry.path)} to a workspace:`,
|
|
12035
13093
|
dirName: basename5(entry.path)
|
|
12036
13094
|
});
|
|
12037
13095
|
if (!ws) {
|
|
12038
|
-
return {
|
|
13096
|
+
return {
|
|
13097
|
+
label: entry.path,
|
|
13098
|
+
dir,
|
|
13099
|
+
disposition: "skip-unbound",
|
|
13100
|
+
argv: [],
|
|
13101
|
+
reason: "unbound \u2014 no workspace chosen (skipped)"
|
|
13102
|
+
};
|
|
12039
13103
|
}
|
|
12040
13104
|
return {
|
|
12041
13105
|
label: entry.path,
|
|
@@ -12050,20 +13114,34 @@ async function resolveFanoutLane(cfg, opts) {
|
|
|
12050
13114
|
let design = opts.designLane ?? process.env.SECHROOM_DESIGN_LANE;
|
|
12051
13115
|
if (!code || !design) {
|
|
12052
13116
|
const clients = detectInstalledClients(process.cwd());
|
|
12053
|
-
const inferred = await inferLanes(
|
|
13117
|
+
const inferred = await inferLanes(
|
|
13118
|
+
cfg,
|
|
13119
|
+
clients.length ? clients : void 0
|
|
13120
|
+
);
|
|
12054
13121
|
code = code ?? inferred.code;
|
|
12055
13122
|
design = design ?? inferred.design;
|
|
12056
13123
|
}
|
|
12057
13124
|
if (!opts.lane && !opts.yes && !opts.dryRun && canPrompt() && (code || design)) {
|
|
12058
|
-
process.stderr.write(
|
|
13125
|
+
process.stderr.write(
|
|
13126
|
+
`
|
|
12059
13127
|
This fan-out will pin the same lane in every repo:
|
|
12060
|
-
`
|
|
12061
|
-
|
|
12062
|
-
|
|
12063
|
-
|
|
12064
|
-
`)
|
|
13128
|
+
`
|
|
13129
|
+
);
|
|
13130
|
+
if (code)
|
|
13131
|
+
process.stderr.write(
|
|
13132
|
+
` ${style.dim("code-lane")} = ${style.cyan(code)}
|
|
13133
|
+
`
|
|
13134
|
+
);
|
|
13135
|
+
if (design)
|
|
13136
|
+
process.stderr.write(
|
|
13137
|
+
` ${style.dim("design-lane")} = ${style.cyan(design)}
|
|
13138
|
+
`
|
|
13139
|
+
);
|
|
12065
13140
|
if (!await promptYesNo("Use this lane for all repos?")) {
|
|
12066
|
-
code = await promptText(
|
|
13141
|
+
code = await promptText(
|
|
13142
|
+
"Code-lane id (blank = let each repo infer)?",
|
|
13143
|
+
code ?? ""
|
|
13144
|
+
) || void 0;
|
|
12067
13145
|
design = await promptText("Design-lane id (blank = skip)?", design ?? "") || void 0;
|
|
12068
13146
|
}
|
|
12069
13147
|
}
|
|
@@ -12081,30 +13159,112 @@ async function runRecurse(cfg, g, opts) {
|
|
|
12081
13159
|
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
12082
13160
|
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
12083
13161
|
if (entries.length === 0) {
|
|
12084
|
-
if (json)
|
|
12085
|
-
|
|
12086
|
-
|
|
13162
|
+
if (json)
|
|
13163
|
+
process.stdout.write(
|
|
13164
|
+
JSON.stringify({ recurse: true, root, repos: [] }) + "\n"
|
|
13165
|
+
);
|
|
13166
|
+
else
|
|
13167
|
+
process.stderr.write(
|
|
13168
|
+
`${warn("\u26A0")} no child repos found ${fromManifest ? `in ${manifestPath}` : `under ${root}`} \u2014 nothing to do.
|
|
13169
|
+
`
|
|
13170
|
+
);
|
|
12087
13171
|
return;
|
|
12088
13172
|
}
|
|
12089
13173
|
if (!json) {
|
|
12090
|
-
process.stderr.write(
|
|
12091
|
-
`)
|
|
13174
|
+
process.stderr.write(
|
|
13175
|
+
`${style.bold("onboard --recurse")} ${style.dim(`(${entries.length} repo${entries.length === 1 ? "" : "s"} from ${sourceLabel})`)}
|
|
13176
|
+
`
|
|
13177
|
+
);
|
|
12092
13178
|
}
|
|
12093
|
-
const lane = await resolveFanoutLane(cfg, {
|
|
12094
|
-
|
|
12095
|
-
|
|
13179
|
+
const lane = await resolveFanoutLane(cfg, {
|
|
13180
|
+
lane: opts.lane,
|
|
13181
|
+
designLane: opts.designLane,
|
|
13182
|
+
yes,
|
|
13183
|
+
dryRun
|
|
13184
|
+
});
|
|
13185
|
+
if (!json && lane.code)
|
|
13186
|
+
process.stderr.write(
|
|
13187
|
+
`${ok("\u2713")} lane ${style.cyan(lane.code)}${lane.design ? ` ${style.dim(`/ ${lane.design}`)}` : ""} for every repo
|
|
13188
|
+
`
|
|
13189
|
+
);
|
|
12096
13190
|
const client = await makeClient(cfg);
|
|
12097
13191
|
const plans = [];
|
|
12098
|
-
for (const entry of entries)
|
|
12099
|
-
|
|
13192
|
+
for (const entry of entries)
|
|
13193
|
+
plans.push(await planRecurseChild(entry, root, client, { yes, dryRun }));
|
|
13194
|
+
const results = runChildren(plans, {
|
|
13195
|
+
globals: passthroughGlobals(g),
|
|
13196
|
+
dryRun,
|
|
13197
|
+
json
|
|
13198
|
+
});
|
|
12100
13199
|
if (json) {
|
|
12101
|
-
process.stdout.write(
|
|
13200
|
+
process.stdout.write(
|
|
13201
|
+
JSON.stringify({ recurse: true, root, dryRun, repos: results }) + "\n"
|
|
13202
|
+
);
|
|
12102
13203
|
return;
|
|
12103
13204
|
}
|
|
12104
13205
|
summarizeFanout(results, { dryRun });
|
|
12105
13206
|
}
|
|
12106
13207
|
function registerOnboard(program2) {
|
|
12107
|
-
program2.command("onboard").description(
|
|
13208
|
+
program2.command("onboard").description(
|
|
13209
|
+
"Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project"
|
|
13210
|
+
).option(
|
|
13211
|
+
"--recurse",
|
|
13212
|
+
"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",
|
|
13213
|
+
false
|
|
13214
|
+
).option(
|
|
13215
|
+
"--lane <id>",
|
|
13216
|
+
"set the code-lane (substrate source identity) explicitly instead of inferring it; with --recurse it's used for every child repo"
|
|
13217
|
+
).option(
|
|
13218
|
+
"--design-lane <id>",
|
|
13219
|
+
"set the design-lane explicitly (substrate-authoring identity); with --recurse applies to every child"
|
|
13220
|
+
).option(
|
|
13221
|
+
"--client <list...>",
|
|
13222
|
+
`clients to wire \u2014 space- or comma-separated (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`
|
|
13223
|
+
).option(
|
|
13224
|
+
"--scope <scope>",
|
|
13225
|
+
"install skills/agents/hooks 'global' (config dir / CLAUDE_CONFIG_DIR) or 'project' (<cwd>/.claude) \u2014 default: prompt, else global"
|
|
13226
|
+
).option(
|
|
13227
|
+
"--local",
|
|
13228
|
+
"save the binding (tenant + base URL + workspace) to a committed .sechroom.json in this repo instead of the global config",
|
|
13229
|
+
false
|
|
13230
|
+
).option(
|
|
13231
|
+
"--here",
|
|
13232
|
+
"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",
|
|
13233
|
+
false
|
|
13234
|
+
).option(
|
|
13235
|
+
"--workspace <id>",
|
|
13236
|
+
"bind this directory to a workspace (skips the interactive workspace pick)"
|
|
13237
|
+
).option(
|
|
13238
|
+
"--cli-only",
|
|
13239
|
+
"configure the CLI only \u2014 don't wire any AI client (no MCP config, no agent files)",
|
|
13240
|
+
false
|
|
13241
|
+
).option(
|
|
13242
|
+
"--no-mcp",
|
|
13243
|
+
"skip the MCP server config (.mcp.json etc.); still write the agent instruction files"
|
|
13244
|
+
).option(
|
|
13245
|
+
"--copy",
|
|
13246
|
+
"make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)"
|
|
13247
|
+
).option(
|
|
13248
|
+
"--dry-run",
|
|
13249
|
+
"walk through without writing files or changing the profile",
|
|
13250
|
+
false
|
|
13251
|
+
).option(
|
|
13252
|
+
"--refresh",
|
|
13253
|
+
"re-fetch descriptors and refresh any out-of-date managed blocks (local edits preserved to .proposed)",
|
|
13254
|
+
false
|
|
13255
|
+
).option(
|
|
13256
|
+
"--force",
|
|
13257
|
+
"rewrite every managed block, overwriting local edits inside the markers (content outside untouched)",
|
|
13258
|
+
false
|
|
13259
|
+
).option(
|
|
13260
|
+
"--check",
|
|
13261
|
+
"report whether anything would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing",
|
|
13262
|
+
false
|
|
13263
|
+
).option(
|
|
13264
|
+
"-y, --yes",
|
|
13265
|
+
"non-interactive: accept defaults (system timezone, detected clients, global config, full wire)",
|
|
13266
|
+
false
|
|
13267
|
+
).addHelpText(
|
|
12108
13268
|
"after",
|
|
12109
13269
|
`
|
|
12110
13270
|
Examples:
|
|
@@ -12127,20 +13287,56 @@ Examples:
|
|
|
12127
13287
|
const check = mode === "check";
|
|
12128
13288
|
const yes = Boolean(opts.yes) || check;
|
|
12129
13289
|
if (check && (opts.recurse || opts.cliOnly)) {
|
|
12130
|
-
fail(
|
|
13290
|
+
fail(
|
|
13291
|
+
"--check inspects this project's agent files and cannot be combined with --recurse or --cli-only."
|
|
13292
|
+
);
|
|
12131
13293
|
}
|
|
12132
13294
|
if (opts.lane) process.env.SECHROOM_CODE_LANE = opts.lane;
|
|
12133
13295
|
if (opts.designLane) process.env.SECHROOM_DESIGN_LANE = opts.designLane;
|
|
12134
13296
|
if (opts.recurse) {
|
|
12135
13297
|
const baseUrl2 = resolveBaseUrl(g);
|
|
12136
|
-
await ensureAuth(
|
|
12137
|
-
|
|
12138
|
-
|
|
13298
|
+
await ensureAuth(
|
|
13299
|
+
{
|
|
13300
|
+
baseUrl: baseUrl2,
|
|
13301
|
+
tenant: "",
|
|
13302
|
+
account: resolveAccountAlias(),
|
|
13303
|
+
clientId: readPersisted().clientId
|
|
13304
|
+
},
|
|
13305
|
+
yes
|
|
13306
|
+
);
|
|
13307
|
+
const cfg2 = await ensureTenant(baseUrl2, g, {
|
|
13308
|
+
yes: true,
|
|
13309
|
+
json,
|
|
13310
|
+
persist: false
|
|
13311
|
+
});
|
|
13312
|
+
await runRecurse(cfg2, g, {
|
|
13313
|
+
yes,
|
|
13314
|
+
dryRun,
|
|
13315
|
+
json,
|
|
13316
|
+
lane: opts.lane,
|
|
13317
|
+
designLane: opts.designLane
|
|
13318
|
+
});
|
|
12139
13319
|
return;
|
|
12140
13320
|
}
|
|
12141
13321
|
const baseUrl = resolveBaseUrl(g);
|
|
12142
|
-
await ensureAuth(
|
|
12143
|
-
|
|
13322
|
+
await ensureAuth(
|
|
13323
|
+
{
|
|
13324
|
+
baseUrl,
|
|
13325
|
+
tenant: "",
|
|
13326
|
+
account: resolveAccountAlias(),
|
|
13327
|
+
clientId: readPersisted().clientId
|
|
13328
|
+
},
|
|
13329
|
+
yes
|
|
13330
|
+
);
|
|
13331
|
+
const scope = await chooseScope(opts.scope, yes);
|
|
13332
|
+
const cfg = await ensureTenant(baseUrl, g, {
|
|
13333
|
+
yes,
|
|
13334
|
+
json,
|
|
13335
|
+
local: Boolean(opts.local) || scope === "project",
|
|
13336
|
+
here: scope === "project" ? true : Boolean(opts.here),
|
|
13337
|
+
workspace: opts.workspace,
|
|
13338
|
+
persist: !check
|
|
13339
|
+
});
|
|
12144
13340
|
const tz = await ensureTimezone(cfg, { yes, dryRun: dryRun || check });
|
|
12145
13341
|
if (!json && tz.action !== "already-set") {
|
|
12146
13342
|
const line = tz.action === "set" ? `${ok("\u2713")} timezone set to ${tz.timezone}
|
|
@@ -12150,21 +13346,48 @@ Examples:
|
|
|
12150
13346
|
process.stderr.write(line);
|
|
12151
13347
|
}
|
|
12152
13348
|
const wire = await chooseWire(opts, yes);
|
|
12153
|
-
const
|
|
12154
|
-
|
|
13349
|
+
const claudeTargets = resolveClaudeTargets({
|
|
13350
|
+
override: g.claudeConfigDir,
|
|
13351
|
+
scope,
|
|
13352
|
+
cwd: process.cwd()
|
|
13353
|
+
});
|
|
12155
13354
|
const codexHomes = resolveCodexHomes({ override: g.codexHome, scope });
|
|
12156
13355
|
if (scope === "project" && g.claudeConfigDir && !json) {
|
|
12157
|
-
process.stderr.write(
|
|
12158
|
-
|
|
13356
|
+
process.stderr.write(
|
|
13357
|
+
`${style.dim("(--claude-config-dir is ignored for --scope project \u2014 project files are repo-relative)")}
|
|
13358
|
+
`
|
|
13359
|
+
);
|
|
12159
13360
|
}
|
|
12160
13361
|
if (wire === "cli-only") {
|
|
12161
13362
|
if (json) {
|
|
12162
|
-
emit(
|
|
13363
|
+
emit(
|
|
13364
|
+
{
|
|
13365
|
+
dryRun,
|
|
13366
|
+
baseUrl: cfg.baseUrl,
|
|
13367
|
+
tenant: cfg.tenant,
|
|
13368
|
+
workspaceId: cfg.workspaceId ?? null,
|
|
13369
|
+
timezone: tz,
|
|
13370
|
+
wire,
|
|
13371
|
+
clients: []
|
|
13372
|
+
},
|
|
13373
|
+
true
|
|
13374
|
+
);
|
|
12163
13375
|
return;
|
|
12164
13376
|
}
|
|
12165
13377
|
if (!dryRun) {
|
|
12166
|
-
await ensureLanePin(cfg, {
|
|
12167
|
-
|
|
13378
|
+
await ensureLanePin(cfg, {
|
|
13379
|
+
yes,
|
|
13380
|
+
dryRun,
|
|
13381
|
+
clients: detectInstalledClients(process.cwd())
|
|
13382
|
+
});
|
|
13383
|
+
await maybeOfferHooks({
|
|
13384
|
+
yes,
|
|
13385
|
+
dryRun,
|
|
13386
|
+
cwd: process.cwd(),
|
|
13387
|
+
scope,
|
|
13388
|
+
claudeConfigDir: g.claudeConfigDir,
|
|
13389
|
+
codexHome: g.codexHome
|
|
13390
|
+
});
|
|
12168
13391
|
}
|
|
12169
13392
|
process.stdout.write(
|
|
12170
13393
|
`
|
|
@@ -12175,12 +13398,36 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
12175
13398
|
await printStarterPrompt("cli");
|
|
12176
13399
|
return;
|
|
12177
13400
|
}
|
|
12178
|
-
const
|
|
12179
|
-
|
|
12180
|
-
|
|
13401
|
+
const requestedKeys = await chooseClients(
|
|
13402
|
+
opts.client,
|
|
13403
|
+
yes,
|
|
13404
|
+
process.cwd()
|
|
13405
|
+
);
|
|
13406
|
+
const keys = scope === "project" ? requestedKeys.filter((key) => key !== "codex") : requestedKeys;
|
|
13407
|
+
if (scope === "project" && requestedKeys.includes("codex") && !json) {
|
|
13408
|
+
process.stderr.write(
|
|
13409
|
+
`${style.dim("Codex has no project scope \u2014 skipped (use --scope global for Codex).")}
|
|
13410
|
+
`
|
|
13411
|
+
);
|
|
13412
|
+
}
|
|
13413
|
+
const setup = await withSpinner(
|
|
13414
|
+
"Fetching setup descriptors",
|
|
13415
|
+
() => fetchSetup(cfg)
|
|
13416
|
+
);
|
|
13417
|
+
const targets = clientTargets(process.cwd(), {
|
|
13418
|
+
claudeDir: claudeTargets[0]?.dir,
|
|
13419
|
+
codexHome: codexHomes[0] ?? null
|
|
13420
|
+
});
|
|
12181
13421
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
12182
13422
|
if (!dryRun && !check) {
|
|
12183
|
-
await maybeOfferCopies(
|
|
13423
|
+
await maybeOfferCopies(
|
|
13424
|
+
cfg,
|
|
13425
|
+
setup,
|
|
13426
|
+
targets,
|
|
13427
|
+
keys,
|
|
13428
|
+
personalWorkspaceId,
|
|
13429
|
+
copyChoice(opts)
|
|
13430
|
+
);
|
|
12184
13431
|
}
|
|
12185
13432
|
const writeMcp = wire === "full";
|
|
12186
13433
|
const result = [];
|
|
@@ -12197,16 +13444,11 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
12197
13444
|
if (!json && !check) printActions(target, actions);
|
|
12198
13445
|
}
|
|
12199
13446
|
if (check) {
|
|
12200
|
-
reportCheckAndExit(
|
|
12201
|
-
|
|
12202
|
-
|
|
12203
|
-
|
|
12204
|
-
|
|
12205
|
-
baseUrl: cfg.baseUrl,
|
|
12206
|
-
tenant: cfg.tenant,
|
|
12207
|
-
workspaceId: cfg.workspaceId ?? null
|
|
12208
|
-
}
|
|
12209
|
-
);
|
|
13447
|
+
reportCheckAndExit(result, json, "sechroom onboard --refresh", {
|
|
13448
|
+
baseUrl: cfg.baseUrl,
|
|
13449
|
+
tenant: cfg.tenant,
|
|
13450
|
+
workspaceId: cfg.workspaceId ?? null
|
|
13451
|
+
});
|
|
12210
13452
|
}
|
|
12211
13453
|
const evalCounts = buildCheckReport(result).eval;
|
|
12212
13454
|
if (!json && !dryRun) {
|
|
@@ -12214,19 +13456,45 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
12214
13456
|
}
|
|
12215
13457
|
if (!json && !dryRun) {
|
|
12216
13458
|
for (const t of claudeTargets) {
|
|
12217
|
-
await maybeOfferSkills(cfg, personalWorkspaceId, {
|
|
13459
|
+
await maybeOfferSkills(cfg, personalWorkspaceId, {
|
|
13460
|
+
yes,
|
|
13461
|
+
dryRun,
|
|
13462
|
+
surface: "claude-code",
|
|
13463
|
+
configDir: t.dir
|
|
13464
|
+
});
|
|
12218
13465
|
}
|
|
12219
13466
|
}
|
|
12220
13467
|
if (!json && !dryRun) {
|
|
12221
|
-
await maybeOfferHooks({
|
|
13468
|
+
await maybeOfferHooks({
|
|
13469
|
+
yes,
|
|
13470
|
+
dryRun,
|
|
13471
|
+
cwd: process.cwd(),
|
|
13472
|
+
scope,
|
|
13473
|
+
claudeConfigDir: g.claudeConfigDir,
|
|
13474
|
+
codexHome: g.codexHome
|
|
13475
|
+
});
|
|
12222
13476
|
}
|
|
12223
13477
|
if (json) {
|
|
12224
|
-
emit(
|
|
13478
|
+
emit(
|
|
13479
|
+
{
|
|
13480
|
+
dryRun,
|
|
13481
|
+
baseUrl: cfg.baseUrl,
|
|
13482
|
+
tenant: cfg.tenant,
|
|
13483
|
+
workspaceId: cfg.workspaceId ?? null,
|
|
13484
|
+
timezone: tz,
|
|
13485
|
+
wire,
|
|
13486
|
+
eval: evalCounts,
|
|
13487
|
+
clients: result
|
|
13488
|
+
},
|
|
13489
|
+
true
|
|
13490
|
+
);
|
|
12225
13491
|
return;
|
|
12226
13492
|
}
|
|
12227
13493
|
if (!dryRun && evalCounts.stale) {
|
|
12228
|
-
process.stderr.write(
|
|
12229
|
-
|
|
13494
|
+
process.stderr.write(
|
|
13495
|
+
`${style.cyan("\u21BB")} refreshed ${evalCounts.stale} section(s) the server had moved
|
|
13496
|
+
`
|
|
13497
|
+
);
|
|
12230
13498
|
}
|
|
12231
13499
|
if (!dryRun && evalCounts.drift) {
|
|
12232
13500
|
process.stderr.write(
|
|
@@ -12235,7 +13503,9 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
12235
13503
|
`
|
|
12236
13504
|
);
|
|
12237
13505
|
}
|
|
12238
|
-
const wroteSomething = result.some(
|
|
13506
|
+
const wroteSomething = result.some(
|
|
13507
|
+
({ actions }) => actions.some((a) => a.status === "created" || a.status === "merged")
|
|
13508
|
+
);
|
|
12239
13509
|
process.stdout.write(
|
|
12240
13510
|
dryRun ? "\n(dry run \u2014 nothing written)\n" : !wroteSomething ? `
|
|
12241
13511
|
${style.bold("Done.")} Everything's already up to date.
|
|
@@ -12254,9 +13524,21 @@ async function chooseWire(opts, yes) {
|
|
|
12254
13524
|
return promptSelect(
|
|
12255
13525
|
"How should I set up Sechroom in this project?",
|
|
12256
13526
|
[
|
|
12257
|
-
{
|
|
12258
|
-
|
|
12259
|
-
|
|
13527
|
+
{
|
|
13528
|
+
label: "Wire my AI client",
|
|
13529
|
+
value: "full",
|
|
13530
|
+
hint: "MCP server (.mcp.json) + agent instructions"
|
|
13531
|
+
},
|
|
13532
|
+
{
|
|
13533
|
+
label: "Agent instructions only",
|
|
13534
|
+
value: "agent-only",
|
|
13535
|
+
hint: "skip MCP config"
|
|
13536
|
+
},
|
|
13537
|
+
{
|
|
13538
|
+
label: "CLI only",
|
|
13539
|
+
value: "cli-only",
|
|
13540
|
+
hint: "don't write any AI-client files"
|
|
13541
|
+
}
|
|
12260
13542
|
],
|
|
12261
13543
|
"full"
|
|
12262
13544
|
);
|
|
@@ -12278,7 +13560,9 @@ ${rule}
|
|
|
12278
13560
|
}
|
|
12279
13561
|
async function printStarterPrompt(mode, cfg) {
|
|
12280
13562
|
if (mode === "cli") {
|
|
12281
|
-
printNextStepBlock("Next \u2014 pick up where you left off:", [
|
|
13563
|
+
printNextStepBlock("Next \u2014 pick up where you left off:", [
|
|
13564
|
+
style.cyan("sechroom continuity resume-me")
|
|
13565
|
+
]);
|
|
12282
13566
|
return;
|
|
12283
13567
|
}
|
|
12284
13568
|
let primary = FALLBACK_AGENT_PROMPT;
|
|
@@ -12290,7 +13574,9 @@ async function printStarterPrompt(mode, cfg) {
|
|
|
12290
13574
|
} catch {
|
|
12291
13575
|
}
|
|
12292
13576
|
}
|
|
12293
|
-
printNextStepBlock("Next \u2014 paste this into your AI agent to get going:", [
|
|
13577
|
+
printNextStepBlock("Next \u2014 paste this into your AI agent to get going:", [
|
|
13578
|
+
style.cyan(`"${primary}"`)
|
|
13579
|
+
]);
|
|
12294
13580
|
}
|
|
12295
13581
|
|
|
12296
13582
|
// src/commands/project.ts
|
|
@@ -12443,7 +13729,13 @@ Examples:
|
|
|
12443
13729
|
$ sechroom relationship suggestions --status Pending --memory mem_XXXX
|
|
12444
13730
|
$ sechroom relationship suggestion accept rsg_XXXX`
|
|
12445
13731
|
);
|
|
12446
|
-
relationship.command("create <fromMemoryId> <toMemoryId>").description(
|
|
13732
|
+
relationship.command("create <fromMemoryId> <toMemoryId>").description(
|
|
13733
|
+
"Create a relationship (POST /memories/{memoryId}/relationships)"
|
|
13734
|
+
).option(
|
|
13735
|
+
"--type <type>",
|
|
13736
|
+
"Relationship type (Reference, Related, Parent, Child, Follows, \u2026)",
|
|
13737
|
+
"Reference"
|
|
13738
|
+
).option(
|
|
12447
13739
|
"--to-version <number>",
|
|
12448
13740
|
"Version of the target memory to pin the edge to (defaults to the target's current version)"
|
|
12449
13741
|
).action(async (fromMemoryId, toMemoryId, opts, cmd) => {
|
|
@@ -12453,15 +13745,21 @@ Examples:
|
|
|
12453
13745
|
if (opts.toVersion !== void 0) {
|
|
12454
13746
|
toVersion = Number(opts.toVersion);
|
|
12455
13747
|
if (!Number.isInteger(toVersion) || toVersion < 1) {
|
|
12456
|
-
fail(
|
|
13748
|
+
fail(
|
|
13749
|
+
`--to-version must be a positive integer (got '${opts.toVersion}').`
|
|
13750
|
+
);
|
|
12457
13751
|
}
|
|
12458
13752
|
} else {
|
|
12459
13753
|
const target = await runApi(
|
|
12460
13754
|
"Resolving target version",
|
|
12461
|
-
async () => client.GET("/memories/{memoryId}", {
|
|
13755
|
+
async () => client.GET("/memories/{memoryId}", {
|
|
13756
|
+
params: { path: { memoryId: toMemoryId } }
|
|
13757
|
+
})
|
|
12462
13758
|
);
|
|
12463
13759
|
if (typeof target.item?.currentVersion !== "number") {
|
|
12464
|
-
fail(
|
|
13760
|
+
fail(
|
|
13761
|
+
`Could not resolve the current version of ${toMemoryId}; pass --to-version explicitly.`
|
|
13762
|
+
);
|
|
12465
13763
|
}
|
|
12466
13764
|
toVersion = target.item.currentVersion;
|
|
12467
13765
|
}
|
|
@@ -12485,22 +13783,28 @@ Examples:
|
|
|
12485
13783
|
cmd.optsWithGlobals().json
|
|
12486
13784
|
);
|
|
12487
13785
|
});
|
|
12488
|
-
relationship.command("list <memoryId>").description(
|
|
13786
|
+
relationship.command("list <memoryId>").description(
|
|
13787
|
+
"List a memory's relationships (GET /memories/{memoryId}/relationships). Walks every page by default."
|
|
13788
|
+
).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
13789
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
12490
|
-
const
|
|
13790
|
+
const filters = {
|
|
13791
|
+
...opts.direction ? { direction: opts.direction } : {},
|
|
13792
|
+
...opts.includeDeleted ? { includeDeleted: true } : {}
|
|
13793
|
+
};
|
|
13794
|
+
const readPage = (query) => runApi("Listing relationships", async () => {
|
|
12491
13795
|
const client = await makeClient(cfg);
|
|
12492
13796
|
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
|
-
}
|
|
13797
|
+
params: { path: { memoryId }, query: { ...filters, ...query } }
|
|
12502
13798
|
});
|
|
12503
13799
|
});
|
|
13800
|
+
const data = shouldAutoPage(opts) ? await fetchAllPages(
|
|
13801
|
+
(page) => readPage({
|
|
13802
|
+
page,
|
|
13803
|
+
...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
|
|
13804
|
+
}),
|
|
13805
|
+
opts,
|
|
13806
|
+
"relationships"
|
|
13807
|
+
) : await readPage(singlePageQuery(opts));
|
|
12504
13808
|
emit(data, cmd.optsWithGlobals().json);
|
|
12505
13809
|
});
|
|
12506
13810
|
relationship.command("delete <id>").description("Delete a relationship (DELETE /relationships/{id})").action(async (id, _opts, cmd) => {
|
|
@@ -12512,9 +13816,15 @@ Examples:
|
|
|
12512
13816
|
body: {}
|
|
12513
13817
|
});
|
|
12514
13818
|
});
|
|
12515
|
-
emitAction(
|
|
13819
|
+
emitAction(
|
|
13820
|
+
`deleted relationship ${style.bold(id)}`,
|
|
13821
|
+
data,
|
|
13822
|
+
cmd.optsWithGlobals().json
|
|
13823
|
+
);
|
|
12516
13824
|
});
|
|
12517
|
-
relationship.command("suggest <memoryId>").description(
|
|
13825
|
+
relationship.command("suggest <memoryId>").description(
|
|
13826
|
+
"Generate relationship suggestions for a memory (POST /memories/{memoryId}/suggest-relationships)"
|
|
13827
|
+
).option("--limit <n>", "Max suggestions to generate").action(async (memoryId, opts, cmd) => {
|
|
12518
13828
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
12519
13829
|
const data = await runApi("Suggesting relationships", async () => {
|
|
12520
13830
|
const client = await makeClient(cfg);
|
|
@@ -12531,38 +13841,51 @@ Examples:
|
|
|
12531
13841
|
cmd.optsWithGlobals().json
|
|
12532
13842
|
);
|
|
12533
13843
|
});
|
|
12534
|
-
relationship.command("suggestions").description(
|
|
13844
|
+
relationship.command("suggestions").description(
|
|
13845
|
+
"List relationship suggestions (GET /relationship-suggestions)"
|
|
13846
|
+
).option("--memory <memoryId>", "Filter to a memory").option(
|
|
12535
13847
|
"--status <status>",
|
|
12536
13848
|
"Pending | Accepted | EditedAndAccepted | Rejected | Superseded | Deferred | Invalidated"
|
|
12537
|
-
).option(
|
|
13849
|
+
).option(...PAGE_OPTION).option(...PAGE_SIZE_OPTION).option(...NO_AUTO_PAGE_OPTION).action(async (opts, cmd) => {
|
|
12538
13850
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
12539
|
-
const
|
|
13851
|
+
const filters = {
|
|
13852
|
+
...opts.memory ? { memoryId: opts.memory } : {},
|
|
13853
|
+
...opts.status ? {
|
|
13854
|
+
status: opts.status
|
|
13855
|
+
} : {}
|
|
13856
|
+
};
|
|
13857
|
+
const readPage = (query) => runApi("Listing suggestions", async () => {
|
|
12540
13858
|
const client = await makeClient(cfg);
|
|
12541
13859
|
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
|
-
}
|
|
13860
|
+
params: { query: { ...filters, ...query } }
|
|
12552
13861
|
});
|
|
12553
13862
|
});
|
|
13863
|
+
const data = shouldAutoPage(opts) ? await fetchAllPages(
|
|
13864
|
+
(page) => readPage({
|
|
13865
|
+
page,
|
|
13866
|
+
...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
|
|
13867
|
+
}),
|
|
13868
|
+
opts,
|
|
13869
|
+
"suggestions"
|
|
13870
|
+
) : await readPage(singlePageQuery(opts));
|
|
12554
13871
|
emit(data, cmd.optsWithGlobals().json);
|
|
12555
13872
|
});
|
|
12556
13873
|
const suggestion = relationship.command("suggestion").description("Inspect and decide on a single relationship suggestion");
|
|
12557
|
-
suggestion.command("get <id>").description(
|
|
13874
|
+
suggestion.command("get <id>").description(
|
|
13875
|
+
"Fetch a suggestion by id (GET /relationship-suggestions/{id})"
|
|
13876
|
+
).action(async (id, _opts, cmd) => {
|
|
12558
13877
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
12559
13878
|
const data = await runApi("Fetching suggestion", async () => {
|
|
12560
13879
|
const client = await makeClient(cfg);
|
|
12561
|
-
return client.GET("/relationship-suggestions/{id}", {
|
|
13880
|
+
return client.GET("/relationship-suggestions/{id}", {
|
|
13881
|
+
params: { path: { id } }
|
|
13882
|
+
});
|
|
12562
13883
|
});
|
|
12563
13884
|
emit(data, cmd.optsWithGlobals().json);
|
|
12564
13885
|
});
|
|
12565
|
-
suggestion.command("accept <id>").description(
|
|
13886
|
+
suggestion.command("accept <id>").description(
|
|
13887
|
+
"Accept a suggestion (POST /relationship-suggestions/{instanceId}/accept)"
|
|
13888
|
+
).action(async (id, _opts, cmd) => {
|
|
12566
13889
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
12567
13890
|
const data = await runApi("Accepting suggestion", async () => {
|
|
12568
13891
|
const client = await makeClient(cfg);
|
|
@@ -12571,9 +13894,15 @@ Examples:
|
|
|
12571
13894
|
body: {}
|
|
12572
13895
|
});
|
|
12573
13896
|
});
|
|
12574
|
-
emitAction(
|
|
13897
|
+
emitAction(
|
|
13898
|
+
`accepted suggestion ${style.bold(id)}`,
|
|
13899
|
+
data,
|
|
13900
|
+
cmd.optsWithGlobals().json
|
|
13901
|
+
);
|
|
12575
13902
|
});
|
|
12576
|
-
suggestion.command("reject <id>").description(
|
|
13903
|
+
suggestion.command("reject <id>").description(
|
|
13904
|
+
"Reject a suggestion (POST /relationship-suggestions/{instanceId}/reject)"
|
|
13905
|
+
).option("--reason <reason>", "Why it's being rejected").option("--reason-code <code>", "Structured reason code").action(async (id, opts, cmd) => {
|
|
12577
13906
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
12578
13907
|
const data = await runApi("Rejecting suggestion", async () => {
|
|
12579
13908
|
const client = await makeClient(cfg);
|
|
@@ -12585,9 +13914,18 @@ Examples:
|
|
|
12585
13914
|
}
|
|
12586
13915
|
});
|
|
12587
13916
|
});
|
|
12588
|
-
emitAction(
|
|
13917
|
+
emitAction(
|
|
13918
|
+
`rejected suggestion ${style.bold(id)}`,
|
|
13919
|
+
data,
|
|
13920
|
+
cmd.optsWithGlobals().json
|
|
13921
|
+
);
|
|
12589
13922
|
});
|
|
12590
|
-
suggestion.command("defer <id>").description(
|
|
13923
|
+
suggestion.command("defer <id>").description(
|
|
13924
|
+
"Defer a suggestion (POST /relationship-suggestions/{id}/defer)"
|
|
13925
|
+
).option(
|
|
13926
|
+
"--until <iso>",
|
|
13927
|
+
"Defer until this ISO date-time (omit to defer indefinitely)"
|
|
13928
|
+
).action(async (id, opts, cmd) => {
|
|
12591
13929
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
12592
13930
|
const data = await runApi("Deferring suggestion", async () => {
|
|
12593
13931
|
const client = await makeClient(cfg);
|
|
@@ -12598,14 +13936,18 @@ Examples:
|
|
|
12598
13936
|
}
|
|
12599
13937
|
});
|
|
12600
13938
|
});
|
|
12601
|
-
emitAction(
|
|
13939
|
+
emitAction(
|
|
13940
|
+
`deferred suggestion ${style.bold(id)}`,
|
|
13941
|
+
data,
|
|
13942
|
+
cmd.optsWithGlobals().json
|
|
13943
|
+
);
|
|
12602
13944
|
});
|
|
12603
13945
|
}
|
|
12604
13946
|
|
|
12605
13947
|
// src/commands/reset.ts
|
|
12606
13948
|
import { homedir as homedir6 } from "os";
|
|
12607
13949
|
import { join as join22 } from "path";
|
|
12608
|
-
import { existsSync as existsSync16, readFileSync as readFileSync18, rmSync as
|
|
13950
|
+
import { existsSync as existsSync16, readFileSync as readFileSync18, rmSync as rmSync8 } from "fs";
|
|
12609
13951
|
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
12610
13952
|
var localSkillsDir = () => join22(process.cwd(), ".claude", "skills");
|
|
12611
13953
|
var globalSkillsDir = () => join22(homedir6(), ".claude", "skills");
|
|
@@ -12621,14 +13963,14 @@ function removeMaterialisedSkills(dir) {
|
|
|
12621
13963
|
for (const name of entry.skills ?? []) {
|
|
12622
13964
|
const p = join22(dir, name);
|
|
12623
13965
|
if (existsSync16(p)) {
|
|
12624
|
-
|
|
13966
|
+
rmSync8(p, { recursive: true, force: true });
|
|
12625
13967
|
removed.push(p);
|
|
12626
13968
|
}
|
|
12627
13969
|
}
|
|
12628
13970
|
}
|
|
12629
13971
|
} catch {
|
|
12630
13972
|
}
|
|
12631
|
-
|
|
13973
|
+
rmSync8(lockPath, { force: true });
|
|
12632
13974
|
removed.push(lockPath);
|
|
12633
13975
|
return removed;
|
|
12634
13976
|
}
|
|
@@ -12667,17 +14009,17 @@ function registerReset(program2) {
|
|
|
12667
14009
|
const removed = [];
|
|
12668
14010
|
const stateDir = join22(process.cwd(), ".sechroom");
|
|
12669
14011
|
if (existsSync16(stateDir)) {
|
|
12670
|
-
|
|
14012
|
+
rmSync8(stateDir, { recursive: true, force: true });
|
|
12671
14013
|
removed.push(stateDir);
|
|
12672
14014
|
}
|
|
12673
14015
|
const legacyCfg = join22(process.cwd(), ".sechroom.json");
|
|
12674
14016
|
if (existsSync16(legacyCfg)) {
|
|
12675
|
-
|
|
14017
|
+
rmSync8(legacyCfg, { force: true });
|
|
12676
14018
|
removed.push(legacyCfg);
|
|
12677
14019
|
}
|
|
12678
14020
|
const legacySem = join22(process.cwd(), ".sem");
|
|
12679
14021
|
if (existsSync16(legacySem)) {
|
|
12680
|
-
|
|
14022
|
+
rmSync8(legacySem, { force: true });
|
|
12681
14023
|
removed.push(legacySem);
|
|
12682
14024
|
}
|
|
12683
14025
|
removed.push(...removeMaterialisedSkills(localSkillsDir()));
|
|
@@ -12702,7 +14044,7 @@ function registerReset(program2) {
|
|
|
12702
14044
|
}
|
|
12703
14045
|
|
|
12704
14046
|
// src/commands/skills.ts
|
|
12705
|
-
import { existsSync as existsSync17, mkdirSync as mkdirSync18, statSync as
|
|
14047
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync18, statSync as statSync7, writeFileSync as writeFileSync17 } from "fs";
|
|
12706
14048
|
import { join as join23 } from "path";
|
|
12707
14049
|
function filenameFromDisposition(header) {
|
|
12708
14050
|
if (!header) return void 0;
|
|
@@ -12712,7 +14054,7 @@ function filenameFromDisposition(header) {
|
|
|
12712
14054
|
function resolveOutputPath(output, serverFilename) {
|
|
12713
14055
|
const filename = serverFilename || "skills.zip";
|
|
12714
14056
|
if (!output) return join23(process.cwd(), filename);
|
|
12715
|
-
const looksLikeDir = output.endsWith("/") || existsSync17(output) &&
|
|
14057
|
+
const looksLikeDir = output.endsWith("/") || existsSync17(output) && statSync7(output).isDirectory();
|
|
12716
14058
|
if (looksLikeDir) {
|
|
12717
14059
|
mkdirSync18(output, { recursive: true });
|
|
12718
14060
|
return join23(output, filename);
|
|
@@ -13311,17 +14653,15 @@ Examples:
|
|
|
13311
14653
|
$ sechroom work-task mark-no-residue mem_XXXX --decomposition wlp_XXXX
|
|
13312
14654
|
$ sechroom work-task residue-produce mem_XXXX --file residue.json`
|
|
13313
14655
|
);
|
|
13314
|
-
workTask.command("list").description(
|
|
14656
|
+
workTask.command("list").description(
|
|
14657
|
+
"List work tasks, newest-first (GET /work-tasks). Walks every page by default."
|
|
14658
|
+
).option("--shape <shape>", "Filter: bare | managed").option(
|
|
13315
14659
|
"--lane <lane>",
|
|
13316
14660
|
"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) => {
|
|
14661
|
+
).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
14662
|
const globals = cmd.optsWithGlobals();
|
|
13323
14663
|
const cfg = resolveConfig(globals);
|
|
13324
|
-
const
|
|
14664
|
+
const readPage = (query) => runApi("Listing work tasks", async () => {
|
|
13325
14665
|
const client = await makeClient(cfg);
|
|
13326
14666
|
return client.GET("/work-tasks", {
|
|
13327
14667
|
params: {
|
|
@@ -13329,12 +14669,19 @@ Examples:
|
|
|
13329
14669
|
shape: opts.shape,
|
|
13330
14670
|
lane: opts.lane,
|
|
13331
14671
|
status: opts.status,
|
|
13332
|
-
|
|
13333
|
-
pageSize: opts.pageSize
|
|
14672
|
+
...query
|
|
13334
14673
|
}
|
|
13335
14674
|
}
|
|
13336
14675
|
});
|
|
13337
14676
|
});
|
|
14677
|
+
const data = shouldAutoPage(opts) ? await fetchAllPages(
|
|
14678
|
+
(page) => readPage({
|
|
14679
|
+
page,
|
|
14680
|
+
...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
|
|
14681
|
+
}),
|
|
14682
|
+
opts,
|
|
14683
|
+
"tasks"
|
|
14684
|
+
) : await readPage(singlePageQuery(opts));
|
|
13338
14685
|
emitAction(
|
|
13339
14686
|
`listed ${style.bold(String(data.items.length))} of ${data.count} task(s)`,
|
|
13340
14687
|
data,
|