@sechroom/cli 2026.6.37-rc.84c4b3e9 → 2026.6.37
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 +1297 -382
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -844,6 +844,50 @@ Examples:
|
|
|
844
844
|
cmd.optsWithGlobals().json
|
|
845
845
|
);
|
|
846
846
|
});
|
|
847
|
+
memory.command("update <memoryId>").description("Update metadata only \u2014 title/tags/type/confidence (PATCH /memories/{memoryId}/metadata; omitted = unchanged)").option("--title <text>", "Set the title").option("--tag <tag...>", "Set the full tag list (replaces existing); repeatable").option("--add-tag <tag...>", "Add tag(s) to the existing set (repeatable)").option("--remove-tag <tag...>", "Remove tag(s) from the existing set (repeatable)").option("--type <type>", "Set the memory type (e.g. reference, note, document)").option("--confidence <n>", "Set confidence (0..1)", (v) => Number(v)).option("--memory-source <src>", "Set the memory's own Source field").option("--bump-version", "Bump the version chain (use for a content reinterpretation, e.g. a type promotion)", false).option("--source <source>", "Contributing lane stamp (attribution)", "cli").action(async (memoryId, opts, cmd) => {
|
|
848
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
849
|
+
const json = cmd.optsWithGlobals().json;
|
|
850
|
+
const hasTagOps = Boolean(opts.tag || opts.addTag || opts.removeTag);
|
|
851
|
+
const hasAny = opts.title !== void 0 || hasTagOps || opts.type !== void 0 || opts.confidence !== void 0 || opts.memorySource !== void 0 || Boolean(opts.bumpVersion);
|
|
852
|
+
if (!hasAny)
|
|
853
|
+
fail("nothing to update \u2014 pass at least one of --title / --tag / --add-tag / --remove-tag / --type / --confidence / --memory-source.");
|
|
854
|
+
let tags;
|
|
855
|
+
if (hasTagOps) {
|
|
856
|
+
let base;
|
|
857
|
+
if (opts.tag) base = opts.tag;
|
|
858
|
+
else {
|
|
859
|
+
const current = await runApi("Reading current tags", async () => {
|
|
860
|
+
const client = await makeClient(cfg);
|
|
861
|
+
return client.GET("/memories/{memoryId}", { params: { path: { memoryId } } });
|
|
862
|
+
});
|
|
863
|
+
base = current?.item?.tags ?? current?.tags ?? [];
|
|
864
|
+
}
|
|
865
|
+
const set = new Set(base);
|
|
866
|
+
for (const t of opts.addTag ?? []) set.add(t);
|
|
867
|
+
for (const t of opts.removeTag ?? []) set.delete(t);
|
|
868
|
+
tags = [...set];
|
|
869
|
+
}
|
|
870
|
+
const body = {
|
|
871
|
+
memoryId,
|
|
872
|
+
source: opts.source,
|
|
873
|
+
bumpVersion: Boolean(opts.bumpVersion)
|
|
874
|
+
};
|
|
875
|
+
if (opts.title !== void 0) body.title = opts.title;
|
|
876
|
+
if (tags !== void 0) body.tags = tags;
|
|
877
|
+
if (opts.type !== void 0) body.type = opts.type;
|
|
878
|
+
if (opts.confidence !== void 0) body.confidence = opts.confidence;
|
|
879
|
+
if (opts.memorySource !== void 0) body.memorySource = opts.memorySource;
|
|
880
|
+
const data = await runApi("Updating metadata", async () => {
|
|
881
|
+
const client = await makeClient(cfg);
|
|
882
|
+
return client.PATCH("/memories/{memoryId}/metadata", {
|
|
883
|
+
params: { path: { memoryId } },
|
|
884
|
+
body
|
|
885
|
+
});
|
|
886
|
+
});
|
|
887
|
+
const changed = data?.changed ?? [];
|
|
888
|
+
const summary = changed.length > 0 ? `updated ${changed.join(", ")}` : "no changes";
|
|
889
|
+
emitAction(`${summary} on ${style.bold(memoryId)} \u2192 v${style.bold(String(data?.version ?? "?"))}`, data, json);
|
|
890
|
+
});
|
|
847
891
|
memory.command("archive <memoryId>").description("Archive a memory (POST /memories/{memoryId}/archive)").option("--source <source>", "Source / lane stamp", "cli").action(async (memoryId, opts, cmd) => {
|
|
848
892
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
849
893
|
const data = await runApi("Archiving memory", async () => {
|
|
@@ -1231,7 +1275,8 @@ Examples:
|
|
|
1231
1275
|
$ sechroom workspace get wsp_XXXX --json
|
|
1232
1276
|
$ sechroom workspace rename wsp_XXXX --name "Renamed"
|
|
1233
1277
|
$ sechroom workspace move wsp_XXXX --parent wsp_YYYY
|
|
1234
|
-
$ sechroom workspace feed wsp_XXXX --limit 20 --cascade
|
|
1278
|
+
$ sechroom workspace feed wsp_XXXX --limit 20 --cascade
|
|
1279
|
+
$ sechroom workspace feed wsp_XXXX --tag kind:plan --since 2026-06-01T00:00:00Z --order UpdatedDesc`
|
|
1235
1280
|
);
|
|
1236
1281
|
workspace.command("create").description("Create a workspace (POST /workspaces)").requiredOption("--name <name>", "Workspace name").option("--description <description>", "Optional description").option("--parent <parentId>", "Parent workspace id (omit for a top-level workspace)").action(async (opts, cmd) => {
|
|
1237
1282
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
@@ -1339,7 +1384,7 @@ Examples:
|
|
|
1339
1384
|
});
|
|
1340
1385
|
emitAction(`restored workspace ${style.bold(workspaceId)}`, data, cmd.optsWithGlobals().json);
|
|
1341
1386
|
});
|
|
1342
|
-
workspace.command("feed <workspaceId>").description("List a workspace's memory feed (GET /workspaces/{workspaceId}/memories/feed)").option("--limit <n>", "Max results", "20").option("--cursor <cursor>", "Paging cursor from a prior page").option("--cascade", "Cascade into descendant workspaces", false).option("--include-projects", "Include the workspace's projects", false).option("--include-archived", "Include archived memories", false).option("--query <query>", "Filter the feed by text").option("--tag <tag>", "Filter tags (comma-separated)").action(async (workspaceId, opts, cmd) => {
|
|
1387
|
+
workspace.command("feed <workspaceId>").description("List a workspace's memory feed (GET /workspaces/{workspaceId}/memories/feed)").option("--limit <n>", "Max results", "20").option("--cursor <cursor>", "Paging cursor from a prior page").option("--cascade", "Cascade into descendant workspaces", false).option("--include-projects", "Include the workspace's projects", false).option("--include-archived", "Include archived memories", false).option("--query <query>", "Filter the feed by text").option("--tag <tag>", "Filter tags (comma-separated)").option("--since <iso>", "Only memories updated since this ISO-8601 timestamp (updatedSince)").option("--order <order>", "Order: UpdatedDesc | UpdatedAsc | CreatedDesc | CreatedAsc").action(async (workspaceId, opts, cmd) => {
|
|
1343
1388
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
1344
1389
|
const data = await runApi("Fetching feed", async () => {
|
|
1345
1390
|
const client = await makeClient(cfg);
|
|
@@ -1353,7 +1398,12 @@ Examples:
|
|
|
1353
1398
|
includeArchived: Boolean(opts.includeArchived),
|
|
1354
1399
|
...opts.cursor ? { cursor: opts.cursor } : {},
|
|
1355
1400
|
...opts.query ? { query: opts.query } : {},
|
|
1356
|
-
...opts.tag ? { filterTags: opts.tag } : {}
|
|
1401
|
+
...opts.tag ? { filterTags: opts.tag } : {},
|
|
1402
|
+
...opts.since ? { updatedSince: opts.since } : {},
|
|
1403
|
+
// orderBy is WorkspaceFeedOrder (Updated*/Created*) post-FR-feed-005
|
|
1404
|
+
// (the FeedOrder schema-name collision is fixed). opts.order is a raw
|
|
1405
|
+
// commander string; cast to a valid member to satisfy the union.
|
|
1406
|
+
...opts.order ? { orderBy: opts.order } : {}
|
|
1357
1407
|
}
|
|
1358
1408
|
}
|
|
1359
1409
|
});
|
|
@@ -1773,16 +1823,19 @@ Examples:
|
|
|
1773
1823
|
});
|
|
1774
1824
|
}
|
|
1775
1825
|
|
|
1826
|
+
// src/commands/checkpoint.ts
|
|
1827
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
1828
|
+
import { dirname as dirname5, join as join6 } from "path";
|
|
1829
|
+
|
|
1776
1830
|
// src/commands/hook.ts
|
|
1777
|
-
import {
|
|
1778
|
-
import {
|
|
1779
|
-
import { delimiter, dirname as dirname4, join as
|
|
1831
|
+
import { createHash as createHash2 } from "crypto";
|
|
1832
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1833
|
+
import { delimiter, dirname as dirname4, join as join5 } from "path";
|
|
1780
1834
|
|
|
1781
1835
|
// src/sem.ts
|
|
1782
|
-
import {
|
|
1783
|
-
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1836
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
1837
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1784
1838
|
var SEM_FILE = join2(".sechroom", "lane.json");
|
|
1785
|
-
var LEGACY_SEM_FILE = ".sem";
|
|
1786
1839
|
var STATE_DIR_NAME2 = ".sechroom";
|
|
1787
1840
|
function localSemPath(cwd = process.cwd()) {
|
|
1788
1841
|
return join2(cwd, SEM_FILE);
|
|
@@ -1792,25 +1845,47 @@ function resolveSemPathForRead(start = process.cwd()) {
|
|
|
1792
1845
|
while (true) {
|
|
1793
1846
|
const candidate = join2(dir, SEM_FILE);
|
|
1794
1847
|
if (existsSync2(candidate)) return candidate;
|
|
1795
|
-
const legacy = join2(dir, LEGACY_SEM_FILE);
|
|
1796
|
-
if (existsSync2(legacy)) return legacy;
|
|
1797
1848
|
const parent = dirname2(dir);
|
|
1798
1849
|
if (parent === dir) return void 0;
|
|
1799
1850
|
dir = parent;
|
|
1800
1851
|
}
|
|
1801
1852
|
}
|
|
1802
|
-
function
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1853
|
+
function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
|
|
1854
|
+
try {
|
|
1855
|
+
let dir = start;
|
|
1856
|
+
let gitPath;
|
|
1857
|
+
for (; ; ) {
|
|
1858
|
+
const candidate = join2(dir, ".git");
|
|
1859
|
+
if (existsSync2(candidate)) {
|
|
1860
|
+
gitPath = candidate;
|
|
1861
|
+
break;
|
|
1862
|
+
}
|
|
1863
|
+
const parent = dirname2(dir);
|
|
1864
|
+
if (parent === dir) break;
|
|
1865
|
+
dir = parent;
|
|
1866
|
+
}
|
|
1867
|
+
if (!gitPath || statSync(gitPath).isDirectory()) return lane;
|
|
1868
|
+
const gitFile = readFileSync2(gitPath, "utf8");
|
|
1869
|
+
const common = gitFile.trim().match(/^gitdir:\s*(.+)\/worktrees\/[^/\s]+\s*$/);
|
|
1870
|
+
if (!common) return lane;
|
|
1871
|
+
const worktreesDir = join2(common[1], "worktrees");
|
|
1872
|
+
const siblings = readdirSync(worktreesDir).filter((n) => {
|
|
1873
|
+
try {
|
|
1874
|
+
return statSync(join2(worktreesDir, n)).isDirectory();
|
|
1875
|
+
} catch {
|
|
1876
|
+
return false;
|
|
1877
|
+
}
|
|
1878
|
+
});
|
|
1879
|
+
return laneWithWorktreeSuffix(lane, gitFile, siblings);
|
|
1880
|
+
} catch {
|
|
1881
|
+
return lane;
|
|
1812
1882
|
}
|
|
1813
|
-
|
|
1883
|
+
}
|
|
1884
|
+
function laneWithWorktreeSuffix(lane, gitFile, siblings) {
|
|
1885
|
+
const m = gitFile.trim().match(/\/worktrees\/([^/\s]+)\s*$/);
|
|
1886
|
+
if (!m) return lane;
|
|
1887
|
+
const idx = [...siblings].sort().indexOf(m[1]);
|
|
1888
|
+
return idx < 0 ? lane : `${lane}-${idx + 2}`;
|
|
1814
1889
|
}
|
|
1815
1890
|
function serializeSem(values) {
|
|
1816
1891
|
return JSON.stringify(values, null, 2) + "\n";
|
|
@@ -1818,15 +1893,11 @@ function serializeSem(values) {
|
|
|
1818
1893
|
function readSem(path) {
|
|
1819
1894
|
const p = path ?? resolveSemPathForRead();
|
|
1820
1895
|
if (!p || !existsSync2(p)) return void 0;
|
|
1821
|
-
|
|
1822
|
-
const values = basename2(p) === LEGACY_SEM_FILE ? parseSem(text) : parseLaneJson(text);
|
|
1823
|
-
return { path: p, values };
|
|
1896
|
+
return { path: p, values: parseLaneJson(readFileSync2(p, "utf8")) };
|
|
1824
1897
|
}
|
|
1825
1898
|
function readLocalSemValues(cwd = process.cwd()) {
|
|
1826
1899
|
const next = join2(cwd, SEM_FILE);
|
|
1827
1900
|
if (existsSync2(next)) return readSem(next)?.values ?? {};
|
|
1828
|
-
const legacy = join2(cwd, LEGACY_SEM_FILE);
|
|
1829
|
-
if (existsSync2(legacy)) return readSem(legacy)?.values ?? {};
|
|
1830
1901
|
return {};
|
|
1831
1902
|
}
|
|
1832
1903
|
function parseLaneJson(text) {
|
|
@@ -1846,8 +1917,34 @@ function writeSem(values, path = localSemPath()) {
|
|
|
1846
1917
|
mkdirSync2(dirname2(path), { recursive: true });
|
|
1847
1918
|
writeFileSync2(path, serializeSem(values));
|
|
1848
1919
|
ensureSemIgnored(path);
|
|
1920
|
+
ensureContinuityScaffold(path);
|
|
1849
1921
|
return path;
|
|
1850
1922
|
}
|
|
1923
|
+
var CONTINUITY_FILE_NAME = "continuity.json";
|
|
1924
|
+
var CONTINUITY_SCAFFOLD = JSON.stringify(
|
|
1925
|
+
{
|
|
1926
|
+
_readme: "Agent-maintained continuity intent. Keep these current during the session; `sechroom checkpoint` and the PreCompact hook snapshot from here. The five required fields (objective, state, lastAction, nextAction, resumeInstruction) must all be non-empty for a snapshot to be created.",
|
|
1927
|
+
objective: "",
|
|
1928
|
+
state: "",
|
|
1929
|
+
lastAction: "",
|
|
1930
|
+
nextAction: "",
|
|
1931
|
+
resumeInstruction: "",
|
|
1932
|
+
constraints: [],
|
|
1933
|
+
questions: [],
|
|
1934
|
+
artifacts: [],
|
|
1935
|
+
confidence: null
|
|
1936
|
+
},
|
|
1937
|
+
null,
|
|
1938
|
+
2
|
|
1939
|
+
) + "\n";
|
|
1940
|
+
function ensureContinuityScaffold(semPath) {
|
|
1941
|
+
try {
|
|
1942
|
+
const target = join2(dirname2(semPath), CONTINUITY_FILE_NAME);
|
|
1943
|
+
if (existsSync2(target)) return;
|
|
1944
|
+
writeFileSync2(target, CONTINUITY_SCAFFOLD);
|
|
1945
|
+
} catch {
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1851
1948
|
function ignoresSem(content) {
|
|
1852
1949
|
return content.split("\n").some((line) => {
|
|
1853
1950
|
const t = line.trim();
|
|
@@ -1896,8 +1993,55 @@ function ensureSemIgnored(semPath) {
|
|
|
1896
1993
|
|
|
1897
1994
|
// src/setup/clients.ts
|
|
1898
1995
|
import { existsSync as existsSync3 } from "fs";
|
|
1996
|
+
import { homedir as homedir3 } from "os";
|
|
1997
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
1998
|
+
|
|
1999
|
+
// src/setup/config-dirs.ts
|
|
1899
2000
|
import { homedir as homedir2 } from "os";
|
|
1900
|
-
import {
|
|
2001
|
+
import { join as join3 } from "path";
|
|
2002
|
+
function expandTilde(p) {
|
|
2003
|
+
if (p === "~") return homedir2();
|
|
2004
|
+
if (p.startsWith("~/")) return join3(homedir2(), p.slice(2));
|
|
2005
|
+
return p;
|
|
2006
|
+
}
|
|
2007
|
+
function splitDirs(raw) {
|
|
2008
|
+
if (!raw) return [];
|
|
2009
|
+
return raw.split(",").map((s) => expandTilde(s.trim())).filter(Boolean);
|
|
2010
|
+
}
|
|
2011
|
+
function resolveScope(flag) {
|
|
2012
|
+
if (flag == null) return "global";
|
|
2013
|
+
if (flag === "global" || flag === "project") return flag;
|
|
2014
|
+
throw new Error(`--scope must be 'global' or 'project' (got '${flag}')`);
|
|
2015
|
+
}
|
|
2016
|
+
function labelFor(dir) {
|
|
2017
|
+
const h = homedir2();
|
|
2018
|
+
if (dir === h) return "~";
|
|
2019
|
+
return dir.startsWith(h + "/") ? "~" + dir.slice(h.length) : dir;
|
|
2020
|
+
}
|
|
2021
|
+
function defaultClaudeDir() {
|
|
2022
|
+
return join3(homedir2(), ".claude");
|
|
2023
|
+
}
|
|
2024
|
+
function defaultCodexHome() {
|
|
2025
|
+
return join3(homedir2(), ".codex");
|
|
2026
|
+
}
|
|
2027
|
+
function resolveClaudeTargets(opts) {
|
|
2028
|
+
const scope = opts.scope ?? "global";
|
|
2029
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
2030
|
+
if (scope === "project") {
|
|
2031
|
+
return [{ dir: join3(cwd, ".claude"), scope, label: "<project>" }];
|
|
2032
|
+
}
|
|
2033
|
+
const fromFlag = splitDirs(opts.override);
|
|
2034
|
+
const fromEnv = splitDirs(process.env.CLAUDE_CONFIG_DIR);
|
|
2035
|
+
const dirs = fromFlag.length ? fromFlag : fromEnv.length ? fromEnv : [defaultClaudeDir()];
|
|
2036
|
+
return dirs.map((dir) => ({ dir, scope, label: labelFor(dir) }));
|
|
2037
|
+
}
|
|
2038
|
+
function resolveCodexHomes(opts) {
|
|
2039
|
+
const scope = opts.scope ?? "global";
|
|
2040
|
+
if (scope === "project") return [];
|
|
2041
|
+
const fromFlag = splitDirs(opts.override);
|
|
2042
|
+
const fromEnv = splitDirs(process.env.CODEX_HOME);
|
|
2043
|
+
return fromFlag.length ? fromFlag : fromEnv.length ? fromEnv : [defaultCodexHome()];
|
|
2044
|
+
}
|
|
1901
2045
|
|
|
1902
2046
|
// src/setup/operator-surface.ts
|
|
1903
2047
|
var SectionType = {
|
|
@@ -1910,15 +2054,25 @@ var SectionType = {
|
|
|
1910
2054
|
* carried a workspaceId and that workspace has agent-setup-bundle memories. */
|
|
1911
2055
|
WorkspaceConventions: "workspace-conventions"
|
|
1912
2056
|
};
|
|
1913
|
-
async function fetchSetup(cfg) {
|
|
2057
|
+
async function fetchSetup(cfg, namespaceSlug) {
|
|
1914
2058
|
const client = await makeClient(cfg);
|
|
2059
|
+
const query = {};
|
|
2060
|
+
if (cfg.workspaceId) query.workspaceId = cfg.workspaceId;
|
|
2061
|
+
if (namespaceSlug) query.namespaceSlug = namespaceSlug;
|
|
2062
|
+
const hasQuery = query.workspaceId !== void 0 || query.namespaceSlug !== void 0;
|
|
1915
2063
|
const { data, error } = await client.GET(
|
|
1916
2064
|
"/operator-surface/setup",
|
|
1917
|
-
|
|
2065
|
+
hasQuery ? { params: { query } } : {}
|
|
1918
2066
|
);
|
|
1919
2067
|
if (error) throw new Error(`GET /operator-surface/setup failed: ${JSON.stringify(error)}`);
|
|
1920
2068
|
return data;
|
|
1921
2069
|
}
|
|
2070
|
+
async function listNamespaces(cfg) {
|
|
2071
|
+
const client = await makeClient(cfg);
|
|
2072
|
+
const { data } = await client.GET("/mcp-aggregator/namespaces", {});
|
|
2073
|
+
const rows = data ?? [];
|
|
2074
|
+
return rows.filter((r) => typeof r.slug === "string").map((r) => ({ slug: r.slug, displayName: r.displayName ?? r.slug }));
|
|
2075
|
+
}
|
|
1922
2076
|
function findSurface(setup, surfaceKey) {
|
|
1923
2077
|
return setup.surfaces.find((s) => s.surfaceKey === surfaceKey);
|
|
1924
2078
|
}
|
|
@@ -1989,12 +2143,14 @@ async function resolveWorkspaceConventions(cfg, section) {
|
|
|
1989
2143
|
if (parseTagArtifactId(artifact.id)) continue;
|
|
1990
2144
|
const mem = await fetchMemoryFields(cfg, artifact.id);
|
|
1991
2145
|
if (typeof mem?.text === "string" && mem.text.trim().length > 0) {
|
|
1992
|
-
|
|
1993
|
-
|
|
2146
|
+
const ref = `${artifact.id}@v${mem.version ?? 1}`;
|
|
2147
|
+
parts.push(`<!-- @sechroom/cli:section source=${ref} -->
|
|
2148
|
+
${mem.text.trim()}`);
|
|
2149
|
+
refs.push(ref);
|
|
1994
2150
|
}
|
|
1995
2151
|
}
|
|
1996
2152
|
if (parts.length === 0) return null;
|
|
1997
|
-
return { body: parts.join("\n\n
|
|
2153
|
+
return { body: parts.join("\n\n"), refs };
|
|
1998
2154
|
}
|
|
1999
2155
|
async function createOverride(cfg, template, personalWorkspaceId) {
|
|
2000
2156
|
const client = await makeClient(cfg);
|
|
@@ -2022,51 +2178,66 @@ async function createOverride(cfg, template, personalWorkspaceId) {
|
|
|
2022
2178
|
function claudeDesktopConfigPath(home) {
|
|
2023
2179
|
switch (process.platform) {
|
|
2024
2180
|
case "darwin":
|
|
2025
|
-
return
|
|
2181
|
+
return join4(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
2026
2182
|
case "win32":
|
|
2027
|
-
return
|
|
2183
|
+
return join4(process.env.APPDATA ?? join4(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
2028
2184
|
default:
|
|
2029
|
-
return
|
|
2185
|
+
return join4(home, ".config", "Claude", "claude_desktop_config.json");
|
|
2030
2186
|
}
|
|
2031
2187
|
}
|
|
2032
|
-
function clientTargets(cwd) {
|
|
2033
|
-
const home =
|
|
2188
|
+
function clientTargets(cwd, opts = {}) {
|
|
2189
|
+
const home = homedir3();
|
|
2190
|
+
const claudeDir = opts.claudeDir ?? join4(home, ".claude");
|
|
2191
|
+
const codexHome = opts.codexHome ?? join4(home, ".codex");
|
|
2034
2192
|
return {
|
|
2035
2193
|
"claude-code": {
|
|
2036
2194
|
key: "claude-code",
|
|
2037
2195
|
label: "Claude Code",
|
|
2038
|
-
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path:
|
|
2039
|
-
instruction: { surfaceKey: "claude-code", path:
|
|
2196
|
+
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join4(cwd, ".mcp.json"), format: "json" },
|
|
2197
|
+
instruction: { surfaceKey: "claude-code", path: join4(cwd, "CLAUDE.md") }
|
|
2040
2198
|
},
|
|
2041
2199
|
"claude-desktop": {
|
|
2042
2200
|
key: "claude-desktop",
|
|
2043
2201
|
label: "Claude Desktop",
|
|
2044
2202
|
mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
|
|
2045
|
-
instruction: { surfaceKey: "claude-desktop", path:
|
|
2203
|
+
instruction: { surfaceKey: "claude-desktop", path: join4(claudeDir, "CLAUDE.md") }
|
|
2046
2204
|
},
|
|
2047
2205
|
codex: {
|
|
2048
2206
|
key: "codex",
|
|
2049
2207
|
label: "Codex CLI",
|
|
2050
|
-
mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path:
|
|
2051
|
-
instruction: { surfaceKey: "chatgpt", path:
|
|
2208
|
+
mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join4(codexHome, "config.toml"), format: "toml" },
|
|
2209
|
+
instruction: { surfaceKey: "chatgpt", path: join4(cwd, "AGENTS.md") }
|
|
2052
2210
|
},
|
|
2053
2211
|
cursor: {
|
|
2054
2212
|
key: "cursor",
|
|
2055
2213
|
label: "Cursor",
|
|
2056
|
-
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path:
|
|
2057
|
-
instruction: { surfaceKey: "chatgpt", path:
|
|
2214
|
+
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join4(cwd, ".cursor", "mcp.json"), format: "json" },
|
|
2215
|
+
instruction: { surfaceKey: "chatgpt", path: join4(cwd, "AGENTS.md") }
|
|
2216
|
+
},
|
|
2217
|
+
antigravity: {
|
|
2218
|
+
key: "antigravity",
|
|
2219
|
+
label: "Google Antigravity",
|
|
2220
|
+
// FR-sechroom-247 — Antigravity reads MCP from a GLOBAL, home-relative
|
|
2221
|
+
// `~/.gemini/config/mcp_config.json` (not cwd; not affected by
|
|
2222
|
+
// CLAUDE_CONFIG_DIR / CODEX_HOME). The snippet — `serverUrl`-shaped, no
|
|
2223
|
+
// `type` — comes from the `antigravity` server surface, so we don't
|
|
2224
|
+
// hardcode it here. Instructions go in the project `AGENTS.md`
|
|
2225
|
+
// (cross-tool, shared with Codex/Cursor).
|
|
2226
|
+
mcp: { surfaceKey: "antigravity", sectionType: SectionType.McpConfig, path: join4(home, ".gemini", "config", "mcp_config.json"), format: "json" },
|
|
2227
|
+
instruction: { surfaceKey: "antigravity", path: join4(cwd, "AGENTS.md") }
|
|
2058
2228
|
}
|
|
2059
2229
|
};
|
|
2060
2230
|
}
|
|
2061
|
-
var ALL_CLIENT_KEYS = ["claude-code", "claude-desktop", "codex", "cursor"];
|
|
2231
|
+
var ALL_CLIENT_KEYS = ["claude-code", "claude-desktop", "codex", "cursor", "antigravity"];
|
|
2062
2232
|
var DEFAULT_CLIENT_KEY = "claude-code";
|
|
2063
2233
|
function detectInstalledClients(cwd) {
|
|
2064
|
-
const home =
|
|
2234
|
+
const home = homedir3();
|
|
2065
2235
|
const detected = [];
|
|
2066
|
-
if (
|
|
2236
|
+
if (resolveClaudeTargets({}).some((t) => existsSync3(t.dir))) detected.push("claude-code");
|
|
2067
2237
|
if (existsSync3(dirname3(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
|
|
2068
|
-
if (
|
|
2069
|
-
if (existsSync3(
|
|
2238
|
+
if (resolveCodexHomes({}).some((d) => existsSync3(d))) detected.push("codex");
|
|
2239
|
+
if (existsSync3(join4(home, ".cursor")) || existsSync3(join4(cwd, ".cursor"))) detected.push("cursor");
|
|
2240
|
+
if (existsSync3(join4(home, ".gemini"))) detected.push("antigravity");
|
|
2070
2241
|
return detected;
|
|
2071
2242
|
}
|
|
2072
2243
|
|
|
@@ -2090,14 +2261,15 @@ function resolveLane(flagLane, cwd) {
|
|
|
2090
2261
|
const env = process.env.SECHROOM_LANE;
|
|
2091
2262
|
if (env) return env;
|
|
2092
2263
|
const start = cwd ?? process.cwd();
|
|
2093
|
-
const
|
|
2094
|
-
return
|
|
2264
|
+
const base = readSem(resolveSemPathForRead(start))?.values["code-lane"];
|
|
2265
|
+
if (!base) return void 0;
|
|
2266
|
+
return applyWorktreeLaneSuffix(base, start);
|
|
2095
2267
|
}
|
|
2096
|
-
var INTENT_FILE =
|
|
2268
|
+
var INTENT_FILE = join5(".sechroom", "continuity.json");
|
|
2097
2269
|
function resolveIntentPath(start) {
|
|
2098
2270
|
let dir = start;
|
|
2099
2271
|
for (; ; ) {
|
|
2100
|
-
const candidate =
|
|
2272
|
+
const candidate = join5(dir, INTENT_FILE);
|
|
2101
2273
|
if (existsSync4(candidate)) return candidate;
|
|
2102
2274
|
const parent = dirname4(dir);
|
|
2103
2275
|
if (parent === dir) return void 0;
|
|
@@ -2118,6 +2290,102 @@ function hasRequiredIntent(i) {
|
|
|
2118
2290
|
i.objective?.trim() && i.state?.trim() && i.lastAction?.trim() && i.nextAction?.trim() && i.resumeInstruction?.trim()
|
|
2119
2291
|
);
|
|
2120
2292
|
}
|
|
2293
|
+
async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScope, opts) {
|
|
2294
|
+
const lane = resolveLane(laneFlag, cwd);
|
|
2295
|
+
if (!lane) return false;
|
|
2296
|
+
const intent = readIntent(cwd);
|
|
2297
|
+
if (!intent || !hasRequiredIntent(intent)) return false;
|
|
2298
|
+
if (opts?.skipIfUnchanged && unchangedSinceLastPush(cwd, intent)) return false;
|
|
2299
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2300
|
+
const client = await makeClient(cfg);
|
|
2301
|
+
await client.POST("/continuity/snapshots", {
|
|
2302
|
+
body: {
|
|
2303
|
+
laneId: lane,
|
|
2304
|
+
scope: scopeFlag ?? intent.scope ?? defaultScope,
|
|
2305
|
+
currentObjective: intent.objective,
|
|
2306
|
+
currentState: intent.state,
|
|
2307
|
+
lastMeaningfulAction: intent.lastAction,
|
|
2308
|
+
nextIntendedAction: intent.nextAction,
|
|
2309
|
+
resumeInstruction: intent.resumeInstruction,
|
|
2310
|
+
activeConstraints: intent.constraints ?? null,
|
|
2311
|
+
openQuestions: intent.questions ?? null,
|
|
2312
|
+
surfaceMarkers: intent.surfaceMarkers ?? null,
|
|
2313
|
+
relevantArtifactIds: intent.artifacts ?? null,
|
|
2314
|
+
confidence: intent.confidence ?? null,
|
|
2315
|
+
// Frequent triggers (compaction, session-end) land within the FR-051 4h
|
|
2316
|
+
// window; Acknowledge lets the checkpoint persist on the lane.
|
|
2317
|
+
concurrentSessionPolicy: "Acknowledge"
|
|
2318
|
+
}
|
|
2319
|
+
});
|
|
2320
|
+
recordPush(cwd, intent);
|
|
2321
|
+
return true;
|
|
2322
|
+
}
|
|
2323
|
+
function ledgerPath(start) {
|
|
2324
|
+
const intent = resolveIntentPath(start);
|
|
2325
|
+
const dir = intent ? dirname4(intent) : join5(start, ".sechroom");
|
|
2326
|
+
return join5(dir, ".checkpoint-state.json");
|
|
2327
|
+
}
|
|
2328
|
+
function readLedger(start) {
|
|
2329
|
+
try {
|
|
2330
|
+
const p = ledgerPath(start);
|
|
2331
|
+
if (!existsSync4(p)) return {};
|
|
2332
|
+
return JSON.parse(readFileSync3(p, "utf8"));
|
|
2333
|
+
} catch {
|
|
2334
|
+
return {};
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
function intentHash(i) {
|
|
2338
|
+
const canonical = JSON.stringify({
|
|
2339
|
+
objective: i.objective ?? "",
|
|
2340
|
+
state: i.state ?? "",
|
|
2341
|
+
lastAction: i.lastAction ?? "",
|
|
2342
|
+
nextAction: i.nextAction ?? "",
|
|
2343
|
+
resumeInstruction: i.resumeInstruction ?? "",
|
|
2344
|
+
scope: i.scope ?? "",
|
|
2345
|
+
constraints: i.constraints ?? [],
|
|
2346
|
+
questions: i.questions ?? [],
|
|
2347
|
+
surfaceMarkers: i.surfaceMarkers ?? [],
|
|
2348
|
+
artifacts: i.artifacts ?? [],
|
|
2349
|
+
confidence: i.confidence ?? null
|
|
2350
|
+
});
|
|
2351
|
+
return createHash2("sha256").update(canonical, "utf8").digest("hex");
|
|
2352
|
+
}
|
|
2353
|
+
function recentlyCheckpointed(start, minutes) {
|
|
2354
|
+
const { lastEpochMs } = readLedger(start);
|
|
2355
|
+
return typeof lastEpochMs === "number" && Date.now() - lastEpochMs < minutes * 6e4;
|
|
2356
|
+
}
|
|
2357
|
+
function unchangedSinceLastPush(start, intent) {
|
|
2358
|
+
const ledger = readLedger(start);
|
|
2359
|
+
if (ledger.lastHash == null) return false;
|
|
2360
|
+
const path = resolveIntentPath(start);
|
|
2361
|
+
if (path && ledger.lastMtimeMs != null) {
|
|
2362
|
+
try {
|
|
2363
|
+
if (statSync2(path).mtimeMs <= ledger.lastMtimeMs) return true;
|
|
2364
|
+
} catch {
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
return intentHash(intent) === ledger.lastHash;
|
|
2368
|
+
}
|
|
2369
|
+
function recordPush(start, intent) {
|
|
2370
|
+
try {
|
|
2371
|
+
const p = ledgerPath(start);
|
|
2372
|
+
const path = resolveIntentPath(start);
|
|
2373
|
+
let mtimeMs;
|
|
2374
|
+
try {
|
|
2375
|
+
if (path) mtimeMs = statSync2(path).mtimeMs;
|
|
2376
|
+
} catch {
|
|
2377
|
+
mtimeMs = void 0;
|
|
2378
|
+
}
|
|
2379
|
+
mkdirSync3(dirname4(p), { recursive: true });
|
|
2380
|
+
const ledger = {
|
|
2381
|
+
lastEpochMs: Date.now(),
|
|
2382
|
+
lastMtimeMs: mtimeMs,
|
|
2383
|
+
lastHash: intentHash(intent)
|
|
2384
|
+
};
|
|
2385
|
+
writeFileSync3(p, JSON.stringify(ledger) + "\n");
|
|
2386
|
+
} catch {
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2121
2389
|
function formatContext(bundle, lane) {
|
|
2122
2390
|
const s = bundle?.latestSnapshot;
|
|
2123
2391
|
if (!s) return null;
|
|
@@ -2154,20 +2422,23 @@ function emitSessionStart(additionalContext) {
|
|
|
2154
2422
|
}) + "\n"
|
|
2155
2423
|
);
|
|
2156
2424
|
}
|
|
2157
|
-
var
|
|
2425
|
+
var CLAUDE_HOOK_COMMANDS = {
|
|
2158
2426
|
SessionStart: "sechroom hook session-start",
|
|
2159
|
-
PreCompact: "sechroom hook pre-compact"
|
|
2427
|
+
PreCompact: "sechroom hook pre-compact",
|
|
2428
|
+
SessionEnd: "sechroom hook session-end"
|
|
2429
|
+
};
|
|
2430
|
+
var CODEX_HOOK_COMMANDS = {
|
|
2431
|
+
SessionStart: "sechroom hook session-start",
|
|
2432
|
+
Stop: "sechroom hook session-end --debounce-minutes 10"
|
|
2160
2433
|
};
|
|
2161
|
-
var HOOK_EVENTS = ["SessionStart", "PreCompact"];
|
|
2162
2434
|
function hasHookCommand(config2, event, command) {
|
|
2163
2435
|
const groups = config2.hooks?.[event] ?? [];
|
|
2164
2436
|
return groups.some((g) => (g.hooks ?? []).some((h) => h.type === "command" && h.command === command));
|
|
2165
2437
|
}
|
|
2166
|
-
function mergeHooks(config2) {
|
|
2438
|
+
function mergeHooks(config2, commands) {
|
|
2167
2439
|
config2.hooks ??= {};
|
|
2168
2440
|
let added = 0;
|
|
2169
|
-
for (const event of
|
|
2170
|
-
const command = HOOK_COMMANDS[event];
|
|
2441
|
+
for (const [event, command] of Object.entries(commands)) {
|
|
2171
2442
|
if (hasHookCommand(config2, event, command)) continue;
|
|
2172
2443
|
const groups = config2.hooks[event] ??= [];
|
|
2173
2444
|
groups.push({ hooks: [{ type: "command", command }] });
|
|
@@ -2181,10 +2452,10 @@ function readJsonConfig2(path) {
|
|
|
2181
2452
|
if (!raw.trim()) return {};
|
|
2182
2453
|
return JSON.parse(raw);
|
|
2183
2454
|
}
|
|
2184
|
-
function installHooksJson(path, dryRun) {
|
|
2455
|
+
function installHooksJson(path, commands, dryRun) {
|
|
2185
2456
|
const existed = existsSync4(path) && readFileSync3(path, "utf8").trim().length > 0;
|
|
2186
2457
|
const config2 = readJsonConfig2(path);
|
|
2187
|
-
const added = mergeHooks(config2);
|
|
2458
|
+
const added = mergeHooks(config2, commands);
|
|
2188
2459
|
if (added === 0 && existed) return { path, status: "current" };
|
|
2189
2460
|
if (!dryRun) {
|
|
2190
2461
|
mkdirSync3(dirname4(path), { recursive: true });
|
|
@@ -2244,11 +2515,11 @@ function installHookSurfaces(surfaces, opts) {
|
|
|
2244
2515
|
const out = [];
|
|
2245
2516
|
for (const surface of surfaces) {
|
|
2246
2517
|
if (surface === "claude") {
|
|
2247
|
-
const path =
|
|
2248
|
-
out.push({ surface, results: [installHooksJson(path, opts.dryRun)] });
|
|
2518
|
+
const path = join5(opts.claudeDir, "settings.json");
|
|
2519
|
+
out.push({ surface, results: [installHooksJson(path, CLAUDE_HOOK_COMMANDS, opts.dryRun)] });
|
|
2249
2520
|
} else {
|
|
2250
|
-
const hooksJson = installHooksJson(
|
|
2251
|
-
const featureFlag = installCodexFeatureFlag(
|
|
2521
|
+
const hooksJson = installHooksJson(join5(opts.codexHome, "hooks.json"), CODEX_HOOK_COMMANDS, opts.dryRun);
|
|
2522
|
+
const featureFlag = installCodexFeatureFlag(join5(opts.codexHome, "config.toml"), opts.dryRun);
|
|
2252
2523
|
out.push({ surface, results: [hooksJson, featureFlag] });
|
|
2253
2524
|
}
|
|
2254
2525
|
}
|
|
@@ -2268,7 +2539,7 @@ function isSechroomOnPath() {
|
|
|
2268
2539
|
for (const dir of pathEnv.split(delimiter)) {
|
|
2269
2540
|
if (!dir) continue;
|
|
2270
2541
|
for (const name of names) {
|
|
2271
|
-
if (existsSync4(
|
|
2542
|
+
if (existsSync4(join5(dir, name))) return true;
|
|
2272
2543
|
}
|
|
2273
2544
|
}
|
|
2274
2545
|
return false;
|
|
@@ -2295,15 +2566,17 @@ Examples:
|
|
|
2295
2566
|
$ sechroom hook install --surface codex Codex only
|
|
2296
2567
|
$ sechroom hook install --local --dry-run preview the project .claude/settings.json
|
|
2297
2568
|
|
|
2298
|
-
Lane source (high -> low): --lane > SECHROOM_LANE > ./.
|
|
2569
|
+
Lane source (high -> low): --lane > SECHROOM_LANE > ./.sechroom/lane.json code-lane (D-binding-5).
|
|
2299
2570
|
Fail-soft: no lane / no auth / no-or-partial intent file / API error -> exit 0, never blocks.`
|
|
2300
2571
|
);
|
|
2301
|
-
hook.command("session-start").description("Resume the checkout's lane and emit continuity context for a SessionStart hook").option("--lane <laneId>", "Override the resolved lane (else SECHROOM_LANE, else ./.
|
|
2572
|
+
hook.command("session-start").description("Resume the checkout's lane and emit continuity context for a SessionStart hook").option("--lane <laneId>", "Override the resolved lane (else SECHROOM_LANE, else ./.sechroom/lane.json code-lane)").option("--surface <surface>", "Target surface: claude | codex (output is identical for session-start)", "claude").option("--max-artifacts <n>", "Cap artifacts in the resume bundle").action(async (opts, cmd) => {
|
|
2302
2573
|
try {
|
|
2303
2574
|
const raw = await readStdin();
|
|
2304
2575
|
const input = parseHookInput(raw);
|
|
2305
2576
|
const lane = resolveLane(opts.lane, input.cwd);
|
|
2306
2577
|
if (!lane) return process.exit(0);
|
|
2578
|
+
const semPath = resolveSemPathForRead(input.cwd ?? process.cwd());
|
|
2579
|
+
if (semPath) ensureContinuityScaffold(semPath);
|
|
2307
2580
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2308
2581
|
const client = await makeClient(cfg);
|
|
2309
2582
|
const { data } = await client.POST("/continuity/resume/lane", {
|
|
@@ -2322,57 +2595,67 @@ Fail-soft: no lane / no auth / no-or-partial intent file / API error -> exit 0,
|
|
|
2322
2595
|
return process.exit(0);
|
|
2323
2596
|
}
|
|
2324
2597
|
});
|
|
2325
|
-
hook.command("pre-compact").description("Save a continuity snapshot from the agent-maintained intent file on a PreCompact hook").option("--lane <laneId>", "Override the resolved lane (else SECHROOM_LANE, else ./.
|
|
2598
|
+
hook.command("pre-compact").description("Save a continuity snapshot from the agent-maintained intent file on a PreCompact hook").option("--lane <laneId>", "Override the resolved lane (else SECHROOM_LANE, else ./.sechroom/lane.json code-lane)").option("--scope <scope>", "Snapshot scope (else the intent file's `scope`, else 'compaction')").option("--surface <surface>", "Target surface: claude | codex (lifecycle-only on both)", "claude").action(async (opts, cmd) => {
|
|
2326
2599
|
try {
|
|
2327
2600
|
const raw = await readStdin();
|
|
2328
2601
|
const input = parseHookInput(raw);
|
|
2329
2602
|
const cwd = input.cwd ?? process.cwd();
|
|
2330
|
-
|
|
2331
|
-
if (!lane) return process.exit(0);
|
|
2332
|
-
const intent = readIntent(cwd);
|
|
2333
|
-
if (!intent || !hasRequiredIntent(intent)) return process.exit(0);
|
|
2334
|
-
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2335
|
-
const client = await makeClient(cfg);
|
|
2336
|
-
await client.POST("/continuity/snapshots", {
|
|
2337
|
-
body: {
|
|
2338
|
-
laneId: lane,
|
|
2339
|
-
scope: opts.scope ?? intent.scope ?? "compaction",
|
|
2340
|
-
currentObjective: intent.objective,
|
|
2341
|
-
currentState: intent.state,
|
|
2342
|
-
lastMeaningfulAction: intent.lastAction,
|
|
2343
|
-
nextIntendedAction: intent.nextAction,
|
|
2344
|
-
resumeInstruction: intent.resumeInstruction,
|
|
2345
|
-
activeConstraints: intent.constraints ?? null,
|
|
2346
|
-
openQuestions: intent.questions ?? null,
|
|
2347
|
-
surfaceMarkers: intent.surfaceMarkers ?? null,
|
|
2348
|
-
relevantArtifactIds: intent.artifacts ?? null,
|
|
2349
|
-
confidence: intent.confidence ?? null,
|
|
2350
|
-
// Compaction is infrequent, so the FR-051 clobber guard doesn't bite;
|
|
2351
|
-
// Acknowledge lets a within-window checkpoint land on the lane.
|
|
2352
|
-
concurrentSessionPolicy: "Acknowledge"
|
|
2353
|
-
}
|
|
2354
|
-
});
|
|
2603
|
+
await saveSnapshotFromIntent(cmd, cwd, opts.lane, opts.scope, "compaction", { skipIfUnchanged: true });
|
|
2355
2604
|
return process.exit(0);
|
|
2356
2605
|
} catch {
|
|
2357
2606
|
return process.exit(0);
|
|
2358
2607
|
}
|
|
2359
2608
|
});
|
|
2360
|
-
hook.command("
|
|
2609
|
+
hook.command("session-end").description("Save a continuity snapshot from the intent file on a SessionEnd (Claude) / Stop (Codex) hook").option("--lane <laneId>", "Override the resolved lane (else SECHROOM_LANE, else ./.sechroom/lane.json code-lane)").option("--scope <scope>", "Snapshot scope (else the intent file's `scope`, else 'session-end')").option("--surface <surface>", "Target surface: claude | codex (lifecycle-only on both)", "claude").option(
|
|
2610
|
+
"--debounce-minutes <n>",
|
|
2611
|
+
"skip if a hook checkpoint ran within this many minutes \u2014 for high-frequency triggers like Codex Stop (Claude SessionEnd passes none)"
|
|
2612
|
+
).action(async (opts, cmd) => {
|
|
2613
|
+
try {
|
|
2614
|
+
const raw = await readStdin();
|
|
2615
|
+
const input = parseHookInput(raw);
|
|
2616
|
+
const cwd = input.cwd ?? process.cwd();
|
|
2617
|
+
const debounce = opts.debounceMinutes != null ? Number(opts.debounceMinutes) : 0;
|
|
2618
|
+
if (debounce > 0 && recentlyCheckpointed(cwd, debounce)) return process.exit(0);
|
|
2619
|
+
await saveSnapshotFromIntent(cmd, cwd, opts.lane, opts.scope, "session-end", { skipIfUnchanged: true });
|
|
2620
|
+
return process.exit(0);
|
|
2621
|
+
} catch {
|
|
2622
|
+
return process.exit(0);
|
|
2623
|
+
}
|
|
2624
|
+
});
|
|
2625
|
+
hook.command("install").description("Wire the session-start + pre-compact hooks into Claude Code and/or Codex config").option("--surface <surface>", "Target surface: claude | codex | both (default: auto-detect installed surfaces)").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 change; write nothing").action((opts, cmd) => {
|
|
2626
|
+
const g = cmd.optsWithGlobals();
|
|
2361
2627
|
const dryRun = Boolean(opts.dryRun);
|
|
2362
2628
|
const cwd = process.cwd();
|
|
2629
|
+
let scope;
|
|
2363
2630
|
let surfaces;
|
|
2364
2631
|
try {
|
|
2632
|
+
scope = opts.local ? "project" : resolveScope(opts.scope);
|
|
2365
2633
|
surfaces = resolveSurfaces(opts.surface, cwd);
|
|
2366
2634
|
} catch (err2) {
|
|
2367
2635
|
process.stderr.write(`${err2.message}
|
|
2368
2636
|
`);
|
|
2369
2637
|
return process.exit(2);
|
|
2370
2638
|
}
|
|
2639
|
+
const claudeTargets = surfaces.includes("claude") ? resolveClaudeTargets({ override: g.claudeConfigDir, scope, cwd }) : [];
|
|
2640
|
+
const codexHomes = surfaces.includes("codex") ? resolveCodexHomes({ override: g.codexHome, scope }) : [];
|
|
2371
2641
|
const results = [];
|
|
2372
2642
|
try {
|
|
2373
|
-
const
|
|
2374
|
-
for (const
|
|
2375
|
-
|
|
2643
|
+
const multiClaude = claudeTargets.length > 1;
|
|
2644
|
+
for (const t of claudeTargets) {
|
|
2645
|
+
const surfaceResults = installHookSurfaces(["claude"], { dryRun, claudeDir: t.dir, codexHome: "" })[0].results;
|
|
2646
|
+
process.stdout.write(`${HOOK_SURFACE_LABEL.claude}${multiClaude ? ` (${t.label})` : ""}:
|
|
2647
|
+
`);
|
|
2648
|
+
for (const r of surfaceResults) {
|
|
2649
|
+
results.push(r);
|
|
2650
|
+
process.stdout.write(describe(r, dryRun) + "\n");
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
if (surfaces.includes("codex") && codexHomes.length === 0) {
|
|
2654
|
+
process.stdout.write("Codex has no project scope \u2014 skipped (use --scope global for Codex).\n");
|
|
2655
|
+
}
|
|
2656
|
+
for (const codexHome of codexHomes) {
|
|
2657
|
+
const surfaceResults = installHookSurfaces(["codex"], { dryRun, claudeDir: "", codexHome })[0].results;
|
|
2658
|
+
process.stdout.write(`${HOOK_SURFACE_LABEL.codex}:
|
|
2376
2659
|
`);
|
|
2377
2660
|
for (const r of surfaceResults) {
|
|
2378
2661
|
results.push(r);
|
|
@@ -2396,6 +2679,101 @@ Fail-soft: no lane / no auth / no-or-partial intent file / API error -> exit 0,
|
|
|
2396
2679
|
});
|
|
2397
2680
|
}
|
|
2398
2681
|
|
|
2682
|
+
// src/commands/checkpoint.ts
|
|
2683
|
+
function registerCheckpoint(program2) {
|
|
2684
|
+
program2.command("checkpoint").description(
|
|
2685
|
+
"Checkpoint working state: create a continuity snapshot (server-validated) AND sync ./.sechroom/continuity.json in one step"
|
|
2686
|
+
).option("--lane <laneId>", "Lane id (else SECHROOM_LANE, else ./.sechroom/lane.json code-lane)").option("--scope <scope>", "Snapshot scope (else the file's scope, else 'session')").option("--objective <text>", "Current objective").option("--state <text>", "Current state").option("--last-action <text>", "Last meaningful action").option("--next-action <text>", "Next intended action").option("--resume-instruction <text>", "Resume instruction").option("--constraint <text...>", "Active constraints (repeatable)").option("--question <text...>", "Open questions (repeatable)").option("--surface-marker <text...>", "Surface markers (repeatable)").option("--artifact <id...>", "Relevant artifact ids (repeatable)").option("--confidence <n>", "Confidence 0..1").option("--dry-run", "validate + print the snapshot payload without creating it or writing the file", false).addHelpText(
|
|
2687
|
+
"after",
|
|
2688
|
+
`
|
|
2689
|
+
File-first: reads ./.sechroom/continuity.json (kept current as you work) as the base; any flag
|
|
2690
|
+
overrides that field. The snapshot is created FIRST (server-validated), then the local file is
|
|
2691
|
+
written/normalized with the returned snapshotId. Lane: --lane > SECHROOM_LANE > ./.sechroom/lane.json code-lane.
|
|
2692
|
+
|
|
2693
|
+
Examples:
|
|
2694
|
+
$ sechroom checkpoint snapshot from ./.sechroom/continuity.json, then sync it
|
|
2695
|
+
$ sechroom checkpoint --next-action "..." override one field, keep the rest from the file
|
|
2696
|
+
$ sechroom checkpoint --lane claude-code-chris --objective "..." --state "..." \\
|
|
2697
|
+
--last-action "..." --next-action "..." --resume-instruction "..."`
|
|
2698
|
+
).action(async (opts, cmd) => {
|
|
2699
|
+
const cwd = process.cwd();
|
|
2700
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2701
|
+
const json = Boolean(cmd.optsWithGlobals().json);
|
|
2702
|
+
const base = readIntent(cwd) ?? {};
|
|
2703
|
+
const merged = {
|
|
2704
|
+
objective: opts.objective ?? base.objective,
|
|
2705
|
+
state: opts.state ?? base.state,
|
|
2706
|
+
lastAction: opts.lastAction ?? base.lastAction,
|
|
2707
|
+
nextAction: opts.nextAction ?? base.nextAction,
|
|
2708
|
+
resumeInstruction: opts.resumeInstruction ?? base.resumeInstruction,
|
|
2709
|
+
scope: opts.scope ?? base.scope,
|
|
2710
|
+
constraints: opts.constraint ?? base.constraints,
|
|
2711
|
+
questions: opts.question ?? base.questions,
|
|
2712
|
+
surfaceMarkers: opts.surfaceMarker ?? base.surfaceMarkers,
|
|
2713
|
+
artifacts: opts.artifact ?? base.artifacts,
|
|
2714
|
+
confidence: opts.confidence != null ? Number(opts.confidence) : base.confidence
|
|
2715
|
+
};
|
|
2716
|
+
const lane = resolveLane(opts.lane, cwd);
|
|
2717
|
+
if (!lane) {
|
|
2718
|
+
fail(
|
|
2719
|
+
"no lane resolved \u2014 pass --lane, set SECHROOM_LANE, or pin one in ./.sechroom/lane.json (code-lane). See `sechroom lane`."
|
|
2720
|
+
);
|
|
2721
|
+
}
|
|
2722
|
+
const required = [
|
|
2723
|
+
["objective", "--objective"],
|
|
2724
|
+
["state", "--state"],
|
|
2725
|
+
["lastAction", "--last-action"],
|
|
2726
|
+
["nextAction", "--next-action"],
|
|
2727
|
+
["resumeInstruction", "--resume-instruction"]
|
|
2728
|
+
];
|
|
2729
|
+
const missing = required.filter(([k]) => !String(merged[k] ?? "").trim()).map(([, flag]) => flag);
|
|
2730
|
+
if (missing.length > 0) {
|
|
2731
|
+
fail(
|
|
2732
|
+
`missing required field(s): ${missing.join(", ")} \u2014 supply via flag or in ./.sechroom/continuity.json`
|
|
2733
|
+
);
|
|
2734
|
+
}
|
|
2735
|
+
const scope = merged.scope ?? "session";
|
|
2736
|
+
const body = {
|
|
2737
|
+
laneId: lane,
|
|
2738
|
+
scope,
|
|
2739
|
+
currentObjective: merged.objective,
|
|
2740
|
+
currentState: merged.state,
|
|
2741
|
+
lastMeaningfulAction: merged.lastAction,
|
|
2742
|
+
nextIntendedAction: merged.nextAction,
|
|
2743
|
+
resumeInstruction: merged.resumeInstruction,
|
|
2744
|
+
activeConstraints: merged.constraints ?? null,
|
|
2745
|
+
openQuestions: merged.questions ?? null,
|
|
2746
|
+
surfaceMarkers: merged.surfaceMarkers ?? null,
|
|
2747
|
+
relevantArtifactIds: merged.artifacts ?? null,
|
|
2748
|
+
confidence: merged.confidence ?? null,
|
|
2749
|
+
// Explicit checkpoints are often within the FR-051 4h window; Acknowledge
|
|
2750
|
+
// lets one land on the lane (matches `hook pre-compact`).
|
|
2751
|
+
concurrentSessionPolicy: "Acknowledge"
|
|
2752
|
+
};
|
|
2753
|
+
if (opts.dryRun) {
|
|
2754
|
+
emit({ dryRun: true, lane, scope, wouldCreate: body }, json);
|
|
2755
|
+
return;
|
|
2756
|
+
}
|
|
2757
|
+
const data = await runApi("Creating snapshot", async () => {
|
|
2758
|
+
const client = await makeClient(cfg);
|
|
2759
|
+
return client.POST("/continuity/snapshots", { body });
|
|
2760
|
+
});
|
|
2761
|
+
const path = resolveIntentPath(cwd) ?? join6(cwd, INTENT_FILE);
|
|
2762
|
+
const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
|
|
2763
|
+
mkdirSync4(dirname5(path), { recursive: true });
|
|
2764
|
+
writeFileSync4(path, JSON.stringify(fileBody, null, 2) + "\n");
|
|
2765
|
+
recordPush(cwd, merged);
|
|
2766
|
+
if (json) {
|
|
2767
|
+
emit({ snapshotId: data.snapshotId, lane, scope, file: path }, true);
|
|
2768
|
+
return;
|
|
2769
|
+
}
|
|
2770
|
+
process.stdout.write(
|
|
2771
|
+
`${style.bold("\u2713")} checkpoint ${style.bold(data.snapshotId)} ${style.dim(`(lane ${lane}, scope ${scope})`)} \u2014 synced ${path}
|
|
2772
|
+
`
|
|
2773
|
+
);
|
|
2774
|
+
});
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2399
2777
|
// src/commands/account.ts
|
|
2400
2778
|
function registerId(program2) {
|
|
2401
2779
|
const id = program2.command("id").description("Allocate human-authored id sequences (FR-*, D-*)");
|
|
@@ -2464,7 +2842,7 @@ Examples:
|
|
|
2464
2842
|
});
|
|
2465
2843
|
emitAction("updated profile", data, cmd.optsWithGlobals().json);
|
|
2466
2844
|
});
|
|
2467
|
-
account.command("feed").description("Your recent memory feed (GET /me/memories/feed)").option("--limit <n>", "Max results", "20").option("--cursor <cursor>", "Opaque paging cursor").option("--query <query>", "Free-text filter").option("--filter-tags <tags>", "Comma-separated tag filter").option("--include-archived", "Include archived memories", false).option("--include-text", "Include memory body text", false).action(async (opts, cmd) => {
|
|
2845
|
+
account.command("feed").description("Your recent memory feed (GET /me/memories/feed)").option("--limit <n>", "Max results", "20").option("--cursor <cursor>", "Opaque paging cursor").option("--query <query>", "Free-text filter").option("--filter-tags <tags>", "Comma-separated tag filter").option("--include-archived", "Include archived memories", false).option("--include-text", "Include memory body text", false).option("--since <iso>", "Only contributions updated since this ISO-8601 timestamp (updatedSince)").option("--order <order>", "Order: LastTouchedDesc | LastTouchedAsc | FirstTouchedDesc | FirstTouchedAsc").action(async (opts, cmd) => {
|
|
2468
2846
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2469
2847
|
const data = await runApi("Fetching feed", async () => {
|
|
2470
2848
|
const client = await makeClient(cfg);
|
|
@@ -2476,7 +2854,9 @@ Examples:
|
|
|
2476
2854
|
includeText: Boolean(opts.includeText),
|
|
2477
2855
|
...opts.cursor ? { cursor: opts.cursor } : {},
|
|
2478
2856
|
...opts.query ? { query: opts.query } : {},
|
|
2479
|
-
...opts.filterTags ? { filterTags: opts.filterTags } : {}
|
|
2857
|
+
...opts.filterTags ? { filterTags: opts.filterTags } : {},
|
|
2858
|
+
...opts.since ? { updatedSince: opts.since } : {},
|
|
2859
|
+
...opts.order ? { orderBy: opts.order } : {}
|
|
2480
2860
|
}
|
|
2481
2861
|
}
|
|
2482
2862
|
});
|
|
@@ -2612,16 +2992,16 @@ Examples:
|
|
|
2612
2992
|
}
|
|
2613
2993
|
|
|
2614
2994
|
// src/setup/apply.ts
|
|
2615
|
-
import { createHash as
|
|
2616
|
-
import { mkdirSync as
|
|
2617
|
-
import { dirname as
|
|
2995
|
+
import { createHash as createHash3 } from "crypto";
|
|
2996
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5, existsSync as existsSync5 } from "fs";
|
|
2997
|
+
import { dirname as dirname6 } from "path";
|
|
2618
2998
|
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
2619
2999
|
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
2620
3000
|
function normalizeBody(s) {
|
|
2621
3001
|
return s.replace(/\r\n/g, "\n").trim();
|
|
2622
3002
|
}
|
|
2623
3003
|
function bodySha256(body) {
|
|
2624
|
-
return
|
|
3004
|
+
return createHash3("sha256").update(normalizeBody(body), "utf8").digest("hex");
|
|
2625
3005
|
}
|
|
2626
3006
|
function renderBlock(write) {
|
|
2627
3007
|
const body = normalizeBody(write.body);
|
|
@@ -2667,7 +3047,7 @@ function parseManagedBlock(content, block) {
|
|
|
2667
3047
|
return null;
|
|
2668
3048
|
}
|
|
2669
3049
|
function ensureDir2(path) {
|
|
2670
|
-
|
|
3050
|
+
mkdirSync5(dirname6(path), { recursive: true });
|
|
2671
3051
|
}
|
|
2672
3052
|
function readOr(path, fallback) {
|
|
2673
3053
|
try {
|
|
@@ -2690,7 +3070,7 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
2690
3070
|
current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
|
|
2691
3071
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
2692
3072
|
ensureDir2(path);
|
|
2693
|
-
|
|
3073
|
+
writeFileSync5(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
|
|
2694
3074
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
2695
3075
|
}
|
|
2696
3076
|
function mergeCodexToml(path, snippet, dryRun) {
|
|
@@ -2701,7 +3081,7 @@ function mergeCodexToml(path, snippet, dryRun) {
|
|
|
2701
3081
|
const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
|
|
2702
3082
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
2703
3083
|
ensureDir2(path);
|
|
2704
|
-
|
|
3084
|
+
writeFileSync5(path, next, { mode: 384 });
|
|
2705
3085
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
2706
3086
|
}
|
|
2707
3087
|
function writeInstructionBlock(path, write, dryRun) {
|
|
@@ -2709,7 +3089,7 @@ function writeInstructionBlock(path, write, dryRun) {
|
|
|
2709
3089
|
const next = computeBlockFile(readOr(path, ""), write);
|
|
2710
3090
|
if (dryRun) return { kind: "instruction", path, status: "dry-run" };
|
|
2711
3091
|
ensureDir2(path);
|
|
2712
|
-
|
|
3092
|
+
writeFileSync5(path, next);
|
|
2713
3093
|
return { kind: "instruction", path, status: existed ? "merged" : "created" };
|
|
2714
3094
|
}
|
|
2715
3095
|
function computeBlockFile(current, write) {
|
|
@@ -2750,7 +3130,7 @@ function applyBlock(path, write, mode, dryRun) {
|
|
|
2750
3130
|
const next = computeBlockFile(current, write);
|
|
2751
3131
|
if (!dryRun) {
|
|
2752
3132
|
ensureDir2(proposedPath);
|
|
2753
|
-
|
|
3133
|
+
writeFileSync5(proposedPath, next);
|
|
2754
3134
|
}
|
|
2755
3135
|
return {
|
|
2756
3136
|
kind: "instruction",
|
|
@@ -2821,12 +3201,14 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
2821
3201
|
}
|
|
2822
3202
|
|
|
2823
3203
|
// src/setup/hooks-offer.ts
|
|
2824
|
-
import { homedir as homedir4 } from "os";
|
|
2825
3204
|
async function maybeOfferHooks(opts) {
|
|
2826
3205
|
if (opts.dryRun) return;
|
|
2827
3206
|
const cwd = opts.cwd ?? process.cwd();
|
|
3207
|
+
const scope = opts.scope ?? "global";
|
|
2828
3208
|
const surfaces = detectHookSurfaces(cwd);
|
|
2829
3209
|
if (surfaces.length === 0) return;
|
|
3210
|
+
const claudeTargets = surfaces.includes("claude") ? resolveClaudeTargets({ override: opts.claudeConfigDir, scope, cwd }) : [];
|
|
3211
|
+
const codexHomes = surfaces.includes("codex") ? resolveCodexHomes({ override: opts.codexHome, scope }) : [];
|
|
2830
3212
|
const names = surfaces.map((s) => HOOK_SURFACE_LABEL[s]).join(" + ");
|
|
2831
3213
|
process.stderr.write(
|
|
2832
3214
|
`
|
|
@@ -2837,15 +3219,28 @@ auto-resumes where you left off and checkpoints working state before compacting.
|
|
|
2837
3219
|
const install = opts.yes ? true : canPrompt() ? await promptYesNo(`Install the continuity hooks for ${names}?`) : false;
|
|
2838
3220
|
if (!install) return;
|
|
2839
3221
|
try {
|
|
2840
|
-
const installed = installHookSurfaces(surfaces, { dryRun: false, cwd, home: homedir4() });
|
|
2841
3222
|
let changed = false;
|
|
2842
|
-
|
|
3223
|
+
const emit2 = (surface, results, label) => {
|
|
2843
3224
|
for (const r of results) {
|
|
2844
3225
|
if (r.status !== "current") changed = true;
|
|
2845
3226
|
const verb = r.status === "current" ? "already configured" : r.status === "created" ? "created" : "updated";
|
|
2846
|
-
|
|
3227
|
+
const tag = label ? ` ${style.dim(`(${label})`)}` : "";
|
|
3228
|
+
process.stderr.write(`${style.green("\u2713")} ${HOOK_SURFACE_LABEL[surface]}${tag}: ${r.path} (${verb})
|
|
2847
3229
|
`);
|
|
2848
3230
|
}
|
|
3231
|
+
};
|
|
3232
|
+
const multiClaude = claudeTargets.length > 1;
|
|
3233
|
+
for (const t of claudeTargets) {
|
|
3234
|
+
const results = installHookSurfaces(["claude"], { dryRun: false, claudeDir: t.dir, codexHome: "" })[0].results;
|
|
3235
|
+
emit2("claude", results, multiClaude ? t.label : void 0);
|
|
3236
|
+
}
|
|
3237
|
+
if (surfaces.includes("codex") && codexHomes.length === 0) {
|
|
3238
|
+
process.stderr.write(`${style.dim("Codex has no project scope \u2014 skipped (use --scope global for Codex).")}
|
|
3239
|
+
`);
|
|
3240
|
+
}
|
|
3241
|
+
for (const codexHome of codexHomes) {
|
|
3242
|
+
const results = installHookSurfaces(["codex"], { dryRun: false, claudeDir: "", codexHome })[0].results;
|
|
3243
|
+
emit2("codex", results);
|
|
2849
3244
|
}
|
|
2850
3245
|
if (changed) {
|
|
2851
3246
|
process.stderr.write(`${style.dim("Restart (or reload) your agent for the hooks to take effect.")}
|
|
@@ -2859,9 +3254,8 @@ auto-resumes where you left off and checkpoints working state before compacting.
|
|
|
2859
3254
|
}
|
|
2860
3255
|
|
|
2861
3256
|
// src/setup/skills-offer.ts
|
|
2862
|
-
import { mkdirSync as
|
|
2863
|
-
import {
|
|
2864
|
-
import { join as join5 } from "path";
|
|
3257
|
+
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
3258
|
+
import { join as join8 } from "path";
|
|
2865
3259
|
|
|
2866
3260
|
// src/setup/lane-pin.ts
|
|
2867
3261
|
var CODE_LANE_PREFIX_BY_CLIENT = {
|
|
@@ -2943,57 +3337,181 @@ I can pin this checkout's lane so operator skills + the continuity hook resolve
|
|
|
2943
3337
|
writePin(code || void 0, design || void 0);
|
|
2944
3338
|
}
|
|
2945
3339
|
|
|
2946
|
-
// src/setup/
|
|
2947
|
-
var
|
|
3340
|
+
// src/setup/skill-resolution.ts
|
|
3341
|
+
var SYSTEM_WORKSPACE_ID = "wsp_system";
|
|
3342
|
+
var SKILL_ROLE_TAG = "sechroom:role:skill-template";
|
|
3343
|
+
var SKILL_NAME_PREFIX = "skill:";
|
|
3344
|
+
var AGENT_ROLE_TAG = "sechroom:role:agent-template";
|
|
3345
|
+
var AGENT_NAME_PREFIX = "agent:";
|
|
3346
|
+
var REFERENCE_ROLE_TAG = "sechroom:role:skill-reference";
|
|
3347
|
+
var REFERENCE_NAME_PREFIX = "component:";
|
|
3348
|
+
function tagsOf(row) {
|
|
3349
|
+
const m = row?.item ?? row;
|
|
3350
|
+
return m?.tags ?? m?.Tags ?? [];
|
|
3351
|
+
}
|
|
2948
3352
|
function tagValue(tags, prefix) {
|
|
2949
3353
|
return tags.find((t) => t.startsWith(prefix))?.slice(prefix.length);
|
|
2950
3354
|
}
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
3355
|
+
function bodyOf(row) {
|
|
3356
|
+
const m = row?.item ?? row;
|
|
3357
|
+
return m?.text ?? m?.Text ?? "";
|
|
3358
|
+
}
|
|
3359
|
+
function entriesFromRows(rows, surface, source, roleTag, namePrefix) {
|
|
3360
|
+
const out = /* @__PURE__ */ new Map();
|
|
3361
|
+
for (const row of rows ?? []) {
|
|
3362
|
+
const tags = tagsOf(row);
|
|
3363
|
+
if (!tags.includes(roleTag)) continue;
|
|
3364
|
+
if (tagValue(tags, "target:") !== surface) continue;
|
|
3365
|
+
const name = tagValue(tags, namePrefix);
|
|
3366
|
+
if (!name) continue;
|
|
3367
|
+
out.set(name, { name, body: bodyOf(row), source });
|
|
3368
|
+
}
|
|
3369
|
+
return out;
|
|
3370
|
+
}
|
|
3371
|
+
function resolveByRole(systemRows, personalRows, surface, roleTag, namePrefix) {
|
|
3372
|
+
const merged = entriesFromRows(systemRows, surface, "system", roleTag, namePrefix);
|
|
3373
|
+
for (const [name, item] of entriesFromRows(personalRows, surface, "personal", roleTag, namePrefix)) {
|
|
3374
|
+
merged.set(name, item);
|
|
3375
|
+
}
|
|
3376
|
+
return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
3377
|
+
}
|
|
3378
|
+
function resolveSkills(systemRows, personalRows, surface) {
|
|
3379
|
+
return resolveByRole(systemRows, personalRows, surface, SKILL_ROLE_TAG, SKILL_NAME_PREFIX);
|
|
3380
|
+
}
|
|
3381
|
+
function resolveAgents(systemRows, personalRows, surface) {
|
|
3382
|
+
return resolveByRole(systemRows, personalRows, surface, AGENT_ROLE_TAG, AGENT_NAME_PREFIX);
|
|
3383
|
+
}
|
|
3384
|
+
function resolveReferences(systemRows, personalRows, surface) {
|
|
3385
|
+
return resolveByRole(systemRows, personalRows, surface, REFERENCE_ROLE_TAG, REFERENCE_NAME_PREFIX);
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
// src/setup/skill-resolution-io.ts
|
|
3389
|
+
var AGENT_TARGET = { "claude-code": "claude-agent" };
|
|
3390
|
+
function agentTargetFor(surface) {
|
|
3391
|
+
return AGENT_TARGET[surface] ?? `${surface}-agent`;
|
|
3392
|
+
}
|
|
3393
|
+
async function fetchFeedRows(cfg, workspaceId) {
|
|
2955
3394
|
try {
|
|
2956
3395
|
const client = await makeClient(cfg);
|
|
2957
3396
|
const feed = await client.GET("/workspaces/{workspaceId}/memories/feed", {
|
|
2958
3397
|
params: {
|
|
2959
|
-
path: { workspaceId
|
|
3398
|
+
path: { workspaceId },
|
|
3399
|
+
// cascadeWorkspaces: skills land in an "Operator Skills" SUB-workspace;
|
|
3400
|
+
// includeText: the feed omits bodies by default, we need them for SKILL.md.
|
|
2960
3401
|
query: { limit: 200, cascadeWorkspaces: true, includeText: true }
|
|
2961
3402
|
}
|
|
2962
3403
|
}).then((r) => r.data).catch(() => void 0);
|
|
2963
|
-
|
|
3404
|
+
return feed?.results ?? feed?.Results ?? [];
|
|
3405
|
+
} catch {
|
|
3406
|
+
return [];
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
async function fetchTemplateRows(cfg, personalWorkspaceId) {
|
|
3410
|
+
const [systemRows, personalRows] = await Promise.all([
|
|
3411
|
+
fetchFeedRows(cfg, SYSTEM_WORKSPACE_ID),
|
|
3412
|
+
personalWorkspaceId ? fetchFeedRows(cfg, personalWorkspaceId) : Promise.resolve([])
|
|
3413
|
+
]);
|
|
3414
|
+
return { systemRows, personalRows };
|
|
3415
|
+
}
|
|
3416
|
+
function resolveSkillSet(rows, surface) {
|
|
3417
|
+
return resolveSkills(rows.systemRows, rows.personalRows, surface);
|
|
3418
|
+
}
|
|
3419
|
+
function resolveAgentSet(rows, surface) {
|
|
3420
|
+
return resolveAgents(rows.systemRows, rows.personalRows, agentTargetFor(surface));
|
|
3421
|
+
}
|
|
3422
|
+
function resolveReferenceSet(rows, surface) {
|
|
3423
|
+
return resolveReferences(rows.systemRows, rows.personalRows, surface);
|
|
3424
|
+
}
|
|
3425
|
+
|
|
3426
|
+
// src/setup/skills-lock.ts
|
|
3427
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
3428
|
+
import { join as join7 } from "path";
|
|
3429
|
+
var SKILLS_LOCK = ".sechroom-skills.json";
|
|
3430
|
+
var DEFAULT_SKILLS_SLUG = "operator-skills";
|
|
3431
|
+
function skillsDir(configDir) {
|
|
3432
|
+
return join7(configDir, "skills");
|
|
3433
|
+
}
|
|
3434
|
+
function agentsDir(configDir) {
|
|
3435
|
+
return join7(configDir, "agents");
|
|
3436
|
+
}
|
|
3437
|
+
function readSkillsLock(dir) {
|
|
3438
|
+
const lockPath = join7(dir, SKILLS_LOCK);
|
|
3439
|
+
if (!existsSync6(lockPath)) return {};
|
|
3440
|
+
try {
|
|
3441
|
+
return JSON.parse(readFileSync5(lockPath, "utf8"));
|
|
2964
3442
|
} catch {
|
|
3443
|
+
return {};
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
function writeSkillsLock(dir, lock) {
|
|
3447
|
+
mkdirSync6(dir, { recursive: true });
|
|
3448
|
+
writeFileSync6(join7(dir, SKILLS_LOCK), JSON.stringify(lock, null, 2) + "\n");
|
|
3449
|
+
}
|
|
3450
|
+
function recordMaterialisedSkills(dir, slug, skills, meta = {}) {
|
|
3451
|
+
const lock = readSkillsLock(dir);
|
|
3452
|
+
lock[slug] = { surface: meta.surface, skills: [...skills].sort() };
|
|
3453
|
+
writeSkillsLock(dir, lock);
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
// src/setup/skills-offer.ts
|
|
3457
|
+
async function maybeOfferSkills(cfg, personalWorkspaceId, opts) {
|
|
3458
|
+
const surface = opts.surface ?? "claude-code";
|
|
3459
|
+
const configDir = opts.configDir ?? resolveClaudeTargets({})[0].dir;
|
|
3460
|
+
const rows = await fetchTemplateRows(cfg, personalWorkspaceId);
|
|
3461
|
+
const skills = resolveSkillSet(rows, surface);
|
|
3462
|
+
const agents = resolveAgentSet(rows, surface);
|
|
3463
|
+
if (skills.length === 0 && agents.length === 0) return;
|
|
3464
|
+
const sDir = skillsDir(configDir);
|
|
3465
|
+
const aDir = agentsDir(configDir);
|
|
3466
|
+
if (opts.dryRun) {
|
|
3467
|
+
const lines = (label, items) => items.length === 0 ? "" : `
|
|
3468
|
+
Would materialise ${style.bold(String(items.length))} ${label} for ${surface}:
|
|
3469
|
+
` + items.map((s) => ` ${s.name} ${style.dim(`[${s.source}]`)}`).join("\n") + "\n";
|
|
3470
|
+
process.stderr.write(lines("operator skill(s)", skills) + lines("agent(s)", agents));
|
|
2965
3471
|
return;
|
|
2966
3472
|
}
|
|
2967
|
-
const
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
}
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
`
|
|
2981
|
-
Found ${style.bold(String(names.length))} operator skill(s) installed in your workspace: ${names.join(", ")}.
|
|
2982
|
-
`
|
|
2983
|
-
);
|
|
2984
|
-
const dir = join5(homedir5(), ".claude", "skills");
|
|
2985
|
-
const materialise = opts.yes ? true : canPrompt() ? await promptYesNo(`Write them to ${dir}/ so ${surface} can use them?`) : false;
|
|
3473
|
+
const summary = [
|
|
3474
|
+
skills.length > 0 ? `${style.bold(String(skills.length))} skill(s)` : "",
|
|
3475
|
+
agents.length > 0 ? `${style.bold(String(agents.length))} agent(s)` : ""
|
|
3476
|
+
].filter(Boolean).join(" + ");
|
|
3477
|
+
process.stderr.write(`
|
|
3478
|
+
Found ${summary} available to you for ${surface}.
|
|
3479
|
+
`);
|
|
3480
|
+
if (skills.length > 0) process.stderr.write(` skills: ${skills.map((s) => s.name).join(", ")}
|
|
3481
|
+
`);
|
|
3482
|
+
if (agents.length > 0) process.stderr.write(` agents: ${agents.map((a) => a.name).join(", ")}
|
|
3483
|
+
`);
|
|
3484
|
+
const dest = [skills.length > 0 ? `${sDir}/` : "", agents.length > 0 ? `${aDir}/` : ""].filter(Boolean).join(" + ");
|
|
3485
|
+
const materialise = opts.yes ? true : canPrompt() ? await promptYesNo(`Write them to ${dest} so ${surface} can use them?`) : false;
|
|
2986
3486
|
if (!materialise) return;
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
const
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
3487
|
+
if (skills.length > 0) {
|
|
3488
|
+
const written = [];
|
|
3489
|
+
for (const s of skills) {
|
|
3490
|
+
mkdirSync7(join8(sDir, s.name), { recursive: true });
|
|
3491
|
+
writeFileSync7(join8(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
|
|
3492
|
+
written.push(s.name);
|
|
3493
|
+
}
|
|
3494
|
+
recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
3495
|
+
process.stderr.write(`${style.green("\u2713")} wrote ${written.length} skill(s) to ${sDir}
|
|
3496
|
+
`);
|
|
2993
3497
|
}
|
|
2994
|
-
|
|
3498
|
+
if (agents.length > 0) {
|
|
3499
|
+
mkdirSync7(aDir, { recursive: true });
|
|
3500
|
+
const written = [];
|
|
3501
|
+
for (const a of agents) {
|
|
3502
|
+
const file = `${a.name}.md`;
|
|
3503
|
+
writeFileSync7(join8(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
|
|
3504
|
+
written.push(file);
|
|
3505
|
+
}
|
|
3506
|
+
recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
3507
|
+
process.stderr.write(`${style.green("\u2713")} wrote ${written.length} agent(s) to ${aDir}
|
|
2995
3508
|
`);
|
|
2996
|
-
|
|
3509
|
+
}
|
|
3510
|
+
await ensureLanePin(cfg, {
|
|
3511
|
+
yes: opts.yes,
|
|
3512
|
+
dryRun: opts.dryRun,
|
|
3513
|
+
clients: [surface]
|
|
3514
|
+
});
|
|
2997
3515
|
}
|
|
2998
3516
|
|
|
2999
3517
|
// src/commands/setup.ts
|
|
@@ -3031,14 +3549,14 @@ version, the shared template stays clean, and you can discard back anytime.
|
|
|
3031
3549
|
}
|
|
3032
3550
|
function resolveClientKeys(raw) {
|
|
3033
3551
|
const targets = clientTargets(process.cwd());
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
for (const k of
|
|
3552
|
+
const tokens = (Array.isArray(raw) ? raw : [raw]).flatMap((t) => t.split(",")).map((k) => k.trim()).filter(Boolean);
|
|
3553
|
+
if (tokens.includes("all")) return [...ALL_CLIENT_KEYS];
|
|
3554
|
+
for (const k of tokens) {
|
|
3037
3555
|
if (!targets[k]) {
|
|
3038
3556
|
fail(`unknown client '${k}'. Known: ${ALL_CLIENT_KEYS.join(", ")}, or 'all'.`);
|
|
3039
3557
|
}
|
|
3040
3558
|
}
|
|
3041
|
-
return
|
|
3559
|
+
return [...new Set(tokens)];
|
|
3042
3560
|
}
|
|
3043
3561
|
function printActions(client, actions) {
|
|
3044
3562
|
process.stdout.write(`
|
|
@@ -3050,24 +3568,96 @@ ${client.label} (${client.key}):
|
|
|
3050
3568
|
`);
|
|
3051
3569
|
}
|
|
3052
3570
|
}
|
|
3571
|
+
function resolveEvalMode(opts) {
|
|
3572
|
+
return opts.check ? "check" : opts.force ? "force" : "apply";
|
|
3573
|
+
}
|
|
3574
|
+
function summarizeEval(result, mode, json, dryRun) {
|
|
3575
|
+
const counts = { current: 0, stale: 0, drift: 0, absent: 0 };
|
|
3576
|
+
for (const { actions } of result) for (const a of actions) if (a.eval) counts[a.eval]++;
|
|
3577
|
+
const wouldChange = counts.stale + counts.drift + counts.absent;
|
|
3578
|
+
if (mode === "check") {
|
|
3579
|
+
if (!json) {
|
|
3580
|
+
if (wouldChange === 0) {
|
|
3581
|
+
process.stdout.write("\u2713 all instruction blocks are up to date.\n");
|
|
3582
|
+
} else {
|
|
3583
|
+
const bits = [];
|
|
3584
|
+
if (counts.stale) bits.push(`${counts.stale} out of date`);
|
|
3585
|
+
if (counts.drift) bits.push(`${counts.drift} with local edits`);
|
|
3586
|
+
if (counts.absent) bits.push(`${counts.absent} not yet written`);
|
|
3587
|
+
process.stderr.write(
|
|
3588
|
+
`\u26A0 ${wouldChange} instruction block(s) would change: ${bits.join(", ")}. Re-run with ${style.cyan("--refresh")}.
|
|
3589
|
+
`
|
|
3590
|
+
);
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
process.exit(wouldChange === 0 ? 0 : 1);
|
|
3594
|
+
}
|
|
3595
|
+
if (json) return;
|
|
3596
|
+
if (!dryRun && counts.stale) {
|
|
3597
|
+
process.stderr.write(`\u21BB refreshed ${counts.stale} section(s) the server had moved
|
|
3598
|
+
`);
|
|
3599
|
+
}
|
|
3600
|
+
if (!dryRun && counts.drift) {
|
|
3601
|
+
process.stderr.write(
|
|
3602
|
+
mode === "force" ? `\u26A0 overwrote ${counts.drift} section(s) that had local edits (--force)
|
|
3603
|
+
` : `\u26A0 ${counts.drift} section(s) have local edits \u2014 wrote a .proposed file alongside (original untouched). Review + merge, or re-run with ${style.cyan("--force")}.
|
|
3604
|
+
`
|
|
3605
|
+
);
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
var GLOBAL_NAMESPACE = "__global__";
|
|
3609
|
+
async function resolveNamespaceChoice(cfg, flag) {
|
|
3610
|
+
if (flag) return flag;
|
|
3611
|
+
if (!canPrompt()) return null;
|
|
3612
|
+
const namespaces = await listNamespaces(cfg);
|
|
3613
|
+
if (namespaces.length === 0) return null;
|
|
3614
|
+
const picked = await promptSelect(
|
|
3615
|
+
"Which namespace should this connection use?",
|
|
3616
|
+
[
|
|
3617
|
+
{ label: "Global (whole tenant)", value: GLOBAL_NAMESPACE },
|
|
3618
|
+
...namespaces.map((n) => ({
|
|
3619
|
+
label: n.displayName,
|
|
3620
|
+
value: n.slug,
|
|
3621
|
+
hint: n.slug
|
|
3622
|
+
}))
|
|
3623
|
+
],
|
|
3624
|
+
GLOBAL_NAMESPACE
|
|
3625
|
+
);
|
|
3626
|
+
return picked === GLOBAL_NAMESPACE ? null : picked;
|
|
3627
|
+
}
|
|
3053
3628
|
function registerInit(program2) {
|
|
3054
|
-
program2.command("init").description("Wire this project for sechroom: write MCP config + agent instruction files from the server's setup descriptors").option("--client <list
|
|
3629
|
+
program2.command("init").description("Wire this project for sechroom: write MCP config + agent instruction files from the server's setup descriptors").option("--client <list...>", `clients to wire \u2014 space- or comma-separated (${ALL_CLIENT_KEYS.join(", ")}) or 'all'`, DEFAULT_CLIENT_KEY).option("--scope <scope>", "install skills/agents/hooks 'global' (config dir / CLAUDE_CONFIG_DIR) or 'project' (<cwd>/.claude) \u2014 default global", "global").option("--dry-run", "print what would be written without writing", false).option("--mcp-only", "only write MCP config (skip agent files)", false).option("--agent-files-only", "only write agent instruction files (skip MCP config)", false).option("--copy", "make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)").option("--namespace <slug>", "MCP namespace for the connection URL (interactive picker if omitted on a TTY; defaults to tenant-global)").option("--refresh", "refresh out-of-date agent-file blocks in place (local edits preserved to .proposed)", false).option("--force", "rewrite agent-file managed blocks, overwriting local edits inside the markers", false).option("--check", "report whether agent files would change and exit (0 = current, 1 = stale/drift/absent); writes nothing", false).addHelpText(
|
|
3055
3630
|
"after",
|
|
3056
3631
|
`
|
|
3057
3632
|
Examples:
|
|
3058
3633
|
$ sechroom init Claude Code (default): ./.mcp.json + ./CLAUDE.md
|
|
3059
|
-
$ sechroom init --client all claude-code, claude-desktop, codex, cursor
|
|
3060
|
-
$ sechroom init --client codex
|
|
3634
|
+
$ sechroom init --client all claude-code, claude-desktop, codex, cursor, antigravity
|
|
3635
|
+
$ sechroom init --client codex cursor space-separated (comma also works)
|
|
3061
3636
|
$ sechroom init --mcp-only just the MCP config (skip agent files)
|
|
3062
3637
|
$ sechroom init --dry-run --json preview the writes, change nothing`
|
|
3063
3638
|
).action(async (opts, cmd) => {
|
|
3064
3639
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3065
|
-
const
|
|
3066
|
-
const
|
|
3640
|
+
const mode = resolveEvalMode(opts);
|
|
3641
|
+
const check = mode === "check";
|
|
3642
|
+
const namespaceSlug = await resolveNamespaceChoice(cfg, opts.namespace);
|
|
3643
|
+
const setup = await withSpinner(
|
|
3644
|
+
"Fetching setup descriptors",
|
|
3645
|
+
() => fetchSetup(cfg, namespaceSlug ?? void 0)
|
|
3646
|
+
);
|
|
3647
|
+
const g = cmd.optsWithGlobals();
|
|
3648
|
+
let scope;
|
|
3649
|
+
try {
|
|
3650
|
+
scope = resolveScope(opts.scope);
|
|
3651
|
+
} catch (err2) {
|
|
3652
|
+
return fail(err2.message);
|
|
3653
|
+
}
|
|
3654
|
+
const claudeTargets = resolveClaudeTargets({ override: g.claudeConfigDir, scope, cwd: process.cwd() });
|
|
3655
|
+
const codexHomes = resolveCodexHomes({ override: g.codexHome, scope });
|
|
3656
|
+
const targets = clientTargets(process.cwd(), { claudeDir: claudeTargets[0]?.dir, codexHome: codexHomes[0] });
|
|
3067
3657
|
const keys = resolveClientKeys(opts.client);
|
|
3068
|
-
const json =
|
|
3658
|
+
const json = g.json;
|
|
3069
3659
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
3070
|
-
if (!opts.dryRun && !opts.mcpOnly) {
|
|
3660
|
+
if (!opts.dryRun && !opts.mcpOnly && !check) {
|
|
3071
3661
|
await maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId, copyChoice(opts));
|
|
3072
3662
|
}
|
|
3073
3663
|
const result = [];
|
|
@@ -3077,16 +3667,20 @@ Examples:
|
|
|
3077
3667
|
dryRun: Boolean(opts.dryRun),
|
|
3078
3668
|
mcp: !opts.agentFilesOnly,
|
|
3079
3669
|
agentFiles: !opts.mcpOnly,
|
|
3080
|
-
personalWorkspaceId
|
|
3670
|
+
personalWorkspaceId,
|
|
3671
|
+
mode
|
|
3081
3672
|
});
|
|
3082
3673
|
result.push({ client: key, actions });
|
|
3083
|
-
if (!json) printActions(target, actions);
|
|
3674
|
+
if (!json && !check) printActions(target, actions);
|
|
3084
3675
|
}
|
|
3676
|
+
summarizeEval(result, mode, Boolean(json), Boolean(opts.dryRun));
|
|
3085
3677
|
if (!json && !opts.dryRun && !opts.mcpOnly) {
|
|
3086
|
-
|
|
3678
|
+
for (const t of claudeTargets) {
|
|
3679
|
+
await maybeOfferSkills(cfg, personalWorkspaceId, { yes: false, dryRun: Boolean(opts.dryRun), surface: "claude-code", configDir: t.dir });
|
|
3680
|
+
}
|
|
3087
3681
|
}
|
|
3088
3682
|
if (!json && !opts.dryRun && !opts.mcpOnly) {
|
|
3089
|
-
await maybeOfferHooks({ yes: false, dryRun: Boolean(opts.dryRun), cwd: process.cwd() });
|
|
3683
|
+
await maybeOfferHooks({ yes: false, dryRun: Boolean(opts.dryRun), cwd: process.cwd(), scope, claudeConfigDir: g.claudeConfigDir, codexHome: g.codexHome });
|
|
3090
3684
|
}
|
|
3091
3685
|
if (json) {
|
|
3092
3686
|
emit({ dryRun: Boolean(opts.dryRun), clients: result }, true);
|
|
@@ -3107,23 +3701,99 @@ Next \u2014 verify: ${verify.description}
|
|
|
3107
3701
|
}
|
|
3108
3702
|
function registerSetup(program2) {
|
|
3109
3703
|
const setup = program2.command("setup").description("Granular onboarding steps (init runs these together)");
|
|
3110
|
-
setup.command("mcp <clients...>").description(`Write only the MCP config for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`).option("--dry-run", "print what would be written without writing", false).addHelpText("after", "\nExamples:\n $ sechroom setup mcp codex\n $ sechroom setup mcp claude-code codex\n $ sechroom setup mcp all").action(async (clients, opts, cmd) => {
|
|
3111
|
-
await runClients(clients, cmd, { dryRun: Boolean(opts.dryRun), mcp: true, agentFiles: false });
|
|
3704
|
+
setup.command("mcp <clients...>").description(`Write only the MCP config for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`).option("--dry-run", "print what would be written without writing", false).option("--namespace <slug>", "MCP namespace for the connection URL (interactive picker if omitted on a TTY; defaults to tenant-global)").addHelpText("after", "\nExamples:\n $ sechroom setup mcp codex\n $ sechroom setup mcp claude-code codex\n $ sechroom setup mcp all").action(async (clients, opts, cmd) => {
|
|
3705
|
+
await runClients(clients, cmd, { dryRun: Boolean(opts.dryRun), mcp: true, agentFiles: false, namespace: opts.namespace });
|
|
3112
3706
|
});
|
|
3113
|
-
setup.command("agent-files <clients...>").description(`Write only the agent instruction file(s) for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`).option("--dry-run", "print what would be written without writing", false).option("--copy", "make a personal copy you can edit (default: prompt on a TTY, else skip)").addHelpText("after", "\nExamples:\n $ sechroom setup agent-files claude-code CLAUDE.md\n $ sechroom setup agent-files claude-code codex CLAUDE.md + AGENTS.md in one run\n $ sechroom setup agent-files all").action(async (clients, opts, cmd) => {
|
|
3114
|
-
await runClients(clients, cmd, { dryRun: Boolean(opts.dryRun), mcp: false, agentFiles: true, copy: opts.copy });
|
|
3707
|
+
setup.command("agent-files <clients...>").description(`Write only the agent instruction file(s) for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`).option("--dry-run", "print what would be written without writing", false).option("--copy", "make a personal copy you can edit (default: prompt on a TTY, else skip)").option("--refresh", "refresh out-of-date blocks in place (local edits preserved to .proposed)", false).option("--force", "rewrite managed blocks, overwriting local edits inside the markers", false).option("--check", "report whether anything would change and exit (0 = current, 1 = stale/drift/absent); writes nothing", false).addHelpText("after", "\nExamples:\n $ sechroom setup agent-files claude-code CLAUDE.md\n $ sechroom setup agent-files claude-code codex CLAUDE.md + AGENTS.md in one run\n $ sechroom setup agent-files all --check CI gate: nonzero exit if out of date\n $ sechroom setup agent-files claude-code --force overwrite local edits in the managed block").action(async (clients, opts, cmd) => {
|
|
3708
|
+
await runClients(clients, cmd, { dryRun: Boolean(opts.dryRun), mcp: false, agentFiles: true, copy: opts.copy, mode: resolveEvalMode(opts) });
|
|
3709
|
+
});
|
|
3710
|
+
setup.command("new-convention <title...>").description("Scaffold a workspace-conventions section: author a correctly-tagged memo (header as first body line) + regen the agent files").option("--kind <kind>", "reference | standard (orders the section; reference first)", "reference").option("--workspace <id>", "workspace to author in (default: the bound workspace)").option("--body <markdown>", "section body (default: a TODO scaffold to edit later)").option("--no-regen", "skip the agent-files regen after authoring").option("--dry-run", "print what would be authored; write nothing", false).addHelpText(
|
|
3711
|
+
"after",
|
|
3712
|
+
`
|
|
3713
|
+
The memo carries the two conventions a workspace-conventions section needs (FR-sechroom-236):
|
|
3714
|
+
the \`agent-setup-bundle\` tag + the \`# Header\` as the FIRST body line. It's authored in the
|
|
3715
|
+
BOUND workspace (so the regen, which sources conventions from there, picks it up). Edit it later
|
|
3716
|
+
in the app or via \`sechroom memory edit-text\`.
|
|
3717
|
+
|
|
3718
|
+
Examples:
|
|
3719
|
+
$ sechroom setup new-convention "Deploy runbook"
|
|
3720
|
+
$ sechroom setup new-convention "Backend testing" --kind standard --body "- run dotnet test ..."
|
|
3721
|
+
$ sechroom setup new-convention "Draft section" --no-regen author only, regen later`
|
|
3722
|
+
).action(async (titleParts, opts, cmd) => {
|
|
3723
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3724
|
+
const json = Boolean(cmd.optsWithGlobals().json);
|
|
3725
|
+
const title = titleParts.join(" ").trim();
|
|
3726
|
+
if (!title) fail('a section title is required, e.g. `sechroom setup new-convention "Deploy runbook"`.');
|
|
3727
|
+
const workspaceId = opts.workspace ?? cfg.workspaceId;
|
|
3728
|
+
if (!workspaceId)
|
|
3729
|
+
fail("no workspace \u2014 pass --workspace <id> or bind one (`sechroom config set --local workspaceId <id>`).");
|
|
3730
|
+
const kind = String(opts.kind).toLowerCase() === "standard" ? "standard" : "reference";
|
|
3731
|
+
const body = typeof opts.body === "string" && opts.body.trim().length > 0 ? opts.body.trim() : "_TODO: write this section, then edit the memo and re-run the regen._";
|
|
3732
|
+
const text = `# ${title}
|
|
3733
|
+
|
|
3734
|
+
${body}
|
|
3735
|
+
`;
|
|
3736
|
+
const tags = ["agent-setup-bundle", "scope:sechroom", `kind:${kind}`, "archetype:document"];
|
|
3737
|
+
if (opts.dryRun) {
|
|
3738
|
+
emit({ dryRun: true, workspaceId, title, kind, tags, text }, json);
|
|
3739
|
+
return;
|
|
3740
|
+
}
|
|
3741
|
+
const data = await runApi("Authoring convention memo", async () => {
|
|
3742
|
+
const client = await makeClient(cfg);
|
|
3743
|
+
return client.POST("/memories", {
|
|
3744
|
+
body: {
|
|
3745
|
+
text,
|
|
3746
|
+
type: kind,
|
|
3747
|
+
content: "{}",
|
|
3748
|
+
confidence: 1,
|
|
3749
|
+
source: "cli-new-convention",
|
|
3750
|
+
archetype: "Document",
|
|
3751
|
+
title,
|
|
3752
|
+
tags,
|
|
3753
|
+
owner: { type: "Workspace", id: workspaceId }
|
|
3754
|
+
}
|
|
3755
|
+
});
|
|
3756
|
+
});
|
|
3757
|
+
if (!json) {
|
|
3758
|
+
const view = resolveViewUrl(cfg.baseUrl, data.url);
|
|
3759
|
+
process.stdout.write(
|
|
3760
|
+
`\u2713 authored convention ${style.bold(data.id)} ${style.dim(`"${title}"`)}${view ? ` ${style.dim("\u2192")} ${view}` : ""}
|
|
3761
|
+
`
|
|
3762
|
+
);
|
|
3763
|
+
}
|
|
3764
|
+
if (opts.regen === false) {
|
|
3765
|
+
if (json) emit({ id: data.id, workspaceId, regen: false }, true);
|
|
3766
|
+
else process.stdout.write("Skipped regen (--no-regen). Run `sechroom setup agent-files all` to apply.\n");
|
|
3767
|
+
return;
|
|
3768
|
+
}
|
|
3769
|
+
await runClients(["claude-code", "codex"], cmd, {
|
|
3770
|
+
dryRun: false,
|
|
3771
|
+
mcp: false,
|
|
3772
|
+
agentFiles: true,
|
|
3773
|
+
copy: false,
|
|
3774
|
+
mode: "apply"
|
|
3775
|
+
});
|
|
3115
3776
|
});
|
|
3116
3777
|
}
|
|
3117
3778
|
async function runClients(clients, cmd, opts) {
|
|
3118
|
-
const
|
|
3119
|
-
const
|
|
3779
|
+
const g = cmd.optsWithGlobals();
|
|
3780
|
+
const cfg = resolveConfig(g);
|
|
3781
|
+
const mode = opts.mode ?? "apply";
|
|
3782
|
+
const check = mode === "check";
|
|
3783
|
+
const claudeDir = resolveClaudeTargets({ override: g.claudeConfigDir })[0]?.dir;
|
|
3784
|
+
const codexHome = resolveCodexHomes({ override: g.codexHome })[0];
|
|
3785
|
+
const targets = clientTargets(process.cwd(), { claudeDir, codexHome });
|
|
3120
3786
|
const keys = resolveClientKeys(clients.join(","));
|
|
3121
|
-
const
|
|
3787
|
+
const namespaceSlug = opts.mcp ? await resolveNamespaceChoice(cfg, opts.namespace) : null;
|
|
3788
|
+
const setupData = await withSpinner(
|
|
3789
|
+
"Fetching setup descriptors",
|
|
3790
|
+
() => fetchSetup(cfg, namespaceSlug ?? void 0)
|
|
3791
|
+
);
|
|
3122
3792
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
3123
|
-
if (opts.agentFiles && !opts.dryRun) {
|
|
3793
|
+
if (opts.agentFiles && !opts.dryRun && !check) {
|
|
3124
3794
|
await maybeOfferCopies(cfg, setupData, targets, keys, personalWorkspaceId, copyChoice(opts));
|
|
3125
3795
|
}
|
|
3126
|
-
const json =
|
|
3796
|
+
const json = g.json;
|
|
3127
3797
|
const result = [];
|
|
3128
3798
|
for (const key of keys) {
|
|
3129
3799
|
const target = targets[key];
|
|
@@ -3131,11 +3801,13 @@ async function runClients(clients, cmd, opts) {
|
|
|
3131
3801
|
dryRun: opts.dryRun,
|
|
3132
3802
|
mcp: opts.mcp,
|
|
3133
3803
|
agentFiles: opts.agentFiles,
|
|
3134
|
-
personalWorkspaceId
|
|
3804
|
+
personalWorkspaceId,
|
|
3805
|
+
mode
|
|
3135
3806
|
});
|
|
3136
3807
|
result.push({ client: key, actions });
|
|
3137
|
-
if (!json) printActions(target, actions);
|
|
3808
|
+
if (!json && !check) printActions(target, actions);
|
|
3138
3809
|
}
|
|
3810
|
+
summarizeEval(result, mode, Boolean(json), opts.dryRun);
|
|
3139
3811
|
if (json) {
|
|
3140
3812
|
emit({ dryRun: opts.dryRun, clients: result }, true);
|
|
3141
3813
|
return;
|
|
@@ -3143,14 +3815,81 @@ async function runClients(clients, cmd, opts) {
|
|
|
3143
3815
|
process.stdout.write(opts.dryRun ? "\n(dry run \u2014 nothing written)\n" : "\nDone.\n");
|
|
3144
3816
|
}
|
|
3145
3817
|
|
|
3818
|
+
// src/commands/namespace.ts
|
|
3819
|
+
function registerNamespace(program2) {
|
|
3820
|
+
const namespace = program2.command("namespace").description("Browse, inspect, and wire up MCP namespaces");
|
|
3821
|
+
namespace.addHelpText(
|
|
3822
|
+
"after",
|
|
3823
|
+
`
|
|
3824
|
+
Examples:
|
|
3825
|
+
$ sechroom namespace list
|
|
3826
|
+
$ sechroom namespace show eng
|
|
3827
|
+
$ sechroom namespace use eng wire Claude Code to the 'eng' namespace
|
|
3828
|
+
$ sechroom namespace use eng --client all`
|
|
3829
|
+
);
|
|
3830
|
+
namespace.command("list").description("List the namespaces you can reach (GET /mcp-aggregator/namespaces)").action(async (_opts, cmd) => {
|
|
3831
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3832
|
+
const data = await runApi("Listing namespaces", async () => {
|
|
3833
|
+
const client = await makeClient(cfg);
|
|
3834
|
+
return client.GET("/mcp-aggregator/namespaces", {});
|
|
3835
|
+
});
|
|
3836
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
3837
|
+
});
|
|
3838
|
+
namespace.command("show <slug>").description("Show a namespace's details (GET /mcp-aggregator/namespaces/{slug}). For the tool list it exposes, point an OpenAPI client at /t/{tenant}/namespaces/{slug}/api/openapi.json.").action(async (slug, _opts, cmd) => {
|
|
3839
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3840
|
+
const data = await runApi("Fetching namespace", async () => {
|
|
3841
|
+
const client = await makeClient(cfg);
|
|
3842
|
+
return client.GET("/mcp-aggregator/namespaces/{slug}", {
|
|
3843
|
+
params: { path: { slug } }
|
|
3844
|
+
});
|
|
3845
|
+
});
|
|
3846
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
3847
|
+
});
|
|
3848
|
+
namespace.command("use <slug>").description("Wire an AI client's MCP config to this namespace's URL").option(
|
|
3849
|
+
"--client <list>",
|
|
3850
|
+
`comma-separated clients (${ALL_CLIENT_KEYS.join(", ")}) or 'all'`,
|
|
3851
|
+
DEFAULT_CLIENT_KEY
|
|
3852
|
+
).option("--dry-run", "print what would be written without writing", false).action(async (slug, opts, cmd) => {
|
|
3853
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3854
|
+
const setup = await withSpinner(
|
|
3855
|
+
"Fetching setup descriptors",
|
|
3856
|
+
() => fetchSetup(cfg, slug)
|
|
3857
|
+
);
|
|
3858
|
+
const targets = clientTargets(process.cwd());
|
|
3859
|
+
const keys = resolveClientKeys(opts.client);
|
|
3860
|
+
const json = cmd.optsWithGlobals().json;
|
|
3861
|
+
const result = [];
|
|
3862
|
+
for (const key of keys) {
|
|
3863
|
+
const target = targets[key];
|
|
3864
|
+
const actions = await applyClient(cfg, setup, target, {
|
|
3865
|
+
dryRun: Boolean(opts.dryRun),
|
|
3866
|
+
mcp: true,
|
|
3867
|
+
agentFiles: false,
|
|
3868
|
+
personalWorkspaceId: null
|
|
3869
|
+
});
|
|
3870
|
+
result.push({ client: key, actions });
|
|
3871
|
+
if (!json) printActions(target, actions);
|
|
3872
|
+
}
|
|
3873
|
+
if (json) {
|
|
3874
|
+
emit({ namespace: slug, dryRun: Boolean(opts.dryRun), clients: result }, true);
|
|
3875
|
+
return;
|
|
3876
|
+
}
|
|
3877
|
+
process.stdout.write(
|
|
3878
|
+
opts.dryRun ? "\n(dry run \u2014 nothing written)\n" : `
|
|
3879
|
+
Wired to namespace '${slug}'. Restart your AI client (or reload MCP) to pick it up.
|
|
3880
|
+
`
|
|
3881
|
+
);
|
|
3882
|
+
});
|
|
3883
|
+
}
|
|
3884
|
+
|
|
3146
3885
|
// src/commands/onboard.ts
|
|
3147
|
-
import { existsSync as
|
|
3148
|
-
import { join as
|
|
3886
|
+
import { existsSync as existsSync8 } from "fs";
|
|
3887
|
+
import { basename as basename2, join as join10 } from "path";
|
|
3149
3888
|
|
|
3150
3889
|
// src/commands/fanout.ts
|
|
3151
3890
|
import { spawnSync } from "child_process";
|
|
3152
|
-
import { existsSync as
|
|
3153
|
-
import { isAbsolute, join as
|
|
3891
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
3892
|
+
import { isAbsolute, join as join9, resolve } from "path";
|
|
3154
3893
|
var ICON = {
|
|
3155
3894
|
refresh: "\u21BB",
|
|
3156
3895
|
bind: "+",
|
|
@@ -3163,28 +3902,28 @@ function resolveChildDir(path, root) {
|
|
|
3163
3902
|
function discoverChildren(root) {
|
|
3164
3903
|
let names;
|
|
3165
3904
|
try {
|
|
3166
|
-
names =
|
|
3905
|
+
names = readdirSync2(root);
|
|
3167
3906
|
} catch {
|
|
3168
3907
|
return [];
|
|
3169
3908
|
}
|
|
3170
3909
|
const out = [];
|
|
3171
3910
|
for (const name of names.sort()) {
|
|
3172
3911
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
3173
|
-
const dir =
|
|
3912
|
+
const dir = join9(root, name);
|
|
3174
3913
|
try {
|
|
3175
|
-
if (!
|
|
3914
|
+
if (!statSync3(dir).isDirectory()) continue;
|
|
3176
3915
|
} catch {
|
|
3177
3916
|
continue;
|
|
3178
3917
|
}
|
|
3179
|
-
if (
|
|
3918
|
+
if (existsSync7(join9(dir, ".git")) || committedBindingPath(dir)) out.push(name);
|
|
3180
3919
|
}
|
|
3181
3920
|
return out;
|
|
3182
3921
|
}
|
|
3183
3922
|
function readManifest(path) {
|
|
3184
|
-
if (!
|
|
3923
|
+
if (!existsSync7(path)) return null;
|
|
3185
3924
|
let parsed;
|
|
3186
3925
|
try {
|
|
3187
|
-
parsed = JSON.parse(
|
|
3926
|
+
parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
3188
3927
|
} catch (err2) {
|
|
3189
3928
|
throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
3190
3929
|
}
|
|
@@ -3322,7 +4061,39 @@ async function warnIfProjectStray(client, projectId, workspaceId, json) {
|
|
|
3322
4061
|
);
|
|
3323
4062
|
}
|
|
3324
4063
|
}
|
|
3325
|
-
async function
|
|
4064
|
+
async function fetchPersonalWorkspaceId(client) {
|
|
4065
|
+
try {
|
|
4066
|
+
const { data } = await client.GET("/me/personal-workspace", {});
|
|
4067
|
+
return data?.workspaceId ?? null;
|
|
4068
|
+
} catch {
|
|
4069
|
+
return null;
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
function nameTokens(s) {
|
|
4073
|
+
return s.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2);
|
|
4074
|
+
}
|
|
4075
|
+
function personalSubtreeIds(personalId, all) {
|
|
4076
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
4077
|
+
for (const w of all) {
|
|
4078
|
+
if (!w.parentId) continue;
|
|
4079
|
+
(childrenOf.get(w.parentId) ?? childrenOf.set(w.parentId, []).get(w.parentId)).push(w);
|
|
4080
|
+
}
|
|
4081
|
+
const ids = /* @__PURE__ */ new Set([personalId]);
|
|
4082
|
+
const queue = [personalId];
|
|
4083
|
+
while (queue.length > 0) {
|
|
4084
|
+
const id = queue.shift();
|
|
4085
|
+
for (const child of childrenOf.get(id) ?? []) {
|
|
4086
|
+
if (!ids.has(child.id)) {
|
|
4087
|
+
ids.add(child.id);
|
|
4088
|
+
queue.push(child.id);
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
return ids;
|
|
4093
|
+
}
|
|
4094
|
+
async function pickWorkspace(client, opts = {}) {
|
|
4095
|
+
const promptLabel = opts.promptLabel ?? "Bind this directory to a workspace:";
|
|
4096
|
+
const dirName = opts.dirName ?? basename2(process.cwd());
|
|
3326
4097
|
const all = await withSpinner("Listing your workspaces", () => fetchWorkspaces(client));
|
|
3327
4098
|
if (all.length === 0) {
|
|
3328
4099
|
process.stderr.write(`no workspaces found \u2014 skipping workspace binding (you can set it later with \`sechroom config set --local workspaceId <id>\`)
|
|
@@ -3330,22 +4101,34 @@ async function pickWorkspace(client, promptLabel = "Bind this directory to a wor
|
|
|
3330
4101
|
return void 0;
|
|
3331
4102
|
}
|
|
3332
4103
|
const byId = new Map(all.map((w) => [w.id, w]));
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
4104
|
+
const personalId = await fetchPersonalWorkspaceId(client);
|
|
4105
|
+
const excluded = personalId ? personalSubtreeIds(personalId, all) : /* @__PURE__ */ new Set();
|
|
4106
|
+
let candidates = all.filter((w) => !excluded.has(w.id));
|
|
4107
|
+
if (candidates.length === 0) candidates = all;
|
|
4108
|
+
const dirToks = new Set(nameTokens(dirName));
|
|
4109
|
+
const isMatch = (w) => nameTokens(w.name).some((t) => dirToks.has(t));
|
|
4110
|
+
const suggestions = candidates.filter(isMatch);
|
|
4111
|
+
let pool = candidates;
|
|
4112
|
+
if (candidates.length > 12 && suggestions.length === 0) {
|
|
4113
|
+
const q = (await promptText(`Filter ${candidates.length} workspaces (substring, Enter to list all)?`, "")).trim().toLowerCase();
|
|
3336
4114
|
if (q) {
|
|
3337
|
-
const hits =
|
|
4115
|
+
const hits = candidates.filter((w) => `${w.name} ${workspacePath(w, byId)}`.toLowerCase().includes(q));
|
|
3338
4116
|
if (hits.length > 0) pool = hits;
|
|
3339
4117
|
else process.stderr.write(`no match for "${q}" \u2014 listing all
|
|
3340
4118
|
`);
|
|
3341
4119
|
}
|
|
3342
4120
|
}
|
|
3343
4121
|
const SKIP = "__skip__";
|
|
4122
|
+
const byPath = (a, b) => workspacePath(a, byId).localeCompare(workspacePath(b, byId));
|
|
4123
|
+
const matched = pool.filter(isMatch).sort(byPath);
|
|
4124
|
+
const rest = pool.filter((w) => !isMatch(w)).sort(byPath);
|
|
3344
4125
|
const choices = [
|
|
3345
|
-
...
|
|
4126
|
+
...matched.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: style.dim(`matches "${dirName}"`) })),
|
|
4127
|
+
...rest.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: w.id })),
|
|
3346
4128
|
{ label: style.dim("skip \u2014 don't bind a workspace"), value: SKIP, hint: void 0 }
|
|
3347
4129
|
];
|
|
3348
|
-
const
|
|
4130
|
+
const defaultValue = matched.length === 1 ? matched[0].id : SKIP;
|
|
4131
|
+
const chosen = await promptSelect(promptLabel, choices, defaultValue);
|
|
3349
4132
|
if (chosen === SKIP) return void 0;
|
|
3350
4133
|
const picked = byId.get(chosen);
|
|
3351
4134
|
const collisions = all.filter((w) => w.id !== picked.id && namesCollide(w.name, picked.name));
|
|
@@ -3372,7 +4155,7 @@ async function resolveWorkspaceBinding(client, existing, opts) {
|
|
|
3372
4155
|
}
|
|
3373
4156
|
if (existing) return existing;
|
|
3374
4157
|
if (!canPrompt() || opts.yes) return void 0;
|
|
3375
|
-
return pickWorkspace(client);
|
|
4158
|
+
return pickWorkspace(client, { dirName: basename2(process.cwd()) });
|
|
3376
4159
|
}
|
|
3377
4160
|
async function ensureTenant(baseUrl, g, opts) {
|
|
3378
4161
|
const persisted = readPersisted();
|
|
@@ -3488,7 +4271,7 @@ async function ensureTimezone(cfg, opts) {
|
|
|
3488
4271
|
return { timezone: tz, action: "set" };
|
|
3489
4272
|
}
|
|
3490
4273
|
async function chooseClients(clientFlag, yes, cwd) {
|
|
3491
|
-
if (clientFlag) return resolveClientKeys(clientFlag);
|
|
4274
|
+
if (clientFlag && clientFlag.length > 0) return resolveClientKeys(clientFlag);
|
|
3492
4275
|
const detected = detectInstalledClients(cwd);
|
|
3493
4276
|
const preselected = detected.length > 0 ? detected : [DEFAULT_CLIENT_KEY];
|
|
3494
4277
|
if (!canPrompt() || yes) return preselected;
|
|
@@ -3503,12 +4286,24 @@ async function chooseClients(clientFlag, yes, cwd) {
|
|
|
3503
4286
|
);
|
|
3504
4287
|
return picks.length > 0 ? picks : preselected;
|
|
3505
4288
|
}
|
|
4289
|
+
async function chooseScope(scopeFlag, yes) {
|
|
4290
|
+
if (scopeFlag != null) return resolveScope(scopeFlag);
|
|
4291
|
+
if (!canPrompt() || yes) return "global";
|
|
4292
|
+
return promptSelect(
|
|
4293
|
+
"Install skills, agents, and hooks globally or just for this project?",
|
|
4294
|
+
[
|
|
4295
|
+
{ label: "Globally", value: "global", hint: "~/.claude (or CLAUDE_CONFIG_DIR) \u2014 all projects" },
|
|
4296
|
+
{ label: "This project", value: "project", hint: "<repo>/.claude" }
|
|
4297
|
+
],
|
|
4298
|
+
"global"
|
|
4299
|
+
);
|
|
4300
|
+
}
|
|
3506
4301
|
async function planRecurseChild(entry, root, client, opts) {
|
|
3507
4302
|
const dir = resolveChildDir(entry.path, root);
|
|
3508
|
-
if (!
|
|
4303
|
+
if (!existsSync8(dir)) {
|
|
3509
4304
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
3510
4305
|
}
|
|
3511
|
-
if (
|
|
4306
|
+
if (existsSync8(join10(dir, ".sechroom.json"))) {
|
|
3512
4307
|
return {
|
|
3513
4308
|
label: entry.path,
|
|
3514
4309
|
dir,
|
|
@@ -3535,7 +4330,10 @@ async function planRecurseChild(entry, root, client, opts) {
|
|
|
3535
4330
|
process.stderr.write(`
|
|
3536
4331
|
${style.bold(entry.path)} ${style.dim("is not bound yet.")}
|
|
3537
4332
|
`);
|
|
3538
|
-
const ws = await pickWorkspace(client,
|
|
4333
|
+
const ws = await pickWorkspace(client, {
|
|
4334
|
+
promptLabel: `Bind ${style.cyan(entry.path)} to a workspace:`,
|
|
4335
|
+
dirName: basename2(entry.path)
|
|
4336
|
+
});
|
|
3539
4337
|
if (!ws) {
|
|
3540
4338
|
return { label: entry.path, dir, disposition: "skip-unbound", argv: [], reason: "unbound \u2014 no workspace chosen (skipped)" };
|
|
3541
4339
|
}
|
|
@@ -3578,7 +4376,7 @@ This fan-out will pin the same lane in every repo:
|
|
|
3578
4376
|
async function runRecurse(cfg, g, opts) {
|
|
3579
4377
|
const { yes, dryRun, json } = opts;
|
|
3580
4378
|
const root = process.cwd();
|
|
3581
|
-
const manifestPath =
|
|
4379
|
+
const manifestPath = join10(root, ".sechroom", "repos.json");
|
|
3582
4380
|
const fromManifest = readManifest(manifestPath);
|
|
3583
4381
|
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
3584
4382
|
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
@@ -3606,7 +4404,7 @@ async function runRecurse(cfg, g, opts) {
|
|
|
3606
4404
|
summarizeFanout(results, { dryRun });
|
|
3607
4405
|
}
|
|
3608
4406
|
function registerOnboard(program2) {
|
|
3609
|
-
program2.command("onboard").description("Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project").option("--recurse", "orchestration-root mode: onboard every child repo under this dir (auto-discovered, or from ./.sechroom/repos.json) \u2014 refreshes bound repos, prompts a workspace per new one", false).option("--lane <id>", "set the code-lane (substrate source identity) explicitly instead of inferring it; with --recurse it's used for every child repo").option("--design-lane <id>", "set the design-lane explicitly (substrate-authoring identity); with --recurse applies to every child").option("--client <list
|
|
4407
|
+
program2.command("onboard").description("Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project").option("--recurse", "orchestration-root mode: onboard every child repo under this dir (auto-discovered, or from ./.sechroom/repos.json) \u2014 refreshes bound repos, prompts a workspace per new one", false).option("--lane <id>", "set the code-lane (substrate source identity) explicitly instead of inferring it; with --recurse it's used for every child repo").option("--design-lane <id>", "set the design-lane explicitly (substrate-authoring identity); with --recurse applies to every child").option("--client <list...>", `clients to wire \u2014 space- or comma-separated (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`).option("--scope <scope>", "install skills/agents/hooks 'global' (config dir / CLAUDE_CONFIG_DIR) or 'project' (<cwd>/.claude) \u2014 default: prompt, else global").option("--local", "save the binding (tenant + base URL + workspace) to a committed .sechroom.json in this repo instead of the global config", false).option("--here", "with --local: write the binding at THIS directory even when a parent already carries one \u2014 binds a subtree (e.g. a monorepo's frontend/) to its own workspace", false).option("--workspace <id>", "bind this directory to a workspace (skips the interactive workspace pick)").option("--cli-only", "configure the CLI only \u2014 don't wire any AI client (no MCP config, no agent files)", false).option("--no-mcp", "skip the MCP server config (.mcp.json etc.); still write the agent instruction files").option("--copy", "make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)").option("--dry-run", "walk through without writing files or changing the profile", false).option("--refresh", "re-fetch descriptors and refresh any out-of-date managed blocks (local edits preserved to .proposed)", false).option("--force", "rewrite every managed block, overwriting local edits inside the markers (content outside untouched)", false).option("--check", "report whether anything would change and exit (0 = all current, 1 = stale/drift/absent); writes nothing", false).option("-y, --yes", "non-interactive: accept defaults (system timezone, detected clients, global config, full wire)", false).addHelpText(
|
|
3610
4408
|
"after",
|
|
3611
4409
|
`
|
|
3612
4410
|
Examples:
|
|
@@ -3649,6 +4447,13 @@ Examples:
|
|
|
3649
4447
|
process.stderr.write(line);
|
|
3650
4448
|
}
|
|
3651
4449
|
const wire = await chooseWire(opts, yes);
|
|
4450
|
+
const scope = await chooseScope(opts.scope, yes);
|
|
4451
|
+
const claudeTargets = resolveClaudeTargets({ override: g.claudeConfigDir, scope, cwd: process.cwd() });
|
|
4452
|
+
const codexHomes = resolveCodexHomes({ override: g.codexHome, scope });
|
|
4453
|
+
if (scope === "project" && g.claudeConfigDir && !json) {
|
|
4454
|
+
process.stderr.write(`${style.dim("(--claude-config-dir is ignored for --scope project \u2014 project files are repo-relative)")}
|
|
4455
|
+
`);
|
|
4456
|
+
}
|
|
3652
4457
|
if (wire === "cli-only") {
|
|
3653
4458
|
if (json) {
|
|
3654
4459
|
emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, workspaceId: cfg.workspaceId ?? null, timezone: tz, wire, clients: [] }, true);
|
|
@@ -3656,7 +4461,7 @@ Examples:
|
|
|
3656
4461
|
}
|
|
3657
4462
|
if (!dryRun) {
|
|
3658
4463
|
await ensureLanePin(cfg, { yes, dryRun, clients: detectInstalledClients(process.cwd()) });
|
|
3659
|
-
await maybeOfferHooks({ yes, dryRun, cwd: process.cwd() });
|
|
4464
|
+
await maybeOfferHooks({ yes, dryRun, cwd: process.cwd(), scope, claudeConfigDir: g.claudeConfigDir, codexHome: g.codexHome });
|
|
3660
4465
|
}
|
|
3661
4466
|
process.stdout.write(
|
|
3662
4467
|
`
|
|
@@ -3669,7 +4474,7 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
3669
4474
|
}
|
|
3670
4475
|
const keys = await chooseClients(opts.client, yes, process.cwd());
|
|
3671
4476
|
const setup = await withSpinner("Fetching setup descriptors", () => fetchSetup(cfg));
|
|
3672
|
-
const targets = clientTargets(process.cwd());
|
|
4477
|
+
const targets = clientTargets(process.cwd(), { claudeDir: claudeTargets[0]?.dir, codexHome: codexHomes[0] });
|
|
3673
4478
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
3674
4479
|
if (!dryRun && !check) {
|
|
3675
4480
|
await maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId, copyChoice(opts));
|
|
@@ -3713,10 +4518,12 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
3713
4518
|
await ensureLanePin(cfg, { yes, dryRun, clients: keys });
|
|
3714
4519
|
}
|
|
3715
4520
|
if (!json && !dryRun) {
|
|
3716
|
-
|
|
4521
|
+
for (const t of claudeTargets) {
|
|
4522
|
+
await maybeOfferSkills(cfg, personalWorkspaceId, { yes, dryRun, surface: "claude-code", configDir: t.dir });
|
|
4523
|
+
}
|
|
3717
4524
|
}
|
|
3718
4525
|
if (!json && !dryRun) {
|
|
3719
|
-
await maybeOfferHooks({ yes, dryRun, cwd: process.cwd() });
|
|
4526
|
+
await maybeOfferHooks({ yes, dryRun, cwd: process.cwd(), scope, claudeConfigDir: g.claudeConfigDir, codexHome: g.codexHome });
|
|
3720
4527
|
}
|
|
3721
4528
|
if (json) {
|
|
3722
4529
|
emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, workspaceId: cfg.workspaceId ?? null, timezone: tz, wire, eval: evalCounts, clients: result }, true);
|
|
@@ -3762,14 +4569,21 @@ async function chooseWire(opts, yes) {
|
|
|
3762
4569
|
return opts.mcp === false ? "agent-only" : "full";
|
|
3763
4570
|
}
|
|
3764
4571
|
var FALLBACK_AGENT_PROMPT = "Resume my sechroom continuity, summarise what I was last working on, then suggest the next step.";
|
|
4572
|
+
function printNextStepBlock(heading, lines) {
|
|
4573
|
+
const rule = style.dim("\u2500".repeat(52));
|
|
4574
|
+
process.stdout.write(
|
|
4575
|
+
`
|
|
4576
|
+
${rule}
|
|
4577
|
+
${style.bold(heading)}
|
|
4578
|
+
|
|
4579
|
+
` + lines.map((l) => ` ${l}`).join("\n") + `
|
|
4580
|
+
${rule}
|
|
4581
|
+
`
|
|
4582
|
+
);
|
|
4583
|
+
}
|
|
3765
4584
|
async function printStarterPrompt(mode, cfg) {
|
|
3766
4585
|
if (mode === "cli") {
|
|
3767
|
-
|
|
3768
|
-
`
|
|
3769
|
-
${style.bold("Next:")} pick up where you left off \u2014
|
|
3770
|
-
${style.cyan("sechroom continuity resume-me")}
|
|
3771
|
-
`
|
|
3772
|
-
);
|
|
4586
|
+
printNextStepBlock("Next \u2014 pick up where you left off:", [style.cyan("sechroom continuity resume-me")]);
|
|
3773
4587
|
return;
|
|
3774
4588
|
}
|
|
3775
4589
|
let primary = FALLBACK_AGENT_PROMPT;
|
|
@@ -3781,21 +4595,16 @@ ${style.bold("Next:")} pick up where you left off \u2014
|
|
|
3781
4595
|
} catch {
|
|
3782
4596
|
}
|
|
3783
4597
|
}
|
|
3784
|
-
|
|
3785
|
-
`
|
|
3786
|
-
${style.bold("Next:")} paste this into your AI agent to get going \u2014
|
|
3787
|
-
${style.cyan(`"${primary}"`)}
|
|
3788
|
-
`
|
|
3789
|
-
);
|
|
4598
|
+
printNextStepBlock("Next \u2014 paste this into your AI agent to get going:", [style.cyan(`"${primary}"`)]);
|
|
3790
4599
|
}
|
|
3791
4600
|
|
|
3792
4601
|
// src/commands/sweep.ts
|
|
3793
|
-
import { existsSync as
|
|
3794
|
-
import { dirname as
|
|
3795
|
-
var DEFAULT_MANIFEST =
|
|
4602
|
+
import { existsSync as existsSync9 } from "fs";
|
|
4603
|
+
import { dirname as dirname7, join as join11, resolve as resolve2 } from "path";
|
|
4604
|
+
var DEFAULT_MANIFEST = join11(".sechroom", "repos.json");
|
|
3796
4605
|
function planEntry(entry, root) {
|
|
3797
4606
|
const dir = resolveChildDir(entry.path, root);
|
|
3798
|
-
if (!
|
|
4607
|
+
if (!existsSync9(dir)) {
|
|
3799
4608
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
3800
4609
|
}
|
|
3801
4610
|
if (committedBindingPath(dir)) {
|
|
@@ -3871,7 +4680,7 @@ Examples:
|
|
|
3871
4680
|
`);
|
|
3872
4681
|
return;
|
|
3873
4682
|
}
|
|
3874
|
-
const root =
|
|
4683
|
+
const root = dirname7(dirname7(manifestPath));
|
|
3875
4684
|
const plans = repos.map((entry) => planEntry(entry, root));
|
|
3876
4685
|
if (!json) {
|
|
3877
4686
|
process.stderr.write(
|
|
@@ -3888,162 +4697,239 @@ Examples:
|
|
|
3888
4697
|
});
|
|
3889
4698
|
}
|
|
3890
4699
|
|
|
3891
|
-
// src/commands/
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
}
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
4700
|
+
// src/commands/lane.ts
|
|
4701
|
+
var LANE_KEYS = ["code-lane", "design-lane"];
|
|
4702
|
+
function showLane(json) {
|
|
4703
|
+
const found = readSem();
|
|
4704
|
+
if (!found) {
|
|
4705
|
+
if (json) return emit({ path: null, values: {} }, true);
|
|
4706
|
+
return console.log(
|
|
4707
|
+
style.dim(`No ./.sechroom/lane.json pin in this checkout. Run 'sechroom lane set'.`)
|
|
4708
|
+
);
|
|
4709
|
+
}
|
|
4710
|
+
const resolved = { ...found.values };
|
|
4711
|
+
let suffixed = false;
|
|
4712
|
+
for (const k of LANE_KEYS) {
|
|
4713
|
+
const v = found.values[k];
|
|
4714
|
+
if (!v) continue;
|
|
4715
|
+
resolved[k] = applyWorktreeLaneSuffix(v);
|
|
4716
|
+
if (resolved[k] !== v) suffixed = true;
|
|
4717
|
+
}
|
|
4718
|
+
if (json) return emit({ path: found.path, values: resolved, worktreeSuffixApplied: suffixed }, true);
|
|
4719
|
+
console.log(style.dim(`from ${found.path}`));
|
|
4720
|
+
Object.entries(resolved).forEach(([k, v]) => console.log(" " + style.bold(k) + " = " + v));
|
|
4721
|
+
if (suffixed) console.log(style.dim(" (worktree -N suffix applied \u2014 non-primary git worktree)"));
|
|
4722
|
+
}
|
|
4723
|
+
function setLane(opts) {
|
|
4724
|
+
if (!opts.codeLane && !opts.designLane) fail("Provide --code-lane and/or --design-lane.");
|
|
4725
|
+
const target = localSemPath();
|
|
4726
|
+
const values = readLocalSemValues();
|
|
4727
|
+
if (opts.codeLane) values["code-lane"] = opts.codeLane;
|
|
4728
|
+
if (opts.designLane) values["design-lane"] = opts.designLane;
|
|
4729
|
+
writeSem(values, target);
|
|
4730
|
+
if (opts.json) return emit({ path: target, values }, true);
|
|
4731
|
+
console.log(style.green(`Wrote lane pin \u2192 ${target} ${style.dim("(git-ignored)")}`));
|
|
4732
|
+
Object.entries(values).forEach(([k, v]) => console.log(" " + style.dim(k) + " = " + v));
|
|
4733
|
+
}
|
|
4734
|
+
function registerLane(program2) {
|
|
4735
|
+
const lane = program2.command("lane").description("Show this checkout's continuity lane pin (worktree-aware -N suffix applied)").option("--json", "machine output").action((opts, cmd) => showLane(Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)));
|
|
4736
|
+
lane.command("set").description("Write this checkout's lane pin to ./.sechroom/lane.json").option("--code-lane <id>", "code-surface lane id (e.g. claude-code-chris)").option("--design-lane <id>", "design / substrate-authoring lane id (e.g. claude-design-chris)").option("--json", "machine output").action(
|
|
4737
|
+
(opts, cmd) => setLane({
|
|
4738
|
+
codeLane: opts.codeLane,
|
|
4739
|
+
designLane: opts.designLane,
|
|
4740
|
+
json: Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)
|
|
4741
|
+
})
|
|
4742
|
+
);
|
|
4743
|
+
lane.addHelpText(
|
|
3910
4744
|
"after",
|
|
3911
4745
|
`
|
|
3912
4746
|
Examples:
|
|
3913
|
-
$ sechroom
|
|
3914
|
-
$ sechroom
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
$ sechroom skills clean`
|
|
4747
|
+
$ sechroom lane show the resolved lane(s)
|
|
4748
|
+
$ sechroom lane set --code-lane claude-code-chris --design-lane claude-design-chris
|
|
4749
|
+
|
|
4750
|
+
In a non-primary git worktree the ratified concurrent-session -N suffix is auto-applied (SBC-1094).
|
|
4751
|
+
(Aliases: 'sechroom skills lane' / 'skills set-lane' \u2014 kept for back-compat.)`
|
|
3919
4752
|
);
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
4753
|
+
}
|
|
4754
|
+
|
|
4755
|
+
// src/setup/materialise.ts
|
|
4756
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync8, rmSync as rmSync2, writeFileSync as writeFileSync8 } from "fs";
|
|
4757
|
+
import { join as join12 } from "path";
|
|
4758
|
+
var CLIENT_SURFACE = "claude-code";
|
|
4759
|
+
function writeSkills(dir, skills, surface) {
|
|
4760
|
+
const written = [];
|
|
4761
|
+
for (const s of skills) {
|
|
4762
|
+
mkdirSync8(join12(dir, s.name), { recursive: true });
|
|
4763
|
+
writeFileSync8(join12(dir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
|
|
4764
|
+
written.push(s.name);
|
|
4765
|
+
}
|
|
4766
|
+
if (written.length) recordMaterialisedSkills(dir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
4767
|
+
return written;
|
|
4768
|
+
}
|
|
4769
|
+
function writeAgents(dir, agents, surface) {
|
|
4770
|
+
if (agents.length) mkdirSync8(dir, { recursive: true });
|
|
4771
|
+
const written = [];
|
|
4772
|
+
for (const a of agents) {
|
|
4773
|
+
const file = `${a.name}.md`;
|
|
4774
|
+
writeFileSync8(join12(dir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
|
|
4775
|
+
written.push(file);
|
|
4776
|
+
}
|
|
4777
|
+
if (written.length) recordMaterialisedSkills(dir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
4778
|
+
return written;
|
|
4779
|
+
}
|
|
4780
|
+
function writeReferencesIntoSkillDirs(dir, skills, refs) {
|
|
4781
|
+
if (!refs.length || !skills.length) return [];
|
|
4782
|
+
for (const s of skills) {
|
|
4783
|
+
const refDir = join12(dir, s.name, "references");
|
|
4784
|
+
mkdirSync8(refDir, { recursive: true });
|
|
4785
|
+
for (const r of refs) {
|
|
4786
|
+
writeFileSync8(join12(refDir, `${r.name}.md`), r.body.endsWith("\n") ? r.body : r.body + "\n");
|
|
3933
4787
|
}
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
4788
|
+
}
|
|
4789
|
+
return refs.map((r) => r.name);
|
|
4790
|
+
}
|
|
4791
|
+
var SKILL_SPEC = { kind: "skill", dir: skillsDir, resolve: resolveSkillSet, write: writeSkills };
|
|
4792
|
+
var AGENT_SPEC = { kind: "agent", dir: agentsDir, resolve: resolveAgentSet, write: writeAgents };
|
|
4793
|
+
function scopeOf(opts) {
|
|
4794
|
+
return opts.local ? "project" : resolveScope(opts.scope);
|
|
4795
|
+
}
|
|
4796
|
+
async function runInstall(spec, cmd, opts) {
|
|
4797
|
+
const g = cmd.optsWithGlobals();
|
|
4798
|
+
let scope;
|
|
4799
|
+
try {
|
|
4800
|
+
scope = scopeOf(opts);
|
|
4801
|
+
} catch (err2) {
|
|
4802
|
+
return fail(err2.message);
|
|
4803
|
+
}
|
|
4804
|
+
const cfg = resolveConfig(g);
|
|
4805
|
+
const dryRun = Boolean(opts.dryRun);
|
|
4806
|
+
const json = Boolean(g.json || opts.json);
|
|
4807
|
+
const targets = resolveClaudeTargets({ override: g.claudeConfigDir, scope, cwd: process.cwd() });
|
|
4808
|
+
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
4809
|
+
const rows = await fetchTemplateRows(cfg, personalWorkspaceId);
|
|
4810
|
+
const items = spec.resolve(rows, CLIENT_SURFACE);
|
|
4811
|
+
const refs = spec.kind === "skill" ? resolveReferenceSet(rows, CLIENT_SURFACE) : [];
|
|
4812
|
+
const results = targets.map((t) => {
|
|
4813
|
+
const dir = spec.dir(t.dir);
|
|
4814
|
+
const written = dryRun ? items.map((i) => i.name) : spec.write(dir, items, CLIENT_SURFACE);
|
|
4815
|
+
const refsWritten = dryRun ? refs.map((r) => r.name) : writeReferencesIntoSkillDirs(dir, items, refs);
|
|
4816
|
+
return { dir, label: t.label, written, refsWritten };
|
|
4817
|
+
});
|
|
4818
|
+
if (json) return emit({ kind: spec.kind, dryRun, available: items.length, references: refs.length, targets: results }, true);
|
|
4819
|
+
if (items.length === 0) {
|
|
4820
|
+
console.log(style.dim(`No ${spec.kind}s available to install \u2014 is the bundle installed for your account?`));
|
|
4821
|
+
return;
|
|
4822
|
+
}
|
|
4823
|
+
for (const r of results) {
|
|
4824
|
+
console.log(
|
|
4825
|
+
`${dryRun ? "" : style.green("\u2713 ")}${dryRun ? "would write" : "wrote"} ${r.written.length} ${spec.kind}(s) ${style.dim("\u2192")} ${r.dir}`
|
|
3945
4826
|
);
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
const
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
4827
|
+
if (r.refsWritten.length)
|
|
4828
|
+
console.log(
|
|
4829
|
+
`${dryRun ? "" : style.green("\u2713 ")}${dryRun ? "would write" : "wrote"} ${r.refsWritten.length} reference(s) into each skill ${style.dim("\u2192")} ${r.dir}/<skill>/references`
|
|
4830
|
+
);
|
|
4831
|
+
if (dryRun) for (const i of items) console.log(` ${i.name} ${style.dim(`[${i.source}]`)}`);
|
|
4832
|
+
}
|
|
4833
|
+
}
|
|
4834
|
+
function runList(spec, cmd, opts) {
|
|
4835
|
+
const g = cmd.optsWithGlobals();
|
|
4836
|
+
let scope;
|
|
4837
|
+
try {
|
|
4838
|
+
scope = scopeOf(opts);
|
|
4839
|
+
} catch (err2) {
|
|
4840
|
+
return fail(err2.message);
|
|
4841
|
+
}
|
|
4842
|
+
const json = Boolean(g.json || opts.json);
|
|
4843
|
+
const targets = resolveClaudeTargets({ override: g.claudeConfigDir, scope, cwd: process.cwd() });
|
|
4844
|
+
const out = targets.map((t) => {
|
|
4845
|
+
const dir = spec.dir(t.dir);
|
|
4846
|
+
const lock = readSkillsLock(dir);
|
|
4847
|
+
const entries = Object.entries(lock).flatMap(
|
|
4848
|
+
([slug, e]) => (e.skills ?? []).map((name) => ({ slug, name, present: existsSync10(join12(dir, name)) }))
|
|
3961
4849
|
);
|
|
3962
|
-
|
|
3963
|
-
const dir = skillsDir(!opts.local);
|
|
3964
|
-
const wantInstance = opts.instance || "default";
|
|
3965
|
-
const written = [];
|
|
3966
|
-
const bundleTagPrefix = `sechroom:bundle:${slug}@`;
|
|
3967
|
-
for (const r of rows) {
|
|
3968
|
-
const m = r.item ?? r;
|
|
3969
|
-
const tags = m.tags ?? m.Tags ?? [];
|
|
3970
|
-
if (!hasAny(tags, ROLE_TAGS)) continue;
|
|
3971
|
-
if (tagValue2(tags, "target:") !== opts.surface) continue;
|
|
3972
|
-
if (!tags.some((t) => t.startsWith(bundleTagPrefix))) continue;
|
|
3973
|
-
if ((tagValue2(tags, "sechroom:skill-instance:") ?? "default") !== wantInstance) continue;
|
|
3974
|
-
const name = tagValue2(tags, "skill:");
|
|
3975
|
-
if (!name) continue;
|
|
3976
|
-
const body = m.text ?? m.Text ?? "";
|
|
3977
|
-
mkdirSync6(join9(dir, name), { recursive: true });
|
|
3978
|
-
writeFileSync6(join9(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
|
|
3979
|
-
written.push(name);
|
|
3980
|
-
}
|
|
3981
|
-
mkdirSync6(dir, { recursive: true });
|
|
3982
|
-
const lockPath = join9(dir, LOCK);
|
|
3983
|
-
const lock = existsSync9(lockPath) ? JSON.parse(readFileSync6(lockPath, "utf8")) : {};
|
|
3984
|
-
lock[slug] = { surface: opts.surface, version, instance: wantInstance, skills: written.sort() };
|
|
3985
|
-
writeFileSync6(lockPath, JSON.stringify(lock, null, 2) + "\n");
|
|
3986
|
-
if (opts.json) return emit({ slug, version, instance: wantInstance, surface: opts.surface, dir, installed: written }, true);
|
|
3987
|
-
const instanceNote = opts.instance ? ` (${opts.instance})` : "";
|
|
3988
|
-
console.log(style.green(`Installed ${slug}@${version}${instanceNote} \u2014 ${written.length} skill(s) \u2192 ${dir}`));
|
|
3989
|
-
written.forEach((n) => console.log(" " + style.dim("\u2022") + " " + n));
|
|
3990
|
-
if (written.length === 0) console.log(style.dim(` (no '${opts.surface}' skill bodies found; check --surface)`));
|
|
3991
|
-
});
|
|
3992
|
-
skills.command("list").description("List your installed bundles (GET /me/bundle-installs)").option("--json", "machine output").action(async (opts, cmd) => {
|
|
3993
|
-
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
3994
|
-
const data = await runApi("reading your installs", () => client.GET("/me/bundle-installs", {}));
|
|
3995
|
-
if (opts.json) return emit(data, true);
|
|
3996
|
-
const installs = data?.installs ?? data?.Installs ?? [];
|
|
3997
|
-
if (installs.length === 0) return console.log(style.dim("No bundles installed."));
|
|
3998
|
-
installs.forEach((i) => {
|
|
3999
|
-
const inst = i.instance ?? i.Instance ?? "";
|
|
4000
|
-
const tag = inst ? style.dim(` [${inst}]`) : "";
|
|
4001
|
-
console.log(` ${i.bundleSlug ?? i.BundleSlug}@${i.bundleVersion ?? i.BundleVersion ?? "?"}${tag}`);
|
|
4002
|
-
});
|
|
4850
|
+
return { dir, label: t.label, entries };
|
|
4003
4851
|
});
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4852
|
+
if (json) return emit({ kind: spec.kind, targets: out }, true);
|
|
4853
|
+
let any = false;
|
|
4854
|
+
for (const t of out) {
|
|
4855
|
+
if (t.entries.length === 0) continue;
|
|
4856
|
+
any = true;
|
|
4857
|
+
console.log(style.bold(t.dir) + ":");
|
|
4858
|
+
for (const e of t.entries) {
|
|
4859
|
+
const flag = e.present ? "" : style.dim("(missing) ");
|
|
4860
|
+
console.log(` ${flag}${e.name} ${style.dim(`[${e.slug}]`)}`);
|
|
4861
|
+
}
|
|
4862
|
+
}
|
|
4863
|
+
if (!any) console.log(style.dim(`No ${spec.kind}s materialised. Run \`sechroom ${spec.kind}s install\`.`));
|
|
4864
|
+
}
|
|
4865
|
+
function runClean(spec, cmd, opts, slugArg) {
|
|
4866
|
+
const g = cmd.optsWithGlobals();
|
|
4867
|
+
const slug = slugArg || DEFAULT_SKILLS_SLUG;
|
|
4868
|
+
let scope;
|
|
4869
|
+
try {
|
|
4870
|
+
scope = scopeOf(opts);
|
|
4871
|
+
} catch (err2) {
|
|
4872
|
+
return fail(err2.message);
|
|
4873
|
+
}
|
|
4874
|
+
const json = Boolean(g.json || opts.json);
|
|
4875
|
+
const targets = resolveClaudeTargets({ override: g.claudeConfigDir, scope, cwd: process.cwd() });
|
|
4876
|
+
const cleaned = [];
|
|
4877
|
+
const missing = [];
|
|
4878
|
+
for (const t of targets) {
|
|
4879
|
+
const dir = spec.dir(t.dir);
|
|
4880
|
+
const lock = readSkillsLock(dir);
|
|
4010
4881
|
const entry = lock[slug];
|
|
4011
|
-
if (!entry)
|
|
4882
|
+
if (!entry) {
|
|
4883
|
+
missing.push(join12(dir, SKILLS_LOCK));
|
|
4884
|
+
continue;
|
|
4885
|
+
}
|
|
4012
4886
|
const removed = [];
|
|
4013
4887
|
for (const name of entry.skills) {
|
|
4014
|
-
const
|
|
4015
|
-
if (
|
|
4016
|
-
rmSync2(
|
|
4888
|
+
const p = join12(dir, name);
|
|
4889
|
+
if (existsSync10(p)) {
|
|
4890
|
+
rmSync2(p, { recursive: true, force: true });
|
|
4017
4891
|
removed.push(name);
|
|
4018
4892
|
}
|
|
4019
4893
|
}
|
|
4020
4894
|
delete lock[slug];
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
skills.
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4895
|
+
writeSkillsLock(dir, lock);
|
|
4896
|
+
cleaned.push({ dir, removed });
|
|
4897
|
+
}
|
|
4898
|
+
if (cleaned.length === 0) {
|
|
4899
|
+
return fail(`No materialised ${spec.kind}s recorded for '${slug}' in ${missing.join(", ")}.`);
|
|
4900
|
+
}
|
|
4901
|
+
if (json) return emit({ kind: spec.kind, slug, cleaned, missing }, true);
|
|
4902
|
+
for (const c of cleaned) {
|
|
4903
|
+
console.log(style.green(`Removed ${c.removed.length} ${spec.kind}(s) for ${slug} from ${c.dir}`));
|
|
4904
|
+
}
|
|
4905
|
+
}
|
|
4906
|
+
|
|
4907
|
+
// src/commands/skills.ts
|
|
4908
|
+
function registerSkills(program2) {
|
|
4909
|
+
const skills = program2.command("skills").description("Manage operator skills (install to disk, list, clean)");
|
|
4910
|
+
skills.addHelpText(
|
|
4911
|
+
"after",
|
|
4912
|
+
`
|
|
4913
|
+
Examples:
|
|
4914
|
+
$ sechroom skills install materialise your installed skills to ~/.claude/skills
|
|
4915
|
+
$ sechroom skills install --scope project write them to ./.claude/skills instead
|
|
4916
|
+
$ sechroom skills list what's materialised on disk
|
|
4917
|
+
$ sechroom skills clean remove the materialised skill files
|
|
4918
|
+
$ sechroom skills set-lane --code-lane claude-code-chris --design-lane claude-design-chris
|
|
4919
|
+
|
|
4920
|
+
`
|
|
4921
|
+
);
|
|
4922
|
+
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));
|
|
4923
|
+
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));
|
|
4924
|
+
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));
|
|
4925
|
+
skills.command("set-lane").description("Alias of `sechroom lane set` (kept for back-compat) \u2014 write this checkout's lane pin").option("--code-lane <id>", "code-surface lane id (e.g. claude-code-chris)").option("--design-lane <id>", "design / substrate-authoring lane id (e.g. claude-design-chris)").option("--json", "machine output").action(
|
|
4926
|
+
(opts, cmd) => setLane({
|
|
4927
|
+
codeLane: opts.codeLane,
|
|
4928
|
+
designLane: opts.designLane,
|
|
4929
|
+
json: Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)
|
|
4930
|
+
})
|
|
4931
|
+
);
|
|
4932
|
+
skills.command("lane").description("Alias of `sechroom lane` (kept for back-compat) \u2014 show this checkout's lane pin").option("--json", "machine output").action((opts, cmd) => showLane(Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)));
|
|
4047
4933
|
skills.command("set-workflow").description("Set your per-operator workflow defaults (server-side; follows you across tenants)").option("--default-code-lane <id>", "personal default code lane (e.g. claude-code-chris)").option("--default-design-lane <id>", "personal default design lane (e.g. claude-design-chris)").option("--handover-recipient <id>", "your daily-handover counterparty (e.g. andy)").option("--json", "machine output").action(async (opts, cmd) => {
|
|
4048
4934
|
if (!opts.defaultCodeLane && !opts.defaultDesignLane && !opts.handoverRecipient)
|
|
4049
4935
|
fail("Provide at least one of --default-code-lane / --default-design-lane / --handover-recipient.");
|
|
@@ -4078,7 +4964,7 @@ Examples:
|
|
|
4078
4964
|
console.log(" " + style.bold("default-design-lane") + " = " + (data?.defaultDesignLane ?? style.dim("(unset)")));
|
|
4079
4965
|
console.log(" " + style.bold("handover-recipient") + " = " + (data?.handoverRecipient ?? style.dim("(unset)")));
|
|
4080
4966
|
});
|
|
4081
|
-
skills.command("resolve").description("Resolve the effective ${identity.*} slot values (per-location .
|
|
4967
|
+
skills.command("resolve").description("Resolve the effective ${identity.*} slot values (per-location .sechroom/lane.json + per-operator workflow prefs)").option("--json", "machine output (a flat slot->value map + per-slot source)").action(async (opts, cmd) => {
|
|
4082
4968
|
const local = readSem()?.values ?? {};
|
|
4083
4969
|
let operator = {};
|
|
4084
4970
|
try {
|
|
@@ -4106,23 +4992,46 @@ Examples:
|
|
|
4106
4992
|
});
|
|
4107
4993
|
}
|
|
4108
4994
|
|
|
4995
|
+
// src/commands/agents.ts
|
|
4996
|
+
function registerAgents(program2) {
|
|
4997
|
+
const agents = program2.command("agents").description("Manage operator subagents (install to disk, list, clean)");
|
|
4998
|
+
agents.addHelpText(
|
|
4999
|
+
"after",
|
|
5000
|
+
`
|
|
5001
|
+
Examples:
|
|
5002
|
+
$ sechroom agents install materialise your installed agents to ~/.claude/agents
|
|
5003
|
+
$ sechroom agents install --scope project write them to ./.claude/agents instead
|
|
5004
|
+
$ sechroom agents install --claude-config-dir ~/.claude-work target another instance
|
|
5005
|
+
$ sechroom agents list what's materialised on disk
|
|
5006
|
+
$ sechroom agents clean remove the materialised agent files
|
|
5007
|
+
|
|
5008
|
+
Agents are resolved from the agent target (target:claude-agent), the dispatchable
|
|
5009
|
+
workers your loop skills call (e.g. find-prior-art \u2192 substrate-miner).`
|
|
5010
|
+
);
|
|
5011
|
+
agents.command("install").description("Materialise your installed subagents 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(AGENT_SPEC, cmd, opts));
|
|
5012
|
+
agents.command("list").description("List the subagents 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(AGENT_SPEC, cmd, opts));
|
|
5013
|
+
agents.command("clean [slug]").description(`Remove subagent 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(AGENT_SPEC, cmd, opts, slugArg));
|
|
5014
|
+
}
|
|
5015
|
+
|
|
4109
5016
|
// src/commands/reset.ts
|
|
4110
|
-
import { homedir as
|
|
4111
|
-
import { join as
|
|
4112
|
-
import { existsSync as
|
|
4113
|
-
var
|
|
4114
|
-
var localSkillsDir = () =>
|
|
4115
|
-
var globalSkillsDir = () =>
|
|
5017
|
+
import { homedir as homedir4 } from "os";
|
|
5018
|
+
import { join as join13 } from "path";
|
|
5019
|
+
import { existsSync as existsSync11, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
|
|
5020
|
+
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
5021
|
+
var localSkillsDir = () => join13(process.cwd(), ".claude", "skills");
|
|
5022
|
+
var globalSkillsDir = () => join13(homedir4(), ".claude", "skills");
|
|
5023
|
+
var localAgentsDir = () => join13(process.cwd(), ".claude", "agents");
|
|
5024
|
+
var globalAgentsDir = () => join13(homedir4(), ".claude", "agents");
|
|
4116
5025
|
function removeMaterialisedSkills(dir) {
|
|
4117
5026
|
const removed = [];
|
|
4118
|
-
const lockPath =
|
|
4119
|
-
if (!
|
|
5027
|
+
const lockPath = join13(dir, SKILLS_LOCK2);
|
|
5028
|
+
if (!existsSync11(lockPath)) return removed;
|
|
4120
5029
|
try {
|
|
4121
5030
|
const lock = JSON.parse(readFileSync7(lockPath, "utf8"));
|
|
4122
5031
|
for (const entry of Object.values(lock)) {
|
|
4123
5032
|
for (const name of entry.skills ?? []) {
|
|
4124
|
-
const p =
|
|
4125
|
-
if (
|
|
5033
|
+
const p = join13(dir, name);
|
|
5034
|
+
if (existsSync11(p)) {
|
|
4126
5035
|
rmSync3(p, { recursive: true, force: true });
|
|
4127
5036
|
removed.push(p);
|
|
4128
5037
|
}
|
|
@@ -4167,28 +5076,30 @@ function registerReset(program2) {
|
|
|
4167
5076
|
}
|
|
4168
5077
|
}
|
|
4169
5078
|
const removed = [];
|
|
4170
|
-
const stateDir =
|
|
4171
|
-
if (
|
|
5079
|
+
const stateDir = join13(process.cwd(), ".sechroom");
|
|
5080
|
+
if (existsSync11(stateDir)) {
|
|
4172
5081
|
rmSync3(stateDir, { recursive: true, force: true });
|
|
4173
5082
|
removed.push(stateDir);
|
|
4174
5083
|
}
|
|
4175
|
-
const legacyCfg =
|
|
4176
|
-
if (
|
|
5084
|
+
const legacyCfg = join13(process.cwd(), ".sechroom.json");
|
|
5085
|
+
if (existsSync11(legacyCfg)) {
|
|
4177
5086
|
rmSync3(legacyCfg, { force: true });
|
|
4178
5087
|
removed.push(legacyCfg);
|
|
4179
5088
|
}
|
|
4180
|
-
const legacySem =
|
|
4181
|
-
if (
|
|
5089
|
+
const legacySem = join13(process.cwd(), ".sem");
|
|
5090
|
+
if (existsSync11(legacySem)) {
|
|
4182
5091
|
rmSync3(legacySem, { force: true });
|
|
4183
5092
|
removed.push(legacySem);
|
|
4184
5093
|
}
|
|
4185
5094
|
removed.push(...removeMaterialisedSkills(localSkillsDir()));
|
|
5095
|
+
removed.push(...removeMaterialisedSkills(localAgentsDir()));
|
|
4186
5096
|
if (global) {
|
|
4187
5097
|
const tok = clearToken();
|
|
4188
5098
|
if (tok) removed.push(tok);
|
|
4189
5099
|
const cfg = clearPersisted();
|
|
4190
5100
|
if (cfg) removed.push(cfg);
|
|
4191
5101
|
removed.push(...removeMaterialisedSkills(globalSkillsDir()));
|
|
5102
|
+
removed.push(...removeMaterialisedSkills(globalAgentsDir()));
|
|
4192
5103
|
}
|
|
4193
5104
|
if (json) return emit({ global, removed }, true);
|
|
4194
5105
|
if (removed.length === 0) {
|
|
@@ -4213,7 +5124,7 @@ function resolveVersion() {
|
|
|
4213
5124
|
}
|
|
4214
5125
|
}
|
|
4215
5126
|
var program = new Command();
|
|
4216
|
-
program.name("sechroom").description("Sechroom CLI \u2014 thin generated client over the Sechroom HTTP API. An agent/human surface alongside MCP.").version(resolveVersion()).option("--base-url <url>", "API base URL (overrides config / SECHROOM_BASE_URL)").option("--tenant <tenant>", "Tenant id (required by the API; overrides config / SECHROOM_TENANT)").option("--binding <name>", "Named workspace binding from .sechroom.json `workspaces` (overrides path auto-selection / SECHROOM_BINDING)").option("--json", "Emit compact JSON (for scripts and agents)", false);
|
|
5127
|
+
program.name("sechroom").description("Sechroom CLI \u2014 thin generated client over the Sechroom HTTP API. An agent/human surface alongside MCP.").version(resolveVersion()).option("--base-url <url>", "API base URL (overrides config / SECHROOM_BASE_URL)").option("--tenant <tenant>", "Tenant id (required by the API; overrides config / SECHROOM_TENANT)").option("--binding <name>", "Named workspace binding from .sechroom.json `workspaces` (overrides path auto-selection / SECHROOM_BINDING)").option("--claude-config-dir <dirs>", "Claude config dir(s), comma-separated (overrides CLAUDE_CONFIG_DIR / ~/.claude)").option("--codex-home <dir>", "Codex home (overrides CODEX_HOME / ~/.codex)").option("--json", "Emit compact JSON (for scripts and agents)", false);
|
|
4217
5128
|
program.addHelpText(
|
|
4218
5129
|
"after",
|
|
4219
5130
|
`
|
|
@@ -4317,15 +5228,19 @@ registerWorkspace(program);
|
|
|
4317
5228
|
registerProject(program);
|
|
4318
5229
|
registerFiling(program);
|
|
4319
5230
|
registerContinuity(program);
|
|
5231
|
+
registerCheckpoint(program);
|
|
4320
5232
|
registerHook(program);
|
|
4321
5233
|
registerId(program);
|
|
4322
5234
|
registerAccount(program);
|
|
4323
5235
|
registerChat(program);
|
|
4324
5236
|
registerInit(program);
|
|
4325
5237
|
registerSetup(program);
|
|
5238
|
+
registerNamespace(program);
|
|
4326
5239
|
registerOnboard(program);
|
|
4327
5240
|
registerSweep(program);
|
|
4328
5241
|
registerSkills(program);
|
|
5242
|
+
registerAgents(program);
|
|
5243
|
+
registerLane(program);
|
|
4329
5244
|
registerReset(program);
|
|
4330
5245
|
program.parseAsync().catch((err2) => {
|
|
4331
5246
|
process.stderr.write(`error: ${err2 instanceof Error ? err2.message : String(err2)}
|