@sechroom/cli 2026.9.1 → 2026.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +309 -136
- package/package.json +1 -1
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,
|
|
@@ -7031,9 +7031,11 @@ function registerChannel(program2) {
|
|
|
7031
7031
|
(typeof payload === "string" ? payload : JSON.stringify(payload)) + "\n"
|
|
7032
7032
|
)
|
|
7033
7033
|
);
|
|
7034
|
+
const authExit = createAuthExpiredExit();
|
|
7034
7035
|
const leaseHeartbeats = createChannelLeaseHeartbeatManager(
|
|
7035
7036
|
cfg,
|
|
7036
|
-
located.state.taskLeaseTtlSeconds ?? 120
|
|
7037
|
+
located.state.taskLeaseTtlSeconds ?? 120,
|
|
7038
|
+
{ onAuthExpired: authExit.onAuthExpired }
|
|
7037
7039
|
);
|
|
7038
7040
|
const drain = createClaimDrain(cfg, instance.id, deliver, {
|
|
7039
7041
|
onClaimed: leaseHeartbeats.onClaimed
|
|
@@ -7072,13 +7074,11 @@ function registerChannel(program2) {
|
|
|
7072
7074
|
) + style.dim("streaming matched events to stdout; Ctrl-C to stop.\n")
|
|
7073
7075
|
);
|
|
7074
7076
|
}
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
leaseHeartbeats.stop();
|
|
7081
|
-
}
|
|
7077
|
+
await holdChannelOpen(conn, authExit.released, [
|
|
7078
|
+
stopReconciliation,
|
|
7079
|
+
stopHeartbeat,
|
|
7080
|
+
leaseHeartbeats.stop
|
|
7081
|
+
]);
|
|
7082
7082
|
});
|
|
7083
7083
|
channel.command("mcp").description(
|
|
7084
7084
|
"Run as a Claude Code channel (local-stdio MCP server) \u2014 claim and push dispatched tasks into the session"
|
|
@@ -7101,9 +7101,11 @@ function registerChannel(program2) {
|
|
|
7101
7101
|
params: { content, meta }
|
|
7102
7102
|
});
|
|
7103
7103
|
});
|
|
7104
|
+
const authExit = createAuthExpiredExit();
|
|
7104
7105
|
const leaseHeartbeats = createChannelLeaseHeartbeatManager(
|
|
7105
7106
|
cfg,
|
|
7106
|
-
located.state.taskLeaseTtlSeconds ?? 120
|
|
7107
|
+
located.state.taskLeaseTtlSeconds ?? 120,
|
|
7108
|
+
{ onAuthExpired: authExit.onAuthExpired }
|
|
7107
7109
|
);
|
|
7108
7110
|
const drain = createClaimDrain(cfg, instance.id, deliver, {
|
|
7109
7111
|
onClaimed: leaseHeartbeats.onClaimed
|
|
@@ -7129,13 +7131,16 @@ function registerChannel(program2) {
|
|
|
7129
7131
|
`
|
|
7130
7132
|
)
|
|
7131
7133
|
);
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7134
|
+
await holdChannelOpen(conn, authExit.released, [
|
|
7135
|
+
stopReconciliation,
|
|
7136
|
+
stopHeartbeat,
|
|
7137
|
+
leaseHeartbeats.stop,
|
|
7138
|
+
// The stdio transport keeps a stdin `data` listener, which holds the event
|
|
7139
|
+
// loop open for as long as the parent holds the pipe. Without this close, a
|
|
7140
|
+
// `process.exitCode` set on the auth path never becomes an exit and the
|
|
7141
|
+
// parent sees a connected-but-inert channel instead of a dead one.
|
|
7142
|
+
() => mcp.close()
|
|
7143
|
+
]);
|
|
7139
7144
|
});
|
|
7140
7145
|
channel.command("install").description(
|
|
7141
7146
|
"Wire `sechroom channel mcp` into the project .mcp.json as a Claude Code channel MCP server (idempotent)"
|
|
@@ -7239,6 +7244,7 @@ function startExecutorHeartbeat(refresh, intervalMilliseconds, dependencies = {}
|
|
|
7239
7244
|
}
|
|
7240
7245
|
var leaseHeartbeatApi = (cfg, path, init, deps = {}) => createAuthedRequest(cfg, deps)(path, init);
|
|
7241
7246
|
function classifyLeaseHeartbeatFailure(error) {
|
|
7247
|
+
if (error instanceof AuthExpiredError) return "auth";
|
|
7242
7248
|
if (!(error instanceof HttpError)) return "retry";
|
|
7243
7249
|
if (error.status === 408 || error.status === 429) return "retry";
|
|
7244
7250
|
if (error.status < 400 || error.status >= 500) return "retry";
|
|
@@ -7246,6 +7252,10 @@ function classifyLeaseHeartbeatFailure(error) {
|
|
|
7246
7252
|
return "released";
|
|
7247
7253
|
return "terminal";
|
|
7248
7254
|
}
|
|
7255
|
+
function authExpiredLine(error) {
|
|
7256
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
7257
|
+
return `auth expired \u2014 run \`sechroom login\` to re-authenticate: ${detail}`;
|
|
7258
|
+
}
|
|
7249
7259
|
function leaseHeartbeatStoppedLine(leaseId, error) {
|
|
7250
7260
|
if (!(error instanceof HttpError))
|
|
7251
7261
|
return `lease heartbeat stopped for ${leaseId}: ${String(error)}`;
|
|
@@ -7288,6 +7298,10 @@ function startChannelTaskLeaseHeartbeat(cfg, claim, intervalMilliseconds = 3e4,
|
|
|
7288
7298
|
if (disposition === "retry") throw error;
|
|
7289
7299
|
stop?.();
|
|
7290
7300
|
dependencies.onLeaseTerminal?.(leaseId, error);
|
|
7301
|
+
if (disposition === "auth") {
|
|
7302
|
+
dependencies.onAuthExpired?.(error);
|
|
7303
|
+
return void 0;
|
|
7304
|
+
}
|
|
7291
7305
|
if (disposition === "terminal")
|
|
7292
7306
|
onError(leaseHeartbeatStoppedLine(leaseId, error));
|
|
7293
7307
|
return void 0;
|
|
@@ -7301,6 +7315,7 @@ function startChannelTaskLeaseHeartbeat(cfg, claim, intervalMilliseconds = 3e4,
|
|
|
7301
7315
|
}
|
|
7302
7316
|
function createChannelLeaseHeartbeatManager(cfg, taskLeaseTtlSeconds, dependencies = {}) {
|
|
7303
7317
|
const stops = /* @__PURE__ */ new Map();
|
|
7318
|
+
let authExpired = false;
|
|
7304
7319
|
const intervalMilliseconds = Math.max(
|
|
7305
7320
|
1e3,
|
|
7306
7321
|
Math.min(3e4, Math.floor(taskLeaseTtlSeconds * 1e3 / 4))
|
|
@@ -7320,6 +7335,16 @@ function createChannelLeaseHeartbeatManager(cfg, taskLeaseTtlSeconds, dependenci
|
|
|
7320
7335
|
// map does not accumulate stopped leases for the life of the channel.
|
|
7321
7336
|
onLeaseTerminal: (id) => {
|
|
7322
7337
|
stops.delete(id);
|
|
7338
|
+
},
|
|
7339
|
+
// A dead credential fails every lease at once, so N held leases would
|
|
7340
|
+
// otherwise raise this N times in the same tick. Latch it: stop every beat
|
|
7341
|
+
// (not just the one that noticed), then escalate exactly once.
|
|
7342
|
+
onAuthExpired: (error) => {
|
|
7343
|
+
if (authExpired) return;
|
|
7344
|
+
authExpired = true;
|
|
7345
|
+
for (const cancel of stops.values()) cancel();
|
|
7346
|
+
stops.clear();
|
|
7347
|
+
dependencies.onAuthExpired?.(error);
|
|
7323
7348
|
}
|
|
7324
7349
|
}
|
|
7325
7350
|
);
|
|
@@ -7414,15 +7439,50 @@ async function openConnection(cfg, onEvent, executorInstanceId) {
|
|
|
7414
7439
|
await conn.start();
|
|
7415
7440
|
return conn;
|
|
7416
7441
|
}
|
|
7417
|
-
function holdOpen(conn) {
|
|
7442
|
+
function holdOpen(conn, until) {
|
|
7418
7443
|
return new Promise((resolve9) => {
|
|
7419
7444
|
const stop = () => {
|
|
7420
7445
|
void conn.stop().finally(resolve9);
|
|
7421
7446
|
};
|
|
7422
7447
|
process.on("SIGINT", stop);
|
|
7423
7448
|
process.on("SIGTERM", stop);
|
|
7449
|
+
void until?.then(stop);
|
|
7424
7450
|
});
|
|
7425
7451
|
}
|
|
7452
|
+
async function holdChannelOpen(conn, released, teardown) {
|
|
7453
|
+
try {
|
|
7454
|
+
await holdOpen(conn, released);
|
|
7455
|
+
} finally {
|
|
7456
|
+
let firstError;
|
|
7457
|
+
let failed = false;
|
|
7458
|
+
for (const step of teardown) {
|
|
7459
|
+
try {
|
|
7460
|
+
await step();
|
|
7461
|
+
} catch (error) {
|
|
7462
|
+
if (!failed) {
|
|
7463
|
+
failed = true;
|
|
7464
|
+
firstError = error;
|
|
7465
|
+
}
|
|
7466
|
+
}
|
|
7467
|
+
}
|
|
7468
|
+
if (failed) throw firstError;
|
|
7469
|
+
}
|
|
7470
|
+
}
|
|
7471
|
+
function createAuthExpiredExit() {
|
|
7472
|
+
let release;
|
|
7473
|
+
const released = new Promise((resolve9) => {
|
|
7474
|
+
release = resolve9;
|
|
7475
|
+
});
|
|
7476
|
+
return {
|
|
7477
|
+
onAuthExpired: (error) => {
|
|
7478
|
+
process.stderr.write(err(`${authExpiredLine(error)}
|
|
7479
|
+
`));
|
|
7480
|
+
process.exitCode = 1;
|
|
7481
|
+
release();
|
|
7482
|
+
},
|
|
7483
|
+
released
|
|
7484
|
+
};
|
|
7485
|
+
}
|
|
7426
7486
|
function parseEvent(payload) {
|
|
7427
7487
|
let data = payload;
|
|
7428
7488
|
if (typeof payload === "string") {
|
|
@@ -7477,92 +7537,6 @@ function str(v) {
|
|
|
7477
7537
|
return typeof v === "string" ? v : v == null ? "" : String(v);
|
|
7478
7538
|
}
|
|
7479
7539
|
|
|
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
7540
|
// src/commands/hook.ts
|
|
7567
7541
|
import { createHash as createHash4 } from "crypto";
|
|
7568
7542
|
import { existsSync as existsSync12, mkdirSync as mkdirSync14, readFileSync as readFileSync12, statSync as statSync4, writeFileSync as writeFileSync13 } from "fs";
|
|
@@ -8160,6 +8134,17 @@ function resolveLane(flagLane, cwd) {
|
|
|
8160
8134
|
if (!base) return void 0;
|
|
8161
8135
|
return applyWorktreeLaneSuffix(base, start);
|
|
8162
8136
|
}
|
|
8137
|
+
function resolveSourceLane(explicit, cwd) {
|
|
8138
|
+
const trimmed = explicit?.trim();
|
|
8139
|
+
if (trimmed) return trimmed;
|
|
8140
|
+
const lane = resolveLane(void 0, cwd);
|
|
8141
|
+
if (!lane) {
|
|
8142
|
+
throw new Error(
|
|
8143
|
+
"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."
|
|
8144
|
+
);
|
|
8145
|
+
}
|
|
8146
|
+
return lane;
|
|
8147
|
+
}
|
|
8163
8148
|
var INTENT_FILE = join16(".sechroom", "continuity.json");
|
|
8164
8149
|
var LOCAL_DRY_RUN_VALIDATION_WARNING = "LOCAL-ONLY \u2014 NOT SERVER-VALIDATED";
|
|
8165
8150
|
function resolveIntentPath(start) {
|
|
@@ -8196,6 +8181,56 @@ function localDryRunMissingFields(i) {
|
|
|
8196
8181
|
];
|
|
8197
8182
|
return required.filter(([key]) => !String(i[key] ?? "").trim()).map(([, flag]) => flag);
|
|
8198
8183
|
}
|
|
8184
|
+
var SNAPSHOT_LIMITS = {
|
|
8185
|
+
/** objective / state / lastAction / nextAction / resumeInstruction. */
|
|
8186
|
+
bodyFieldLength: 2e3,
|
|
8187
|
+
/** Any single entry in constraints / questions / surfaceMarkers / artifacts. */
|
|
8188
|
+
listItemLength: 500,
|
|
8189
|
+
/** Entries per list. */
|
|
8190
|
+
listLength: 25
|
|
8191
|
+
};
|
|
8192
|
+
function snapshotLimitViolations(i) {
|
|
8193
|
+
const violations = [];
|
|
8194
|
+
const bodyFields = [
|
|
8195
|
+
"objective",
|
|
8196
|
+
"state",
|
|
8197
|
+
"lastAction",
|
|
8198
|
+
"nextAction",
|
|
8199
|
+
"resumeInstruction"
|
|
8200
|
+
];
|
|
8201
|
+
for (const key of bodyFields) {
|
|
8202
|
+
const value = i[key];
|
|
8203
|
+
if (typeof value !== "string") continue;
|
|
8204
|
+
if (value.length > SNAPSHOT_LIMITS.bodyFieldLength) {
|
|
8205
|
+
violations.push(
|
|
8206
|
+
`${key}: ${value.length} characters exceeds the ${SNAPSHOT_LIMITS.bodyFieldLength}-character limit`
|
|
8207
|
+
);
|
|
8208
|
+
}
|
|
8209
|
+
}
|
|
8210
|
+
const lists = [
|
|
8211
|
+
"constraints",
|
|
8212
|
+
"questions",
|
|
8213
|
+
"surfaceMarkers",
|
|
8214
|
+
"artifacts"
|
|
8215
|
+
];
|
|
8216
|
+
for (const key of lists) {
|
|
8217
|
+
const items = i[key];
|
|
8218
|
+
if (!Array.isArray(items)) continue;
|
|
8219
|
+
if (items.length > SNAPSHOT_LIMITS.listLength) {
|
|
8220
|
+
violations.push(
|
|
8221
|
+
`${key}: ${items.length} entries exceeds the ${SNAPSHOT_LIMITS.listLength}-entry limit`
|
|
8222
|
+
);
|
|
8223
|
+
}
|
|
8224
|
+
items.forEach((item, index) => {
|
|
8225
|
+
if (typeof item === "string" && item.length > SNAPSHOT_LIMITS.listItemLength) {
|
|
8226
|
+
violations.push(
|
|
8227
|
+
`${key}[${index}]: ${item.length} characters exceeds the ${SNAPSHOT_LIMITS.listItemLength}-character limit`
|
|
8228
|
+
);
|
|
8229
|
+
}
|
|
8230
|
+
});
|
|
8231
|
+
}
|
|
8232
|
+
return violations;
|
|
8233
|
+
}
|
|
8199
8234
|
async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScope, opts) {
|
|
8200
8235
|
const lane = resolveLane(laneFlag, cwd);
|
|
8201
8236
|
if (!lane) return false;
|
|
@@ -8510,7 +8545,96 @@ Fail-soft: failures exit 0 and never block; session-context refresh failures ren
|
|
|
8510
8545
|
});
|
|
8511
8546
|
}
|
|
8512
8547
|
|
|
8548
|
+
// src/commands/chat.ts
|
|
8549
|
+
function registerChat(program2) {
|
|
8550
|
+
const chat = program2.command("chat").description("Send and read Slack / Discord channel messages").option("--surface <surface>", "slack | discord", "slack");
|
|
8551
|
+
chat.addHelpText(
|
|
8552
|
+
"after",
|
|
8553
|
+
`
|
|
8554
|
+
Examples:
|
|
8555
|
+
$ sechroom chat send C0123456789 "deploy is green" --surface slack
|
|
8556
|
+
$ sechroom chat send 987654321098765432 "deploy is green" --surface discord --guild 123456789012345678
|
|
8557
|
+
$ sechroom chat send C0123456789 "lgtm" --surface slack --as user --parent 1718049600.123456
|
|
8558
|
+
$ sechroom chat messages --surface slack
|
|
8559
|
+
$ sechroom chat replies 1718049600.123456 --surface slack
|
|
8560
|
+
$ sechroom chat stop-tracking 1718049600.123456 --surface slack`
|
|
8561
|
+
);
|
|
8562
|
+
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) => {
|
|
8563
|
+
const source = resolveSourceLane(opts.source);
|
|
8564
|
+
const { surface, ...globals } = cmd.optsWithGlobals();
|
|
8565
|
+
const json = Boolean(cmd.optsWithGlobals().json);
|
|
8566
|
+
const cfg = resolveConfig(globals);
|
|
8567
|
+
const data = await runApi("Sending message", async () => {
|
|
8568
|
+
const client = await makeClient(cfg);
|
|
8569
|
+
return client.POST("/chat/channel-messages/{surface}", {
|
|
8570
|
+
params: { path: { surface: String(surface) } },
|
|
8571
|
+
body: {
|
|
8572
|
+
channelId,
|
|
8573
|
+
text: text2,
|
|
8574
|
+
guildId: opts.guild ?? null,
|
|
8575
|
+
attachedMemoryId: opts.memory ?? null,
|
|
8576
|
+
trackReplies: opts.track,
|
|
8577
|
+
parentMessage: opts.parent ?? null,
|
|
8578
|
+
source,
|
|
8579
|
+
as: opts.as
|
|
8580
|
+
}
|
|
8581
|
+
});
|
|
8582
|
+
});
|
|
8583
|
+
if (!data.ok) {
|
|
8584
|
+
if (json) {
|
|
8585
|
+
emit(data, true);
|
|
8586
|
+
} else {
|
|
8587
|
+
process.stderr.write(
|
|
8588
|
+
`${err("\u2717")} send failed: ${data.upstreamError ?? "error"}${data.errorDescription ? ` \u2014 ${data.errorDescription}` : ""}
|
|
8589
|
+
`
|
|
8590
|
+
);
|
|
8591
|
+
}
|
|
8592
|
+
process.exit(1);
|
|
8593
|
+
}
|
|
8594
|
+
const idPart = data.persistedId ? ` ${style.dim(`(${data.persistedId})`)}` : "";
|
|
8595
|
+
emitAction(`sent to ${surface} ${style.bold(channelId)}${idPart}`, data, json);
|
|
8596
|
+
});
|
|
8597
|
+
chat.command("messages").description("List recent channel messages (GET /chat/channel-messages/{surface})").action(async (_opts, cmd) => {
|
|
8598
|
+
const { surface, ...globals } = cmd.optsWithGlobals();
|
|
8599
|
+
const cfg = resolveConfig(globals);
|
|
8600
|
+
const data = await runApi("Fetching messages", async () => {
|
|
8601
|
+
const client = await makeClient(cfg);
|
|
8602
|
+
return client.GET("/chat/channel-messages/{surface}", {
|
|
8603
|
+
params: { path: { surface: String(surface) } }
|
|
8604
|
+
});
|
|
8605
|
+
});
|
|
8606
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
8607
|
+
});
|
|
8608
|
+
chat.command("replies <messageId>").description("List thread replies for a message (GET /chat/channel-messages/by-id/{id}/replies)").action(async (messageId, _opts, cmd) => {
|
|
8609
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
8610
|
+
const data = await runApi("Fetching replies", async () => {
|
|
8611
|
+
const client = await makeClient(cfg);
|
|
8612
|
+
return client.GET("/chat/channel-messages/by-id/{id}/replies", {
|
|
8613
|
+
params: { path: { id: messageId } }
|
|
8614
|
+
});
|
|
8615
|
+
});
|
|
8616
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
8617
|
+
});
|
|
8618
|
+
chat.command("stop-tracking <messageId>").description("Stop watching a message for replies (POST .../by-id/{id}/stop-tracking-replies)").action(async (messageId, _opts, cmd) => {
|
|
8619
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
8620
|
+
const data = await runApi("Stopping reply tracking", async () => {
|
|
8621
|
+
const client = await makeClient(cfg);
|
|
8622
|
+
return client.POST("/chat/channel-messages/by-id/{id}/stop-tracking-replies", {
|
|
8623
|
+
params: { path: { id: messageId } },
|
|
8624
|
+
body: {}
|
|
8625
|
+
});
|
|
8626
|
+
});
|
|
8627
|
+
emitAction(`stopped tracking replies on ${style.bold(messageId)}`, data, cmd.optsWithGlobals().json);
|
|
8628
|
+
});
|
|
8629
|
+
}
|
|
8630
|
+
|
|
8513
8631
|
// src/commands/checkpoint.ts
|
|
8632
|
+
import { mkdirSync as mkdirSync15, writeFileSync as writeFileSync14 } from "fs";
|
|
8633
|
+
import { dirname as dirname14, join as join17 } from "path";
|
|
8634
|
+
var CHECKPOINT_UNCHANGED_EXIT_CODE = 3;
|
|
8635
|
+
function checkpointUnchangedLine(snapshotId) {
|
|
8636
|
+
return `${style.bold("=")} unchanged ${style.dim(`(${snapshotId} already current)`)} \u2014 no new snapshot; the local file was left as-is`;
|
|
8637
|
+
}
|
|
8514
8638
|
function registerCheckpoint(program2) {
|
|
8515
8639
|
program2.command("checkpoint").description(
|
|
8516
8640
|
"Checkpoint working state: create a continuity snapshot (server-validated) AND sync ./.sechroom/continuity.json in one step"
|
|
@@ -8558,6 +8682,13 @@ Examples:
|
|
|
8558
8682
|
"no lane resolved \u2014 pass --lane, set SECHROOM_LANE, or pin one in ./.sechroom/lane.json (code-lane). See `sechroom lane`."
|
|
8559
8683
|
);
|
|
8560
8684
|
}
|
|
8685
|
+
const overLimit = snapshotLimitViolations(merged);
|
|
8686
|
+
if (overLimit.length > 0) {
|
|
8687
|
+
fail(
|
|
8688
|
+
`snapshot exceeds the field limits \u2014 trim and retry:
|
|
8689
|
+
${overLimit.join("\n ")}`
|
|
8690
|
+
);
|
|
8691
|
+
}
|
|
8561
8692
|
const scope = merged.scope ?? "session";
|
|
8562
8693
|
const body = {
|
|
8563
8694
|
laneId: lane,
|
|
@@ -8604,6 +8735,16 @@ Examples:
|
|
|
8604
8735
|
const client = await makeClient(cfg);
|
|
8605
8736
|
return client.POST("/continuity/snapshots", { body });
|
|
8606
8737
|
});
|
|
8738
|
+
const previousSnapshotId = base.lastSnapshotId;
|
|
8739
|
+
if (previousSnapshotId && data.snapshotId === previousSnapshotId) {
|
|
8740
|
+
process.exitCode = CHECKPOINT_UNCHANGED_EXIT_CODE;
|
|
8741
|
+
if (json) {
|
|
8742
|
+
emit({ snapshotId: data.snapshotId, lane, scope, unchanged: true, file: null }, true);
|
|
8743
|
+
return;
|
|
8744
|
+
}
|
|
8745
|
+
process.stdout.write(checkpointUnchangedLine(data.snapshotId) + "\n");
|
|
8746
|
+
return;
|
|
8747
|
+
}
|
|
8607
8748
|
const path = resolveIntentPath(cwd) ?? join17(cwd, INTENT_FILE);
|
|
8608
8749
|
const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
|
|
8609
8750
|
mkdirSync15(dirname14(path), { recursive: true });
|
|
@@ -8641,7 +8782,7 @@ function registerClose(program2) {
|
|
|
8641
8782
|
).option(
|
|
8642
8783
|
"--to-version <n>",
|
|
8643
8784
|
"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
|
|
8785
|
+
).option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").addHelpText(
|
|
8645
8786
|
"after",
|
|
8646
8787
|
`
|
|
8647
8788
|
Examples:
|
|
@@ -8653,6 +8794,7 @@ Examples:
|
|
|
8653
8794
|
$ sechroom close --task mem_XXXX --verdict pass --workspace wsp_ZZZZ \\
|
|
8654
8795
|
--title "done" --file ./closeout.md`
|
|
8655
8796
|
).action(async (opts, cmd) => {
|
|
8797
|
+
const source = resolveSourceLane(opts.source);
|
|
8656
8798
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
8657
8799
|
const json = cmd.optsWithGlobals().json;
|
|
8658
8800
|
const verdict = String(opts.verdict);
|
|
@@ -8712,7 +8854,7 @@ Examples:
|
|
|
8712
8854
|
type: "reference",
|
|
8713
8855
|
content: "{}",
|
|
8714
8856
|
confidence: 1,
|
|
8715
|
-
source
|
|
8857
|
+
source,
|
|
8716
8858
|
archetype: "Document",
|
|
8717
8859
|
title: opts.title,
|
|
8718
8860
|
tags,
|
|
@@ -8769,7 +8911,7 @@ Examples:
|
|
|
8769
8911
|
params: { path: { memoryId: opts.task } },
|
|
8770
8912
|
body: {
|
|
8771
8913
|
memoryId: opts.task,
|
|
8772
|
-
source
|
|
8914
|
+
source,
|
|
8773
8915
|
tags: nextTags
|
|
8774
8916
|
}
|
|
8775
8917
|
})
|
|
@@ -10915,7 +11057,8 @@ Examples:
|
|
|
10915
11057
|
"--owner-type <ownerType>",
|
|
10916
11058
|
"Workspace | Project | Unfiled",
|
|
10917
11059
|
"Unfiled"
|
|
10918
|
-
).option("--owner-id <ownerId>", "Owner id (required for Workspace/Project)").option("--source <source>", "Source / lane stamp
|
|
11060
|
+
).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) => {
|
|
11061
|
+
const source = resolveSourceLane(opts.source);
|
|
10919
11062
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
10920
11063
|
const { text: text2, defaultTitle } = resolveCreateBody(opts.text, opts.file);
|
|
10921
11064
|
const title = opts.title ?? defaultTitle;
|
|
@@ -10928,7 +11071,7 @@ Examples:
|
|
|
10928
11071
|
type: opts.type,
|
|
10929
11072
|
content: "{}",
|
|
10930
11073
|
confidence: Number(opts.confidence),
|
|
10931
|
-
source
|
|
11074
|
+
source,
|
|
10932
11075
|
archetype: "Document",
|
|
10933
11076
|
title: title ?? null,
|
|
10934
11077
|
tags: opts.tag ?? null,
|
|
@@ -10957,7 +11100,8 @@ Examples:
|
|
|
10957
11100
|
"--create-workspace",
|
|
10958
11101
|
"Create the --workspace when its name matches nothing",
|
|
10959
11102
|
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
|
|
11103
|
+
).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) => {
|
|
11104
|
+
const source = resolveSourceLane(opts.source);
|
|
10961
11105
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
10962
11106
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
10963
11107
|
const dryRun = Boolean(opts.dryRun);
|
|
@@ -11070,7 +11214,7 @@ ${plan.rows.map(
|
|
|
11070
11214
|
workspaceId: workspace.id,
|
|
11071
11215
|
type: opts.type,
|
|
11072
11216
|
tags: opts.tag ?? null,
|
|
11073
|
-
source
|
|
11217
|
+
source,
|
|
11074
11218
|
confidence: Number(opts.confidence)
|
|
11075
11219
|
},
|
|
11076
11220
|
ports,
|
|
@@ -11192,7 +11336,8 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11192
11336
|
"--replace-all",
|
|
11193
11337
|
"Replace every occurrence (default: first only)",
|
|
11194
11338
|
false
|
|
11195
|
-
).option("--regenerate-filing", "Re-run filing after the edit", false).option("--source <source>", "Source / lane stamp
|
|
11339
|
+
).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) => {
|
|
11340
|
+
const source = resolveSourceLane(opts.source);
|
|
11196
11341
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
11197
11342
|
const data = await runApi("Editing memory text", async () => {
|
|
11198
11343
|
const client = await makeClient(cfg);
|
|
@@ -11204,7 +11349,7 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11204
11349
|
newText: opts.new,
|
|
11205
11350
|
replaceAll: Boolean(opts.replaceAll),
|
|
11206
11351
|
regenerateFiling: Boolean(opts.regenerateFiling),
|
|
11207
|
-
source
|
|
11352
|
+
source
|
|
11208
11353
|
}
|
|
11209
11354
|
});
|
|
11210
11355
|
});
|
|
@@ -11216,7 +11361,8 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11216
11361
|
});
|
|
11217
11362
|
memory.command("edit-text-batch <memoryId>").description(
|
|
11218
11363
|
"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
|
|
11364
|
+
).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) => {
|
|
11365
|
+
const source = resolveSourceLane(opts.source);
|
|
11220
11366
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
11221
11367
|
const replaceAll = Boolean(opts.replaceAll);
|
|
11222
11368
|
const edits = opts.edit.map((spec) => {
|
|
@@ -11242,7 +11388,7 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11242
11388
|
memoryId,
|
|
11243
11389
|
edits,
|
|
11244
11390
|
regenerateFiling: Boolean(opts.regenerateFiling),
|
|
11245
|
-
source
|
|
11391
|
+
source
|
|
11246
11392
|
}
|
|
11247
11393
|
});
|
|
11248
11394
|
});
|
|
@@ -11267,7 +11413,8 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11267
11413
|
"--bump-version",
|
|
11268
11414
|
"Bump the version chain (use for a content reinterpretation, e.g. a type promotion)",
|
|
11269
11415
|
false
|
|
11270
|
-
).option("--source <source>", "Contributing lane stamp (attribution)"
|
|
11416
|
+
).option("--source <source>", "Contributing lane stamp (attribution; default: this checkout's pinned code-lane)").action(async (memoryId, opts, cmd) => {
|
|
11417
|
+
const source = resolveSourceLane(opts.source);
|
|
11271
11418
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
11272
11419
|
const json = cmd.optsWithGlobals().json;
|
|
11273
11420
|
const hasTagOps = Boolean(opts.tag || opts.addTag || opts.removeTag);
|
|
@@ -11299,7 +11446,7 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11299
11446
|
}
|
|
11300
11447
|
const body = {
|
|
11301
11448
|
memoryId,
|
|
11302
|
-
source
|
|
11449
|
+
source,
|
|
11303
11450
|
bumpVersion: Boolean(opts.bumpVersion)
|
|
11304
11451
|
};
|
|
11305
11452
|
if (opts.title !== void 0) body.title = opts.title;
|
|
@@ -11323,13 +11470,14 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11323
11470
|
json
|
|
11324
11471
|
);
|
|
11325
11472
|
});
|
|
11326
|
-
memory.command("archive <memoryId>").description("Archive a memory (POST /memories/{memoryId}/archive)").option("--source <source>", "Source / lane stamp
|
|
11473
|
+
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) => {
|
|
11474
|
+
const source = resolveSourceLane(opts.source);
|
|
11327
11475
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
11328
11476
|
const data = await runApi("Archiving memory", async () => {
|
|
11329
11477
|
const client = await makeClient(cfg);
|
|
11330
11478
|
return client.POST("/memories/{memoryId}/archive", {
|
|
11331
11479
|
params: { path: { memoryId } },
|
|
11332
|
-
body: { source
|
|
11480
|
+
body: { source }
|
|
11333
11481
|
});
|
|
11334
11482
|
});
|
|
11335
11483
|
emitAction(
|
|
@@ -11340,13 +11488,14 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11340
11488
|
});
|
|
11341
11489
|
memory.command("restore <memoryId>").description(
|
|
11342
11490
|
"Restore an archived memory (POST /memories/{memoryId}/restore)"
|
|
11343
|
-
).option("--source <source>", "Source / lane stamp
|
|
11491
|
+
).option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").action(async (memoryId, opts, cmd) => {
|
|
11492
|
+
const source = resolveSourceLane(opts.source);
|
|
11344
11493
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
11345
11494
|
const data = await runApi("Restoring memory", async () => {
|
|
11346
11495
|
const client = await makeClient(cfg);
|
|
11347
11496
|
return client.POST("/memories/{memoryId}/restore", {
|
|
11348
11497
|
params: { path: { memoryId } },
|
|
11349
|
-
body: { source
|
|
11498
|
+
body: { source }
|
|
11350
11499
|
});
|
|
11351
11500
|
});
|
|
11352
11501
|
emitAction(
|
|
@@ -11360,7 +11509,8 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11360
11509
|
).requiredOption(
|
|
11361
11510
|
"--owner-type <ownerType>",
|
|
11362
11511
|
"Unfiled | Workspace | Project | Candidate"
|
|
11363
|
-
).option("--owner-id <ownerId>", "Owner id (required unless Unfiled)").option("--source <source>", "Source / lane stamp
|
|
11512
|
+
).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) => {
|
|
11513
|
+
const source = resolveSourceLane(opts.source);
|
|
11364
11514
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
11365
11515
|
const data = await runApi("Moving memory", async () => {
|
|
11366
11516
|
const client = await makeClient(cfg);
|
|
@@ -11371,7 +11521,7 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11371
11521
|
type: opts.ownerType,
|
|
11372
11522
|
id: String(opts.ownerId ?? "")
|
|
11373
11523
|
},
|
|
11374
|
-
source
|
|
11524
|
+
source
|
|
11375
11525
|
}
|
|
11376
11526
|
});
|
|
11377
11527
|
});
|
|
@@ -11423,7 +11573,8 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11423
11573
|
).requiredOption(
|
|
11424
11574
|
"--content <content>",
|
|
11425
11575
|
"Reverted content JSON (the target version's content)"
|
|
11426
|
-
).option("--source <source>", "Source / lane stamp
|
|
11576
|
+
).option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").action(async (memoryId, opts, cmd) => {
|
|
11577
|
+
const source = resolveSourceLane(opts.source);
|
|
11427
11578
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
11428
11579
|
const data = await runApi("Reverting memory", async () => {
|
|
11429
11580
|
const client = await makeClient(cfg);
|
|
@@ -11433,7 +11584,7 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11433
11584
|
fromVersion: Number(opts.fromVersion),
|
|
11434
11585
|
revertedContent: opts.content,
|
|
11435
11586
|
revertedText: opts.text,
|
|
11436
|
-
source
|
|
11587
|
+
source
|
|
11437
11588
|
}
|
|
11438
11589
|
});
|
|
11439
11590
|
});
|
|
@@ -12473,7 +12624,8 @@ version, the shared template stays clean, and you can discard back anytime.
|
|
|
12473
12624
|
make = await promptYesNo("Make a personal copy to customise?");
|
|
12474
12625
|
}
|
|
12475
12626
|
if (make) {
|
|
12476
|
-
|
|
12627
|
+
const source = resolveSourceLane(void 0);
|
|
12628
|
+
await createOverride(cfg, resolved, personalWorkspaceId, source);
|
|
12477
12629
|
process.stderr.write(
|
|
12478
12630
|
`\u2713 personal copy created for ${instr.surfaceKey} \u2014 edit it on the Agent setup page or via the API.
|
|
12479
12631
|
`
|
|
@@ -12837,7 +12989,10 @@ function registerSetup(program2, deps = {}) {
|
|
|
12837
12989
|
).option(
|
|
12838
12990
|
"--body <markdown>",
|
|
12839
12991
|
"section body (default: a TODO scaffold to edit later)"
|
|
12840
|
-
).option("--no-regen", "skip the agent-files regen after authoring").option(
|
|
12992
|
+
).option("--no-regen", "skip the agent-files regen after authoring").option(
|
|
12993
|
+
"--source <source>",
|
|
12994
|
+
"Source / lane stamp (default: this checkout's pinned code-lane)"
|
|
12995
|
+
).option("--dry-run", "print what would be authored; write nothing", false).addHelpText(
|
|
12841
12996
|
"after",
|
|
12842
12997
|
`
|
|
12843
12998
|
The memo carries the two conventions a workspace-conventions section needs (FR-sechroom-236):
|
|
@@ -12850,6 +13005,7 @@ Examples:
|
|
|
12850
13005
|
$ sechroom setup new-convention "Backend testing" --kind standard --body "- run dotnet test ..."
|
|
12851
13006
|
$ sechroom setup new-convention "Draft section" --no-regen author only, regen later`
|
|
12852
13007
|
).action(async (titleParts, opts, cmd) => {
|
|
13008
|
+
const source = resolveSourceLane(opts.source);
|
|
12853
13009
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
12854
13010
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
12855
13011
|
const title = titleParts.join(" ").trim();
|
|
@@ -12885,7 +13041,7 @@ Examples:
|
|
|
12885
13041
|
type: draft.kind,
|
|
12886
13042
|
content: "{}",
|
|
12887
13043
|
confidence: 1,
|
|
12888
|
-
source
|
|
13044
|
+
source,
|
|
12889
13045
|
archetype: "Document",
|
|
12890
13046
|
title: draft.title,
|
|
12891
13047
|
tags: draft.tags,
|
|
@@ -13463,11 +13619,20 @@ async function ensureTenant(baseUrl, g, opts) {
|
|
|
13463
13619
|
clientId: persisted.clientId
|
|
13464
13620
|
};
|
|
13465
13621
|
}
|
|
13466
|
-
|
|
13467
|
-
if (process.env.SECHROOM_TOKEN) return;
|
|
13622
|
+
function hasUsableCredential() {
|
|
13623
|
+
if (process.env.SECHROOM_TOKEN) return true;
|
|
13468
13624
|
const cached = readToken();
|
|
13469
|
-
|
|
13470
|
-
|
|
13625
|
+
return Boolean(cached?.accessToken) && (cached.expiresAt === void 0 || Date.now() < cached.expiresAt - 6e4 || Boolean(cached.refreshToken));
|
|
13626
|
+
}
|
|
13627
|
+
function requireCredentialForPreview(preview, dryRun) {
|
|
13628
|
+
if (!preview || hasUsableCredential()) return;
|
|
13629
|
+
const flag = dryRun ? "--dry-run" : "--check";
|
|
13630
|
+
fail(
|
|
13631
|
+
`sechroom onboard ${flag} needs an existing sign-in; run \`sechroom login\` first, then re-run the preview.`
|
|
13632
|
+
);
|
|
13633
|
+
}
|
|
13634
|
+
async function ensureAuth(cfg, yes) {
|
|
13635
|
+
if (hasUsableCredential()) return;
|
|
13471
13636
|
if (!canPrompt() || yes) {
|
|
13472
13637
|
fail(
|
|
13473
13638
|
"Not signed in. Run `sechroom login` first, or set SECHROOM_TOKEN for headless use."
|
|
@@ -13806,6 +13971,7 @@ Examples:
|
|
|
13806
13971
|
if (opts.designLane) process.env.SECHROOM_DESIGN_LANE = opts.designLane;
|
|
13807
13972
|
if (opts.recurse) {
|
|
13808
13973
|
const baseUrl2 = resolveBaseUrl(g);
|
|
13974
|
+
requireCredentialForPreview(dryRun || check, dryRun);
|
|
13809
13975
|
await ensureAuth(
|
|
13810
13976
|
{
|
|
13811
13977
|
baseUrl: baseUrl2,
|
|
@@ -13830,6 +13996,7 @@ Examples:
|
|
|
13830
13996
|
return;
|
|
13831
13997
|
}
|
|
13832
13998
|
const baseUrl = resolveBaseUrl(g);
|
|
13999
|
+
requireCredentialForPreview(dryRun || check, dryRun);
|
|
13833
14000
|
await ensureAuth(
|
|
13834
14001
|
{
|
|
13835
14002
|
baseUrl,
|
|
@@ -13846,7 +14013,12 @@ Examples:
|
|
|
13846
14013
|
local: Boolean(opts.local) || scope === "project",
|
|
13847
14014
|
here: scope === "project" ? true : Boolean(opts.here),
|
|
13848
14015
|
workspace: opts.workspace,
|
|
13849
|
-
|
|
14016
|
+
// Both previews are read-only. `--dry-run` promises to "walk through without
|
|
14017
|
+
// writing files or changing the profile", but only `--check` was in this
|
|
14018
|
+
// guard, so a dry run could still create or rewrite `.sechroom.json` /
|
|
14019
|
+
// `~/.config/sechroom/config.json` before the operator approved anything
|
|
14020
|
+
// (FR-sechroom-710). Matches how `ensureTimezone` is already gated below.
|
|
14021
|
+
persist: !(dryRun || check)
|
|
13850
14022
|
});
|
|
13851
14023
|
const tz = await ensureTimezone(cfg, { yes, dryRun: dryRun || check });
|
|
13852
14024
|
if (!json && tz.action !== "already-set") {
|
|
@@ -14877,14 +15049,15 @@ Examples:
|
|
|
14877
15049
|
$ sechroom worklog append --text "shipped CLI help + onboarding scope; PR #1430"
|
|
14878
15050
|
$ sechroom worklog append --text "smoke passed" --source claude-code-chris --title "CLI smoke"`
|
|
14879
15051
|
);
|
|
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
|
|
15052
|
+
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) => {
|
|
15053
|
+
const source = resolveSourceLane(opts.source);
|
|
14881
15054
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
14882
15055
|
const data = await runApi("Appending work-log entry", async () => {
|
|
14883
15056
|
const client = await makeClient(cfg);
|
|
14884
15057
|
return client.POST("/operator-surface/work-log/append", {
|
|
14885
15058
|
body: {
|
|
14886
15059
|
bullet: opts.text,
|
|
14887
|
-
laneId:
|
|
15060
|
+
laneId: source,
|
|
14888
15061
|
workspaceId: opts.workspace ?? null,
|
|
14889
15062
|
title: opts.title ?? null
|
|
14890
15063
|
}
|
package/package.json
CHANGED