clawstrap 1.4.0 → 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 +556 -82
- 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
|
});
|
|
@@ -751,7 +779,8 @@ var WORKLOAD_LABELS2 = {
|
|
|
751
779
|
custom: "General Purpose"
|
|
752
780
|
};
|
|
753
781
|
async function init(directory, options) {
|
|
754
|
-
const
|
|
782
|
+
const answers = options.yes ? getDefaults(directory) : await runPrompts();
|
|
783
|
+
const targetDir = directory === "." ? import_node_path3.default.resolve(answers.workspaceName) : import_node_path3.default.resolve(directory);
|
|
755
784
|
if (import_node_fs2.default.existsSync(targetDir)) {
|
|
756
785
|
const hasClawstrap = import_node_fs2.default.existsSync(
|
|
757
786
|
import_node_path3.default.join(targetDir, ".clawstrap.json")
|
|
@@ -775,9 +804,8 @@ async function init(directory, options) {
|
|
|
775
804
|
} else {
|
|
776
805
|
import_node_fs2.default.mkdirSync(targetDir, { recursive: true });
|
|
777
806
|
}
|
|
778
|
-
const answers = options.yes ? getDefaults(directory) : await runPrompts();
|
|
779
807
|
const config = ClawstrapConfigSchema.parse({
|
|
780
|
-
version: "1.
|
|
808
|
+
version: "1.4.2",
|
|
781
809
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
782
810
|
workspaceName: answers.workspaceName,
|
|
783
811
|
targetDirectory: directory,
|
|
@@ -806,9 +834,16 @@ async function init(directory, options) {
|
|
|
806
834
|
for (const dir of result.dirsCreated) {
|
|
807
835
|
console.log(` \u2713 ${dir}/`);
|
|
808
836
|
}
|
|
809
|
-
|
|
837
|
+
const folderName = import_node_path3.default.basename(targetDir);
|
|
838
|
+
if (directory === ".") {
|
|
839
|
+
console.log(`
|
|
840
|
+
Done. Run \`cd ${folderName}\` and open GETTING_STARTED.md to begin.
|
|
841
|
+
`);
|
|
842
|
+
} else {
|
|
843
|
+
console.log(`
|
|
810
844
|
Done. Open GETTING_STARTED.md to begin.
|
|
811
845
|
`);
|
|
846
|
+
}
|
|
812
847
|
}
|
|
813
848
|
|
|
814
849
|
// src/add-agent.ts
|
|
@@ -1492,9 +1527,8 @@ Exported to ${import_node_path14.default.relative(process.cwd(), outDir) || outD
|
|
|
1492
1527
|
}
|
|
1493
1528
|
|
|
1494
1529
|
// src/watch.ts
|
|
1495
|
-
var
|
|
1496
|
-
var
|
|
1497
|
-
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);
|
|
1498
1532
|
|
|
1499
1533
|
// src/watch/git.ts
|
|
1500
1534
|
var import_node_child_process = require("child_process");
|
|
@@ -1837,17 +1871,220 @@ async function runScan(rootDir) {
|
|
|
1837
1871
|
init_writers();
|
|
1838
1872
|
|
|
1839
1873
|
// src/watch/daemon.ts
|
|
1840
|
-
var
|
|
1841
|
-
var
|
|
1874
|
+
var import_node_fs20 = __toESM(require("fs"), 1);
|
|
1875
|
+
var import_node_path21 = __toESM(require("path"), 1);
|
|
1842
1876
|
init_writers();
|
|
1843
1877
|
|
|
1844
|
-
// src/watch/
|
|
1878
|
+
// src/watch/synthesize.ts
|
|
1845
1879
|
var import_node_fs17 = __toESM(require("fs"), 1);
|
|
1846
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);
|
|
1847
2084
|
async function processTranscript(filePath, adapter) {
|
|
1848
2085
|
let content;
|
|
1849
2086
|
try {
|
|
1850
|
-
content =
|
|
2087
|
+
content = import_node_fs19.default.readFileSync(filePath, "utf-8");
|
|
1851
2088
|
} catch {
|
|
1852
2089
|
return null;
|
|
1853
2090
|
}
|
|
@@ -1890,13 +2127,13 @@ Each item must be a concise one-sentence string. Arrays may be empty.`;
|
|
|
1890
2127
|
}
|
|
1891
2128
|
}
|
|
1892
2129
|
function watchTranscriptDir(rootDir, onNewFile) {
|
|
1893
|
-
const sessionsDir =
|
|
1894
|
-
|
|
1895
|
-
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) => {
|
|
1896
2133
|
if (event !== "rename" || !filename) return;
|
|
1897
2134
|
if (!filename.endsWith(".md")) return;
|
|
1898
|
-
const filePath =
|
|
1899
|
-
if (!
|
|
2135
|
+
const filePath = import_node_path20.default.join(sessionsDir, filename);
|
|
2136
|
+
if (!import_node_fs19.default.existsSync(filePath)) return;
|
|
1900
2137
|
onNewFile(filePath).catch(() => {
|
|
1901
2138
|
});
|
|
1902
2139
|
});
|
|
@@ -1906,12 +2143,12 @@ function watchTranscriptDir(rootDir, onNewFile) {
|
|
|
1906
2143
|
}
|
|
1907
2144
|
|
|
1908
2145
|
// src/watch/adapters/claude-local.ts
|
|
1909
|
-
var
|
|
2146
|
+
var import_node_child_process3 = require("child_process");
|
|
1910
2147
|
var ClaudeLocalAdapter = class {
|
|
1911
2148
|
async complete(prompt) {
|
|
1912
2149
|
const escaped = prompt.replace(/'/g, "'\\''");
|
|
1913
2150
|
try {
|
|
1914
|
-
const result = (0,
|
|
2151
|
+
const result = (0, import_node_child_process3.execSync)(`claude -p '${escaped}'`, {
|
|
1915
2152
|
encoding: "utf-8",
|
|
1916
2153
|
timeout: 6e4,
|
|
1917
2154
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1992,12 +2229,12 @@ var OllamaAdapter = class {
|
|
|
1992
2229
|
};
|
|
1993
2230
|
|
|
1994
2231
|
// src/watch/adapters/codex-local.ts
|
|
1995
|
-
var
|
|
2232
|
+
var import_node_child_process4 = require("child_process");
|
|
1996
2233
|
var CodexLocalAdapter = class {
|
|
1997
2234
|
async complete(prompt) {
|
|
1998
2235
|
const escaped = prompt.replace(/'/g, "'\\''");
|
|
1999
2236
|
try {
|
|
2000
|
-
const result = (0,
|
|
2237
|
+
const result = (0, import_node_child_process4.execSync)(`codex '${escaped}'`, {
|
|
2001
2238
|
encoding: "utf-8",
|
|
2002
2239
|
timeout: 6e4,
|
|
2003
2240
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -2031,78 +2268,320 @@ function createAdapter(config) {
|
|
|
2031
2268
|
}
|
|
2032
2269
|
|
|
2033
2270
|
// src/watch/daemon.ts
|
|
2034
|
-
async function runDaemon(rootDir, config) {
|
|
2035
|
-
const silent = config.watch?.silent ?? false;
|
|
2036
|
-
const log = silent ? () => {
|
|
2037
|
-
} : (msg) => process.stdout.write(msg + "\n");
|
|
2271
|
+
async function runDaemon(rootDir, config, ui) {
|
|
2038
2272
|
const cleanup = [];
|
|
2039
2273
|
const shutdown = () => {
|
|
2040
2274
|
cleanup.forEach((fn) => fn());
|
|
2275
|
+
ui.clear();
|
|
2041
2276
|
clearPid(rootDir);
|
|
2042
2277
|
process.exit(0);
|
|
2043
2278
|
};
|
|
2044
2279
|
process.on("SIGTERM", shutdown);
|
|
2045
2280
|
process.on("SIGINT", shutdown);
|
|
2046
|
-
|
|
2281
|
+
ui.daemonStarted();
|
|
2047
2282
|
const sinceCommit = config.watchState?.lastGitCommit ?? null;
|
|
2283
|
+
ui.gitStart();
|
|
2048
2284
|
const gitResult = await runGitObserver(rootDir, sinceCommit);
|
|
2285
|
+
ui.gitDone(gitResult ? { entriesWritten: gitResult.entriesWritten, lastCommit: gitResult.lastCommit } : null);
|
|
2286
|
+
let lastGitCommit = gitResult?.lastCommit ?? sinceCommit;
|
|
2049
2287
|
if (gitResult) {
|
|
2050
2288
|
updateWatchState(rootDir, { lastGitCommit: gitResult.lastCommit });
|
|
2051
|
-
log(`[clawstrap watch] git: ${gitResult.entriesWritten} entries written`);
|
|
2052
2289
|
}
|
|
2053
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
|
+
};
|
|
2054
2302
|
const stopTranscripts = watchTranscriptDir(rootDir, async (filePath) => {
|
|
2055
|
-
|
|
2303
|
+
ui.transcriptStart(import_node_path21.default.basename(filePath));
|
|
2304
|
+
ui.llmCallStart();
|
|
2056
2305
|
const result = await processTranscript(filePath, adapter);
|
|
2306
|
+
ui.llmCallDone(result ? { decisions: result.decisions.length, corrections: result.corrections.length, openThreads: result.openThreads.length } : null);
|
|
2057
2307
|
if (result) {
|
|
2058
|
-
const { appendToMemory: appendToMemory2, appendToGotchaLog: appendToGotchaLog2, appendToFutureConsiderations: appendToFutureConsiderations2 } = await Promise.resolve().then(() => (init_writers(), writers_exports));
|
|
2059
|
-
|
|
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");
|
|
2060
2311
|
if (result.corrections.length) appendToGotchaLog2(rootDir, result.corrections);
|
|
2061
2312
|
if (result.deferredIdeas.length) appendToFutureConsiderations2(rootDir, result.deferredIdeas);
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
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();
|
|
2066
2321
|
}
|
|
2067
2322
|
});
|
|
2068
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));
|
|
2069
2343
|
const intervalDays = config.watch?.scan?.intervalDays ?? 7;
|
|
2070
2344
|
const lastScan = config.watchState?.lastScanAt ? new Date(config.watchState.lastScanAt) : null;
|
|
2071
2345
|
const msSinceLastScan = lastScan ? Date.now() - lastScan.getTime() : Infinity;
|
|
2072
2346
|
const scanIntervalMs = intervalDays * 24 * 60 * 60 * 1e3;
|
|
2073
2347
|
const doScan = async () => {
|
|
2074
|
-
|
|
2348
|
+
ui.scanStart(lastScan);
|
|
2349
|
+
ui.scanFilesStart();
|
|
2075
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
|
+
}
|
|
2076
2358
|
writeConventions(rootDir, sections);
|
|
2077
2359
|
updateWatchState(rootDir, { lastScanAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2078
|
-
|
|
2360
|
+
ui.scanDone(sections.naming[0] ?? "");
|
|
2079
2361
|
};
|
|
2080
2362
|
if (msSinceLastScan >= scanIntervalMs) {
|
|
2081
2363
|
await doScan();
|
|
2082
2364
|
}
|
|
2083
2365
|
const scanTimer = setInterval(doScan, scanIntervalMs);
|
|
2084
2366
|
cleanup.push(() => clearInterval(scanTimer));
|
|
2085
|
-
|
|
2367
|
+
ui.showIdle(import_node_path21.default.join(rootDir, "tmp", "sessions"));
|
|
2086
2368
|
await new Promise(() => {
|
|
2087
2369
|
});
|
|
2088
2370
|
}
|
|
2089
2371
|
function updateWatchState(rootDir, updates) {
|
|
2090
|
-
const configPath =
|
|
2372
|
+
const configPath = import_node_path21.default.join(rootDir, ".clawstrap.json");
|
|
2091
2373
|
try {
|
|
2092
|
-
const raw = JSON.parse(
|
|
2374
|
+
const raw = JSON.parse(import_node_fs20.default.readFileSync(configPath, "utf-8"));
|
|
2093
2375
|
raw["watchState"] = { ...raw["watchState"] ?? {}, ...updates };
|
|
2094
|
-
|
|
2376
|
+
import_node_fs20.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
|
|
2095
2377
|
} catch {
|
|
2096
2378
|
}
|
|
2097
2379
|
}
|
|
2098
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
|
+
|
|
2099
2582
|
// src/watch.ts
|
|
2100
2583
|
async function watch(options) {
|
|
2101
2584
|
const { config, rootDir } = loadWorkspace();
|
|
2102
|
-
if (options._daemon) {
|
|
2103
|
-
await runDaemon(rootDir, config);
|
|
2104
|
-
return;
|
|
2105
|
-
}
|
|
2106
2585
|
if (options.stop) {
|
|
2107
2586
|
const pid = readPid(rootDir);
|
|
2108
2587
|
if (!pid || !isDaemonRunning(rootDir)) {
|
|
@@ -2116,61 +2595,56 @@ Daemon stopped (pid ${pid}).
|
|
|
2116
2595
|
`);
|
|
2117
2596
|
return;
|
|
2118
2597
|
}
|
|
2598
|
+
const silent = options.silent ?? config.watch?.silent ?? false;
|
|
2599
|
+
const ui = createUI(silent);
|
|
2119
2600
|
if (options.once) {
|
|
2120
|
-
|
|
2601
|
+
ui.gitStart();
|
|
2121
2602
|
const gitResult = await runGitObserver(rootDir, config.watchState?.lastGitCommit ?? null);
|
|
2603
|
+
ui.gitDone(gitResult ? { entriesWritten: gitResult.entriesWritten, lastCommit: gitResult.lastCommit } : null);
|
|
2122
2604
|
if (gitResult) {
|
|
2123
2605
|
persistWatchState(rootDir, { lastGitCommit: gitResult.lastCommit });
|
|
2124
|
-
console.log(` \u2713 git: ${gitResult.entriesWritten} entries`);
|
|
2125
2606
|
}
|
|
2607
|
+
const lastScanAt = config.watchState?.lastScanAt ? new Date(config.watchState.lastScanAt) : null;
|
|
2608
|
+
ui.scanStart(lastScanAt);
|
|
2609
|
+
ui.scanFilesStart();
|
|
2126
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
|
+
}
|
|
2127
2619
|
writeConventions(rootDir, sections);
|
|
2128
2620
|
persistWatchState(rootDir, { lastScanAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2129
|
-
|
|
2130
|
-
console.log("\nDone.\n");
|
|
2621
|
+
ui.scanDone(sections.naming[0] ?? "");
|
|
2131
2622
|
return;
|
|
2132
2623
|
}
|
|
2133
2624
|
if (isDaemonRunning(rootDir)) {
|
|
2134
2625
|
const pid = readPid(rootDir);
|
|
2135
2626
|
console.log(`
|
|
2136
|
-
|
|
2627
|
+
Watch is already running (pid ${pid}). Use --stop to stop it.
|
|
2137
2628
|
`);
|
|
2138
2629
|
return;
|
|
2139
2630
|
}
|
|
2140
2631
|
injectWatchHook(rootDir, config);
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
detached: true,
|
|
2144
|
-
stdio: "ignore",
|
|
2145
|
-
cwd: rootDir
|
|
2146
|
-
});
|
|
2147
|
-
child.unref();
|
|
2148
|
-
if (child.pid) {
|
|
2149
|
-
writePid(rootDir, child.pid);
|
|
2150
|
-
if (!options.silent) {
|
|
2151
|
-
console.log(`
|
|
2152
|
-
Daemon started (pid ${child.pid}).`);
|
|
2153
|
-
console.log(`Run 'clawstrap watch --stop' to stop it.
|
|
2154
|
-
`);
|
|
2155
|
-
}
|
|
2156
|
-
} else {
|
|
2157
|
-
console.error("\nFailed to start daemon.\n");
|
|
2158
|
-
process.exit(1);
|
|
2159
|
-
}
|
|
2632
|
+
writePid(rootDir, process.pid);
|
|
2633
|
+
await runDaemon(rootDir, config, ui);
|
|
2160
2634
|
}
|
|
2161
2635
|
function persistWatchState(rootDir, updates) {
|
|
2162
|
-
const configPath =
|
|
2636
|
+
const configPath = import_node_path22.default.join(rootDir, ".clawstrap.json");
|
|
2163
2637
|
try {
|
|
2164
|
-
const raw = JSON.parse(
|
|
2638
|
+
const raw = JSON.parse(import_node_fs21.default.readFileSync(configPath, "utf-8"));
|
|
2165
2639
|
raw.watchState = { ...raw.watchState ?? {}, ...updates };
|
|
2166
|
-
|
|
2640
|
+
import_node_fs21.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
|
|
2167
2641
|
} catch {
|
|
2168
2642
|
}
|
|
2169
2643
|
}
|
|
2170
2644
|
function injectWatchHook(rootDir, config) {
|
|
2171
|
-
const governanceFile =
|
|
2172
|
-
if (!
|
|
2173
|
-
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");
|
|
2174
2648
|
if (content.includes("<!-- CLAWSTRAP:WATCH -->")) return;
|
|
2175
2649
|
const _config = config;
|
|
2176
2650
|
void _config;
|
|
@@ -2197,23 +2671,23 @@ function injectWatchHook(rootDir, config) {
|
|
|
2197
2671
|
|
|
2198
2672
|
The watch daemon picks this up automatically and updates MEMORY.md and gotcha-log.md.
|
|
2199
2673
|
`;
|
|
2200
|
-
|
|
2674
|
+
import_node_fs21.default.appendFileSync(governanceFile, hook, "utf-8");
|
|
2201
2675
|
}
|
|
2202
2676
|
|
|
2203
2677
|
// src/analyze.ts
|
|
2204
|
-
var
|
|
2205
|
-
var
|
|
2678
|
+
var import_node_fs22 = __toESM(require("fs"), 1);
|
|
2679
|
+
var import_node_path23 = __toESM(require("path"), 1);
|
|
2206
2680
|
init_writers();
|
|
2207
2681
|
async function analyze() {
|
|
2208
2682
|
const { rootDir } = loadWorkspace();
|
|
2209
2683
|
console.log("\nScanning codebase conventions...\n");
|
|
2210
2684
|
const sections = await runScan(rootDir);
|
|
2211
2685
|
writeConventions(rootDir, sections);
|
|
2212
|
-
const configPath =
|
|
2686
|
+
const configPath = import_node_path23.default.join(rootDir, ".clawstrap.json");
|
|
2213
2687
|
try {
|
|
2214
|
-
const raw = JSON.parse(
|
|
2688
|
+
const raw = JSON.parse(import_node_fs22.default.readFileSync(configPath, "utf-8"));
|
|
2215
2689
|
raw["watchState"] = { ...raw["watchState"] ?? {}, lastScanAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2216
|
-
|
|
2690
|
+
import_node_fs22.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
|
|
2217
2691
|
} catch {
|
|
2218
2692
|
}
|
|
2219
2693
|
console.log(" \u2713 .claude/rules/conventions.md updated\n");
|
|
@@ -2221,7 +2695,7 @@ async function analyze() {
|
|
|
2221
2695
|
|
|
2222
2696
|
// src/index.ts
|
|
2223
2697
|
var program = new import_commander.Command();
|
|
2224
|
-
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");
|
|
2225
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) => {
|
|
2226
2700
|
await init(directory, options);
|
|
2227
2701
|
});
|
|
@@ -2249,7 +2723,7 @@ Unknown format: ${options.format}. Supported: paperclip
|
|
|
2249
2723
|
}
|
|
2250
2724
|
await exportPaperclip(options);
|
|
2251
2725
|
});
|
|
2252
|
-
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) => {
|
|
2253
2727
|
await watch(options);
|
|
2254
2728
|
});
|
|
2255
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": {
|