@sechroom/cli 2026.9.1 → 2026.9.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 +425 -157
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1234,7 +1234,7 @@ ${mem.text.trim()}`
|
|
|
1234
1234
|
if (parts.length === 0) return null;
|
|
1235
1235
|
return { body: parts.join("\n\n"), refs };
|
|
1236
1236
|
}
|
|
1237
|
-
async function createOverride(cfg, template, personalWorkspaceId) {
|
|
1237
|
+
async function createOverride(cfg, template, personalWorkspaceId, source) {
|
|
1238
1238
|
const client = await makeClient(cfg);
|
|
1239
1239
|
const overrideTags = template.templateTags.filter(
|
|
1240
1240
|
(t) => t !== "sechroom:role:template" && !t.startsWith("sechroom:bundle:") && !t.startsWith("sechroom:template-ref:")
|
|
@@ -1249,7 +1249,7 @@ async function createOverride(cfg, template, personalWorkspaceId) {
|
|
|
1249
1249
|
type: "reference",
|
|
1250
1250
|
content: "{}",
|
|
1251
1251
|
confidence: 1,
|
|
1252
|
-
source
|
|
1252
|
+
source,
|
|
1253
1253
|
archetype: "Document",
|
|
1254
1254
|
title: template.title ?? null,
|
|
1255
1255
|
tags: overrideTags,
|
|
@@ -2260,6 +2260,7 @@ function resolveSemPathForRead(start = process.cwd()) {
|
|
|
2260
2260
|
while (true) {
|
|
2261
2261
|
const candidate = join6(dir, SEM_FILE);
|
|
2262
2262
|
if (existsSync4(candidate)) return candidate;
|
|
2263
|
+
if (existsSync4(join6(dir, ".git"))) return void 0;
|
|
2263
2264
|
const parent = dirname2(dir);
|
|
2264
2265
|
if (parent === dir) return void 0;
|
|
2265
2266
|
dir = parent;
|
|
@@ -7031,9 +7032,11 @@ function registerChannel(program2) {
|
|
|
7031
7032
|
(typeof payload === "string" ? payload : JSON.stringify(payload)) + "\n"
|
|
7032
7033
|
)
|
|
7033
7034
|
);
|
|
7035
|
+
const authExit = createAuthExpiredExit();
|
|
7034
7036
|
const leaseHeartbeats = createChannelLeaseHeartbeatManager(
|
|
7035
7037
|
cfg,
|
|
7036
|
-
located.state.taskLeaseTtlSeconds ?? 120
|
|
7038
|
+
located.state.taskLeaseTtlSeconds ?? 120,
|
|
7039
|
+
{ onAuthExpired: authExit.onAuthExpired }
|
|
7037
7040
|
);
|
|
7038
7041
|
const drain = createClaimDrain(cfg, instance.id, deliver, {
|
|
7039
7042
|
onClaimed: leaseHeartbeats.onClaimed
|
|
@@ -7072,13 +7075,11 @@ function registerChannel(program2) {
|
|
|
7072
7075
|
) + style.dim("streaming matched events to stdout; Ctrl-C to stop.\n")
|
|
7073
7076
|
);
|
|
7074
7077
|
}
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
leaseHeartbeats.stop();
|
|
7081
|
-
}
|
|
7078
|
+
await holdChannelOpen(conn, authExit.released, [
|
|
7079
|
+
stopReconciliation,
|
|
7080
|
+
stopHeartbeat,
|
|
7081
|
+
leaseHeartbeats.stop
|
|
7082
|
+
]);
|
|
7082
7083
|
});
|
|
7083
7084
|
channel.command("mcp").description(
|
|
7084
7085
|
"Run as a Claude Code channel (local-stdio MCP server) \u2014 claim and push dispatched tasks into the session"
|
|
@@ -7101,9 +7102,11 @@ function registerChannel(program2) {
|
|
|
7101
7102
|
params: { content, meta }
|
|
7102
7103
|
});
|
|
7103
7104
|
});
|
|
7105
|
+
const authExit = createAuthExpiredExit();
|
|
7104
7106
|
const leaseHeartbeats = createChannelLeaseHeartbeatManager(
|
|
7105
7107
|
cfg,
|
|
7106
|
-
located.state.taskLeaseTtlSeconds ?? 120
|
|
7108
|
+
located.state.taskLeaseTtlSeconds ?? 120,
|
|
7109
|
+
{ onAuthExpired: authExit.onAuthExpired }
|
|
7107
7110
|
);
|
|
7108
7111
|
const drain = createClaimDrain(cfg, instance.id, deliver, {
|
|
7109
7112
|
onClaimed: leaseHeartbeats.onClaimed
|
|
@@ -7129,13 +7132,16 @@ function registerChannel(program2) {
|
|
|
7129
7132
|
`
|
|
7130
7133
|
)
|
|
7131
7134
|
);
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7135
|
+
await holdChannelOpen(conn, authExit.released, [
|
|
7136
|
+
stopReconciliation,
|
|
7137
|
+
stopHeartbeat,
|
|
7138
|
+
leaseHeartbeats.stop,
|
|
7139
|
+
// The stdio transport keeps a stdin `data` listener, which holds the event
|
|
7140
|
+
// loop open for as long as the parent holds the pipe. Without this close, a
|
|
7141
|
+
// `process.exitCode` set on the auth path never becomes an exit and the
|
|
7142
|
+
// parent sees a connected-but-inert channel instead of a dead one.
|
|
7143
|
+
() => mcp.close()
|
|
7144
|
+
]);
|
|
7139
7145
|
});
|
|
7140
7146
|
channel.command("install").description(
|
|
7141
7147
|
"Wire `sechroom channel mcp` into the project .mcp.json as a Claude Code channel MCP server (idempotent)"
|
|
@@ -7239,6 +7245,7 @@ function startExecutorHeartbeat(refresh, intervalMilliseconds, dependencies = {}
|
|
|
7239
7245
|
}
|
|
7240
7246
|
var leaseHeartbeatApi = (cfg, path, init, deps = {}) => createAuthedRequest(cfg, deps)(path, init);
|
|
7241
7247
|
function classifyLeaseHeartbeatFailure(error) {
|
|
7248
|
+
if (error instanceof AuthExpiredError) return "auth";
|
|
7242
7249
|
if (!(error instanceof HttpError)) return "retry";
|
|
7243
7250
|
if (error.status === 408 || error.status === 429) return "retry";
|
|
7244
7251
|
if (error.status < 400 || error.status >= 500) return "retry";
|
|
@@ -7246,6 +7253,10 @@ function classifyLeaseHeartbeatFailure(error) {
|
|
|
7246
7253
|
return "released";
|
|
7247
7254
|
return "terminal";
|
|
7248
7255
|
}
|
|
7256
|
+
function authExpiredLine(error) {
|
|
7257
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
7258
|
+
return `auth expired \u2014 run \`sechroom login\` to re-authenticate: ${detail}`;
|
|
7259
|
+
}
|
|
7249
7260
|
function leaseHeartbeatStoppedLine(leaseId, error) {
|
|
7250
7261
|
if (!(error instanceof HttpError))
|
|
7251
7262
|
return `lease heartbeat stopped for ${leaseId}: ${String(error)}`;
|
|
@@ -7288,6 +7299,10 @@ function startChannelTaskLeaseHeartbeat(cfg, claim, intervalMilliseconds = 3e4,
|
|
|
7288
7299
|
if (disposition === "retry") throw error;
|
|
7289
7300
|
stop?.();
|
|
7290
7301
|
dependencies.onLeaseTerminal?.(leaseId, error);
|
|
7302
|
+
if (disposition === "auth") {
|
|
7303
|
+
dependencies.onAuthExpired?.(error);
|
|
7304
|
+
return void 0;
|
|
7305
|
+
}
|
|
7291
7306
|
if (disposition === "terminal")
|
|
7292
7307
|
onError(leaseHeartbeatStoppedLine(leaseId, error));
|
|
7293
7308
|
return void 0;
|
|
@@ -7301,6 +7316,7 @@ function startChannelTaskLeaseHeartbeat(cfg, claim, intervalMilliseconds = 3e4,
|
|
|
7301
7316
|
}
|
|
7302
7317
|
function createChannelLeaseHeartbeatManager(cfg, taskLeaseTtlSeconds, dependencies = {}) {
|
|
7303
7318
|
const stops = /* @__PURE__ */ new Map();
|
|
7319
|
+
let authExpired = false;
|
|
7304
7320
|
const intervalMilliseconds = Math.max(
|
|
7305
7321
|
1e3,
|
|
7306
7322
|
Math.min(3e4, Math.floor(taskLeaseTtlSeconds * 1e3 / 4))
|
|
@@ -7320,6 +7336,16 @@ function createChannelLeaseHeartbeatManager(cfg, taskLeaseTtlSeconds, dependenci
|
|
|
7320
7336
|
// map does not accumulate stopped leases for the life of the channel.
|
|
7321
7337
|
onLeaseTerminal: (id) => {
|
|
7322
7338
|
stops.delete(id);
|
|
7339
|
+
},
|
|
7340
|
+
// A dead credential fails every lease at once, so N held leases would
|
|
7341
|
+
// otherwise raise this N times in the same tick. Latch it: stop every beat
|
|
7342
|
+
// (not just the one that noticed), then escalate exactly once.
|
|
7343
|
+
onAuthExpired: (error) => {
|
|
7344
|
+
if (authExpired) return;
|
|
7345
|
+
authExpired = true;
|
|
7346
|
+
for (const cancel of stops.values()) cancel();
|
|
7347
|
+
stops.clear();
|
|
7348
|
+
dependencies.onAuthExpired?.(error);
|
|
7323
7349
|
}
|
|
7324
7350
|
}
|
|
7325
7351
|
);
|
|
@@ -7414,15 +7440,50 @@ async function openConnection(cfg, onEvent, executorInstanceId) {
|
|
|
7414
7440
|
await conn.start();
|
|
7415
7441
|
return conn;
|
|
7416
7442
|
}
|
|
7417
|
-
function holdOpen(conn) {
|
|
7443
|
+
function holdOpen(conn, until) {
|
|
7418
7444
|
return new Promise((resolve9) => {
|
|
7419
7445
|
const stop = () => {
|
|
7420
7446
|
void conn.stop().finally(resolve9);
|
|
7421
7447
|
};
|
|
7422
7448
|
process.on("SIGINT", stop);
|
|
7423
7449
|
process.on("SIGTERM", stop);
|
|
7450
|
+
void until?.then(stop);
|
|
7424
7451
|
});
|
|
7425
7452
|
}
|
|
7453
|
+
async function holdChannelOpen(conn, released, teardown) {
|
|
7454
|
+
try {
|
|
7455
|
+
await holdOpen(conn, released);
|
|
7456
|
+
} finally {
|
|
7457
|
+
let firstError;
|
|
7458
|
+
let failed = false;
|
|
7459
|
+
for (const step of teardown) {
|
|
7460
|
+
try {
|
|
7461
|
+
await step();
|
|
7462
|
+
} catch (error) {
|
|
7463
|
+
if (!failed) {
|
|
7464
|
+
failed = true;
|
|
7465
|
+
firstError = error;
|
|
7466
|
+
}
|
|
7467
|
+
}
|
|
7468
|
+
}
|
|
7469
|
+
if (failed) throw firstError;
|
|
7470
|
+
}
|
|
7471
|
+
}
|
|
7472
|
+
function createAuthExpiredExit() {
|
|
7473
|
+
let release;
|
|
7474
|
+
const released = new Promise((resolve9) => {
|
|
7475
|
+
release = resolve9;
|
|
7476
|
+
});
|
|
7477
|
+
return {
|
|
7478
|
+
onAuthExpired: (error) => {
|
|
7479
|
+
process.stderr.write(err(`${authExpiredLine(error)}
|
|
7480
|
+
`));
|
|
7481
|
+
process.exitCode = 1;
|
|
7482
|
+
release();
|
|
7483
|
+
},
|
|
7484
|
+
released
|
|
7485
|
+
};
|
|
7486
|
+
}
|
|
7426
7487
|
function parseEvent(payload) {
|
|
7427
7488
|
let data = payload;
|
|
7428
7489
|
if (typeof payload === "string") {
|
|
@@ -7477,92 +7538,6 @@ function str(v) {
|
|
|
7477
7538
|
return typeof v === "string" ? v : v == null ? "" : String(v);
|
|
7478
7539
|
}
|
|
7479
7540
|
|
|
7480
|
-
// src/commands/chat.ts
|
|
7481
|
-
function registerChat(program2) {
|
|
7482
|
-
const chat = program2.command("chat").description("Send and read Slack / Discord channel messages").option("--surface <surface>", "slack | discord", "slack");
|
|
7483
|
-
chat.addHelpText(
|
|
7484
|
-
"after",
|
|
7485
|
-
`
|
|
7486
|
-
Examples:
|
|
7487
|
-
$ sechroom chat send C0123456789 "deploy is green" --surface slack
|
|
7488
|
-
$ sechroom chat send 987654321098765432 "deploy is green" --surface discord --guild 123456789012345678
|
|
7489
|
-
$ sechroom chat send C0123456789 "lgtm" --surface slack --as user --parent 1718049600.123456
|
|
7490
|
-
$ sechroom chat messages --surface slack
|
|
7491
|
-
$ sechroom chat replies 1718049600.123456 --surface slack
|
|
7492
|
-
$ sechroom chat stop-tracking 1718049600.123456 --surface slack`
|
|
7493
|
-
);
|
|
7494
|
-
chat.command("send <channelId> <text>").description("Send a message to a channel (POST /chat/channel-messages/{surface})").option("--guild <guildId>", "Discord guild snowflake \u2014 required for --surface discord").option("--memory <memoryId>", "Attach a sechroom memory id").option("--no-track", "Don't capture replies to this message").option("--parent <parentMessage>", "Thread under a parent (Slack thread_ts / Discord message id)").option("--source <source>", "Source / lane stamp (renders an attribution footer)", "cli").option("--as <as>", "Slack only: 'bot' (default) or 'user' (your linked Slack identity)", "bot").action(async (channelId, text2, opts, cmd) => {
|
|
7495
|
-
const { surface, ...globals } = cmd.optsWithGlobals();
|
|
7496
|
-
const json = Boolean(cmd.optsWithGlobals().json);
|
|
7497
|
-
const cfg = resolveConfig(globals);
|
|
7498
|
-
const data = await runApi("Sending message", async () => {
|
|
7499
|
-
const client = await makeClient(cfg);
|
|
7500
|
-
return client.POST("/chat/channel-messages/{surface}", {
|
|
7501
|
-
params: { path: { surface: String(surface) } },
|
|
7502
|
-
body: {
|
|
7503
|
-
channelId,
|
|
7504
|
-
text: text2,
|
|
7505
|
-
guildId: opts.guild ?? null,
|
|
7506
|
-
attachedMemoryId: opts.memory ?? null,
|
|
7507
|
-
trackReplies: opts.track,
|
|
7508
|
-
parentMessage: opts.parent ?? null,
|
|
7509
|
-
source: opts.source,
|
|
7510
|
-
as: opts.as
|
|
7511
|
-
}
|
|
7512
|
-
});
|
|
7513
|
-
});
|
|
7514
|
-
if (!data.ok) {
|
|
7515
|
-
if (json) {
|
|
7516
|
-
emit(data, true);
|
|
7517
|
-
} else {
|
|
7518
|
-
process.stderr.write(
|
|
7519
|
-
`${err("\u2717")} send failed: ${data.upstreamError ?? "error"}${data.errorDescription ? ` \u2014 ${data.errorDescription}` : ""}
|
|
7520
|
-
`
|
|
7521
|
-
);
|
|
7522
|
-
}
|
|
7523
|
-
process.exit(1);
|
|
7524
|
-
}
|
|
7525
|
-
const idPart = data.persistedId ? ` ${style.dim(`(${data.persistedId})`)}` : "";
|
|
7526
|
-
emitAction(`sent to ${surface} ${style.bold(channelId)}${idPart}`, data, json);
|
|
7527
|
-
});
|
|
7528
|
-
chat.command("messages").description("List recent channel messages (GET /chat/channel-messages/{surface})").action(async (_opts, cmd) => {
|
|
7529
|
-
const { surface, ...globals } = cmd.optsWithGlobals();
|
|
7530
|
-
const cfg = resolveConfig(globals);
|
|
7531
|
-
const data = await runApi("Fetching messages", async () => {
|
|
7532
|
-
const client = await makeClient(cfg);
|
|
7533
|
-
return client.GET("/chat/channel-messages/{surface}", {
|
|
7534
|
-
params: { path: { surface: String(surface) } }
|
|
7535
|
-
});
|
|
7536
|
-
});
|
|
7537
|
-
emit(data, cmd.optsWithGlobals().json);
|
|
7538
|
-
});
|
|
7539
|
-
chat.command("replies <messageId>").description("List thread replies for a message (GET /chat/channel-messages/by-id/{id}/replies)").action(async (messageId, _opts, cmd) => {
|
|
7540
|
-
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
7541
|
-
const data = await runApi("Fetching replies", async () => {
|
|
7542
|
-
const client = await makeClient(cfg);
|
|
7543
|
-
return client.GET("/chat/channel-messages/by-id/{id}/replies", {
|
|
7544
|
-
params: { path: { id: messageId } }
|
|
7545
|
-
});
|
|
7546
|
-
});
|
|
7547
|
-
emit(data, cmd.optsWithGlobals().json);
|
|
7548
|
-
});
|
|
7549
|
-
chat.command("stop-tracking <messageId>").description("Stop watching a message for replies (POST .../by-id/{id}/stop-tracking-replies)").action(async (messageId, _opts, cmd) => {
|
|
7550
|
-
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
7551
|
-
const data = await runApi("Stopping reply tracking", async () => {
|
|
7552
|
-
const client = await makeClient(cfg);
|
|
7553
|
-
return client.POST("/chat/channel-messages/by-id/{id}/stop-tracking-replies", {
|
|
7554
|
-
params: { path: { id: messageId } },
|
|
7555
|
-
body: {}
|
|
7556
|
-
});
|
|
7557
|
-
});
|
|
7558
|
-
emitAction(`stopped tracking replies on ${style.bold(messageId)}`, data, cmd.optsWithGlobals().json);
|
|
7559
|
-
});
|
|
7560
|
-
}
|
|
7561
|
-
|
|
7562
|
-
// src/commands/checkpoint.ts
|
|
7563
|
-
import { mkdirSync as mkdirSync15, writeFileSync as writeFileSync14 } from "fs";
|
|
7564
|
-
import { dirname as dirname14, join as join17 } from "path";
|
|
7565
|
-
|
|
7566
7541
|
// src/commands/hook.ts
|
|
7567
7542
|
import { createHash as createHash4 } from "crypto";
|
|
7568
7543
|
import { existsSync as existsSync12, mkdirSync as mkdirSync14, readFileSync as readFileSync12, statSync as statSync4, writeFileSync as writeFileSync13 } from "fs";
|
|
@@ -8156,10 +8131,22 @@ function resolveLane(flagLane, cwd) {
|
|
|
8156
8131
|
const env = process.env.SECHROOM_LANE;
|
|
8157
8132
|
if (env) return env;
|
|
8158
8133
|
const start = cwd ?? process.cwd();
|
|
8159
|
-
const
|
|
8134
|
+
const semPath = resolveSemPathForRead(start);
|
|
8135
|
+
const base = semPath ? readSem(semPath)?.values["code-lane"] : void 0;
|
|
8160
8136
|
if (!base) return void 0;
|
|
8161
8137
|
return applyWorktreeLaneSuffix(base, start);
|
|
8162
8138
|
}
|
|
8139
|
+
function resolveSourceLane(explicit, cwd) {
|
|
8140
|
+
const trimmed = explicit?.trim();
|
|
8141
|
+
if (trimmed) return trimmed;
|
|
8142
|
+
const lane = resolveLane(void 0, cwd);
|
|
8143
|
+
if (!lane) {
|
|
8144
|
+
throw new Error(
|
|
8145
|
+
"no --source and no lane pinned for this checkout \u2014 a record's source must name the lane that wrote it, never a generic value. Pin one with `sechroom lane set --code-lane <id>` (see `sechroom lane`), set SECHROOM_LANE, or pass --source explicitly."
|
|
8146
|
+
);
|
|
8147
|
+
}
|
|
8148
|
+
return lane;
|
|
8149
|
+
}
|
|
8163
8150
|
var INTENT_FILE = join16(".sechroom", "continuity.json");
|
|
8164
8151
|
var LOCAL_DRY_RUN_VALIDATION_WARNING = "LOCAL-ONLY \u2014 NOT SERVER-VALIDATED";
|
|
8165
8152
|
function resolveIntentPath(start) {
|
|
@@ -8196,6 +8183,56 @@ function localDryRunMissingFields(i) {
|
|
|
8196
8183
|
];
|
|
8197
8184
|
return required.filter(([key]) => !String(i[key] ?? "").trim()).map(([, flag]) => flag);
|
|
8198
8185
|
}
|
|
8186
|
+
var SNAPSHOT_LIMITS = {
|
|
8187
|
+
/** objective / state / lastAction / nextAction / resumeInstruction. */
|
|
8188
|
+
bodyFieldLength: 2e3,
|
|
8189
|
+
/** Any single entry in constraints / questions / surfaceMarkers / artifacts. */
|
|
8190
|
+
listItemLength: 500,
|
|
8191
|
+
/** Entries per list. */
|
|
8192
|
+
listLength: 25
|
|
8193
|
+
};
|
|
8194
|
+
function snapshotLimitViolations(i) {
|
|
8195
|
+
const violations = [];
|
|
8196
|
+
const bodyFields = [
|
|
8197
|
+
"objective",
|
|
8198
|
+
"state",
|
|
8199
|
+
"lastAction",
|
|
8200
|
+
"nextAction",
|
|
8201
|
+
"resumeInstruction"
|
|
8202
|
+
];
|
|
8203
|
+
for (const key of bodyFields) {
|
|
8204
|
+
const value = i[key];
|
|
8205
|
+
if (typeof value !== "string") continue;
|
|
8206
|
+
if (value.length > SNAPSHOT_LIMITS.bodyFieldLength) {
|
|
8207
|
+
violations.push(
|
|
8208
|
+
`${key}: ${value.length} characters exceeds the ${SNAPSHOT_LIMITS.bodyFieldLength}-character limit`
|
|
8209
|
+
);
|
|
8210
|
+
}
|
|
8211
|
+
}
|
|
8212
|
+
const lists = [
|
|
8213
|
+
"constraints",
|
|
8214
|
+
"questions",
|
|
8215
|
+
"surfaceMarkers",
|
|
8216
|
+
"artifacts"
|
|
8217
|
+
];
|
|
8218
|
+
for (const key of lists) {
|
|
8219
|
+
const items = i[key];
|
|
8220
|
+
if (!Array.isArray(items)) continue;
|
|
8221
|
+
if (items.length > SNAPSHOT_LIMITS.listLength) {
|
|
8222
|
+
violations.push(
|
|
8223
|
+
`${key}: ${items.length} entries exceeds the ${SNAPSHOT_LIMITS.listLength}-entry limit`
|
|
8224
|
+
);
|
|
8225
|
+
}
|
|
8226
|
+
items.forEach((item, index) => {
|
|
8227
|
+
if (typeof item === "string" && item.length > SNAPSHOT_LIMITS.listItemLength) {
|
|
8228
|
+
violations.push(
|
|
8229
|
+
`${key}[${index}]: ${item.length} characters exceeds the ${SNAPSHOT_LIMITS.listItemLength}-character limit`
|
|
8230
|
+
);
|
|
8231
|
+
}
|
|
8232
|
+
});
|
|
8233
|
+
}
|
|
8234
|
+
return violations;
|
|
8235
|
+
}
|
|
8199
8236
|
async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScope, opts) {
|
|
8200
8237
|
const lane = resolveLane(laneFlag, cwd);
|
|
8201
8238
|
if (!lane) return false;
|
|
@@ -8510,7 +8547,96 @@ Fail-soft: failures exit 0 and never block; session-context refresh failures ren
|
|
|
8510
8547
|
});
|
|
8511
8548
|
}
|
|
8512
8549
|
|
|
8550
|
+
// src/commands/chat.ts
|
|
8551
|
+
function registerChat(program2) {
|
|
8552
|
+
const chat = program2.command("chat").description("Send and read Slack / Discord channel messages").option("--surface <surface>", "slack | discord", "slack");
|
|
8553
|
+
chat.addHelpText(
|
|
8554
|
+
"after",
|
|
8555
|
+
`
|
|
8556
|
+
Examples:
|
|
8557
|
+
$ sechroom chat send C0123456789 "deploy is green" --surface slack
|
|
8558
|
+
$ sechroom chat send 987654321098765432 "deploy is green" --surface discord --guild 123456789012345678
|
|
8559
|
+
$ sechroom chat send C0123456789 "lgtm" --surface slack --as user --parent 1718049600.123456
|
|
8560
|
+
$ sechroom chat messages --surface slack
|
|
8561
|
+
$ sechroom chat replies 1718049600.123456 --surface slack
|
|
8562
|
+
$ sechroom chat stop-tracking 1718049600.123456 --surface slack`
|
|
8563
|
+
);
|
|
8564
|
+
chat.command("send <channelId> <text>").description("Send a message to a channel (POST /chat/channel-messages/{surface})").option("--guild <guildId>", "Discord guild snowflake \u2014 required for --surface discord").option("--memory <memoryId>", "Attach a sechroom memory id").option("--no-track", "Don't capture replies to this message").option("--parent <parentMessage>", "Thread under a parent (Slack thread_ts / Discord message id)").option("--source <source>", "Source / lane stamp (renders an attribution footer; default: this checkout's pinned code-lane)").option("--as <as>", "Slack only: 'bot' (default) or 'user' (your linked Slack identity)", "bot").action(async (channelId, text2, opts, cmd) => {
|
|
8565
|
+
const source = resolveSourceLane(opts.source);
|
|
8566
|
+
const { surface, ...globals } = cmd.optsWithGlobals();
|
|
8567
|
+
const json = Boolean(cmd.optsWithGlobals().json);
|
|
8568
|
+
const cfg = resolveConfig(globals);
|
|
8569
|
+
const data = await runApi("Sending message", async () => {
|
|
8570
|
+
const client = await makeClient(cfg);
|
|
8571
|
+
return client.POST("/chat/channel-messages/{surface}", {
|
|
8572
|
+
params: { path: { surface: String(surface) } },
|
|
8573
|
+
body: {
|
|
8574
|
+
channelId,
|
|
8575
|
+
text: text2,
|
|
8576
|
+
guildId: opts.guild ?? null,
|
|
8577
|
+
attachedMemoryId: opts.memory ?? null,
|
|
8578
|
+
trackReplies: opts.track,
|
|
8579
|
+
parentMessage: opts.parent ?? null,
|
|
8580
|
+
source,
|
|
8581
|
+
as: opts.as
|
|
8582
|
+
}
|
|
8583
|
+
});
|
|
8584
|
+
});
|
|
8585
|
+
if (!data.ok) {
|
|
8586
|
+
if (json) {
|
|
8587
|
+
emit(data, true);
|
|
8588
|
+
} else {
|
|
8589
|
+
process.stderr.write(
|
|
8590
|
+
`${err("\u2717")} send failed: ${data.upstreamError ?? "error"}${data.errorDescription ? ` \u2014 ${data.errorDescription}` : ""}
|
|
8591
|
+
`
|
|
8592
|
+
);
|
|
8593
|
+
}
|
|
8594
|
+
process.exit(1);
|
|
8595
|
+
}
|
|
8596
|
+
const idPart = data.persistedId ? ` ${style.dim(`(${data.persistedId})`)}` : "";
|
|
8597
|
+
emitAction(`sent to ${surface} ${style.bold(channelId)}${idPart}`, data, json);
|
|
8598
|
+
});
|
|
8599
|
+
chat.command("messages").description("List recent channel messages (GET /chat/channel-messages/{surface})").action(async (_opts, cmd) => {
|
|
8600
|
+
const { surface, ...globals } = cmd.optsWithGlobals();
|
|
8601
|
+
const cfg = resolveConfig(globals);
|
|
8602
|
+
const data = await runApi("Fetching messages", async () => {
|
|
8603
|
+
const client = await makeClient(cfg);
|
|
8604
|
+
return client.GET("/chat/channel-messages/{surface}", {
|
|
8605
|
+
params: { path: { surface: String(surface) } }
|
|
8606
|
+
});
|
|
8607
|
+
});
|
|
8608
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
8609
|
+
});
|
|
8610
|
+
chat.command("replies <messageId>").description("List thread replies for a message (GET /chat/channel-messages/by-id/{id}/replies)").action(async (messageId, _opts, cmd) => {
|
|
8611
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
8612
|
+
const data = await runApi("Fetching replies", async () => {
|
|
8613
|
+
const client = await makeClient(cfg);
|
|
8614
|
+
return client.GET("/chat/channel-messages/by-id/{id}/replies", {
|
|
8615
|
+
params: { path: { id: messageId } }
|
|
8616
|
+
});
|
|
8617
|
+
});
|
|
8618
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
8619
|
+
});
|
|
8620
|
+
chat.command("stop-tracking <messageId>").description("Stop watching a message for replies (POST .../by-id/{id}/stop-tracking-replies)").action(async (messageId, _opts, cmd) => {
|
|
8621
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
8622
|
+
const data = await runApi("Stopping reply tracking", async () => {
|
|
8623
|
+
const client = await makeClient(cfg);
|
|
8624
|
+
return client.POST("/chat/channel-messages/by-id/{id}/stop-tracking-replies", {
|
|
8625
|
+
params: { path: { id: messageId } },
|
|
8626
|
+
body: {}
|
|
8627
|
+
});
|
|
8628
|
+
});
|
|
8629
|
+
emitAction(`stopped tracking replies on ${style.bold(messageId)}`, data, cmd.optsWithGlobals().json);
|
|
8630
|
+
});
|
|
8631
|
+
}
|
|
8632
|
+
|
|
8513
8633
|
// src/commands/checkpoint.ts
|
|
8634
|
+
import { mkdirSync as mkdirSync15, writeFileSync as writeFileSync14 } from "fs";
|
|
8635
|
+
import { dirname as dirname14, join as join17 } from "path";
|
|
8636
|
+
var CHECKPOINT_UNCHANGED_EXIT_CODE = 3;
|
|
8637
|
+
function checkpointUnchangedLine(snapshotId) {
|
|
8638
|
+
return `${style.bold("=")} unchanged ${style.dim(`(${snapshotId} already current)`)} \u2014 no new snapshot; the local file was left as-is`;
|
|
8639
|
+
}
|
|
8514
8640
|
function registerCheckpoint(program2) {
|
|
8515
8641
|
program2.command("checkpoint").description(
|
|
8516
8642
|
"Checkpoint working state: create a continuity snapshot (server-validated) AND sync ./.sechroom/continuity.json in one step"
|
|
@@ -8558,6 +8684,13 @@ Examples:
|
|
|
8558
8684
|
"no lane resolved \u2014 pass --lane, set SECHROOM_LANE, or pin one in ./.sechroom/lane.json (code-lane). See `sechroom lane`."
|
|
8559
8685
|
);
|
|
8560
8686
|
}
|
|
8687
|
+
const overLimit = snapshotLimitViolations(merged);
|
|
8688
|
+
if (overLimit.length > 0) {
|
|
8689
|
+
fail(
|
|
8690
|
+
`snapshot exceeds the field limits \u2014 trim and retry:
|
|
8691
|
+
${overLimit.join("\n ")}`
|
|
8692
|
+
);
|
|
8693
|
+
}
|
|
8561
8694
|
const scope = merged.scope ?? "session";
|
|
8562
8695
|
const body = {
|
|
8563
8696
|
laneId: lane,
|
|
@@ -8604,6 +8737,16 @@ Examples:
|
|
|
8604
8737
|
const client = await makeClient(cfg);
|
|
8605
8738
|
return client.POST("/continuity/snapshots", { body });
|
|
8606
8739
|
});
|
|
8740
|
+
const previousSnapshotId = base.lastSnapshotId;
|
|
8741
|
+
if (previousSnapshotId && data.snapshotId === previousSnapshotId) {
|
|
8742
|
+
process.exitCode = CHECKPOINT_UNCHANGED_EXIT_CODE;
|
|
8743
|
+
if (json) {
|
|
8744
|
+
emit({ snapshotId: data.snapshotId, lane, scope, unchanged: true, file: null }, true);
|
|
8745
|
+
return;
|
|
8746
|
+
}
|
|
8747
|
+
process.stdout.write(checkpointUnchangedLine(data.snapshotId) + "\n");
|
|
8748
|
+
return;
|
|
8749
|
+
}
|
|
8607
8750
|
const path = resolveIntentPath(cwd) ?? join17(cwd, INTENT_FILE);
|
|
8608
8751
|
const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
|
|
8609
8752
|
mkdirSync15(dirname14(path), { recursive: true });
|
|
@@ -8641,7 +8784,7 @@ function registerClose(program2) {
|
|
|
8641
8784
|
).option(
|
|
8642
8785
|
"--to-version <n>",
|
|
8643
8786
|
"Pin the Reference edge at this task version \u2014 the DISPATCHED version the executor worked against (D-continuity-2). Default: the task's current version (status flips are metadata edits that don't bump the version, so current == dispatched in the normal case; pass this when the task's content changed between dispatch and close)."
|
|
8644
|
-
).option("--source <source>", "Source / lane stamp
|
|
8787
|
+
).option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").addHelpText(
|
|
8645
8788
|
"after",
|
|
8646
8789
|
`
|
|
8647
8790
|
Examples:
|
|
@@ -8653,6 +8796,7 @@ Examples:
|
|
|
8653
8796
|
$ sechroom close --task mem_XXXX --verdict pass --workspace wsp_ZZZZ \\
|
|
8654
8797
|
--title "done" --file ./closeout.md`
|
|
8655
8798
|
).action(async (opts, cmd) => {
|
|
8799
|
+
const source = resolveSourceLane(opts.source);
|
|
8656
8800
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
8657
8801
|
const json = cmd.optsWithGlobals().json;
|
|
8658
8802
|
const verdict = String(opts.verdict);
|
|
@@ -8712,7 +8856,7 @@ Examples:
|
|
|
8712
8856
|
type: "reference",
|
|
8713
8857
|
content: "{}",
|
|
8714
8858
|
confidence: 1,
|
|
8715
|
-
source
|
|
8859
|
+
source,
|
|
8716
8860
|
archetype: "Document",
|
|
8717
8861
|
title: opts.title,
|
|
8718
8862
|
tags,
|
|
@@ -8769,7 +8913,7 @@ Examples:
|
|
|
8769
8913
|
params: { path: { memoryId: opts.task } },
|
|
8770
8914
|
body: {
|
|
8771
8915
|
memoryId: opts.task,
|
|
8772
|
-
source
|
|
8916
|
+
source,
|
|
8773
8917
|
tags: nextTags
|
|
8774
8918
|
}
|
|
8775
8919
|
})
|
|
@@ -10583,6 +10727,64 @@ AGPL \u2014 no Herdr code is vendored here.`
|
|
|
10583
10727
|
|
|
10584
10728
|
// src/commands/lane.ts
|
|
10585
10729
|
var LANE_KEYS = ["code-lane", "design-lane"];
|
|
10730
|
+
function apiFlagsFrom(options) {
|
|
10731
|
+
return {
|
|
10732
|
+
baseUrl: typeof options.baseUrl === "string" ? options.baseUrl : void 0,
|
|
10733
|
+
tenant: typeof options.tenant === "string" ? options.tenant : void 0,
|
|
10734
|
+
account: typeof options.account === "string" ? options.account : void 0,
|
|
10735
|
+
binding: typeof options.binding === "string" ? options.binding : void 0
|
|
10736
|
+
};
|
|
10737
|
+
}
|
|
10738
|
+
function resolvedPinnedLanes(values) {
|
|
10739
|
+
const lanes = LANE_KEYS.flatMap((key) => {
|
|
10740
|
+
const lane = values[key];
|
|
10741
|
+
return lane ? [applyWorktreeLaneSuffix(lane)] : [];
|
|
10742
|
+
});
|
|
10743
|
+
return [...new Set(lanes)];
|
|
10744
|
+
}
|
|
10745
|
+
function reportRegistrationWarning(laneId, error) {
|
|
10746
|
+
const detail = formatFailureMessage(error);
|
|
10747
|
+
const message = `could not register source lane '${laneId}': ${detail}`;
|
|
10748
|
+
process.stderr.write(`${warn(`warning: ${message}`)}
|
|
10749
|
+
`);
|
|
10750
|
+
return message;
|
|
10751
|
+
}
|
|
10752
|
+
async function registerPinnedLanes(laneIds, flags) {
|
|
10753
|
+
if (laneIds.length === 0) return [];
|
|
10754
|
+
let client;
|
|
10755
|
+
try {
|
|
10756
|
+
client = await makeClient(resolveConfig(flags));
|
|
10757
|
+
} catch (error) {
|
|
10758
|
+
return laneIds.map((laneId) => ({
|
|
10759
|
+
laneId,
|
|
10760
|
+
registered: false,
|
|
10761
|
+
warning: reportRegistrationWarning(laneId, error)
|
|
10762
|
+
}));
|
|
10763
|
+
}
|
|
10764
|
+
const results = [];
|
|
10765
|
+
for (const laneId of laneIds) {
|
|
10766
|
+
try {
|
|
10767
|
+
const response = await client.POST("/identity/source-lanes", {
|
|
10768
|
+
body: { laneId }
|
|
10769
|
+
});
|
|
10770
|
+
const failure = response.error ?? (response.response && !response.response.ok ? `HTTP ${response.response.status} ${response.response.statusText}`.trim() : void 0);
|
|
10771
|
+
if (failure !== void 0) throw new Error(formatFailureMessage(failure));
|
|
10772
|
+
results.push({ laneId, registered: true });
|
|
10773
|
+
} catch (error) {
|
|
10774
|
+
results.push({
|
|
10775
|
+
laneId,
|
|
10776
|
+
registered: false,
|
|
10777
|
+
warning: reportRegistrationWarning(laneId, error)
|
|
10778
|
+
});
|
|
10779
|
+
}
|
|
10780
|
+
}
|
|
10781
|
+
return results;
|
|
10782
|
+
}
|
|
10783
|
+
function printRegistrationSummary(registrations) {
|
|
10784
|
+
const registered = registrations.filter((result) => result.registered).map((result) => result.laneId);
|
|
10785
|
+
if (registered.length > 0)
|
|
10786
|
+
console.log(style.dim(`Registered source lane${registered.length === 1 ? "" : "s"}: ${registered.join(", ")}`));
|
|
10787
|
+
}
|
|
10586
10788
|
function showLane(json) {
|
|
10587
10789
|
const found = readSem();
|
|
10588
10790
|
if (!found) {
|
|
@@ -10604,35 +10806,57 @@ function showLane(json) {
|
|
|
10604
10806
|
Object.entries(resolved).forEach(([k, v]) => console.log(" " + style.bold(k) + " = " + v));
|
|
10605
10807
|
if (suffixed) console.log(style.dim(" (worktree -N suffix applied \u2014 non-primary git worktree)"));
|
|
10606
10808
|
}
|
|
10607
|
-
function setLane(opts) {
|
|
10809
|
+
async function setLane(opts) {
|
|
10608
10810
|
if (!opts.codeLane && !opts.designLane) fail("Provide --code-lane and/or --design-lane.");
|
|
10609
10811
|
const target = localSemPath();
|
|
10610
10812
|
const values = readLocalSemValues();
|
|
10611
10813
|
if (opts.codeLane) values["code-lane"] = opts.codeLane;
|
|
10612
10814
|
if (opts.designLane) values["design-lane"] = opts.designLane;
|
|
10613
10815
|
writeSem(values, target);
|
|
10614
|
-
|
|
10816
|
+
const registrations = await registerPinnedLanes(
|
|
10817
|
+
resolvedPinnedLanes(values),
|
|
10818
|
+
opts.apiFlags ?? {}
|
|
10819
|
+
);
|
|
10820
|
+
if (opts.json) return emit({ path: target, values, registrations }, true);
|
|
10615
10821
|
console.log(style.green(`Wrote lane pin \u2192 ${target} ${style.dim("(git-ignored)")}`));
|
|
10616
10822
|
Object.entries(values).forEach(([k, v]) => console.log(" " + style.dim(k) + " = " + v));
|
|
10823
|
+
printRegistrationSummary(registrations);
|
|
10617
10824
|
}
|
|
10618
10825
|
function registerLane(program2) {
|
|
10619
10826
|
const lane = program2.command("lane").description("Show this checkout's continuity lane pin (worktree-aware -N suffix applied)").option("--json", "machine output").action((opts, cmd) => showLane(Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)));
|
|
10620
|
-
lane.command("set").description("Write this checkout's lane pin to ./.sechroom/lane.json").option("--code-lane <id>", "code-surface lane id (e.g. claude-code-chris)").option("--design-lane <id>", "design / substrate-authoring lane id (e.g. claude-design-chris)").option("--json", "machine output").action(
|
|
10621
|
-
|
|
10827
|
+
lane.command("set").description("Write this checkout's lane pin to ./.sechroom/lane.json").option("--code-lane <id>", "code-surface lane id (e.g. claude-code-chris)").option("--design-lane <id>", "design / substrate-authoring lane id (e.g. claude-design-chris)").option("--json", "machine output").action((opts, cmd) => {
|
|
10828
|
+
const globals = cmd.optsWithGlobals();
|
|
10829
|
+
return setLane({
|
|
10622
10830
|
codeLane: opts.codeLane,
|
|
10623
10831
|
designLane: opts.designLane,
|
|
10624
|
-
json: Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)
|
|
10625
|
-
|
|
10626
|
-
|
|
10832
|
+
json: Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json),
|
|
10833
|
+
apiFlags: apiFlagsFrom(globals)
|
|
10834
|
+
});
|
|
10835
|
+
});
|
|
10836
|
+
lane.command("register").description("Register the source lanes already pinned in this checkout").option("--json", "machine output").action(async (opts, cmd) => {
|
|
10837
|
+
const found = readSem();
|
|
10838
|
+
if (!found)
|
|
10839
|
+
fail("No ./.sechroom/lane.json pin in this checkout. Run 'sechroom lane set'.");
|
|
10840
|
+
const globals = cmd.optsWithGlobals();
|
|
10841
|
+
const registrations = await registerPinnedLanes(
|
|
10842
|
+
resolvedPinnedLanes(found.values),
|
|
10843
|
+
apiFlagsFrom(globals)
|
|
10844
|
+
);
|
|
10845
|
+
const json = Boolean(opts.json) || Boolean(globals.json);
|
|
10846
|
+
if (json) return emit({ path: found.path, registrations }, true);
|
|
10847
|
+
printRegistrationSummary(registrations);
|
|
10848
|
+
});
|
|
10627
10849
|
lane.addHelpText(
|
|
10628
10850
|
"after",
|
|
10629
10851
|
`
|
|
10630
10852
|
Examples:
|
|
10631
10853
|
$ sechroom lane show the resolved lane(s)
|
|
10632
10854
|
$ sechroom lane set --code-lane claude-code-chris --design-lane claude-design-chris
|
|
10855
|
+
$ sechroom lane register register existing pinned lane(s)
|
|
10633
10856
|
|
|
10634
10857
|
In a non-primary git worktree the ratified concurrent-session -N suffix is auto-applied (SBC-1094).
|
|
10635
|
-
|
|
10858
|
+
Lane resolution stops at the nearest git worktree/repository root; a parent checkout's .sechroom/lane.json is never inherited.
|
|
10859
|
+
(Aliases: 'sechroom skills lane' / 'skills set-lane' \u2014 kept for back-compat; set-lane also registers.)`
|
|
10636
10860
|
);
|
|
10637
10861
|
}
|
|
10638
10862
|
|
|
@@ -10915,7 +11139,8 @@ Examples:
|
|
|
10915
11139
|
"--owner-type <ownerType>",
|
|
10916
11140
|
"Workspace | Project | Unfiled",
|
|
10917
11141
|
"Unfiled"
|
|
10918
|
-
).option("--owner-id <ownerId>", "Owner id (required for Workspace/Project)").option("--source <source>", "Source / lane stamp
|
|
11142
|
+
).option("--owner-id <ownerId>", "Owner id (required for Workspace/Project)").option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").option("--confidence <n>", "Confidence 0..1", "1.0").action(async (opts, cmd) => {
|
|
11143
|
+
const source = resolveSourceLane(opts.source);
|
|
10919
11144
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
10920
11145
|
const { text: text2, defaultTitle } = resolveCreateBody(opts.text, opts.file);
|
|
10921
11146
|
const title = opts.title ?? defaultTitle;
|
|
@@ -10928,7 +11153,7 @@ Examples:
|
|
|
10928
11153
|
type: opts.type,
|
|
10929
11154
|
content: "{}",
|
|
10930
11155
|
confidence: Number(opts.confidence),
|
|
10931
|
-
source
|
|
11156
|
+
source,
|
|
10932
11157
|
archetype: "Document",
|
|
10933
11158
|
title: title ?? null,
|
|
10934
11159
|
tags: opts.tag ?? null,
|
|
@@ -10957,7 +11182,8 @@ Examples:
|
|
|
10957
11182
|
"--create-workspace",
|
|
10958
11183
|
"Create the --workspace when its name matches nothing",
|
|
10959
11184
|
false
|
|
10960
|
-
).option("--recursive", "Walk subdirectories of a given directory", false).option("--type <type>", "Memory type", "reference").option("--tag <tag...>", "Tags applied to every memory (repeatable)").option("--source <source>", "Source / lane stamp
|
|
11185
|
+
).option("--recursive", "Walk subdirectories of a given directory", false).option("--type <type>", "Memory type", "reference").option("--tag <tag...>", "Tags applied to every memory (repeatable)").option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").option("--confidence <n>", "Confidence 0..1", "1.0").option("--dry-run", "Resolve and print the plan; write nothing", false).action(async (paths, opts, cmd) => {
|
|
11186
|
+
const source = resolveSourceLane(opts.source);
|
|
10961
11187
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
10962
11188
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
10963
11189
|
const dryRun = Boolean(opts.dryRun);
|
|
@@ -11070,7 +11296,7 @@ ${plan.rows.map(
|
|
|
11070
11296
|
workspaceId: workspace.id,
|
|
11071
11297
|
type: opts.type,
|
|
11072
11298
|
tags: opts.tag ?? null,
|
|
11073
|
-
source
|
|
11299
|
+
source,
|
|
11074
11300
|
confidence: Number(opts.confidence)
|
|
11075
11301
|
},
|
|
11076
11302
|
ports,
|
|
@@ -11192,7 +11418,8 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11192
11418
|
"--replace-all",
|
|
11193
11419
|
"Replace every occurrence (default: first only)",
|
|
11194
11420
|
false
|
|
11195
|
-
).option("--regenerate-filing", "Re-run filing after the edit", false).option("--source <source>", "Source / lane stamp
|
|
11421
|
+
).option("--regenerate-filing", "Re-run filing after the edit", false).option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").action(async (memoryId, opts, cmd) => {
|
|
11422
|
+
const source = resolveSourceLane(opts.source);
|
|
11196
11423
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
11197
11424
|
const data = await runApi("Editing memory text", async () => {
|
|
11198
11425
|
const client = await makeClient(cfg);
|
|
@@ -11204,7 +11431,7 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11204
11431
|
newText: opts.new,
|
|
11205
11432
|
replaceAll: Boolean(opts.replaceAll),
|
|
11206
11433
|
regenerateFiling: Boolean(opts.regenerateFiling),
|
|
11207
|
-
source
|
|
11434
|
+
source
|
|
11208
11435
|
}
|
|
11209
11436
|
});
|
|
11210
11437
|
});
|
|
@@ -11216,7 +11443,8 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11216
11443
|
});
|
|
11217
11444
|
memory.command("edit-text-batch <memoryId>").description(
|
|
11218
11445
|
"Apply many find/replace edits (POST /memories/{memoryId}/edit-text-batch)"
|
|
11219
|
-
).requiredOption("--edit <old=>new...>", "Edit as 'old=>new' (repeatable)").option("--replace-all", "Apply replaceAll to every edit", false).option("--regenerate-filing", "Re-run filing after the edits", false).option("--source <source>", "Source / lane stamp
|
|
11446
|
+
).requiredOption("--edit <old=>new...>", "Edit as 'old=>new' (repeatable)").option("--replace-all", "Apply replaceAll to every edit", false).option("--regenerate-filing", "Re-run filing after the edits", false).option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").action(async (memoryId, opts, cmd) => {
|
|
11447
|
+
const source = resolveSourceLane(opts.source);
|
|
11220
11448
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
11221
11449
|
const replaceAll = Boolean(opts.replaceAll);
|
|
11222
11450
|
const edits = opts.edit.map((spec) => {
|
|
@@ -11242,7 +11470,7 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11242
11470
|
memoryId,
|
|
11243
11471
|
edits,
|
|
11244
11472
|
regenerateFiling: Boolean(opts.regenerateFiling),
|
|
11245
|
-
source
|
|
11473
|
+
source
|
|
11246
11474
|
}
|
|
11247
11475
|
});
|
|
11248
11476
|
});
|
|
@@ -11267,7 +11495,8 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11267
11495
|
"--bump-version",
|
|
11268
11496
|
"Bump the version chain (use for a content reinterpretation, e.g. a type promotion)",
|
|
11269
11497
|
false
|
|
11270
|
-
).option("--source <source>", "Contributing lane stamp (attribution)"
|
|
11498
|
+
).option("--source <source>", "Contributing lane stamp (attribution; default: this checkout's pinned code-lane)").action(async (memoryId, opts, cmd) => {
|
|
11499
|
+
const source = resolveSourceLane(opts.source);
|
|
11271
11500
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
11272
11501
|
const json = cmd.optsWithGlobals().json;
|
|
11273
11502
|
const hasTagOps = Boolean(opts.tag || opts.addTag || opts.removeTag);
|
|
@@ -11299,7 +11528,7 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11299
11528
|
}
|
|
11300
11529
|
const body = {
|
|
11301
11530
|
memoryId,
|
|
11302
|
-
source
|
|
11531
|
+
source,
|
|
11303
11532
|
bumpVersion: Boolean(opts.bumpVersion)
|
|
11304
11533
|
};
|
|
11305
11534
|
if (opts.title !== void 0) body.title = opts.title;
|
|
@@ -11323,13 +11552,14 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11323
11552
|
json
|
|
11324
11553
|
);
|
|
11325
11554
|
});
|
|
11326
|
-
memory.command("archive <memoryId>").description("Archive a memory (POST /memories/{memoryId}/archive)").option("--source <source>", "Source / lane stamp
|
|
11555
|
+
memory.command("archive <memoryId>").description("Archive a memory (POST /memories/{memoryId}/archive)").option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").action(async (memoryId, opts, cmd) => {
|
|
11556
|
+
const source = resolveSourceLane(opts.source);
|
|
11327
11557
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
11328
11558
|
const data = await runApi("Archiving memory", async () => {
|
|
11329
11559
|
const client = await makeClient(cfg);
|
|
11330
11560
|
return client.POST("/memories/{memoryId}/archive", {
|
|
11331
11561
|
params: { path: { memoryId } },
|
|
11332
|
-
body: { source
|
|
11562
|
+
body: { source }
|
|
11333
11563
|
});
|
|
11334
11564
|
});
|
|
11335
11565
|
emitAction(
|
|
@@ -11340,13 +11570,14 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11340
11570
|
});
|
|
11341
11571
|
memory.command("restore <memoryId>").description(
|
|
11342
11572
|
"Restore an archived memory (POST /memories/{memoryId}/restore)"
|
|
11343
|
-
).option("--source <source>", "Source / lane stamp
|
|
11573
|
+
).option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").action(async (memoryId, opts, cmd) => {
|
|
11574
|
+
const source = resolveSourceLane(opts.source);
|
|
11344
11575
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
11345
11576
|
const data = await runApi("Restoring memory", async () => {
|
|
11346
11577
|
const client = await makeClient(cfg);
|
|
11347
11578
|
return client.POST("/memories/{memoryId}/restore", {
|
|
11348
11579
|
params: { path: { memoryId } },
|
|
11349
|
-
body: { source
|
|
11580
|
+
body: { source }
|
|
11350
11581
|
});
|
|
11351
11582
|
});
|
|
11352
11583
|
emitAction(
|
|
@@ -11360,7 +11591,8 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11360
11591
|
).requiredOption(
|
|
11361
11592
|
"--owner-type <ownerType>",
|
|
11362
11593
|
"Unfiled | Workspace | Project | Candidate"
|
|
11363
|
-
).option("--owner-id <ownerId>", "Owner id (required unless Unfiled)").option("--source <source>", "Source / lane stamp
|
|
11594
|
+
).option("--owner-id <ownerId>", "Owner id (required unless Unfiled)").option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").action(async (memoryId, opts, cmd) => {
|
|
11595
|
+
const source = resolveSourceLane(opts.source);
|
|
11364
11596
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
11365
11597
|
const data = await runApi("Moving memory", async () => {
|
|
11366
11598
|
const client = await makeClient(cfg);
|
|
@@ -11371,7 +11603,7 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11371
11603
|
type: opts.ownerType,
|
|
11372
11604
|
id: String(opts.ownerId ?? "")
|
|
11373
11605
|
},
|
|
11374
|
-
source
|
|
11606
|
+
source
|
|
11375
11607
|
}
|
|
11376
11608
|
});
|
|
11377
11609
|
});
|
|
@@ -11423,7 +11655,8 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11423
11655
|
).requiredOption(
|
|
11424
11656
|
"--content <content>",
|
|
11425
11657
|
"Reverted content JSON (the target version's content)"
|
|
11426
|
-
).option("--source <source>", "Source / lane stamp
|
|
11658
|
+
).option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").action(async (memoryId, opts, cmd) => {
|
|
11659
|
+
const source = resolveSourceLane(opts.source);
|
|
11427
11660
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
11428
11661
|
const data = await runApi("Reverting memory", async () => {
|
|
11429
11662
|
const client = await makeClient(cfg);
|
|
@@ -11433,7 +11666,7 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11433
11666
|
fromVersion: Number(opts.fromVersion),
|
|
11434
11667
|
revertedContent: opts.content,
|
|
11435
11668
|
revertedText: opts.text,
|
|
11436
|
-
source
|
|
11669
|
+
source
|
|
11437
11670
|
}
|
|
11438
11671
|
});
|
|
11439
11672
|
});
|
|
@@ -12445,6 +12678,7 @@ function copyChoice(opts) {
|
|
|
12445
12678
|
}
|
|
12446
12679
|
async function maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId, choice) {
|
|
12447
12680
|
if (!personalWorkspaceId || choice === "no") return;
|
|
12681
|
+
let source;
|
|
12448
12682
|
const seen = /* @__PURE__ */ new Set();
|
|
12449
12683
|
for (const key of keys) {
|
|
12450
12684
|
const instr = targets[key]?.instruction;
|
|
@@ -12473,7 +12707,8 @@ version, the shared template stays clean, and you can discard back anytime.
|
|
|
12473
12707
|
make = await promptYesNo("Make a personal copy to customise?");
|
|
12474
12708
|
}
|
|
12475
12709
|
if (make) {
|
|
12476
|
-
|
|
12710
|
+
const copySource = source ??= resolveSourceLane(void 0);
|
|
12711
|
+
await createOverride(cfg, resolved, personalWorkspaceId, copySource);
|
|
12477
12712
|
process.stderr.write(
|
|
12478
12713
|
`\u2713 personal copy created for ${instr.surfaceKey} \u2014 edit it on the Agent setup page or via the API.
|
|
12479
12714
|
`
|
|
@@ -12837,7 +13072,10 @@ function registerSetup(program2, deps = {}) {
|
|
|
12837
13072
|
).option(
|
|
12838
13073
|
"--body <markdown>",
|
|
12839
13074
|
"section body (default: a TODO scaffold to edit later)"
|
|
12840
|
-
).option("--no-regen", "skip the agent-files regen after authoring").option(
|
|
13075
|
+
).option("--no-regen", "skip the agent-files regen after authoring").option(
|
|
13076
|
+
"--source <source>",
|
|
13077
|
+
"Source / lane stamp (default: this checkout's pinned code-lane)"
|
|
13078
|
+
).option("--dry-run", "print what would be authored; write nothing", false).addHelpText(
|
|
12841
13079
|
"after",
|
|
12842
13080
|
`
|
|
12843
13081
|
The memo carries the two conventions a workspace-conventions section needs (FR-sechroom-236):
|
|
@@ -12850,6 +13088,7 @@ Examples:
|
|
|
12850
13088
|
$ sechroom setup new-convention "Backend testing" --kind standard --body "- run dotnet test ..."
|
|
12851
13089
|
$ sechroom setup new-convention "Draft section" --no-regen author only, regen later`
|
|
12852
13090
|
).action(async (titleParts, opts, cmd) => {
|
|
13091
|
+
const source = resolveSourceLane(opts.source);
|
|
12853
13092
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
12854
13093
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
12855
13094
|
const title = titleParts.join(" ").trim();
|
|
@@ -12885,7 +13124,7 @@ Examples:
|
|
|
12885
13124
|
type: draft.kind,
|
|
12886
13125
|
content: "{}",
|
|
12887
13126
|
confidence: 1,
|
|
12888
|
-
source
|
|
13127
|
+
source,
|
|
12889
13128
|
archetype: "Document",
|
|
12890
13129
|
title: draft.title,
|
|
12891
13130
|
tags: draft.tags,
|
|
@@ -13463,11 +13702,20 @@ async function ensureTenant(baseUrl, g, opts) {
|
|
|
13463
13702
|
clientId: persisted.clientId
|
|
13464
13703
|
};
|
|
13465
13704
|
}
|
|
13466
|
-
|
|
13467
|
-
if (process.env.SECHROOM_TOKEN) return;
|
|
13705
|
+
function hasUsableCredential() {
|
|
13706
|
+
if (process.env.SECHROOM_TOKEN) return true;
|
|
13468
13707
|
const cached = readToken();
|
|
13469
|
-
|
|
13470
|
-
|
|
13708
|
+
return Boolean(cached?.accessToken) && (cached.expiresAt === void 0 || Date.now() < cached.expiresAt - 6e4 || Boolean(cached.refreshToken));
|
|
13709
|
+
}
|
|
13710
|
+
function requireCredentialForPreview(preview, dryRun) {
|
|
13711
|
+
if (!preview || hasUsableCredential()) return;
|
|
13712
|
+
const flag = dryRun ? "--dry-run" : "--check";
|
|
13713
|
+
fail(
|
|
13714
|
+
`sechroom onboard ${flag} needs an existing sign-in; run \`sechroom login\` first, then re-run the preview.`
|
|
13715
|
+
);
|
|
13716
|
+
}
|
|
13717
|
+
async function ensureAuth(cfg, yes) {
|
|
13718
|
+
if (hasUsableCredential()) return;
|
|
13471
13719
|
if (!canPrompt() || yes) {
|
|
13472
13720
|
fail(
|
|
13473
13721
|
"Not signed in. Run `sechroom login` first, or set SECHROOM_TOKEN for headless use."
|
|
@@ -13715,6 +13963,10 @@ async function runRecurse(cfg, g, opts) {
|
|
|
13715
13963
|
}
|
|
13716
13964
|
summarizeFanout(results, { dryRun });
|
|
13717
13965
|
}
|
|
13966
|
+
async function runOnboardCopyPhase(opts) {
|
|
13967
|
+
if (!opts.json && !opts.dryRun && !opts.check) await opts.ensureLanePin();
|
|
13968
|
+
if (!opts.dryRun && !opts.check) await opts.maybeOfferCopies();
|
|
13969
|
+
}
|
|
13718
13970
|
function registerOnboard(program2) {
|
|
13719
13971
|
program2.command("onboard").description(
|
|
13720
13972
|
"Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project"
|
|
@@ -13806,6 +14058,7 @@ Examples:
|
|
|
13806
14058
|
if (opts.designLane) process.env.SECHROOM_DESIGN_LANE = opts.designLane;
|
|
13807
14059
|
if (opts.recurse) {
|
|
13808
14060
|
const baseUrl2 = resolveBaseUrl(g);
|
|
14061
|
+
requireCredentialForPreview(dryRun || check, dryRun);
|
|
13809
14062
|
await ensureAuth(
|
|
13810
14063
|
{
|
|
13811
14064
|
baseUrl: baseUrl2,
|
|
@@ -13830,6 +14083,7 @@ Examples:
|
|
|
13830
14083
|
return;
|
|
13831
14084
|
}
|
|
13832
14085
|
const baseUrl = resolveBaseUrl(g);
|
|
14086
|
+
requireCredentialForPreview(dryRun || check, dryRun);
|
|
13833
14087
|
await ensureAuth(
|
|
13834
14088
|
{
|
|
13835
14089
|
baseUrl,
|
|
@@ -13846,7 +14100,12 @@ Examples:
|
|
|
13846
14100
|
local: Boolean(opts.local) || scope === "project",
|
|
13847
14101
|
here: scope === "project" ? true : Boolean(opts.here),
|
|
13848
14102
|
workspace: opts.workspace,
|
|
13849
|
-
|
|
14103
|
+
// Both previews are read-only. `--dry-run` promises to "walk through without
|
|
14104
|
+
// writing files or changing the profile", but only `--check` was in this
|
|
14105
|
+
// guard, so a dry run could still create or rewrite `.sechroom.json` /
|
|
14106
|
+
// `~/.config/sechroom/config.json` before the operator approved anything
|
|
14107
|
+
// (FR-sechroom-710). Matches how `ensureTimezone` is already gated below.
|
|
14108
|
+
persist: !(dryRun || check)
|
|
13850
14109
|
});
|
|
13851
14110
|
const tz = await ensureTimezone(cfg, { yes, dryRun: dryRun || check });
|
|
13852
14111
|
if (!json && tz.action !== "already-set") {
|
|
@@ -13925,16 +14184,20 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
13925
14184
|
codexScope: scope
|
|
13926
14185
|
});
|
|
13927
14186
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
13928
|
-
|
|
13929
|
-
|
|
14187
|
+
await runOnboardCopyPhase({
|
|
14188
|
+
json,
|
|
14189
|
+
dryRun,
|
|
14190
|
+
check,
|
|
14191
|
+
ensureLanePin: () => ensureLanePin(cfg, { yes, dryRun, clients: keys }),
|
|
14192
|
+
maybeOfferCopies: () => maybeOfferCopies(
|
|
13930
14193
|
cfg,
|
|
13931
14194
|
setup,
|
|
13932
14195
|
targets,
|
|
13933
14196
|
keys,
|
|
13934
14197
|
personalWorkspaceId,
|
|
13935
14198
|
copyChoice(opts)
|
|
13936
|
-
)
|
|
13937
|
-
}
|
|
14199
|
+
)
|
|
14200
|
+
});
|
|
13938
14201
|
const writeMcp = wire === "full";
|
|
13939
14202
|
const result = [];
|
|
13940
14203
|
for (const key of keys) {
|
|
@@ -13957,9 +14220,6 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
13957
14220
|
});
|
|
13958
14221
|
}
|
|
13959
14222
|
const evalCounts = buildCheckReport(result).eval;
|
|
13960
|
-
if (!json && !dryRun) {
|
|
13961
|
-
await ensureLanePin(cfg, { yes, dryRun, clients: keys });
|
|
13962
|
-
}
|
|
13963
14223
|
if (!json && !dryRun) {
|
|
13964
14224
|
for (const t of claudeTargets) {
|
|
13965
14225
|
await maybeOfferSkills(cfg, personalWorkspaceId, {
|
|
@@ -14698,13 +14958,20 @@ clients select all; an unconfigured machine preserves the legacy Claude default.
|
|
|
14698
14958
|
if (json) return emit(result, true);
|
|
14699
14959
|
console.log(style.green("\u2713") + ` wrote ${style.bold(result.path)} ${style.dim(`(${result.bytes} bytes)`)}`);
|
|
14700
14960
|
});
|
|
14701
|
-
skills.command("set-lane").description("Alias of `sechroom lane set` (kept for back-compat) \u2014 write this checkout's lane pin").option("--code-lane <id>", "code-surface lane id (e.g. claude-code-chris)").option("--design-lane <id>", "design / substrate-authoring lane id (e.g. claude-design-chris)").option("--json", "machine output").action(
|
|
14702
|
-
|
|
14961
|
+
skills.command("set-lane").description("Alias of `sechroom lane set` (kept for back-compat) \u2014 write this checkout's lane pin").option("--code-lane <id>", "code-surface lane id (e.g. claude-code-chris)").option("--design-lane <id>", "design / substrate-authoring lane id (e.g. claude-design-chris)").option("--json", "machine output").action((opts, cmd) => {
|
|
14962
|
+
const globals = cmd.optsWithGlobals();
|
|
14963
|
+
return setLane({
|
|
14703
14964
|
codeLane: opts.codeLane,
|
|
14704
14965
|
designLane: opts.designLane,
|
|
14705
|
-
json: Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)
|
|
14706
|
-
|
|
14707
|
-
|
|
14966
|
+
json: Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json),
|
|
14967
|
+
apiFlags: {
|
|
14968
|
+
baseUrl: globals.baseUrl,
|
|
14969
|
+
tenant: globals.tenant,
|
|
14970
|
+
account: globals.account,
|
|
14971
|
+
binding: globals.binding
|
|
14972
|
+
}
|
|
14973
|
+
});
|
|
14974
|
+
});
|
|
14708
14975
|
skills.command("lane").description("Alias of `sechroom lane` (kept for back-compat) \u2014 show this checkout's lane pin").option("--json", "machine output").action((opts, cmd) => showLane(Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)));
|
|
14709
14976
|
skills.command("set-workflow").description("Set your per-operator workflow defaults (server-side; follows you across tenants)").option("--default-code-lane <id>", "personal default code lane (e.g. claude-code-chris)").option("--default-design-lane <id>", "personal default design lane (e.g. claude-design-chris)").option("--handover-recipient <id>", "your daily-handover counterparty (e.g. andy)").option("--json", "machine output").action(async (opts, cmd) => {
|
|
14710
14977
|
if (!opts.defaultCodeLane && !opts.defaultDesignLane && !opts.handoverRecipient)
|
|
@@ -14877,14 +15144,15 @@ Examples:
|
|
|
14877
15144
|
$ sechroom worklog append --text "shipped CLI help + onboarding scope; PR #1430"
|
|
14878
15145
|
$ sechroom worklog append --text "smoke passed" --source claude-code-chris --title "CLI smoke"`
|
|
14879
15146
|
);
|
|
14880
|
-
worklog.command("append").description("Append a work-log entry (POST /operator-surface/work-log/append)").requiredOption("--text <text>", "Entry body (short bullets / pointers) \u2014 the bullet").option("--source <source>", "Lane stamp (e.g. claude-code-chris) \u2014 laneId
|
|
15147
|
+
worklog.command("append").description("Append a work-log entry (POST /operator-surface/work-log/append)").requiredOption("--text <text>", "Entry body (short bullets / pointers) \u2014 the bullet").option("--source <source>", "Lane stamp (e.g. claude-code-chris) \u2014 laneId (default: this checkout's pinned code-lane)").option("--workspace <workspaceId>", "Target work-log workspace (default: caller's daily log)").option("--title <title>", "Optional entry title").action(async (opts, cmd) => {
|
|
15148
|
+
const source = resolveSourceLane(opts.source);
|
|
14881
15149
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
14882
15150
|
const data = await runApi("Appending work-log entry", async () => {
|
|
14883
15151
|
const client = await makeClient(cfg);
|
|
14884
15152
|
return client.POST("/operator-surface/work-log/append", {
|
|
14885
15153
|
body: {
|
|
14886
15154
|
bullet: opts.text,
|
|
14887
|
-
laneId:
|
|
15155
|
+
laneId: source,
|
|
14888
15156
|
workspaceId: opts.workspace ?? null,
|
|
14889
15157
|
title: opts.title ?? null
|
|
14890
15158
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sechroom/cli",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.3",
|
|
4
4
|
"description": "Command-line interface for Sechroom — sign in, wire your AI tools, and work with your sechroom from the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"gen": "openapi-typescript \"${SECHROOM_OPENAPI_URL:-https://app.sechroom.ai/api/openapi/v1.json}\" -o src/generated/api.d.ts",
|
|
44
44
|
"build": "tsup src/index.ts --format esm --target node20 --clean",
|
|
45
45
|
"dev": "tsx src/index.ts",
|
|
46
|
-
"test": "node
|
|
46
|
+
"test": "node scripts/run-tests.mjs",
|
|
47
47
|
"check-types": "tsc --noEmit"
|
|
48
48
|
}
|
|
49
49
|
}
|