@sechroom/cli 2026.7.6 → 2026.7.7-rc.56782977
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 +692 -94
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/auth.ts
|
|
@@ -439,6 +439,9 @@ var quiet = false;
|
|
|
439
439
|
function setQuiet(q) {
|
|
440
440
|
quiet = q;
|
|
441
441
|
}
|
|
442
|
+
function isQuiet() {
|
|
443
|
+
return quiet;
|
|
444
|
+
}
|
|
442
445
|
function colorOn() {
|
|
443
446
|
return !quiet && !process.env.NO_COLOR && process.env.FORCE_COLOR !== "0" && Boolean(process.stdout.isTTY);
|
|
444
447
|
}
|
|
@@ -615,6 +618,22 @@ function emitAction(summary, data, json) {
|
|
|
615
618
|
process.stdout.write(`${ok("\u2713")} ${summary}
|
|
616
619
|
`);
|
|
617
620
|
}
|
|
621
|
+
var GOVERNANCE_QUEUED_PROBLEM_TYPE = "https://sechroom.dev/problems/governance-review-queued";
|
|
622
|
+
function isGovernanceQueued(body) {
|
|
623
|
+
return typeof body === "object" && body !== null && "type" in body && body.type === GOVERNANCE_QUEUED_PROBLEM_TYPE;
|
|
624
|
+
}
|
|
625
|
+
function formatQueuedForApproval(body, json) {
|
|
626
|
+
if (json) return JSON.stringify(body) + "\n";
|
|
627
|
+
const b = body ?? {};
|
|
628
|
+
const detail = typeof b.detail === "string" ? b.detail : void 0;
|
|
629
|
+
const requestId = typeof b.requestId === "string" ? b.requestId : void 0;
|
|
630
|
+
const message = detail ?? "This change requires operator approval before it takes effect.";
|
|
631
|
+
let out = `${warn("\u29D7")} Pending approval \u2014 ${message}
|
|
632
|
+
`;
|
|
633
|
+
if (requestId) out += ` ${style.dim(`request: ${requestId}`)}
|
|
634
|
+
`;
|
|
635
|
+
return out;
|
|
636
|
+
}
|
|
618
637
|
async function runApi(label, fn) {
|
|
619
638
|
const s = spinner(label);
|
|
620
639
|
let res;
|
|
@@ -624,6 +643,12 @@ async function runApi(label, fn) {
|
|
|
624
643
|
s.fail();
|
|
625
644
|
fail(err2);
|
|
626
645
|
}
|
|
646
|
+
const queuedBody = res.data ?? res.error;
|
|
647
|
+
if (res.response?.status === 202 && isGovernanceQueued(queuedBody)) {
|
|
648
|
+
s.stop();
|
|
649
|
+
process.stdout.write(formatQueuedForApproval(queuedBody, isQuiet()));
|
|
650
|
+
process.exit(0);
|
|
651
|
+
}
|
|
627
652
|
const httpFailed = res.response !== void 0 && !res.response.ok;
|
|
628
653
|
if (res.error !== void 0 && res.error !== null || httpFailed) {
|
|
629
654
|
s.fail();
|
|
@@ -633,7 +658,22 @@ async function runApi(label, fn) {
|
|
|
633
658
|
return res.data;
|
|
634
659
|
}
|
|
635
660
|
function fail(error) {
|
|
636
|
-
|
|
661
|
+
let msg;
|
|
662
|
+
if (typeof error === "object" && error !== null && "title" in error) {
|
|
663
|
+
const problem = error;
|
|
664
|
+
msg = String(problem.title);
|
|
665
|
+
if (problem.errors && typeof problem.errors === "object") {
|
|
666
|
+
const detail = Object.entries(problem.errors).flatMap(
|
|
667
|
+
([field, msgs]) => (Array.isArray(msgs) ? msgs : [msgs]).map((m) => ` ${field}: ${String(m)}`)
|
|
668
|
+
);
|
|
669
|
+
if (detail.length > 0) msg += `
|
|
670
|
+
${detail.join("\n")}`;
|
|
671
|
+
}
|
|
672
|
+
} else if (typeof error === "object" && error !== null) {
|
|
673
|
+
msg = JSON.stringify(error);
|
|
674
|
+
} else {
|
|
675
|
+
msg = String(error);
|
|
676
|
+
}
|
|
637
677
|
process.stderr.write(`error: ${msg}
|
|
638
678
|
`);
|
|
639
679
|
process.exit(1);
|
|
@@ -1061,14 +1101,23 @@ function bodyOf(row) {
|
|
|
1061
1101
|
const m = row?.item ?? row;
|
|
1062
1102
|
return m?.text ?? m?.Text ?? "";
|
|
1063
1103
|
}
|
|
1104
|
+
function idOf(row) {
|
|
1105
|
+
const m = row?.item ?? row;
|
|
1106
|
+
return String(m?.id ?? m?.memoryId ?? "unknown");
|
|
1107
|
+
}
|
|
1064
1108
|
function entriesFromRows(rows, surface, source, roleTag, namePrefix) {
|
|
1065
1109
|
const out = /* @__PURE__ */ new Map();
|
|
1066
1110
|
for (const row of rows ?? []) {
|
|
1067
1111
|
const tags = tagsOf(row);
|
|
1068
1112
|
if (!tags.includes(roleTag)) continue;
|
|
1069
|
-
if (
|
|
1113
|
+
if (!tags.includes(`target:${surface}`)) continue;
|
|
1070
1114
|
const name = tagValue(tags, namePrefix);
|
|
1071
1115
|
if (!name) continue;
|
|
1116
|
+
if (out.has(name)) {
|
|
1117
|
+
throw new Error(
|
|
1118
|
+
`Ambiguous ${source} ${roleTag} '${name}' for target:${surface}: ${idOf(row)} duplicates another eligible component. Resolve the duplicate before installing.`
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1072
1121
|
out.set(name, { name, body: bodyOf(row), source });
|
|
1073
1122
|
}
|
|
1074
1123
|
return out;
|
|
@@ -1096,20 +1145,33 @@ function agentTargetFor(surface) {
|
|
|
1096
1145
|
return AGENT_TARGET[surface] ?? `${surface}-agent`;
|
|
1097
1146
|
}
|
|
1098
1147
|
async function fetchFeedRows(cfg, workspaceId) {
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1148
|
+
const client = await makeClient(cfg);
|
|
1149
|
+
const rows = [];
|
|
1150
|
+
let cursor;
|
|
1151
|
+
do {
|
|
1152
|
+
const { data, error, response } = await client.GET("/workspaces/{workspaceId}/memories/feed", {
|
|
1102
1153
|
params: {
|
|
1103
1154
|
path: { workspaceId },
|
|
1104
1155
|
// cascadeWorkspaces: skills land in an "Operator Skills" SUB-workspace;
|
|
1105
1156
|
// includeText: the feed omits bodies by default, we need them for SKILL.md.
|
|
1106
|
-
query: {
|
|
1157
|
+
query: {
|
|
1158
|
+
limit: 200,
|
|
1159
|
+
cascadeWorkspaces: true,
|
|
1160
|
+
includeText: true,
|
|
1161
|
+
...cursor ? { cursor } : {}
|
|
1162
|
+
}
|
|
1107
1163
|
}
|
|
1108
|
-
})
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1164
|
+
});
|
|
1165
|
+
if (error || !response.ok || !data) {
|
|
1166
|
+
throw new Error(
|
|
1167
|
+
`Could not read operator-skill catalogue from ${workspaceId}: HTTP ${response.status}.`
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
const feed = data;
|
|
1171
|
+
rows.push(...feed.results ?? feed.Results ?? []);
|
|
1172
|
+
cursor = feed.nextCursor ?? feed.NextCursor ?? void 0;
|
|
1173
|
+
} while (cursor);
|
|
1174
|
+
return rows;
|
|
1113
1175
|
}
|
|
1114
1176
|
async function fetchTemplateRows(cfg, personalWorkspaceId) {
|
|
1115
1177
|
const [systemRows, personalRows] = await Promise.all([
|
|
@@ -1129,7 +1191,10 @@ function resolveReferenceSet(rows, surface) {
|
|
|
1129
1191
|
}
|
|
1130
1192
|
|
|
1131
1193
|
// src/setup/materialise.ts
|
|
1132
|
-
var CLIENT_SURFACE =
|
|
1194
|
+
var CLIENT_SURFACE = {
|
|
1195
|
+
claude: "claude-code",
|
|
1196
|
+
codex: "gpt-codex"
|
|
1197
|
+
};
|
|
1133
1198
|
function writeSkills(dir, skills, surface) {
|
|
1134
1199
|
const written = [];
|
|
1135
1200
|
for (const s of skills) {
|
|
@@ -1162,11 +1227,71 @@ function writeReferencesIntoSkillDirs(dir, skills, refs) {
|
|
|
1162
1227
|
}
|
|
1163
1228
|
return refs.map((r) => r.name);
|
|
1164
1229
|
}
|
|
1165
|
-
var SKILL_SPEC = {
|
|
1166
|
-
|
|
1230
|
+
var SKILL_SPEC = {
|
|
1231
|
+
kind: "skill",
|
|
1232
|
+
dir: skillsDir,
|
|
1233
|
+
resolve: resolveSkillSet,
|
|
1234
|
+
write: writeSkills,
|
|
1235
|
+
supportsCodex: true
|
|
1236
|
+
};
|
|
1237
|
+
var AGENT_SPEC = {
|
|
1238
|
+
kind: "agent",
|
|
1239
|
+
dir: agentsDir,
|
|
1240
|
+
resolve: resolveAgentSet,
|
|
1241
|
+
write: writeAgents,
|
|
1242
|
+
supportsCodex: false
|
|
1243
|
+
};
|
|
1167
1244
|
function scopeOf(opts) {
|
|
1168
1245
|
return opts.local ? "project" : resolveScope(opts.scope);
|
|
1169
1246
|
}
|
|
1247
|
+
function parseClient(value) {
|
|
1248
|
+
if (value == null) return void 0;
|
|
1249
|
+
if (value === "claude" || value === "codex" || value === "all") return value;
|
|
1250
|
+
throw new Error(`--client must be 'claude', 'codex', or 'all' (got '${value}')`);
|
|
1251
|
+
}
|
|
1252
|
+
function clientSelection(spec, g, raw) {
|
|
1253
|
+
if (!spec.supportsCodex) return "claude";
|
|
1254
|
+
const explicit = parseClient(raw);
|
|
1255
|
+
if (explicit) return explicit;
|
|
1256
|
+
const claudeConfigured = Boolean(g.claudeConfigDir || process.env.CLAUDE_CONFIG_DIR);
|
|
1257
|
+
const codexConfigured = Boolean(g.codexHome || process.env.CODEX_HOME);
|
|
1258
|
+
if (claudeConfigured || codexConfigured) {
|
|
1259
|
+
if (claudeConfigured && codexConfigured) return "all";
|
|
1260
|
+
return codexConfigured ? "codex" : "claude";
|
|
1261
|
+
}
|
|
1262
|
+
const claudeDetected = resolveClaudeTargets({})[0]?.dir;
|
|
1263
|
+
const codexDetected = resolveCodexHomes({})[0];
|
|
1264
|
+
const hasClaude = Boolean(claudeDetected && existsSync3(claudeDetected));
|
|
1265
|
+
const hasCodex = Boolean(codexDetected && existsSync3(codexDetected));
|
|
1266
|
+
if (hasClaude && hasCodex) return "all";
|
|
1267
|
+
if (hasCodex) return "codex";
|
|
1268
|
+
return "claude";
|
|
1269
|
+
}
|
|
1270
|
+
function targetsFor(spec, g, scope, selection) {
|
|
1271
|
+
const clients = selection === "all" ? ["claude", "codex"] : [selection];
|
|
1272
|
+
if (scope === "project" && clients.includes("codex")) {
|
|
1273
|
+
throw new Error("Codex skills have no supported project scope; use --scope global or --client claude.");
|
|
1274
|
+
}
|
|
1275
|
+
const targets = [];
|
|
1276
|
+
for (const client of clients) {
|
|
1277
|
+
if (client === "claude") {
|
|
1278
|
+
targets.push(...resolveClaudeTargets({ override: g.claudeConfigDir, scope, cwd: process.cwd() }).map((target) => ({
|
|
1279
|
+
client,
|
|
1280
|
+
surface: CLIENT_SURFACE[client],
|
|
1281
|
+
dir: spec.dir(target.dir),
|
|
1282
|
+
label: target.label
|
|
1283
|
+
})));
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
targets.push(...resolveCodexHomes({ override: g.codexHome, scope }).map((home) => ({
|
|
1287
|
+
client,
|
|
1288
|
+
surface: CLIENT_SURFACE[client],
|
|
1289
|
+
dir: spec.dir(home),
|
|
1290
|
+
label: home
|
|
1291
|
+
})));
|
|
1292
|
+
}
|
|
1293
|
+
return targets;
|
|
1294
|
+
}
|
|
1170
1295
|
async function runInstall(spec, cmd, opts) {
|
|
1171
1296
|
const g = cmd.optsWithGlobals();
|
|
1172
1297
|
let scope;
|
|
@@ -1178,31 +1303,48 @@ async function runInstall(spec, cmd, opts) {
|
|
|
1178
1303
|
const cfg = resolveConfig(g);
|
|
1179
1304
|
const dryRun = Boolean(opts.dryRun);
|
|
1180
1305
|
const json = Boolean(g.json || opts.json);
|
|
1181
|
-
|
|
1306
|
+
let selection;
|
|
1307
|
+
let targets;
|
|
1308
|
+
try {
|
|
1309
|
+
selection = clientSelection(spec, g, opts.client);
|
|
1310
|
+
targets = targetsFor(spec, g, scope, selection);
|
|
1311
|
+
} catch (err2) {
|
|
1312
|
+
return fail(err2.message);
|
|
1313
|
+
}
|
|
1182
1314
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
1183
1315
|
const rows = await fetchTemplateRows(cfg, personalWorkspaceId);
|
|
1184
|
-
const items = spec.resolve(rows, CLIENT_SURFACE);
|
|
1185
|
-
const refs = spec.kind === "skill" ? resolveReferenceSet(rows, CLIENT_SURFACE) : [];
|
|
1186
1316
|
const results = targets.map((t) => {
|
|
1187
|
-
const
|
|
1188
|
-
const
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1317
|
+
const items = spec.resolve(rows, t.surface);
|
|
1318
|
+
const refs = spec.kind === "skill" ? resolveReferenceSet(rows, t.surface) : [];
|
|
1319
|
+
const written = dryRun ? items.map((i) => i.name) : spec.write(t.dir, items, t.surface);
|
|
1320
|
+
const refsWritten = dryRun ? refs.map((r) => r.name) : writeReferencesIntoSkillDirs(t.dir, items, refs);
|
|
1321
|
+
return {
|
|
1322
|
+
client: t.client,
|
|
1323
|
+
surface: t.surface,
|
|
1324
|
+
dir: t.dir,
|
|
1325
|
+
label: t.label,
|
|
1326
|
+
available: items.length,
|
|
1327
|
+
references: refs.length,
|
|
1328
|
+
items: items.map(({ name, source }) => ({ name, source })),
|
|
1329
|
+
referenceItems: refs.map(({ name, source }) => ({ name, source })),
|
|
1330
|
+
written,
|
|
1331
|
+
refsWritten
|
|
1332
|
+
};
|
|
1191
1333
|
});
|
|
1192
|
-
if (json) return emit({ kind: spec.kind,
|
|
1193
|
-
if (
|
|
1334
|
+
if (json) return emit({ kind: spec.kind, client: selection, dryRun, targets: results }, true);
|
|
1335
|
+
if (results.every((r) => r.available === 0)) {
|
|
1194
1336
|
console.log(style.dim(`No ${spec.kind}s available to install \u2014 is the bundle installed for your account?`));
|
|
1195
1337
|
return;
|
|
1196
1338
|
}
|
|
1197
1339
|
for (const r of results) {
|
|
1198
1340
|
console.log(
|
|
1199
|
-
`${dryRun ? "" : style.green("\u2713 ")}${dryRun ? "would write" : "wrote"} ${r.written.length} ${spec.kind}(s) ${style.dim("\u2192")} ${r.dir}`
|
|
1341
|
+
`${dryRun ? "" : style.green("\u2713 ")}${dryRun ? "would write" : "wrote"} ${r.written.length} ${spec.kind}(s) for ${r.client} ${style.dim("\u2192")} ${r.dir}`
|
|
1200
1342
|
);
|
|
1201
1343
|
if (r.refsWritten.length)
|
|
1202
1344
|
console.log(
|
|
1203
1345
|
`${dryRun ? "" : style.green("\u2713 ")}${dryRun ? "would write" : "wrote"} ${r.refsWritten.length} reference(s) into each skill ${style.dim("\u2192")} ${r.dir}/<skill>/references`
|
|
1204
1346
|
);
|
|
1205
|
-
if (dryRun) for (const i of items) console.log(` ${i.name} ${style.dim(`[${i.source}]`)}`);
|
|
1347
|
+
if (dryRun) for (const i of r.items) console.log(` ${i.name} ${style.dim(`[${i.source}]`)}`);
|
|
1206
1348
|
}
|
|
1207
1349
|
}
|
|
1208
1350
|
function runList(spec, cmd, opts) {
|
|
@@ -1214,16 +1356,22 @@ function runList(spec, cmd, opts) {
|
|
|
1214
1356
|
return fail(err2.message);
|
|
1215
1357
|
}
|
|
1216
1358
|
const json = Boolean(g.json || opts.json);
|
|
1217
|
-
|
|
1359
|
+
let selection;
|
|
1360
|
+
let targets;
|
|
1361
|
+
try {
|
|
1362
|
+
selection = clientSelection(spec, g, opts.client);
|
|
1363
|
+
targets = targetsFor(spec, g, scope, selection);
|
|
1364
|
+
} catch (err2) {
|
|
1365
|
+
return fail(err2.message);
|
|
1366
|
+
}
|
|
1218
1367
|
const out = targets.map((t) => {
|
|
1219
|
-
const
|
|
1220
|
-
const lock = readSkillsLock(dir);
|
|
1368
|
+
const lock = readSkillsLock(t.dir);
|
|
1221
1369
|
const entries = Object.entries(lock).flatMap(
|
|
1222
|
-
([slug, e]) => (e.skills ?? []).map((name) => ({ slug, name, present: existsSync3(join4(dir, name)) }))
|
|
1370
|
+
([slug, e]) => (e.skills ?? []).map((name) => ({ slug, name, present: existsSync3(join4(t.dir, name)) }))
|
|
1223
1371
|
);
|
|
1224
|
-
return { dir, label: t.label, entries };
|
|
1372
|
+
return { client: t.client, surface: t.surface, dir: t.dir, label: t.label, entries };
|
|
1225
1373
|
});
|
|
1226
|
-
if (json) return emit({ kind: spec.kind, targets: out }, true);
|
|
1374
|
+
if (json) return emit({ kind: spec.kind, client: selection, targets: out }, true);
|
|
1227
1375
|
let any = false;
|
|
1228
1376
|
for (const t of out) {
|
|
1229
1377
|
if (t.entries.length === 0) continue;
|
|
@@ -1246,33 +1394,39 @@ function runClean(spec, cmd, opts, slugArg) {
|
|
|
1246
1394
|
return fail(err2.message);
|
|
1247
1395
|
}
|
|
1248
1396
|
const json = Boolean(g.json || opts.json);
|
|
1249
|
-
|
|
1397
|
+
let selection;
|
|
1398
|
+
let targets;
|
|
1399
|
+
try {
|
|
1400
|
+
selection = clientSelection(spec, g, opts.client);
|
|
1401
|
+
targets = targetsFor(spec, g, scope, selection);
|
|
1402
|
+
} catch (err2) {
|
|
1403
|
+
return fail(err2.message);
|
|
1404
|
+
}
|
|
1250
1405
|
const cleaned = [];
|
|
1251
1406
|
const missing = [];
|
|
1252
1407
|
for (const t of targets) {
|
|
1253
|
-
const
|
|
1254
|
-
const lock = readSkillsLock(dir);
|
|
1408
|
+
const lock = readSkillsLock(t.dir);
|
|
1255
1409
|
const entry = lock[slug];
|
|
1256
1410
|
if (!entry) {
|
|
1257
|
-
missing.push(join4(dir, SKILLS_LOCK));
|
|
1411
|
+
missing.push(join4(t.dir, SKILLS_LOCK));
|
|
1258
1412
|
continue;
|
|
1259
1413
|
}
|
|
1260
1414
|
const removed = [];
|
|
1261
1415
|
for (const name of entry.skills) {
|
|
1262
|
-
const p = join4(dir, name);
|
|
1416
|
+
const p = join4(t.dir, name);
|
|
1263
1417
|
if (existsSync3(p)) {
|
|
1264
1418
|
rmSync2(p, { recursive: true, force: true });
|
|
1265
1419
|
removed.push(name);
|
|
1266
1420
|
}
|
|
1267
1421
|
}
|
|
1268
1422
|
delete lock[slug];
|
|
1269
|
-
writeSkillsLock(dir, lock);
|
|
1270
|
-
cleaned.push({ dir, removed });
|
|
1423
|
+
writeSkillsLock(t.dir, lock);
|
|
1424
|
+
cleaned.push({ client: t.client, surface: t.surface, dir: t.dir, removed });
|
|
1271
1425
|
}
|
|
1272
1426
|
if (cleaned.length === 0) {
|
|
1273
1427
|
return fail(`No materialised ${spec.kind}s recorded for '${slug}' in ${missing.join(", ")}.`);
|
|
1274
1428
|
}
|
|
1275
|
-
if (json) return emit({ kind: spec.kind, slug, cleaned, missing }, true);
|
|
1429
|
+
if (json) return emit({ kind: spec.kind, client: selection, slug, cleaned, missing }, true);
|
|
1276
1430
|
for (const c of cleaned) {
|
|
1277
1431
|
console.log(style.green(`Removed ${c.removed.length} ${spec.kind}(s) for ${slug} from ${c.dir}`));
|
|
1278
1432
|
}
|
|
@@ -1388,15 +1542,24 @@ var CLAUDE_HOOK_COMMANDS = {
|
|
|
1388
1542
|
SessionStart: "sechroom hook session-start",
|
|
1389
1543
|
PreCompact: "sechroom hook pre-compact",
|
|
1390
1544
|
SessionEnd: "sechroom hook session-end",
|
|
1391
|
-
// WLP telemetry tap (D-WLP-10) — per-turn executor self-report.
|
|
1392
|
-
//
|
|
1393
|
-
//
|
|
1394
|
-
|
|
1545
|
+
// WLP telemetry tap (D-WLP-10 + FR-352 Tier 1) — per-turn executor self-report. The one
|
|
1546
|
+
// `telemetry hook` verb dispatches on hook_event_name: Stop/SubagentStop → parsed (token/context) +
|
|
1547
|
+
// terminal (turn end), Notification/PermissionDenied → approval. No-op (exit 0) unless this checkout
|
|
1548
|
+
// is bound via `sechroom telemetry bind`, so it's safe to wire for every Claude install; an event a
|
|
1549
|
+
// given Claude Code version doesn't know is inert (never fires). Claude-only.
|
|
1550
|
+
Stop: "sechroom telemetry hook",
|
|
1551
|
+
SubagentStop: "sechroom telemetry hook",
|
|
1552
|
+
Notification: "sechroom telemetry hook",
|
|
1553
|
+
PermissionDenied: "sechroom telemetry hook"
|
|
1395
1554
|
};
|
|
1396
1555
|
var CODEX_HOOK_COMMANDS = {
|
|
1397
1556
|
SessionStart: "sechroom hook session-start",
|
|
1557
|
+
PreCompact: "sechroom hook pre-compact",
|
|
1398
1558
|
Stop: "sechroom hook session-end --debounce-minutes 10"
|
|
1399
1559
|
};
|
|
1560
|
+
function hookCommandsForSurface(surface) {
|
|
1561
|
+
return surface === "claude" ? CLAUDE_HOOK_COMMANDS : CODEX_HOOK_COMMANDS;
|
|
1562
|
+
}
|
|
1400
1563
|
function hasHookCommand(config2, event, command) {
|
|
1401
1564
|
const groups = config2.hooks?.[event] ?? [];
|
|
1402
1565
|
return groups.some((g) => (g.hooks ?? []).some((h) => h.type === "command" && h.command === command));
|
|
@@ -1544,12 +1707,16 @@ function registerChannel(program2) {
|
|
|
1544
1707
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
1545
1708
|
const filter = readFilter(opts);
|
|
1546
1709
|
const sub = await ensureSubscription(cfg, opts.name, filter);
|
|
1547
|
-
const
|
|
1548
|
-
|
|
1549
|
-
|
|
1710
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1711
|
+
const deliver = makeDeliver(
|
|
1712
|
+
filter,
|
|
1713
|
+
seen,
|
|
1714
|
+
(payload) => process.stdout.write(
|
|
1550
1715
|
(typeof payload === "string" ? payload : JSON.stringify(payload)) + "\n"
|
|
1551
|
-
)
|
|
1552
|
-
|
|
1716
|
+
)
|
|
1717
|
+
);
|
|
1718
|
+
const conn = await openConnection(cfg, deliver);
|
|
1719
|
+
await reconcile(cfg, filter, deliver);
|
|
1553
1720
|
if (json) {
|
|
1554
1721
|
emit(
|
|
1555
1722
|
{
|
|
@@ -1586,8 +1753,8 @@ function registerChannel(program2) {
|
|
|
1586
1753
|
);
|
|
1587
1754
|
await mcp.connect(new StdioServerTransport());
|
|
1588
1755
|
await ensureSubscription(cfg, opts.name, filter);
|
|
1589
|
-
const
|
|
1590
|
-
|
|
1756
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1757
|
+
const deliver = makeDeliver(filter, seen, (payload) => {
|
|
1591
1758
|
const { content, meta } = summarizeEvent(payload);
|
|
1592
1759
|
void mcp.notification({
|
|
1593
1760
|
method: "notifications/claude/channel",
|
|
@@ -1597,6 +1764,8 @@ function registerChannel(program2) {
|
|
|
1597
1764
|
`))
|
|
1598
1765
|
);
|
|
1599
1766
|
});
|
|
1767
|
+
const conn = await openConnection(cfg, deliver);
|
|
1768
|
+
await reconcile(cfg, filter, deliver);
|
|
1600
1769
|
process.stderr.write(
|
|
1601
1770
|
style.dim(
|
|
1602
1771
|
`sechroom channel (mcp) \u2014 tenant ${cfg.tenant}, tags [${filter.tags.join(", ")}]
|
|
@@ -1760,6 +1929,82 @@ function shouldDeliver(payload, filter) {
|
|
|
1760
1929
|
}
|
|
1761
1930
|
return true;
|
|
1762
1931
|
}
|
|
1932
|
+
function makeDeliver(filter, seen, forward) {
|
|
1933
|
+
return (payload) => {
|
|
1934
|
+
if (!shouldDeliver(payload, filter)) return;
|
|
1935
|
+
const { memoryId } = parseEvent(payload);
|
|
1936
|
+
if (memoryId) {
|
|
1937
|
+
if (seen.has(memoryId)) return;
|
|
1938
|
+
seen.add(memoryId);
|
|
1939
|
+
}
|
|
1940
|
+
forward(payload);
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
async function reconcile(cfg, filter, deliver) {
|
|
1944
|
+
if (filter.workspaceScope.length === 0) {
|
|
1945
|
+
process.stderr.write(
|
|
1946
|
+
style.dim(
|
|
1947
|
+
"channel: no --workspace to reconcile against; live feed only (a dropped dispatch won't be recovered).\n"
|
|
1948
|
+
)
|
|
1949
|
+
);
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
if (filter.tags.length === 0) return;
|
|
1953
|
+
let token;
|
|
1954
|
+
try {
|
|
1955
|
+
token = await requireToken(cfg);
|
|
1956
|
+
} catch {
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
const qs = `filterTags=${encodeURIComponent(filter.tags.join(","))}&limit=100`;
|
|
1960
|
+
let recovered = 0;
|
|
1961
|
+
for (const ws of filter.workspaceScope) {
|
|
1962
|
+
try {
|
|
1963
|
+
const resp = await fetch(
|
|
1964
|
+
`${cfg.baseUrl}/workspaces/${encodeURIComponent(ws)}/memories/feed?${qs}`,
|
|
1965
|
+
{
|
|
1966
|
+
headers: {
|
|
1967
|
+
authorization: `Bearer ${token}`,
|
|
1968
|
+
tenant: cfg.tenant,
|
|
1969
|
+
"x-sechroom-surface": "cli"
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
);
|
|
1973
|
+
if (!resp.ok) {
|
|
1974
|
+
process.stderr.write(
|
|
1975
|
+
err(
|
|
1976
|
+
`channel: reconcile query for ${ws} failed (HTTP ${resp.status})
|
|
1977
|
+
`
|
|
1978
|
+
)
|
|
1979
|
+
);
|
|
1980
|
+
continue;
|
|
1981
|
+
}
|
|
1982
|
+
const data = await resp.json();
|
|
1983
|
+
for (const m of data.results ?? []) {
|
|
1984
|
+
if (!m.id) continue;
|
|
1985
|
+
deliver({
|
|
1986
|
+
eventType: "reconcile",
|
|
1987
|
+
memoryId: m.id,
|
|
1988
|
+
workspaceId: ws,
|
|
1989
|
+
tags: m.tags ?? []
|
|
1990
|
+
});
|
|
1991
|
+
recovered++;
|
|
1992
|
+
}
|
|
1993
|
+
} catch (e) {
|
|
1994
|
+
process.stderr.write(
|
|
1995
|
+
err(`channel: reconcile error for ${ws}: ${String(e)}
|
|
1996
|
+
`)
|
|
1997
|
+
);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
if (recovered > 0)
|
|
2001
|
+
process.stderr.write(
|
|
2002
|
+
style.dim(
|
|
2003
|
+
`channel: reconciled ${recovered} already-queued event(s) on connect.
|
|
2004
|
+
`
|
|
2005
|
+
)
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
1763
2008
|
function facetedTagMatch(eventTags, filterTags) {
|
|
1764
2009
|
const have = new Set(eventTags);
|
|
1765
2010
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -2334,6 +2579,12 @@ Fail-soft: no lane / no auth / no-or-partial intent file / API error -> exit 0,
|
|
|
2334
2579
|
const surfaceResults = installHookSurfaces(["codex"], { dryRun, claudeDir: "", codexHome })[0].results;
|
|
2335
2580
|
process.stdout.write(`${HOOK_SURFACE_LABEL.codex}:
|
|
2336
2581
|
`);
|
|
2582
|
+
if (dryRun) {
|
|
2583
|
+
for (const [event, command] of Object.entries(hookCommandsForSurface("codex"))) {
|
|
2584
|
+
process.stdout.write(` ${event}: ${command}
|
|
2585
|
+
`);
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2337
2588
|
for (const r of surfaceResults) {
|
|
2338
2589
|
results.push(r);
|
|
2339
2590
|
process.stdout.write(describe(r, dryRun) + "\n");
|
|
@@ -2451,6 +2702,178 @@ Examples:
|
|
|
2451
2702
|
});
|
|
2452
2703
|
}
|
|
2453
2704
|
|
|
2705
|
+
// src/commands/close.ts
|
|
2706
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
2707
|
+
var VERDICTS = ["pass", "soft-fail", "plan-invalid", "blocked"];
|
|
2708
|
+
function registerClose(program2) {
|
|
2709
|
+
program2.command("close").description(
|
|
2710
|
+
"Close a dispatched WorkTask in one call: closeout memo (verdict + correlation) + Reference edge + source status flip. Managed runs stamp the run's matchTags verbatim (SBC-1180)."
|
|
2711
|
+
).requiredOption("--task <id>", "The WorkTask memory id being closed").requiredOption("--verdict <verdict>", `Verdict: ${VERDICTS.join(" | ")}`).requiredOption(
|
|
2712
|
+
"--workspace <wsp>",
|
|
2713
|
+
"Workspace that owns the closeout memo"
|
|
2714
|
+
).requiredOption("--title <title>", "Closeout title").option(
|
|
2715
|
+
"--file <path>",
|
|
2716
|
+
"Read the closeout body from a file (default: stdin)"
|
|
2717
|
+
).option(
|
|
2718
|
+
"--decomposition <id>",
|
|
2719
|
+
"Managed-run selector \u2014 the sug_ decomposition the task belongs to. Locates the parked run so its matchTags/verdict contract are read from state, not assembled from args."
|
|
2720
|
+
).option(
|
|
2721
|
+
"--no-status-flip",
|
|
2722
|
+
"Skip the status flip on a BARE task. (Managed runs never flip here \u2014 the run engine reflects the verdict onto the task on resume.)"
|
|
2723
|
+
).option(
|
|
2724
|
+
"--to-version <n>",
|
|
2725
|
+
"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)."
|
|
2726
|
+
).option("--source <source>", "Source / lane stamp", "cli").addHelpText(
|
|
2727
|
+
"after",
|
|
2728
|
+
`
|
|
2729
|
+
Examples:
|
|
2730
|
+
# Managed run \u2014 reads matchTags from run state, stamps them verbatim, un-parks the run:
|
|
2731
|
+
$ echo "Shipped X. Tests green." | sechroom close --task mem_XXXX --decomposition sug_YYYY \\
|
|
2732
|
+
--verdict pass --workspace wsp_ZZZZ --title "w4_build closeout"
|
|
2733
|
+
|
|
2734
|
+
# Bare task:
|
|
2735
|
+
$ sechroom close --task mem_XXXX --verdict pass --workspace wsp_ZZZZ \\
|
|
2736
|
+
--title "done" --file ./closeout.md`
|
|
2737
|
+
).action(async (opts, cmd) => {
|
|
2738
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2739
|
+
const json = cmd.optsWithGlobals().json;
|
|
2740
|
+
const verdict = String(opts.verdict);
|
|
2741
|
+
if (!VERDICTS.includes(verdict))
|
|
2742
|
+
fail(
|
|
2743
|
+
`--verdict must be one of ${VERDICTS.join(" | ")} \u2014 got '${opts.verdict}'`
|
|
2744
|
+
);
|
|
2745
|
+
let bodyText;
|
|
2746
|
+
try {
|
|
2747
|
+
bodyText = opts.file ? readFileSync7(opts.file, "utf8") : readFileSync7(0, "utf8");
|
|
2748
|
+
} catch {
|
|
2749
|
+
fail(
|
|
2750
|
+
opts.file ? `could not read --file ${opts.file}` : "no closeout body \u2014 pass --file <path> or pipe it on stdin"
|
|
2751
|
+
);
|
|
2752
|
+
}
|
|
2753
|
+
if (!bodyText.trim())
|
|
2754
|
+
fail(
|
|
2755
|
+
"closeout body is empty \u2014 pass --file <path> or pipe the body on stdin"
|
|
2756
|
+
);
|
|
2757
|
+
const client = await makeClient(cfg);
|
|
2758
|
+
let matchTags;
|
|
2759
|
+
let dispatchedVersion;
|
|
2760
|
+
if (opts.decomposition) {
|
|
2761
|
+
const run = await runApi(
|
|
2762
|
+
"Reading run contract",
|
|
2763
|
+
async () => client.GET("/decompositions/{id}/run", {
|
|
2764
|
+
params: { path: { id: opts.decomposition } }
|
|
2765
|
+
})
|
|
2766
|
+
);
|
|
2767
|
+
const await_ = run.awaitingCompletion;
|
|
2768
|
+
if (!await_ || !Array.isArray(await_.matchTags) || await_.matchTags.length === 0)
|
|
2769
|
+
fail(
|
|
2770
|
+
`decomposition ${opts.decomposition} has no parked run awaiting a completion \u2014 nothing to close (run outcome: ${run.outcome ?? "unknown"}). Idempotent no-op on an already-resumed/terminal run.`
|
|
2771
|
+
);
|
|
2772
|
+
if (run.awaitingTaskId && run.awaitingTaskId !== opts.task)
|
|
2773
|
+
fail(
|
|
2774
|
+
`run ${opts.decomposition} is awaiting task ${run.awaitingTaskId}, not ${opts.task} \u2014 refusing to mis-key the resume.`
|
|
2775
|
+
);
|
|
2776
|
+
matchTags = await_.matchTags;
|
|
2777
|
+
if (typeof await_.dispatchedTaskVersion === "number")
|
|
2778
|
+
dispatchedVersion = await_.dispatchedTaskVersion;
|
|
2779
|
+
} else {
|
|
2780
|
+
matchTags = [`wlp-task:${opts.task}`];
|
|
2781
|
+
}
|
|
2782
|
+
const tags = [
|
|
2783
|
+
.../* @__PURE__ */ new Set([
|
|
2784
|
+
...matchTags,
|
|
2785
|
+
`wlp-task:${opts.task}`,
|
|
2786
|
+
`verdict:${verdict}`
|
|
2787
|
+
])
|
|
2788
|
+
];
|
|
2789
|
+
const closeout = await runApi(
|
|
2790
|
+
"Creating closeout",
|
|
2791
|
+
async () => client.POST("/memories", {
|
|
2792
|
+
body: {
|
|
2793
|
+
text: bodyText,
|
|
2794
|
+
type: "reference",
|
|
2795
|
+
content: "{}",
|
|
2796
|
+
confidence: 1,
|
|
2797
|
+
source: opts.source,
|
|
2798
|
+
archetype: "Document",
|
|
2799
|
+
title: opts.title,
|
|
2800
|
+
tags,
|
|
2801
|
+
owner: { type: "Workspace", id: opts.workspace }
|
|
2802
|
+
}
|
|
2803
|
+
})
|
|
2804
|
+
);
|
|
2805
|
+
let toVersion;
|
|
2806
|
+
if (opts.toVersion !== void 0) {
|
|
2807
|
+
toVersion = Number(opts.toVersion);
|
|
2808
|
+
if (!Number.isInteger(toVersion) || toVersion < 1)
|
|
2809
|
+
fail(
|
|
2810
|
+
`--to-version must be an integer >= 1 \u2014 got '${opts.toVersion}'`
|
|
2811
|
+
);
|
|
2812
|
+
} else if (dispatchedVersion !== void 0) {
|
|
2813
|
+
toVersion = dispatchedVersion;
|
|
2814
|
+
} else {
|
|
2815
|
+
const task = await runApi(
|
|
2816
|
+
"Reading task version",
|
|
2817
|
+
async () => client.GET("/memories/{memoryId}", {
|
|
2818
|
+
params: { path: { memoryId: opts.task } }
|
|
2819
|
+
})
|
|
2820
|
+
);
|
|
2821
|
+
const v = task?.item?.currentVersion ?? task?.currentVersion;
|
|
2822
|
+
if (typeof v !== "number" || v < 1)
|
|
2823
|
+
fail(
|
|
2824
|
+
`could not read task ${opts.task} version to pin the Reference edge \u2014 pass --to-version <n>`
|
|
2825
|
+
);
|
|
2826
|
+
toVersion = v;
|
|
2827
|
+
}
|
|
2828
|
+
const edge = await runApi(
|
|
2829
|
+
"Wiring Reference edge",
|
|
2830
|
+
async () => client.POST("/memories/{memoryId}/relationships", {
|
|
2831
|
+
params: { path: { memoryId: closeout.id } },
|
|
2832
|
+
body: { toMemoryId: opts.task, type: "Reference", toVersion }
|
|
2833
|
+
})
|
|
2834
|
+
);
|
|
2835
|
+
let taskStatus;
|
|
2836
|
+
if (opts.statusFlip !== false && !opts.decomposition) {
|
|
2837
|
+
const current = await runApi(
|
|
2838
|
+
"Reading task tags",
|
|
2839
|
+
async () => client.GET("/memories/{memoryId}", {
|
|
2840
|
+
params: { path: { memoryId: opts.task } }
|
|
2841
|
+
})
|
|
2842
|
+
);
|
|
2843
|
+
const currentTags = current?.item?.tags ?? current?.tags ?? [];
|
|
2844
|
+
const target = verdict === "blocked" ? "status:blocked" : "status:done";
|
|
2845
|
+
const nextTags = [
|
|
2846
|
+
...new Set(currentTags.filter((t) => !t.startsWith("status:")))
|
|
2847
|
+
].concat(target);
|
|
2848
|
+
await runApi(
|
|
2849
|
+
"Flipping task status",
|
|
2850
|
+
async () => client.PATCH("/memories/{memoryId}/metadata", {
|
|
2851
|
+
params: { path: { memoryId: opts.task } },
|
|
2852
|
+
body: {
|
|
2853
|
+
memoryId: opts.task,
|
|
2854
|
+
source: opts.source,
|
|
2855
|
+
tags: nextTags
|
|
2856
|
+
}
|
|
2857
|
+
})
|
|
2858
|
+
);
|
|
2859
|
+
taskStatus = target.slice("status:".length);
|
|
2860
|
+
}
|
|
2861
|
+
const view = resolveViewUrl(cfg.baseUrl, closeout.url);
|
|
2862
|
+
const result = {
|
|
2863
|
+
closeoutId: closeout.id,
|
|
2864
|
+
edgeId: edge.id,
|
|
2865
|
+
taskStatus: taskStatus ?? null,
|
|
2866
|
+
verdict,
|
|
2867
|
+
tags
|
|
2868
|
+
};
|
|
2869
|
+
emitAction(
|
|
2870
|
+
`closed ${style.bold(opts.task)} verdict:${style.bold(verdict)} \u2192 closeout ${style.bold(closeout.id)}${taskStatus ? ` ${style.dim(`(task ${taskStatus})`)}` : ""}${view ? ` ${style.dim("\u2192")} ${view}` : ""}`,
|
|
2871
|
+
result,
|
|
2872
|
+
json
|
|
2873
|
+
);
|
|
2874
|
+
});
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2454
2877
|
// src/commands/continuity.ts
|
|
2455
2878
|
function registerContinuity(program2) {
|
|
2456
2879
|
const continuity = program2.command("continuity").description("Continuity snapshots: checkpoint and resume work");
|
|
@@ -2611,6 +3034,108 @@ Examples:
|
|
|
2611
3034
|
});
|
|
2612
3035
|
}
|
|
2613
3036
|
|
|
3037
|
+
// src/commands/decomposition.ts
|
|
3038
|
+
function registerDecomposition(program2) {
|
|
3039
|
+
const decomposition = program2.command("decomposition").description(
|
|
3040
|
+
"Drive a WLP decomposition: decompose a brief, then execute / accept / reject"
|
|
3041
|
+
);
|
|
3042
|
+
decomposition.addHelpText(
|
|
3043
|
+
"after",
|
|
3044
|
+
`
|
|
3045
|
+
Examples:
|
|
3046
|
+
$ sechroom decomposition decompose mem_XXXX
|
|
3047
|
+
$ sechroom decomposition execute sug_XXXX
|
|
3048
|
+
$ sechroom decomposition publish-run sug_XXXX
|
|
3049
|
+
$ sechroom decomposition accept sug_XXXX
|
|
3050
|
+
$ sechroom decomposition reject sug_XXXX --reason "wrong shape"`
|
|
3051
|
+
);
|
|
3052
|
+
decomposition.command("decompose <briefId>").description(
|
|
3053
|
+
"Decompose a work brief into a candidate Task graph (POST /work-briefs/{id}/decompose)"
|
|
3054
|
+
).action(async (briefId, _opts, cmd) => {
|
|
3055
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3056
|
+
const data = await runApi("Queueing decomposition", async () => {
|
|
3057
|
+
const client = await makeClient(cfg);
|
|
3058
|
+
return client.POST("/work-briefs/{id}/decompose", {
|
|
3059
|
+
params: { path: { id: briefId } },
|
|
3060
|
+
body: { id: briefId }
|
|
3061
|
+
});
|
|
3062
|
+
});
|
|
3063
|
+
emitAction(
|
|
3064
|
+
`queued decomposition of ${style.bold(briefId)} \u2192 ${style.bold(data.suggestionId)}`,
|
|
3065
|
+
data,
|
|
3066
|
+
cmd.optsWithGlobals().json
|
|
3067
|
+
);
|
|
3068
|
+
});
|
|
3069
|
+
decomposition.command("execute <decompositionId>").description(
|
|
3070
|
+
"Execute a decomposition's Task graph (POST /decompositions/{id}/execute)"
|
|
3071
|
+
).action(async (decompositionId, _opts, cmd) => {
|
|
3072
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3073
|
+
const data = await runApi("Executing decomposition", async () => {
|
|
3074
|
+
const client = await makeClient(cfg);
|
|
3075
|
+
return client.POST("/decompositions/{id}/execute", {
|
|
3076
|
+
params: { path: { id: decompositionId } },
|
|
3077
|
+
body: {}
|
|
3078
|
+
});
|
|
3079
|
+
});
|
|
3080
|
+
emitAction(
|
|
3081
|
+
`executed ${style.bold(decompositionId)} \u2192 run ${style.bold(data.runRecordId)} (${data.outcome})`,
|
|
3082
|
+
data,
|
|
3083
|
+
cmd.optsWithGlobals().json
|
|
3084
|
+
);
|
|
3085
|
+
});
|
|
3086
|
+
decomposition.command("publish-run <decompositionId>").description(
|
|
3087
|
+
"Publish an accepted decomposition's context pack on demand (POST /decompositions/{id}/publish-run)"
|
|
3088
|
+
).action(async (decompositionId, _opts, cmd) => {
|
|
3089
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3090
|
+
const data = await runApi("Publishing context pack", async () => {
|
|
3091
|
+
const client = await makeClient(cfg);
|
|
3092
|
+
return client.POST("/decompositions/{id}/publish-run", {
|
|
3093
|
+
params: { path: { id: decompositionId } },
|
|
3094
|
+
body: {}
|
|
3095
|
+
});
|
|
3096
|
+
});
|
|
3097
|
+
emitAction(
|
|
3098
|
+
`published ${style.bold(decompositionId)} \u2192 run ${style.bold(data.runRecordId)} (${data.outcome})`,
|
|
3099
|
+
data,
|
|
3100
|
+
cmd.optsWithGlobals().json
|
|
3101
|
+
);
|
|
3102
|
+
});
|
|
3103
|
+
decomposition.command("accept <decompositionId>").description(
|
|
3104
|
+
"Accept a Pending decomposition \u2014 promote + ratify its Tasks (POST /decompositions/{id}/accept)"
|
|
3105
|
+
).action(async (decompositionId, _opts, cmd) => {
|
|
3106
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3107
|
+
const data = await runApi("Accepting decomposition", async () => {
|
|
3108
|
+
const client = await makeClient(cfg);
|
|
3109
|
+
return client.POST("/decompositions/{id}/accept", {
|
|
3110
|
+
params: { path: { id: decompositionId } },
|
|
3111
|
+
body: {}
|
|
3112
|
+
});
|
|
3113
|
+
});
|
|
3114
|
+
emitAction(
|
|
3115
|
+
`accepted decomposition ${style.bold(decompositionId)}`,
|
|
3116
|
+
data,
|
|
3117
|
+
cmd.optsWithGlobals().json
|
|
3118
|
+
);
|
|
3119
|
+
});
|
|
3120
|
+
decomposition.command("reject <decompositionId>").description(
|
|
3121
|
+
"Reject a Pending decomposition \u2014 archive its Tasks, bounce the brief (POST /decompositions/{id}/reject)"
|
|
3122
|
+
).option("--reason <reason>", "Optional free-text rejection reason").action(async (decompositionId, opts, cmd) => {
|
|
3123
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3124
|
+
const data = await runApi("Rejecting decomposition", async () => {
|
|
3125
|
+
const client = await makeClient(cfg);
|
|
3126
|
+
return client.POST("/decompositions/{id}/reject", {
|
|
3127
|
+
params: { path: { id: decompositionId } },
|
|
3128
|
+
body: { reasonText: opts.reason ?? null }
|
|
3129
|
+
});
|
|
3130
|
+
});
|
|
3131
|
+
emitAction(
|
|
3132
|
+
`rejected decomposition ${style.bold(decompositionId)}`,
|
|
3133
|
+
data,
|
|
3134
|
+
cmd.optsWithGlobals().json
|
|
3135
|
+
);
|
|
3136
|
+
});
|
|
3137
|
+
}
|
|
3138
|
+
|
|
2614
3139
|
// src/commands/filing.ts
|
|
2615
3140
|
function registerFiling(program2) {
|
|
2616
3141
|
const filing = program2.command("filing").description("Review and act on filing suggestions");
|
|
@@ -3122,7 +3647,7 @@ Examples:
|
|
|
3122
3647
|
|
|
3123
3648
|
// src/setup/apply.ts
|
|
3124
3649
|
import { createHash as createHash3 } from "crypto";
|
|
3125
|
-
import { mkdirSync as mkdirSync9, readFileSync as
|
|
3650
|
+
import { mkdirSync as mkdirSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync9, existsSync as existsSync9 } from "fs";
|
|
3126
3651
|
import { dirname as dirname8 } from "path";
|
|
3127
3652
|
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
3128
3653
|
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
@@ -3180,7 +3705,7 @@ function ensureDir2(path) {
|
|
|
3180
3705
|
}
|
|
3181
3706
|
function readOr(path, fallback) {
|
|
3182
3707
|
try {
|
|
3183
|
-
return
|
|
3708
|
+
return readFileSync8(path, "utf8");
|
|
3184
3709
|
} catch {
|
|
3185
3710
|
return fallback;
|
|
3186
3711
|
}
|
|
@@ -3191,7 +3716,7 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
3191
3716
|
let current = {};
|
|
3192
3717
|
if (existed) {
|
|
3193
3718
|
try {
|
|
3194
|
-
current = JSON.parse(
|
|
3719
|
+
current = JSON.parse(readFileSync8(path, "utf8"));
|
|
3195
3720
|
} catch {
|
|
3196
3721
|
return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
|
|
3197
3722
|
}
|
|
@@ -3907,7 +4432,7 @@ import { basename as basename2, join as join13 } from "path";
|
|
|
3907
4432
|
|
|
3908
4433
|
// src/commands/fanout.ts
|
|
3909
4434
|
import { spawnSync } from "child_process";
|
|
3910
|
-
import { existsSync as existsSync10, readFileSync as
|
|
4435
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
3911
4436
|
import { isAbsolute, join as join12, resolve } from "path";
|
|
3912
4437
|
var ICON = {
|
|
3913
4438
|
refresh: "\u21BB",
|
|
@@ -3942,7 +4467,7 @@ function readManifest(path) {
|
|
|
3942
4467
|
if (!existsSync10(path)) return null;
|
|
3943
4468
|
let parsed;
|
|
3944
4469
|
try {
|
|
3945
|
-
parsed = JSON.parse(
|
|
4470
|
+
parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
3946
4471
|
} catch (err2) {
|
|
3947
4472
|
throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
3948
4473
|
}
|
|
@@ -4756,24 +5281,45 @@ Examples:
|
|
|
4756
5281
|
$ sechroom relationship suggestions --status Pending --memory mem_XXXX
|
|
4757
5282
|
$ sechroom relationship suggestion accept rsg_XXXX`
|
|
4758
5283
|
);
|
|
4759
|
-
relationship.command("create <fromMemoryId> <toMemoryId>").description("Create a relationship (POST /memories/{memoryId}/relationships)").option("--type <type>", "Relationship type (Reference, Related, Parent, Child, Follows, \u2026)", "Reference").
|
|
5284
|
+
relationship.command("create <fromMemoryId> <toMemoryId>").description("Create a relationship (POST /memories/{memoryId}/relationships)").option("--type <type>", "Relationship type (Reference, Related, Parent, Child, Follows, \u2026)", "Reference").option(
|
|
5285
|
+
"--to-version <number>",
|
|
5286
|
+
"Version of the target memory to pin the edge to (defaults to the target's current version)"
|
|
5287
|
+
).action(async (fromMemoryId, toMemoryId, opts, cmd) => {
|
|
4760
5288
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
4761
|
-
const
|
|
4762
|
-
|
|
4763
|
-
|
|
5289
|
+
const client = await makeClient(cfg);
|
|
5290
|
+
let toVersion;
|
|
5291
|
+
if (opts.toVersion !== void 0) {
|
|
5292
|
+
toVersion = Number(opts.toVersion);
|
|
5293
|
+
if (!Number.isInteger(toVersion) || toVersion < 1) {
|
|
5294
|
+
fail(`--to-version must be a positive integer (got '${opts.toVersion}').`);
|
|
5295
|
+
}
|
|
5296
|
+
} else {
|
|
5297
|
+
const target = await runApi(
|
|
5298
|
+
"Resolving target version",
|
|
5299
|
+
async () => client.GET("/memories/{memoryId}", { params: { path: { memoryId: toMemoryId } } })
|
|
5300
|
+
);
|
|
5301
|
+
if (typeof target.item?.currentVersion !== "number") {
|
|
5302
|
+
fail(`Could not resolve the current version of ${toMemoryId}; pass --to-version explicitly.`);
|
|
5303
|
+
}
|
|
5304
|
+
toVersion = target.item.currentVersion;
|
|
5305
|
+
}
|
|
5306
|
+
const data = await runApi(
|
|
5307
|
+
"Creating relationship",
|
|
5308
|
+
async () => client.POST("/memories/{memoryId}/relationships", {
|
|
4764
5309
|
params: { path: { memoryId: fromMemoryId } },
|
|
4765
5310
|
body: {
|
|
4766
5311
|
toMemoryId,
|
|
4767
|
-
type: opts.type
|
|
5312
|
+
type: opts.type,
|
|
5313
|
+
toVersion
|
|
4768
5314
|
}
|
|
4769
|
-
})
|
|
4770
|
-
|
|
5315
|
+
})
|
|
5316
|
+
);
|
|
4771
5317
|
const inversePart = data.inverseId ? ` ${style.dim(`(inverse ${data.inverseId})`)}` : "";
|
|
4772
5318
|
const view = resolveViewUrl(cfg.baseUrl, data.url);
|
|
4773
5319
|
const urlPart = view ? ` ${style.dim("\u2192")} ${view}` : "";
|
|
4774
5320
|
emitAction(
|
|
4775
|
-
`created relationship ${style.bold(data.id)} ${style.dim(`${fromMemoryId} \u2192 ${toMemoryId}`)}${inversePart}${urlPart}`,
|
|
4776
|
-
data,
|
|
5321
|
+
`created relationship ${style.bold(data.id)} ${style.dim(`${fromMemoryId} \u2192 ${toMemoryId} @v${toVersion}`)}${inversePart}${urlPart}`,
|
|
5322
|
+
{ ...data, toVersion },
|
|
4777
5323
|
cmd.optsWithGlobals().json
|
|
4778
5324
|
);
|
|
4779
5325
|
});
|
|
@@ -4897,7 +5443,7 @@ Examples:
|
|
|
4897
5443
|
// src/commands/reset.ts
|
|
4898
5444
|
import { homedir as homedir4 } from "os";
|
|
4899
5445
|
import { join as join14 } from "path";
|
|
4900
|
-
import { existsSync as existsSync12, readFileSync as
|
|
5446
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10, rmSync as rmSync3 } from "fs";
|
|
4901
5447
|
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
4902
5448
|
var localSkillsDir = () => join14(process.cwd(), ".claude", "skills");
|
|
4903
5449
|
var globalSkillsDir = () => join14(homedir4(), ".claude", "skills");
|
|
@@ -4908,7 +5454,7 @@ function removeMaterialisedSkills(dir) {
|
|
|
4908
5454
|
const lockPath = join14(dir, SKILLS_LOCK2);
|
|
4909
5455
|
if (!existsSync12(lockPath)) return removed;
|
|
4910
5456
|
try {
|
|
4911
|
-
const lock = JSON.parse(
|
|
5457
|
+
const lock = JSON.parse(readFileSync10(lockPath, "utf8"));
|
|
4912
5458
|
for (const entry of Object.values(lock)) {
|
|
4913
5459
|
for (const name of entry.skills ?? []) {
|
|
4914
5460
|
const p = join14(dir, name);
|
|
@@ -5047,7 +5593,9 @@ function registerSkills(program2) {
|
|
|
5047
5593
|
"after",
|
|
5048
5594
|
`
|
|
5049
5595
|
Examples:
|
|
5050
|
-
$ sechroom skills install
|
|
5596
|
+
$ sechroom skills install --client claude materialise target:claude-code skills to ~/.claude/skills
|
|
5597
|
+
$ sechroom skills install --client codex materialise target:gpt-codex skills to ~/.codex/skills
|
|
5598
|
+
$ sechroom skills install --client all keep both global clients in sync
|
|
5051
5599
|
$ sechroom skills install --scope project write them to ./.claude/skills instead
|
|
5052
5600
|
$ sechroom skills list what's materialised on disk
|
|
5053
5601
|
$ sechroom skills clean remove the materialised skill files
|
|
@@ -5056,11 +5604,15 @@ Examples:
|
|
|
5056
5604
|
$ sechroom skills package --from-source --workspace wsp_abc -o ./dist zip a draft from source
|
|
5057
5605
|
$ sechroom skills set-lane --code-lane claude-code-chris --design-lane claude-design-chris
|
|
5058
5606
|
|
|
5607
|
+
When --client is omitted, configured client flags/environment variables win;
|
|
5608
|
+
otherwise existing default client homes are detected. Both configured/detected
|
|
5609
|
+
clients select all; an unconfigured machine preserves the legacy Claude default.
|
|
5610
|
+
|
|
5059
5611
|
`
|
|
5060
5612
|
);
|
|
5061
|
-
skills.command("install").description("Materialise your installed skills to disk (the already-installed bundle \u2014 no server install)").option("--scope <scope>", "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global").option("--local", "alias for --scope project").option("--dry-run", "print what would be written; write nothing").option("--json", "machine output").action((opts, cmd) => runInstall(SKILL_SPEC, cmd, opts));
|
|
5062
|
-
skills.command("list").description("List the skills materialised on disk (per resolved config dir)").option("--scope <scope>", "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global").option("--local", "alias for --scope project").option("--json", "machine output").action((opts, cmd) => runList(SKILL_SPEC, cmd, opts));
|
|
5063
|
-
skills.command("clean [slug]").description(`Remove skill files materialised to disk (default ${DEFAULT_SKILLS_SLUG})`).option("--scope <scope>", "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global").option("--local", "alias for --scope project").option("--json", "machine output").action((slugArg, opts, cmd) => runClean(SKILL_SPEC, cmd, opts, slugArg));
|
|
5613
|
+
skills.command("install").description("Materialise your installed skills to disk (the already-installed bundle \u2014 no server install)").option("--scope <scope>", "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global").option("--local", "alias for --scope project").option("--client <client>", "claude, codex, or all (default: configured/detected clients)").option("--dry-run", "print what would be written; write nothing").option("--json", "machine output").action((opts, cmd) => runInstall(SKILL_SPEC, cmd, opts));
|
|
5614
|
+
skills.command("list").description("List the skills materialised on disk (per resolved config dir)").option("--scope <scope>", "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global").option("--local", "alias for --scope project").option("--client <client>", "claude, codex, or all (default: configured/detected clients)").option("--json", "machine output").action((opts, cmd) => runList(SKILL_SPEC, cmd, opts));
|
|
5615
|
+
skills.command("clean [slug]").description(`Remove skill files materialised to disk (default ${DEFAULT_SKILLS_SLUG})`).option("--scope <scope>", "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global").option("--local", "alias for --scope project").option("--client <client>", "claude, codex, or all (default: configured/detected clients)").option("--json", "machine output").action((slugArg, opts, cmd) => runClean(SKILL_SPEC, cmd, opts, slugArg));
|
|
5064
5616
|
skills.command("preview").description("Render a workspace's draft bundle from source (no publish/install) and report the components").requiredOption("--workspace <id>", "workspace holding the draft bundle sources (wsp_\u2026)").option("--slug <slug>", "override the derived bundle slug").option("--title <title>", "override the derived bundle title").option("--version <version>", "override the derived bundle version").option("--default-install-parent <path>", "override the derived default install parent").option("--json", "machine output (the full RenderBundlePreviewResponse)").action(async (opts, cmd) => {
|
|
5065
5617
|
const json = Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json);
|
|
5066
5618
|
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
@@ -5308,7 +5860,7 @@ Examples:
|
|
|
5308
5860
|
import {
|
|
5309
5861
|
existsSync as existsSync15,
|
|
5310
5862
|
mkdirSync as mkdirSync12,
|
|
5311
|
-
readFileSync as
|
|
5863
|
+
readFileSync as readFileSync11,
|
|
5312
5864
|
rmSync as rmSync4,
|
|
5313
5865
|
writeFileSync as writeFileSync12
|
|
5314
5866
|
} from "fs";
|
|
@@ -5414,7 +5966,7 @@ function registerTelemetry(program2) {
|
|
|
5414
5966
|
);
|
|
5415
5967
|
});
|
|
5416
5968
|
telemetry.command("hook").description(
|
|
5417
|
-
"Per-turn telemetry self-report for
|
|
5969
|
+
"Per-turn telemetry self-report for Claude Code hooks \u2014 Stop/SubagentStop \u2192 parsed + terminal, Notification/PermissionDenied \u2192 approval (reads stdin; no-op unless bound). Fail-soft."
|
|
5418
5970
|
).action(async (_opts, cmd) => {
|
|
5419
5971
|
try {
|
|
5420
5972
|
const raw = await readStdin2();
|
|
@@ -5423,21 +5975,10 @@ function registerTelemetry(program2) {
|
|
|
5423
5975
|
const binding = findBinding(cwd);
|
|
5424
5976
|
if (!binding) return process.exit(0);
|
|
5425
5977
|
const usage = input.transcript_path ? parseTranscript(input.transcript_path) : null;
|
|
5426
|
-
|
|
5978
|
+
const events = buildHookEvents(input, usage, binding.taskId);
|
|
5979
|
+
if (events.length === 0) return process.exit(0);
|
|
5427
5980
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
5428
|
-
await postTelemetry(cfg, binding.decompositionId,
|
|
5429
|
-
{
|
|
5430
|
-
taskId: binding.taskId,
|
|
5431
|
-
kind: "Parsed",
|
|
5432
|
-
tokensIn: usage.tokensIn,
|
|
5433
|
-
tokensOut: usage.tokensOut,
|
|
5434
|
-
contextUsed: usage.contextUsed,
|
|
5435
|
-
contextWindow: usage.contextWindow,
|
|
5436
|
-
text: null,
|
|
5437
|
-
approvalState: null,
|
|
5438
|
-
verdict: null
|
|
5439
|
-
}
|
|
5440
|
-
]);
|
|
5981
|
+
await postTelemetry(cfg, binding.decompositionId, events);
|
|
5441
5982
|
return process.exit(0);
|
|
5442
5983
|
} catch {
|
|
5443
5984
|
return process.exit(0);
|
|
@@ -5465,7 +6006,12 @@ function registerTelemetry(program2) {
|
|
|
5465
6006
|
scope,
|
|
5466
6007
|
cwd
|
|
5467
6008
|
});
|
|
5468
|
-
const commands = {
|
|
6009
|
+
const commands = {
|
|
6010
|
+
Stop: "sechroom telemetry hook",
|
|
6011
|
+
SubagentStop: "sechroom telemetry hook",
|
|
6012
|
+
Notification: "sechroom telemetry hook",
|
|
6013
|
+
PermissionDenied: "sechroom telemetry hook"
|
|
6014
|
+
};
|
|
5469
6015
|
try {
|
|
5470
6016
|
const multi = targets.length > 1;
|
|
5471
6017
|
const results = targets.map((t) => {
|
|
@@ -5523,7 +6069,7 @@ function findBinding(start) {
|
|
|
5523
6069
|
if (existsSync15(path)) {
|
|
5524
6070
|
try {
|
|
5525
6071
|
const b = JSON.parse(
|
|
5526
|
-
|
|
6072
|
+
readFileSync11(path, "utf8")
|
|
5527
6073
|
);
|
|
5528
6074
|
if (b.decompositionId && b.taskId)
|
|
5529
6075
|
return { decompositionId: b.decompositionId, taskId: b.taskId };
|
|
@@ -5542,7 +6088,7 @@ function parseTranscript(path) {
|
|
|
5542
6088
|
let tokensOut = 0;
|
|
5543
6089
|
let contextUsed = 0;
|
|
5544
6090
|
let model = "";
|
|
5545
|
-
for (const line of
|
|
6091
|
+
for (const line of readFileSync11(path, "utf8").split("\n")) {
|
|
5546
6092
|
if (!line.trim()) continue;
|
|
5547
6093
|
let obj;
|
|
5548
6094
|
try {
|
|
@@ -5559,11 +6105,61 @@ function parseTranscript(path) {
|
|
|
5559
6105
|
if (obj.message?.model) model = obj.message.model;
|
|
5560
6106
|
}
|
|
5561
6107
|
if (tokensIn === 0 && tokensOut === 0) return null;
|
|
5562
|
-
return { tokensIn, tokensOut, contextUsed, contextWindow: windowFor(model) };
|
|
6108
|
+
return { tokensIn, tokensOut, contextUsed, contextWindow: windowFor(model, contextUsed) };
|
|
5563
6109
|
}
|
|
5564
|
-
function windowFor(model) {
|
|
6110
|
+
function windowFor(model, contextUsed = 0) {
|
|
5565
6111
|
const m = model.toLowerCase();
|
|
5566
|
-
|
|
6112
|
+
if (m.includes("[1m]") || m.includes("-1m")) return 1e6;
|
|
6113
|
+
return contextUsed > 2e5 ? 1e6 : 2e5;
|
|
6114
|
+
}
|
|
6115
|
+
function buildHookEvents(input, usage, taskId) {
|
|
6116
|
+
const events = [];
|
|
6117
|
+
const base = (kind, over) => ({
|
|
6118
|
+
taskId,
|
|
6119
|
+
kind,
|
|
6120
|
+
tokensIn: null,
|
|
6121
|
+
tokensOut: null,
|
|
6122
|
+
contextUsed: null,
|
|
6123
|
+
contextWindow: null,
|
|
6124
|
+
text: null,
|
|
6125
|
+
approvalState: null,
|
|
6126
|
+
verdict: null,
|
|
6127
|
+
...over
|
|
6128
|
+
});
|
|
6129
|
+
if (usage) {
|
|
6130
|
+
events.push(
|
|
6131
|
+
base("Parsed", {
|
|
6132
|
+
tokensIn: usage.tokensIn,
|
|
6133
|
+
tokensOut: usage.tokensOut,
|
|
6134
|
+
contextUsed: usage.contextUsed,
|
|
6135
|
+
contextWindow: usage.contextWindow
|
|
6136
|
+
})
|
|
6137
|
+
);
|
|
6138
|
+
}
|
|
6139
|
+
switch (input.hook_event_name) {
|
|
6140
|
+
case "PermissionDenied":
|
|
6141
|
+
events.push(
|
|
6142
|
+
base("Approval", {
|
|
6143
|
+
approvalState: "denied",
|
|
6144
|
+
text: input.tool_name ?? input.message ?? null
|
|
6145
|
+
})
|
|
6146
|
+
);
|
|
6147
|
+
break;
|
|
6148
|
+
case "Notification":
|
|
6149
|
+
if (isPermissionNotification(input))
|
|
6150
|
+
events.push(base("Approval", { text: input.message ?? null }));
|
|
6151
|
+
break;
|
|
6152
|
+
case "Stop":
|
|
6153
|
+
case "SubagentStop":
|
|
6154
|
+
events.push(base("Terminal", { text: input.last_assistant_message ?? null }));
|
|
6155
|
+
break;
|
|
6156
|
+
}
|
|
6157
|
+
return events;
|
|
6158
|
+
}
|
|
6159
|
+
function isPermissionNotification(input) {
|
|
6160
|
+
const t = (input.notification_type ?? input.type ?? "").toLowerCase();
|
|
6161
|
+
if (t) return t.includes("permission");
|
|
6162
|
+
return (input.message ?? "").toLowerCase().includes("permission");
|
|
5567
6163
|
}
|
|
5568
6164
|
async function readStdin2() {
|
|
5569
6165
|
if (process.stdin.isTTY) return "";
|
|
@@ -5799,7 +6395,7 @@ Examples:
|
|
|
5799
6395
|
function resolveVersion() {
|
|
5800
6396
|
try {
|
|
5801
6397
|
const pkg = JSON.parse(
|
|
5802
|
-
|
|
6398
|
+
readFileSync12(new URL("../package.json", import.meta.url), "utf8")
|
|
5803
6399
|
);
|
|
5804
6400
|
return pkg.version ?? "0.0.0";
|
|
5805
6401
|
} catch {
|
|
@@ -5957,6 +6553,8 @@ registerLookup(program);
|
|
|
5957
6553
|
registerRelationships(program);
|
|
5958
6554
|
registerWorkspace(program);
|
|
5959
6555
|
registerProject(program);
|
|
6556
|
+
registerDecomposition(program);
|
|
6557
|
+
registerClose(program);
|
|
5960
6558
|
registerFiling(program);
|
|
5961
6559
|
registerContinuity(program);
|
|
5962
6560
|
registerCheckpoint(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sechroom/cli",
|
|
3
|
-
"version": "2026.7.
|
|
3
|
+
"version": "2026.7.7-rc.56782977",
|
|
4
4
|
"description": "Sechroom CLI — a thin, generated client over the Sechroom HTTP API. An agent/human surface alongside MCP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -36,12 +36,14 @@
|
|
|
36
36
|
"openapi-typescript": "^7.4.0",
|
|
37
37
|
"tsup": "^8.3.0",
|
|
38
38
|
"tsx": "^4.19.0",
|
|
39
|
-
"typescript": "^5.6.0"
|
|
39
|
+
"typescript": "^5.6.0",
|
|
40
|
+
"typescript-7": "npm:typescript@^7.0.2"
|
|
40
41
|
},
|
|
41
42
|
"scripts": {
|
|
42
43
|
"gen": "openapi-typescript \"${SECHROOM_OPENAPI_URL:-https://app.sechroom.ai/api/openapi/v1.json}\" -o src/generated/api.d.ts",
|
|
43
44
|
"build": "tsup src/index.ts --format esm --target node20 --clean",
|
|
44
45
|
"dev": "tsx src/index.ts",
|
|
46
|
+
"test": "node --import tsx --test \"src/**/*.test.ts\"",
|
|
45
47
|
"check-types": "tsc --noEmit"
|
|
46
48
|
}
|
|
47
49
|
}
|