clawstrap 1.4.1 → 1.5.0
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.cjs +546 -79
- package/package.json +3 -1
package/dist/index.cjs
CHANGED
|
@@ -88,6 +88,7 @@ __export(writers_exports, {
|
|
|
88
88
|
appendToFutureConsiderations: () => appendToFutureConsiderations,
|
|
89
89
|
appendToGotchaLog: () => appendToGotchaLog,
|
|
90
90
|
appendToMemory: () => appendToMemory,
|
|
91
|
+
appendToOpenThreads: () => appendToOpenThreads,
|
|
91
92
|
writeConventions: () => writeConventions
|
|
92
93
|
});
|
|
93
94
|
function formatEntry(source, text) {
|
|
@@ -114,6 +115,7 @@ function appendToMemory(rootDir, entries, source) {
|
|
|
114
115
|
const appendText = "\n" + toAppend.join("\n") + "\n";
|
|
115
116
|
import_node_fs14.default.appendFileSync(memoryPath, appendText, "utf-8");
|
|
116
117
|
}
|
|
118
|
+
return toAppend.length;
|
|
117
119
|
}
|
|
118
120
|
function appendToGotchaLog(rootDir, entries) {
|
|
119
121
|
const logPath = import_node_path15.default.join(rootDir, ".claude", "gotcha-log.md");
|
|
@@ -141,6 +143,19 @@ function appendToFutureConsiderations(rootDir, entries) {
|
|
|
141
143
|
const toAppend = entries.map((e) => formatEntry("session", e)).join("\n");
|
|
142
144
|
import_node_fs14.default.appendFileSync(fcPath, "\n" + toAppend + "\n", "utf-8");
|
|
143
145
|
}
|
|
146
|
+
function appendToOpenThreads(rootDir, entries) {
|
|
147
|
+
const otPath = import_node_path15.default.join(rootDir, ".claude", "memory", "open-threads.md");
|
|
148
|
+
import_node_fs14.default.mkdirSync(import_node_path15.default.dirname(otPath), { recursive: true });
|
|
149
|
+
if (!import_node_fs14.default.existsSync(otPath)) {
|
|
150
|
+
import_node_fs14.default.writeFileSync(
|
|
151
|
+
otPath,
|
|
152
|
+
"# Open Threads\n\nUnresolved questions and next steps.\n\n",
|
|
153
|
+
"utf-8"
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
const toAppend = entries.map((e) => formatEntry("session", e)).join("\n");
|
|
157
|
+
import_node_fs14.default.appendFileSync(otPath, "\n" + toAppend + "\n", "utf-8");
|
|
158
|
+
}
|
|
144
159
|
function buildAutoBlock(sections) {
|
|
145
160
|
const lines = [AUTO_START];
|
|
146
161
|
lines.push("## Naming Conventions");
|
|
@@ -177,6 +192,11 @@ function buildAutoBlock(sections) {
|
|
|
177
192
|
} else {
|
|
178
193
|
lines.push("- No comment patterns detected.");
|
|
179
194
|
}
|
|
195
|
+
if (sections.architecture && sections.architecture.length > 0) {
|
|
196
|
+
lines.push("");
|
|
197
|
+
lines.push("## Architecture & Design Patterns");
|
|
198
|
+
for (const item of sections.architecture) lines.push(`- ${item}`);
|
|
199
|
+
}
|
|
180
200
|
lines.push(AUTO_END);
|
|
181
201
|
return lines.join("\n");
|
|
182
202
|
}
|
|
@@ -263,12 +283,20 @@ var ClawstrapConfigSchema = import_zod.z.object({
|
|
|
263
283
|
scan: import_zod.z.object({
|
|
264
284
|
intervalDays: import_zod.z.number().default(7)
|
|
265
285
|
}).default({}),
|
|
286
|
+
git: import_zod.z.object({
|
|
287
|
+
pollIntervalMinutes: import_zod.z.number().default(5)
|
|
288
|
+
}).default({}),
|
|
289
|
+
synthesis: import_zod.z.object({
|
|
290
|
+
enabled: import_zod.z.boolean().default(false),
|
|
291
|
+
triggerEveryN: import_zod.z.number().default(10)
|
|
292
|
+
}).default({}),
|
|
266
293
|
silent: import_zod.z.boolean().default(false)
|
|
267
294
|
}).optional(),
|
|
268
295
|
watchState: import_zod.z.object({
|
|
269
296
|
lastGitCommit: import_zod.z.string().optional(),
|
|
270
297
|
lastScanAt: import_zod.z.string().optional(),
|
|
271
|
-
lastTranscriptAt: import_zod.z.string().optional()
|
|
298
|
+
lastTranscriptAt: import_zod.z.string().optional(),
|
|
299
|
+
entriesSinceLastSynthesis: import_zod.z.coerce.number().optional()
|
|
272
300
|
}).optional(),
|
|
273
301
|
lastExport: LastExportSchema
|
|
274
302
|
});
|
|
@@ -777,7 +805,7 @@ async function init(directory, options) {
|
|
|
777
805
|
import_node_fs2.default.mkdirSync(targetDir, { recursive: true });
|
|
778
806
|
}
|
|
779
807
|
const config = ClawstrapConfigSchema.parse({
|
|
780
|
-
version: "1.4.
|
|
808
|
+
version: "1.4.2",
|
|
781
809
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
782
810
|
workspaceName: answers.workspaceName,
|
|
783
811
|
targetDirectory: directory,
|
|
@@ -1499,9 +1527,8 @@ Exported to ${import_node_path14.default.relative(process.cwd(), outDir) || outD
|
|
|
1499
1527
|
}
|
|
1500
1528
|
|
|
1501
1529
|
// src/watch.ts
|
|
1502
|
-
var
|
|
1503
|
-
var
|
|
1504
|
-
var import_node_fs19 = __toESM(require("fs"), 1);
|
|
1530
|
+
var import_node_path22 = __toESM(require("path"), 1);
|
|
1531
|
+
var import_node_fs21 = __toESM(require("fs"), 1);
|
|
1505
1532
|
|
|
1506
1533
|
// src/watch/git.ts
|
|
1507
1534
|
var import_node_child_process = require("child_process");
|
|
@@ -1844,17 +1871,220 @@ async function runScan(rootDir) {
|
|
|
1844
1871
|
init_writers();
|
|
1845
1872
|
|
|
1846
1873
|
// src/watch/daemon.ts
|
|
1847
|
-
var
|
|
1848
|
-
var
|
|
1874
|
+
var import_node_fs20 = __toESM(require("fs"), 1);
|
|
1875
|
+
var import_node_path21 = __toESM(require("path"), 1);
|
|
1849
1876
|
init_writers();
|
|
1850
1877
|
|
|
1851
|
-
// src/watch/
|
|
1878
|
+
// src/watch/synthesize.ts
|
|
1852
1879
|
var import_node_fs17 = __toESM(require("fs"), 1);
|
|
1853
1880
|
var import_node_path18 = __toESM(require("path"), 1);
|
|
1881
|
+
init_dedup();
|
|
1882
|
+
var SYNTH_START = "<!-- CLAWSTRAP:SYNTHESIS:START -->";
|
|
1883
|
+
var SYNTH_END = "<!-- CLAWSTRAP:SYNTHESIS:END -->";
|
|
1884
|
+
var MAX_ENTRIES_TO_SEND = 20;
|
|
1885
|
+
function extractExistingSummary(content) {
|
|
1886
|
+
const startIdx = content.indexOf(SYNTH_START);
|
|
1887
|
+
const endIdx = content.indexOf(SYNTH_END);
|
|
1888
|
+
if (startIdx === -1 || endIdx === -1) return null;
|
|
1889
|
+
const block = content.slice(startIdx + SYNTH_START.length, endIdx).trim();
|
|
1890
|
+
const lines = block.split("\n");
|
|
1891
|
+
let start = 0;
|
|
1892
|
+
if (/^##\s+Living Summary/.test(lines[start] ?? "")) start++;
|
|
1893
|
+
if (/^>\s+Updated:/.test(lines[start] ?? "")) start++;
|
|
1894
|
+
return lines.slice(start).join("\n").trim() || null;
|
|
1895
|
+
}
|
|
1896
|
+
function buildSynthBlock(summary) {
|
|
1897
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1898
|
+
return [
|
|
1899
|
+
SYNTH_START,
|
|
1900
|
+
"## Living Summary",
|
|
1901
|
+
`> Updated: ${ts}`,
|
|
1902
|
+
"",
|
|
1903
|
+
summary,
|
|
1904
|
+
SYNTH_END
|
|
1905
|
+
].join("\n");
|
|
1906
|
+
}
|
|
1907
|
+
function writeSynthBlock(memoryPath, summary) {
|
|
1908
|
+
const content = import_node_fs17.default.existsSync(memoryPath) ? import_node_fs17.default.readFileSync(memoryPath, "utf-8") : "";
|
|
1909
|
+
const block = buildSynthBlock(summary);
|
|
1910
|
+
const startIdx = content.indexOf(SYNTH_START);
|
|
1911
|
+
const endIdx = content.indexOf(SYNTH_END);
|
|
1912
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
1913
|
+
const before = content.slice(0, startIdx);
|
|
1914
|
+
const after = content.slice(endIdx + SYNTH_END.length);
|
|
1915
|
+
import_node_fs17.default.writeFileSync(memoryPath, before + block + after, "utf-8");
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
const headingMatch = /^#[^\n]*\n?/m.exec(content);
|
|
1919
|
+
if (headingMatch) {
|
|
1920
|
+
const insertAt = headingMatch.index + headingMatch[0].length;
|
|
1921
|
+
const updated = content.slice(0, insertAt) + "\n" + block + "\n" + content.slice(insertAt);
|
|
1922
|
+
import_node_fs17.default.writeFileSync(memoryPath, updated, "utf-8");
|
|
1923
|
+
} else {
|
|
1924
|
+
import_node_fs17.default.writeFileSync(memoryPath, block + "\n\n" + content, "utf-8");
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
async function synthesizeMemory(rootDir, adapter) {
|
|
1928
|
+
const memoryPath = import_node_path18.default.join(rootDir, ".claude", "memory", "MEMORY.md");
|
|
1929
|
+
if (!import_node_fs17.default.existsSync(memoryPath)) return null;
|
|
1930
|
+
const content = import_node_fs17.default.readFileSync(memoryPath, "utf-8");
|
|
1931
|
+
const contentWithoutSynthBlock = content.replace(
|
|
1932
|
+
/<!-- CLAWSTRAP:SYNTHESIS:START -->[\s\S]*?<!-- CLAWSTRAP:SYNTHESIS:END -->/,
|
|
1933
|
+
""
|
|
1934
|
+
);
|
|
1935
|
+
const allEntries = parseMemoryEntries(contentWithoutSynthBlock);
|
|
1936
|
+
if (allEntries.length === 0) return null;
|
|
1937
|
+
const recentEntries = allEntries.slice(-MAX_ENTRIES_TO_SEND);
|
|
1938
|
+
const existingSummary = extractExistingSummary(content);
|
|
1939
|
+
let prompt;
|
|
1940
|
+
if (existingSummary) {
|
|
1941
|
+
prompt = `You are maintaining a living summary of an AI agent workspace.
|
|
1942
|
+
|
|
1943
|
+
Current summary:
|
|
1944
|
+
${existingSummary}
|
|
1945
|
+
|
|
1946
|
+
Recent new memory entries:
|
|
1947
|
+
${recentEntries.join("\n---\n")}
|
|
1948
|
+
|
|
1949
|
+
Update the summary to incorporate the new information. Write 3\u20135 sentences of persistent truths about how this workspace operates. Output only the updated paragraph \u2014 no heading, no markdown, no explanation.`;
|
|
1950
|
+
} else {
|
|
1951
|
+
prompt = `You are summarising an AI agent workspace from its memory log.
|
|
1952
|
+
|
|
1953
|
+
Recent memory entries:
|
|
1954
|
+
${recentEntries.join("\n---\n")}
|
|
1955
|
+
|
|
1956
|
+
Write a concise 3\u20135 sentence summary of the persistent truths about how this workspace operates. Output only the paragraph \u2014 no heading, no markdown, no explanation.`;
|
|
1957
|
+
}
|
|
1958
|
+
let response;
|
|
1959
|
+
try {
|
|
1960
|
+
response = await adapter.complete(prompt);
|
|
1961
|
+
} catch {
|
|
1962
|
+
return null;
|
|
1963
|
+
}
|
|
1964
|
+
const summary = response.replace(/^```(?:markdown)?\s*/m, "").replace(/\s*```\s*$/m, "").trim();
|
|
1965
|
+
if (!summary) return null;
|
|
1966
|
+
try {
|
|
1967
|
+
writeSynthBlock(memoryPath, summary);
|
|
1968
|
+
} catch {
|
|
1969
|
+
return null;
|
|
1970
|
+
}
|
|
1971
|
+
return summary;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
// src/watch/infer.ts
|
|
1975
|
+
var import_node_child_process2 = require("child_process");
|
|
1976
|
+
var import_node_fs18 = __toESM(require("fs"), 1);
|
|
1977
|
+
var import_node_path19 = __toESM(require("path"), 1);
|
|
1978
|
+
var CODE_EXTS2 = /* @__PURE__ */ new Set([".ts", ".js", ".tsx", ".jsx"]);
|
|
1979
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", "tmp", "dist", ".claude"]);
|
|
1980
|
+
var MAX_FILES = 10;
|
|
1981
|
+
var MAX_LINES_PER_FILE = 150;
|
|
1982
|
+
var MIN_FILES = 3;
|
|
1983
|
+
function walkCodeFiles(rootDir) {
|
|
1984
|
+
const results = [];
|
|
1985
|
+
function walk(dir, depth = 0) {
|
|
1986
|
+
if (depth > 8) return;
|
|
1987
|
+
let entries;
|
|
1988
|
+
try {
|
|
1989
|
+
entries = import_node_fs18.default.readdirSync(dir, { withFileTypes: true });
|
|
1990
|
+
} catch {
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
for (const entry of entries) {
|
|
1994
|
+
if (SKIP_DIRS2.has(entry.name)) continue;
|
|
1995
|
+
const fullPath = import_node_path19.default.join(dir, entry.name);
|
|
1996
|
+
if (entry.isDirectory()) {
|
|
1997
|
+
walk(fullPath, depth + 1);
|
|
1998
|
+
} else if (entry.isFile() && CODE_EXTS2.has(import_node_path19.default.extname(entry.name))) {
|
|
1999
|
+
if (!/\.(test|spec)\.(ts|js|tsx|jsx)$/.test(entry.name)) {
|
|
2000
|
+
results.push(fullPath);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
walk(rootDir);
|
|
2006
|
+
return results;
|
|
2007
|
+
}
|
|
2008
|
+
function getRecentlyChangedFiles(rootDir) {
|
|
2009
|
+
try {
|
|
2010
|
+
const output = (0, import_node_child_process2.execSync)(
|
|
2011
|
+
`git -C "${rootDir}" log --format='' --name-only -n 100`,
|
|
2012
|
+
{ encoding: "utf-8" }
|
|
2013
|
+
).trim();
|
|
2014
|
+
if (!output) return [];
|
|
2015
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2016
|
+
const files = [];
|
|
2017
|
+
for (const line of output.split("\n")) {
|
|
2018
|
+
const trimmed = line.trim();
|
|
2019
|
+
if (!trimmed || seen.has(trimmed)) continue;
|
|
2020
|
+
seen.add(trimmed);
|
|
2021
|
+
const ext = import_node_path19.default.extname(trimmed);
|
|
2022
|
+
if (!CODE_EXTS2.has(ext)) continue;
|
|
2023
|
+
if (/\.(test|spec)\.(ts|js|tsx|jsx)$/.test(trimmed)) continue;
|
|
2024
|
+
const abs = import_node_path19.default.join(rootDir, trimmed);
|
|
2025
|
+
if (import_node_fs18.default.existsSync(abs)) files.push(abs);
|
|
2026
|
+
}
|
|
2027
|
+
return files;
|
|
2028
|
+
} catch {
|
|
2029
|
+
return [];
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
function readTruncated(filePath, rootDir) {
|
|
2033
|
+
let content;
|
|
2034
|
+
try {
|
|
2035
|
+
content = import_node_fs18.default.readFileSync(filePath, "utf-8");
|
|
2036
|
+
} catch {
|
|
2037
|
+
return "";
|
|
2038
|
+
}
|
|
2039
|
+
const lines = content.split("\n");
|
|
2040
|
+
const relPath = import_node_path19.default.relative(rootDir, filePath);
|
|
2041
|
+
const truncated = lines.length > MAX_LINES_PER_FILE ? lines.slice(0, MAX_LINES_PER_FILE).join("\n") + "\n// ... truncated" : lines.join("\n");
|
|
2042
|
+
return `=== ${relPath} ===
|
|
2043
|
+
${truncated}`;
|
|
2044
|
+
}
|
|
2045
|
+
async function inferArchitecturePatterns(rootDir, syntacticSections, adapter) {
|
|
2046
|
+
let candidates = getRecentlyChangedFiles(rootDir);
|
|
2047
|
+
if (candidates.length < MIN_FILES) {
|
|
2048
|
+
candidates = walkCodeFiles(rootDir);
|
|
2049
|
+
}
|
|
2050
|
+
if (candidates.length < MIN_FILES) return [];
|
|
2051
|
+
const sampled = candidates.slice(0, MAX_FILES);
|
|
2052
|
+
const fileSamples = sampled.map((f) => readTruncated(f, rootDir)).filter(Boolean).join("\n\n");
|
|
2053
|
+
if (!fileSamples) return [];
|
|
2054
|
+
const syntacticSummary = [
|
|
2055
|
+
`Naming: ${syntacticSections.naming.join("; ")}`,
|
|
2056
|
+
`Imports: ${syntacticSections.imports.join("; ")}`,
|
|
2057
|
+
`Error handling: ${syntacticSections.errorHandling.join("; ")}`
|
|
2058
|
+
].join("\n");
|
|
2059
|
+
const prompt = `You are analysing a software project to infer its architectural and design conventions.
|
|
2060
|
+
|
|
2061
|
+
Syntactic analysis already found:
|
|
2062
|
+
${syntacticSummary}
|
|
2063
|
+
|
|
2064
|
+
Source file samples:
|
|
2065
|
+
${fileSamples}
|
|
2066
|
+
|
|
2067
|
+
Based on the code, identify 3\u20138 architectural or design patterns as imperative rules.
|
|
2068
|
+
Rules must be specific to this codebase, not generic best practices.
|
|
2069
|
+
Format: one rule per line, starting with "Always", "Never", or "When".
|
|
2070
|
+
Output only the rules \u2014 no explanation, no numbering, no markdown.`;
|
|
2071
|
+
let response;
|
|
2072
|
+
try {
|
|
2073
|
+
response = await adapter.complete(prompt);
|
|
2074
|
+
} catch {
|
|
2075
|
+
return [];
|
|
2076
|
+
}
|
|
2077
|
+
const rules = response.replace(/^```(?:markdown)?\s*/m, "").replace(/\s*```\s*$/m, "").split("\n").map((line) => line.replace(/^\s*[-*\d.]+\s*/, "").trim()).filter((line) => /^(Always|Never|When)\b/i.test(line));
|
|
2078
|
+
return rules;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// src/watch/transcripts.ts
|
|
2082
|
+
var import_node_fs19 = __toESM(require("fs"), 1);
|
|
2083
|
+
var import_node_path20 = __toESM(require("path"), 1);
|
|
1854
2084
|
async function processTranscript(filePath, adapter) {
|
|
1855
2085
|
let content;
|
|
1856
2086
|
try {
|
|
1857
|
-
content =
|
|
2087
|
+
content = import_node_fs19.default.readFileSync(filePath, "utf-8");
|
|
1858
2088
|
} catch {
|
|
1859
2089
|
return null;
|
|
1860
2090
|
}
|
|
@@ -1897,13 +2127,13 @@ Each item must be a concise one-sentence string. Arrays may be empty.`;
|
|
|
1897
2127
|
}
|
|
1898
2128
|
}
|
|
1899
2129
|
function watchTranscriptDir(rootDir, onNewFile) {
|
|
1900
|
-
const sessionsDir =
|
|
1901
|
-
|
|
1902
|
-
const watcher =
|
|
2130
|
+
const sessionsDir = import_node_path20.default.join(rootDir, "tmp", "sessions");
|
|
2131
|
+
import_node_fs19.default.mkdirSync(sessionsDir, { recursive: true });
|
|
2132
|
+
const watcher = import_node_fs19.default.watch(sessionsDir, (event, filename) => {
|
|
1903
2133
|
if (event !== "rename" || !filename) return;
|
|
1904
2134
|
if (!filename.endsWith(".md")) return;
|
|
1905
|
-
const filePath =
|
|
1906
|
-
if (!
|
|
2135
|
+
const filePath = import_node_path20.default.join(sessionsDir, filename);
|
|
2136
|
+
if (!import_node_fs19.default.existsSync(filePath)) return;
|
|
1907
2137
|
onNewFile(filePath).catch(() => {
|
|
1908
2138
|
});
|
|
1909
2139
|
});
|
|
@@ -1913,12 +2143,12 @@ function watchTranscriptDir(rootDir, onNewFile) {
|
|
|
1913
2143
|
}
|
|
1914
2144
|
|
|
1915
2145
|
// src/watch/adapters/claude-local.ts
|
|
1916
|
-
var
|
|
2146
|
+
var import_node_child_process3 = require("child_process");
|
|
1917
2147
|
var ClaudeLocalAdapter = class {
|
|
1918
2148
|
async complete(prompt) {
|
|
1919
2149
|
const escaped = prompt.replace(/'/g, "'\\''");
|
|
1920
2150
|
try {
|
|
1921
|
-
const result = (0,
|
|
2151
|
+
const result = (0, import_node_child_process3.execSync)(`claude -p '${escaped}'`, {
|
|
1922
2152
|
encoding: "utf-8",
|
|
1923
2153
|
timeout: 6e4,
|
|
1924
2154
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1999,12 +2229,12 @@ var OllamaAdapter = class {
|
|
|
1999
2229
|
};
|
|
2000
2230
|
|
|
2001
2231
|
// src/watch/adapters/codex-local.ts
|
|
2002
|
-
var
|
|
2232
|
+
var import_node_child_process4 = require("child_process");
|
|
2003
2233
|
var CodexLocalAdapter = class {
|
|
2004
2234
|
async complete(prompt) {
|
|
2005
2235
|
const escaped = prompt.replace(/'/g, "'\\''");
|
|
2006
2236
|
try {
|
|
2007
|
-
const result = (0,
|
|
2237
|
+
const result = (0, import_node_child_process4.execSync)(`codex '${escaped}'`, {
|
|
2008
2238
|
encoding: "utf-8",
|
|
2009
2239
|
timeout: 6e4,
|
|
2010
2240
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -2038,78 +2268,320 @@ function createAdapter(config) {
|
|
|
2038
2268
|
}
|
|
2039
2269
|
|
|
2040
2270
|
// src/watch/daemon.ts
|
|
2041
|
-
async function runDaemon(rootDir, config) {
|
|
2042
|
-
const silent = config.watch?.silent ?? false;
|
|
2043
|
-
const log = silent ? () => {
|
|
2044
|
-
} : (msg) => process.stdout.write(msg + "\n");
|
|
2271
|
+
async function runDaemon(rootDir, config, ui) {
|
|
2045
2272
|
const cleanup = [];
|
|
2046
2273
|
const shutdown = () => {
|
|
2047
2274
|
cleanup.forEach((fn) => fn());
|
|
2275
|
+
ui.clear();
|
|
2048
2276
|
clearPid(rootDir);
|
|
2049
2277
|
process.exit(0);
|
|
2050
2278
|
};
|
|
2051
2279
|
process.on("SIGTERM", shutdown);
|
|
2052
2280
|
process.on("SIGINT", shutdown);
|
|
2053
|
-
|
|
2281
|
+
ui.daemonStarted();
|
|
2054
2282
|
const sinceCommit = config.watchState?.lastGitCommit ?? null;
|
|
2283
|
+
ui.gitStart();
|
|
2055
2284
|
const gitResult = await runGitObserver(rootDir, sinceCommit);
|
|
2285
|
+
ui.gitDone(gitResult ? { entriesWritten: gitResult.entriesWritten, lastCommit: gitResult.lastCommit } : null);
|
|
2286
|
+
let lastGitCommit = gitResult?.lastCommit ?? sinceCommit;
|
|
2056
2287
|
if (gitResult) {
|
|
2057
2288
|
updateWatchState(rootDir, { lastGitCommit: gitResult.lastCommit });
|
|
2058
|
-
log(`[clawstrap watch] git: ${gitResult.entriesWritten} entries written`);
|
|
2059
2289
|
}
|
|
2060
2290
|
const adapter = createAdapter(config);
|
|
2291
|
+
let entriesSinceLastSynthesis = config.watchState?.entriesSinceLastSynthesis ?? 0;
|
|
2292
|
+
const synthEnabled = config.watch?.synthesis?.enabled ?? false;
|
|
2293
|
+
const triggerEveryN = config.watch?.synthesis?.triggerEveryN ?? 10;
|
|
2294
|
+
const maybeSynthesize = async () => {
|
|
2295
|
+
if (!synthEnabled || entriesSinceLastSynthesis < triggerEveryN) return;
|
|
2296
|
+
ui.synthStart();
|
|
2297
|
+
const summary = await synthesizeMemory(rootDir, adapter);
|
|
2298
|
+
ui.synthDone(summary);
|
|
2299
|
+
entriesSinceLastSynthesis = 0;
|
|
2300
|
+
updateWatchState(rootDir, { entriesSinceLastSynthesis: "0" });
|
|
2301
|
+
};
|
|
2061
2302
|
const stopTranscripts = watchTranscriptDir(rootDir, async (filePath) => {
|
|
2062
|
-
|
|
2303
|
+
ui.transcriptStart(import_node_path21.default.basename(filePath));
|
|
2304
|
+
ui.llmCallStart();
|
|
2063
2305
|
const result = await processTranscript(filePath, adapter);
|
|
2306
|
+
ui.llmCallDone(result ? { decisions: result.decisions.length, corrections: result.corrections.length, openThreads: result.openThreads.length } : null);
|
|
2064
2307
|
if (result) {
|
|
2065
|
-
const { appendToMemory: appendToMemory2, appendToGotchaLog: appendToGotchaLog2, appendToFutureConsiderations: appendToFutureConsiderations2 } = await Promise.resolve().then(() => (init_writers(), writers_exports));
|
|
2066
|
-
|
|
2308
|
+
const { appendToMemory: appendToMemory2, appendToGotchaLog: appendToGotchaLog2, appendToFutureConsiderations: appendToFutureConsiderations2, appendToOpenThreads: appendToOpenThreads2 } = await Promise.resolve().then(() => (init_writers(), writers_exports));
|
|
2309
|
+
let written = 0;
|
|
2310
|
+
if (result.decisions.length) written += appendToMemory2(rootDir, result.decisions, "session");
|
|
2067
2311
|
if (result.corrections.length) appendToGotchaLog2(rootDir, result.corrections);
|
|
2068
2312
|
if (result.deferredIdeas.length) appendToFutureConsiderations2(rootDir, result.deferredIdeas);
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2313
|
+
if (result.openThreads.length) appendToOpenThreads2(rootDir, result.openThreads);
|
|
2314
|
+
entriesSinceLastSynthesis += written;
|
|
2315
|
+
updateWatchState(rootDir, {
|
|
2316
|
+
lastTranscriptAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2317
|
+
entriesSinceLastSynthesis: String(entriesSinceLastSynthesis)
|
|
2318
|
+
});
|
|
2319
|
+
ui.transcriptWriteDone();
|
|
2320
|
+
await maybeSynthesize();
|
|
2073
2321
|
}
|
|
2074
2322
|
});
|
|
2075
2323
|
cleanup.push(stopTranscripts);
|
|
2324
|
+
let gitRunning = false;
|
|
2325
|
+
const pollIntervalMinutes = config.watch?.git?.pollIntervalMinutes ?? 5;
|
|
2326
|
+
const gitPollTimer = setInterval(async () => {
|
|
2327
|
+
if (gitRunning) return;
|
|
2328
|
+
gitRunning = true;
|
|
2329
|
+
try {
|
|
2330
|
+
const result = await runGitObserver(rootDir, lastGitCommit);
|
|
2331
|
+
if (result && result.entriesWritten > 0) {
|
|
2332
|
+
ui.gitPollDone({ entriesWritten: result.entriesWritten, lastCommit: result.lastCommit });
|
|
2333
|
+
}
|
|
2334
|
+
if (result) {
|
|
2335
|
+
lastGitCommit = result.lastCommit;
|
|
2336
|
+
updateWatchState(rootDir, { lastGitCommit: result.lastCommit });
|
|
2337
|
+
}
|
|
2338
|
+
} finally {
|
|
2339
|
+
gitRunning = false;
|
|
2340
|
+
}
|
|
2341
|
+
}, pollIntervalMinutes * 60 * 1e3);
|
|
2342
|
+
cleanup.push(() => clearInterval(gitPollTimer));
|
|
2076
2343
|
const intervalDays = config.watch?.scan?.intervalDays ?? 7;
|
|
2077
2344
|
const lastScan = config.watchState?.lastScanAt ? new Date(config.watchState.lastScanAt) : null;
|
|
2078
2345
|
const msSinceLastScan = lastScan ? Date.now() - lastScan.getTime() : Infinity;
|
|
2079
2346
|
const scanIntervalMs = intervalDays * 24 * 60 * 60 * 1e3;
|
|
2080
2347
|
const doScan = async () => {
|
|
2081
|
-
|
|
2348
|
+
ui.scanStart(lastScan);
|
|
2349
|
+
ui.scanFilesStart();
|
|
2082
2350
|
const sections = await runScan(rootDir);
|
|
2351
|
+
ui.scanFilesDone();
|
|
2352
|
+
if (config.watch?.adapter) {
|
|
2353
|
+
ui.inferStart();
|
|
2354
|
+
const rules = await inferArchitecturePatterns(rootDir, sections, adapter);
|
|
2355
|
+
ui.inferDone(rules.length > 0 ? rules.length : null);
|
|
2356
|
+
if (rules.length > 0) sections.architecture = rules;
|
|
2357
|
+
}
|
|
2083
2358
|
writeConventions(rootDir, sections);
|
|
2084
2359
|
updateWatchState(rootDir, { lastScanAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2085
|
-
|
|
2360
|
+
ui.scanDone(sections.naming[0] ?? "");
|
|
2086
2361
|
};
|
|
2087
2362
|
if (msSinceLastScan >= scanIntervalMs) {
|
|
2088
2363
|
await doScan();
|
|
2089
2364
|
}
|
|
2090
2365
|
const scanTimer = setInterval(doScan, scanIntervalMs);
|
|
2091
2366
|
cleanup.push(() => clearInterval(scanTimer));
|
|
2092
|
-
|
|
2367
|
+
ui.showIdle(import_node_path21.default.join(rootDir, "tmp", "sessions"));
|
|
2093
2368
|
await new Promise(() => {
|
|
2094
2369
|
});
|
|
2095
2370
|
}
|
|
2096
2371
|
function updateWatchState(rootDir, updates) {
|
|
2097
|
-
const configPath =
|
|
2372
|
+
const configPath = import_node_path21.default.join(rootDir, ".clawstrap.json");
|
|
2098
2373
|
try {
|
|
2099
|
-
const raw = JSON.parse(
|
|
2374
|
+
const raw = JSON.parse(import_node_fs20.default.readFileSync(configPath, "utf-8"));
|
|
2100
2375
|
raw["watchState"] = { ...raw["watchState"] ?? {}, ...updates };
|
|
2101
|
-
|
|
2376
|
+
import_node_fs20.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
|
|
2102
2377
|
} catch {
|
|
2103
2378
|
}
|
|
2104
2379
|
}
|
|
2105
2380
|
|
|
2381
|
+
// src/watch/ui.ts
|
|
2382
|
+
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
2383
|
+
var import_ora = __toESM(require("ora"), 1);
|
|
2384
|
+
function formatAgo(date) {
|
|
2385
|
+
if (!date) return "never";
|
|
2386
|
+
const diffMs = Date.now() - date.getTime();
|
|
2387
|
+
const diffMins = Math.floor(diffMs / 6e4);
|
|
2388
|
+
if (diffMins < 60) return `${diffMins} minute${diffMins === 1 ? "" : "s"} ago`;
|
|
2389
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
2390
|
+
if (diffHours < 24) return `${diffHours} hour${diffHours === 1 ? "" : "s"} ago`;
|
|
2391
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
2392
|
+
return `${diffDays} day${diffDays === 1 ? "" : "s"} ago`;
|
|
2393
|
+
}
|
|
2394
|
+
var T = {
|
|
2395
|
+
branch: import_picocolors.default.gray("\u251C\u2500"),
|
|
2396
|
+
last: import_picocolors.default.gray("\u2514\u2500"),
|
|
2397
|
+
check: import_picocolors.default.green("\u2713")
|
|
2398
|
+
};
|
|
2399
|
+
function header(label) {
|
|
2400
|
+
process.stdout.write(`
|
|
2401
|
+
${import_picocolors.default.cyan("\u25C6")} ${import_picocolors.default.bold(label)}
|
|
2402
|
+
`);
|
|
2403
|
+
}
|
|
2404
|
+
function row(connector, label, value) {
|
|
2405
|
+
const val = value !== void 0 ? ` ${import_picocolors.default.bold(value)}` : "";
|
|
2406
|
+
process.stdout.write(`${connector} ${label}${val}
|
|
2407
|
+
`);
|
|
2408
|
+
}
|
|
2409
|
+
var SilentUI = class {
|
|
2410
|
+
daemonStarted() {
|
|
2411
|
+
}
|
|
2412
|
+
gitStart() {
|
|
2413
|
+
}
|
|
2414
|
+
gitDone(_result) {
|
|
2415
|
+
}
|
|
2416
|
+
gitPollDone(_result) {
|
|
2417
|
+
}
|
|
2418
|
+
transcriptStart(_filename) {
|
|
2419
|
+
}
|
|
2420
|
+
llmCallStart() {
|
|
2421
|
+
}
|
|
2422
|
+
llmCallDone(_counts) {
|
|
2423
|
+
}
|
|
2424
|
+
transcriptWriteDone() {
|
|
2425
|
+
}
|
|
2426
|
+
scanStart(_lastRunAt) {
|
|
2427
|
+
}
|
|
2428
|
+
scanFilesStart() {
|
|
2429
|
+
}
|
|
2430
|
+
scanFilesDone() {
|
|
2431
|
+
}
|
|
2432
|
+
scanDone(_namingStyle) {
|
|
2433
|
+
}
|
|
2434
|
+
synthStart() {
|
|
2435
|
+
}
|
|
2436
|
+
synthDone(_summary) {
|
|
2437
|
+
}
|
|
2438
|
+
inferStart() {
|
|
2439
|
+
}
|
|
2440
|
+
inferDone(_rulesCount) {
|
|
2441
|
+
}
|
|
2442
|
+
showIdle(_watchDir) {
|
|
2443
|
+
}
|
|
2444
|
+
clear() {
|
|
2445
|
+
}
|
|
2446
|
+
};
|
|
2447
|
+
var RichUI = class {
|
|
2448
|
+
spinner = null;
|
|
2449
|
+
daemonStarted() {
|
|
2450
|
+
process.stdout.write(`
|
|
2451
|
+
${import_picocolors.default.cyan("clawstrap watch")} ${import_picocolors.default.dim("daemon started")}
|
|
2452
|
+
`);
|
|
2453
|
+
}
|
|
2454
|
+
// Git observer ──────────────────────────────────────────────────────────────
|
|
2455
|
+
gitStart() {
|
|
2456
|
+
header("Git observer");
|
|
2457
|
+
}
|
|
2458
|
+
gitDone(result) {
|
|
2459
|
+
if (!result) {
|
|
2460
|
+
row(T.last, import_picocolors.default.dim("No new commits found."));
|
|
2461
|
+
return;
|
|
2462
|
+
}
|
|
2463
|
+
row(T.branch, "Last processed commit", result.lastCommit.slice(0, 7));
|
|
2464
|
+
row(T.branch, "Entries written", String(result.entriesWritten));
|
|
2465
|
+
row(T.last, `Writing to MEMORY.md... ${T.check} done`);
|
|
2466
|
+
}
|
|
2467
|
+
gitPollDone(result) {
|
|
2468
|
+
const time = (/* @__PURE__ */ new Date()).toTimeString().slice(0, 5);
|
|
2469
|
+
process.stdout.write(
|
|
2470
|
+
`
|
|
2471
|
+
${import_picocolors.default.cyan("\u25C6")} ${import_picocolors.default.bold("Git:")} +${result.entriesWritten} entr${result.entriesWritten === 1 ? "y" : "ies"} written ${import_picocolors.default.dim(result.lastCommit.slice(0, 7))} ${import_picocolors.default.dim(time)}
|
|
2472
|
+
`
|
|
2473
|
+
);
|
|
2474
|
+
}
|
|
2475
|
+
// Transcript ────────────────────────────────────────────────────────────────
|
|
2476
|
+
transcriptStart(filename) {
|
|
2477
|
+
header(`New session summary detected ${import_picocolors.default.cyan(filename)}`);
|
|
2478
|
+
}
|
|
2479
|
+
llmCallStart() {
|
|
2480
|
+
this.spinner = (0, import_ora.default)({
|
|
2481
|
+
text: `${T.branch} Sending to LLM adapter...`,
|
|
2482
|
+
prefixText: ""
|
|
2483
|
+
}).start();
|
|
2484
|
+
}
|
|
2485
|
+
llmCallDone(counts) {
|
|
2486
|
+
if (this.spinner) {
|
|
2487
|
+
if (counts) {
|
|
2488
|
+
this.spinner.succeed(`${T.branch} Sending to LLM adapter... ${T.check}`);
|
|
2489
|
+
} else {
|
|
2490
|
+
this.spinner.fail(`${T.branch} Sending to LLM adapter... failed`);
|
|
2491
|
+
}
|
|
2492
|
+
this.spinner = null;
|
|
2493
|
+
}
|
|
2494
|
+
if (counts) {
|
|
2495
|
+
row(T.branch, "Decisions found ", String(counts.decisions));
|
|
2496
|
+
row(T.branch, "Corrections found ", String(counts.corrections));
|
|
2497
|
+
row(T.branch, "Open threads found ", String(counts.openThreads));
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
transcriptWriteDone() {
|
|
2501
|
+
row(T.last, `Writing to memory files... ${T.check} done`);
|
|
2502
|
+
}
|
|
2503
|
+
// Convention scan ───────────────────────────────────────────────────────────
|
|
2504
|
+
scanStart(lastRunAt) {
|
|
2505
|
+
header(`Convention scan ${import_picocolors.default.dim(`(last run: ${formatAgo(lastRunAt)})`)}`);
|
|
2506
|
+
}
|
|
2507
|
+
scanFilesStart() {
|
|
2508
|
+
this.spinner = (0, import_ora.default)({
|
|
2509
|
+
text: `${T.branch} Scanning files...`,
|
|
2510
|
+
prefixText: ""
|
|
2511
|
+
}).start();
|
|
2512
|
+
}
|
|
2513
|
+
scanFilesDone() {
|
|
2514
|
+
if (this.spinner) {
|
|
2515
|
+
this.spinner.succeed(`${T.branch} Scanning files... done`);
|
|
2516
|
+
this.spinner = null;
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
scanDone(namingStyle) {
|
|
2520
|
+
if (namingStyle) {
|
|
2521
|
+
row(T.branch, "Naming convention ", namingStyle);
|
|
2522
|
+
}
|
|
2523
|
+
row(T.last, `Writing conventions.md... ${T.check} done`);
|
|
2524
|
+
}
|
|
2525
|
+
// Memory synthesis ──────────────────────────────────────────────────────────
|
|
2526
|
+
synthStart() {
|
|
2527
|
+
this.spinner?.stop();
|
|
2528
|
+
this.spinner = (0, import_ora.default)({
|
|
2529
|
+
text: `${T.branch} Synthesising memory...`,
|
|
2530
|
+
prefixText: ""
|
|
2531
|
+
}).start();
|
|
2532
|
+
}
|
|
2533
|
+
synthDone(summary) {
|
|
2534
|
+
if (this.spinner) {
|
|
2535
|
+
if (summary) {
|
|
2536
|
+
const preview = summary.length > 60 ? summary.slice(0, 60) + "\u2026" : summary;
|
|
2537
|
+
this.spinner.succeed(`${T.branch} Living summary updated ${import_picocolors.default.dim(preview)}`);
|
|
2538
|
+
} else {
|
|
2539
|
+
this.spinner.fail(`${T.branch} Memory synthesis failed`);
|
|
2540
|
+
}
|
|
2541
|
+
this.spinner = null;
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
// Architecture inference ────────────────────────────────────────────────────
|
|
2545
|
+
inferStart() {
|
|
2546
|
+
this.spinner?.stop();
|
|
2547
|
+
this.spinner = (0, import_ora.default)({
|
|
2548
|
+
text: `${T.branch} Inferring architecture patterns...`,
|
|
2549
|
+
prefixText: ""
|
|
2550
|
+
}).start();
|
|
2551
|
+
}
|
|
2552
|
+
inferDone(rulesCount) {
|
|
2553
|
+
if (this.spinner) {
|
|
2554
|
+
if (rulesCount !== null && rulesCount > 0) {
|
|
2555
|
+
this.spinner.succeed(`${T.branch} Architecture patterns inferred ${import_picocolors.default.bold(String(rulesCount))} rules`);
|
|
2556
|
+
} else {
|
|
2557
|
+
this.spinner.fail(`${T.branch} Architecture inference failed`);
|
|
2558
|
+
}
|
|
2559
|
+
this.spinner = null;
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
// Idle ──────────────────────────────────────────────────────────────────────
|
|
2563
|
+
showIdle(watchDir) {
|
|
2564
|
+
process.stdout.write(`
|
|
2565
|
+
${import_picocolors.default.dim("\u25C7")} ${import_picocolors.default.dim("Watching for changes...")}
|
|
2566
|
+
`);
|
|
2567
|
+
row(T.last, import_picocolors.default.dim("Transcripts"), import_picocolors.default.dim(watchDir + " (listening)"));
|
|
2568
|
+
process.stdout.write("\n");
|
|
2569
|
+
}
|
|
2570
|
+
// Cleanup ───────────────────────────────────────────────────────────────────
|
|
2571
|
+
clear() {
|
|
2572
|
+
if (this.spinner) {
|
|
2573
|
+
this.spinner.stop();
|
|
2574
|
+
this.spinner = null;
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
};
|
|
2578
|
+
function createUI(silent) {
|
|
2579
|
+
return silent ? new SilentUI() : new RichUI();
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2106
2582
|
// src/watch.ts
|
|
2107
2583
|
async function watch(options) {
|
|
2108
2584
|
const { config, rootDir } = loadWorkspace();
|
|
2109
|
-
if (options._daemon) {
|
|
2110
|
-
await runDaemon(rootDir, config);
|
|
2111
|
-
return;
|
|
2112
|
-
}
|
|
2113
2585
|
if (options.stop) {
|
|
2114
2586
|
const pid = readPid(rootDir);
|
|
2115
2587
|
if (!pid || !isDaemonRunning(rootDir)) {
|
|
@@ -2123,61 +2595,56 @@ Daemon stopped (pid ${pid}).
|
|
|
2123
2595
|
`);
|
|
2124
2596
|
return;
|
|
2125
2597
|
}
|
|
2598
|
+
const silent = options.silent ?? config.watch?.silent ?? false;
|
|
2599
|
+
const ui = createUI(silent);
|
|
2126
2600
|
if (options.once) {
|
|
2127
|
-
|
|
2601
|
+
ui.gitStart();
|
|
2128
2602
|
const gitResult = await runGitObserver(rootDir, config.watchState?.lastGitCommit ?? null);
|
|
2603
|
+
ui.gitDone(gitResult ? { entriesWritten: gitResult.entriesWritten, lastCommit: gitResult.lastCommit } : null);
|
|
2129
2604
|
if (gitResult) {
|
|
2130
2605
|
persistWatchState(rootDir, { lastGitCommit: gitResult.lastCommit });
|
|
2131
|
-
console.log(` \u2713 git: ${gitResult.entriesWritten} entries`);
|
|
2132
2606
|
}
|
|
2607
|
+
const lastScanAt = config.watchState?.lastScanAt ? new Date(config.watchState.lastScanAt) : null;
|
|
2608
|
+
ui.scanStart(lastScanAt);
|
|
2609
|
+
ui.scanFilesStart();
|
|
2133
2610
|
const sections = await runScan(rootDir);
|
|
2611
|
+
ui.scanFilesDone();
|
|
2612
|
+
if (config.watch?.adapter) {
|
|
2613
|
+
const adapter = createAdapter(config);
|
|
2614
|
+
ui.inferStart();
|
|
2615
|
+
const rules = await inferArchitecturePatterns(rootDir, sections, adapter);
|
|
2616
|
+
ui.inferDone(rules.length > 0 ? rules.length : null);
|
|
2617
|
+
if (rules.length > 0) sections.architecture = rules;
|
|
2618
|
+
}
|
|
2134
2619
|
writeConventions(rootDir, sections);
|
|
2135
2620
|
persistWatchState(rootDir, { lastScanAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2136
|
-
|
|
2137
|
-
console.log("\nDone.\n");
|
|
2621
|
+
ui.scanDone(sections.naming[0] ?? "");
|
|
2138
2622
|
return;
|
|
2139
2623
|
}
|
|
2140
2624
|
if (isDaemonRunning(rootDir)) {
|
|
2141
2625
|
const pid = readPid(rootDir);
|
|
2142
2626
|
console.log(`
|
|
2143
|
-
|
|
2627
|
+
Watch is already running (pid ${pid}). Use --stop to stop it.
|
|
2144
2628
|
`);
|
|
2145
2629
|
return;
|
|
2146
2630
|
}
|
|
2147
2631
|
injectWatchHook(rootDir, config);
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
detached: true,
|
|
2151
|
-
stdio: "ignore",
|
|
2152
|
-
cwd: rootDir
|
|
2153
|
-
});
|
|
2154
|
-
child.unref();
|
|
2155
|
-
if (child.pid) {
|
|
2156
|
-
writePid(rootDir, child.pid);
|
|
2157
|
-
if (!options.silent) {
|
|
2158
|
-
console.log(`
|
|
2159
|
-
Daemon started (pid ${child.pid}).`);
|
|
2160
|
-
console.log(`Run 'clawstrap watch --stop' to stop it.
|
|
2161
|
-
`);
|
|
2162
|
-
}
|
|
2163
|
-
} else {
|
|
2164
|
-
console.error("\nFailed to start daemon.\n");
|
|
2165
|
-
process.exit(1);
|
|
2166
|
-
}
|
|
2632
|
+
writePid(rootDir, process.pid);
|
|
2633
|
+
await runDaemon(rootDir, config, ui);
|
|
2167
2634
|
}
|
|
2168
2635
|
function persistWatchState(rootDir, updates) {
|
|
2169
|
-
const configPath =
|
|
2636
|
+
const configPath = import_node_path22.default.join(rootDir, ".clawstrap.json");
|
|
2170
2637
|
try {
|
|
2171
|
-
const raw = JSON.parse(
|
|
2638
|
+
const raw = JSON.parse(import_node_fs21.default.readFileSync(configPath, "utf-8"));
|
|
2172
2639
|
raw.watchState = { ...raw.watchState ?? {}, ...updates };
|
|
2173
|
-
|
|
2640
|
+
import_node_fs21.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
|
|
2174
2641
|
} catch {
|
|
2175
2642
|
}
|
|
2176
2643
|
}
|
|
2177
2644
|
function injectWatchHook(rootDir, config) {
|
|
2178
|
-
const governanceFile =
|
|
2179
|
-
if (!
|
|
2180
|
-
const content =
|
|
2645
|
+
const governanceFile = import_node_path22.default.join(rootDir, "CLAUDE.md");
|
|
2646
|
+
if (!import_node_fs21.default.existsSync(governanceFile)) return;
|
|
2647
|
+
const content = import_node_fs21.default.readFileSync(governanceFile, "utf-8");
|
|
2181
2648
|
if (content.includes("<!-- CLAWSTRAP:WATCH -->")) return;
|
|
2182
2649
|
const _config = config;
|
|
2183
2650
|
void _config;
|
|
@@ -2204,23 +2671,23 @@ function injectWatchHook(rootDir, config) {
|
|
|
2204
2671
|
|
|
2205
2672
|
The watch daemon picks this up automatically and updates MEMORY.md and gotcha-log.md.
|
|
2206
2673
|
`;
|
|
2207
|
-
|
|
2674
|
+
import_node_fs21.default.appendFileSync(governanceFile, hook, "utf-8");
|
|
2208
2675
|
}
|
|
2209
2676
|
|
|
2210
2677
|
// src/analyze.ts
|
|
2211
|
-
var
|
|
2212
|
-
var
|
|
2678
|
+
var import_node_fs22 = __toESM(require("fs"), 1);
|
|
2679
|
+
var import_node_path23 = __toESM(require("path"), 1);
|
|
2213
2680
|
init_writers();
|
|
2214
2681
|
async function analyze() {
|
|
2215
2682
|
const { rootDir } = loadWorkspace();
|
|
2216
2683
|
console.log("\nScanning codebase conventions...\n");
|
|
2217
2684
|
const sections = await runScan(rootDir);
|
|
2218
2685
|
writeConventions(rootDir, sections);
|
|
2219
|
-
const configPath =
|
|
2686
|
+
const configPath = import_node_path23.default.join(rootDir, ".clawstrap.json");
|
|
2220
2687
|
try {
|
|
2221
|
-
const raw = JSON.parse(
|
|
2688
|
+
const raw = JSON.parse(import_node_fs22.default.readFileSync(configPath, "utf-8"));
|
|
2222
2689
|
raw["watchState"] = { ...raw["watchState"] ?? {}, lastScanAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2223
|
-
|
|
2690
|
+
import_node_fs22.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
|
|
2224
2691
|
} catch {
|
|
2225
2692
|
}
|
|
2226
2693
|
console.log(" \u2713 .claude/rules/conventions.md updated\n");
|
|
@@ -2228,7 +2695,7 @@ async function analyze() {
|
|
|
2228
2695
|
|
|
2229
2696
|
// src/index.ts
|
|
2230
2697
|
var program = new import_commander.Command();
|
|
2231
|
-
program.name("clawstrap").description("Scaffold a production-ready AI agent workspace").version("1.4.
|
|
2698
|
+
program.name("clawstrap").description("Scaffold a production-ready AI agent workspace").version("1.4.2");
|
|
2232
2699
|
program.command("init").description("Create a new AI workspace in the current directory").argument("[directory]", "Target directory", ".").option("-y, --yes", "Use defaults, skip prompts").option("--sdd", "Enable Spec-Driven Development mode").action(async (directory, options) => {
|
|
2233
2700
|
await init(directory, options);
|
|
2234
2701
|
});
|
|
@@ -2256,7 +2723,7 @@ Unknown format: ${options.format}. Supported: paperclip
|
|
|
2256
2723
|
}
|
|
2257
2724
|
await exportPaperclip(options);
|
|
2258
2725
|
});
|
|
2259
|
-
program.command("watch").description("Start adaptive memory daemon for this workspace").option("--stop", "Stop the running daemon").option("--silent", "Run without output").option("--once", "Run all observers once and exit (no persistent daemon)").
|
|
2726
|
+
program.command("watch").description("Start adaptive memory daemon for this workspace").option("--stop", "Stop the running daemon").option("--silent", "Run without output").option("--once", "Run all observers once and exit (no persistent daemon)").action(async (options) => {
|
|
2260
2727
|
await watch(options);
|
|
2261
2728
|
});
|
|
2262
2729
|
program.command("analyze").description("Run codebase convention scan immediately").action(async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawstrap",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Scaffold a production-ready AI agent workspace in under 2 minutes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@inquirer/prompts": "^7.0.0",
|
|
48
48
|
"commander": "^13.0.0",
|
|
49
|
+
"ora": "^8.2.0",
|
|
50
|
+
"picocolors": "^1.1.1",
|
|
49
51
|
"zod": "^3.24.0"
|
|
50
52
|
},
|
|
51
53
|
"devDependencies": {
|