clawstrap 1.3.0 → 1.4.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/README.md +168 -124
- package/dist/index.cjs +1057 -60
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -6,6 +6,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
9
16
|
var __copyProps = (to, from, except, desc) => {
|
|
10
17
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
18
|
for (let key of __getOwnPropNames(from))
|
|
@@ -23,6 +30,199 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
30
|
mod
|
|
24
31
|
));
|
|
25
32
|
|
|
33
|
+
// src/watch/dedup.ts
|
|
34
|
+
function tokenize(text) {
|
|
35
|
+
const words = text.split(/\s+/).map((w) => w.replace(/[^a-z0-9]/gi, "").toLowerCase()).filter(Boolean);
|
|
36
|
+
return new Set(words);
|
|
37
|
+
}
|
|
38
|
+
function jaccard(a, b) {
|
|
39
|
+
const setA = tokenize(a);
|
|
40
|
+
const setB = tokenize(b);
|
|
41
|
+
if (setA.size === 0 && setB.size === 0) return 1;
|
|
42
|
+
if (setA.size === 0 || setB.size === 0) return 0;
|
|
43
|
+
let intersection = 0;
|
|
44
|
+
for (const token of setA) {
|
|
45
|
+
if (setB.has(token)) intersection++;
|
|
46
|
+
}
|
|
47
|
+
const union = setA.size + setB.size - intersection;
|
|
48
|
+
return intersection / union;
|
|
49
|
+
}
|
|
50
|
+
function isDuplicate(newEntry, existingEntries, threshold = 0.75) {
|
|
51
|
+
for (const existing of existingEntries) {
|
|
52
|
+
if (jaccard(newEntry, existing) >= threshold) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
function parseMemoryEntries(content) {
|
|
59
|
+
const lines = content.split("\n");
|
|
60
|
+
const entries = [];
|
|
61
|
+
let current = [];
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
if (line.startsWith("---")) {
|
|
64
|
+
if (current.length > 0) {
|
|
65
|
+
const entry = current.join("\n").trim();
|
|
66
|
+
if (entry) entries.push(entry);
|
|
67
|
+
current = [];
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
current.push(line);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (current.length > 0) {
|
|
74
|
+
const entry = current.join("\n").trim();
|
|
75
|
+
if (entry) entries.push(entry);
|
|
76
|
+
}
|
|
77
|
+
return entries;
|
|
78
|
+
}
|
|
79
|
+
var init_dedup = __esm({
|
|
80
|
+
"src/watch/dedup.ts"() {
|
|
81
|
+
"use strict";
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// src/watch/writers.ts
|
|
86
|
+
var writers_exports = {};
|
|
87
|
+
__export(writers_exports, {
|
|
88
|
+
appendToFutureConsiderations: () => appendToFutureConsiderations,
|
|
89
|
+
appendToGotchaLog: () => appendToGotchaLog,
|
|
90
|
+
appendToMemory: () => appendToMemory,
|
|
91
|
+
writeConventions: () => writeConventions
|
|
92
|
+
});
|
|
93
|
+
function formatEntry(source, text) {
|
|
94
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
95
|
+
return `---
|
|
96
|
+
[${source}] ${ts}
|
|
97
|
+
${text}`;
|
|
98
|
+
}
|
|
99
|
+
function appendToMemory(rootDir, entries, source) {
|
|
100
|
+
const memoryPath = import_node_path15.default.join(rootDir, ".claude", "memory", "MEMORY.md");
|
|
101
|
+
import_node_fs14.default.mkdirSync(import_node_path15.default.dirname(memoryPath), { recursive: true });
|
|
102
|
+
let existingContent = "";
|
|
103
|
+
if (import_node_fs14.default.existsSync(memoryPath)) {
|
|
104
|
+
existingContent = import_node_fs14.default.readFileSync(memoryPath, "utf-8");
|
|
105
|
+
}
|
|
106
|
+
const existingEntries = parseMemoryEntries(existingContent);
|
|
107
|
+
const toAppend = [];
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
if (!isDuplicate(entry, existingEntries)) {
|
|
110
|
+
toAppend.push(formatEntry(source, entry));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (toAppend.length > 0) {
|
|
114
|
+
const appendText = "\n" + toAppend.join("\n") + "\n";
|
|
115
|
+
import_node_fs14.default.appendFileSync(memoryPath, appendText, "utf-8");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function appendToGotchaLog(rootDir, entries) {
|
|
119
|
+
const logPath = import_node_path15.default.join(rootDir, ".claude", "gotcha-log.md");
|
|
120
|
+
import_node_fs14.default.mkdirSync(import_node_path15.default.dirname(logPath), { recursive: true });
|
|
121
|
+
if (!import_node_fs14.default.existsSync(logPath)) {
|
|
122
|
+
import_node_fs14.default.writeFileSync(
|
|
123
|
+
logPath,
|
|
124
|
+
"# Gotcha Log\n\nIncident log \u2014 why rules exist.\n\n",
|
|
125
|
+
"utf-8"
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const toAppend = entries.map((e) => formatEntry("session", e)).join("\n");
|
|
129
|
+
import_node_fs14.default.appendFileSync(logPath, "\n" + toAppend + "\n", "utf-8");
|
|
130
|
+
}
|
|
131
|
+
function appendToFutureConsiderations(rootDir, entries) {
|
|
132
|
+
const fcPath = import_node_path15.default.join(rootDir, ".claude", "future-considerations.md");
|
|
133
|
+
import_node_fs14.default.mkdirSync(import_node_path15.default.dirname(fcPath), { recursive: true });
|
|
134
|
+
if (!import_node_fs14.default.existsSync(fcPath)) {
|
|
135
|
+
import_node_fs14.default.writeFileSync(
|
|
136
|
+
fcPath,
|
|
137
|
+
"# Future Considerations\n\nDeferred ideas and potential improvements.\n\n",
|
|
138
|
+
"utf-8"
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const toAppend = entries.map((e) => formatEntry("session", e)).join("\n");
|
|
142
|
+
import_node_fs14.default.appendFileSync(fcPath, "\n" + toAppend + "\n", "utf-8");
|
|
143
|
+
}
|
|
144
|
+
function buildAutoBlock(sections) {
|
|
145
|
+
const lines = [AUTO_START];
|
|
146
|
+
lines.push("## Naming Conventions");
|
|
147
|
+
if (sections.naming.length > 0) {
|
|
148
|
+
for (const item of sections.naming) lines.push(`- ${item}`);
|
|
149
|
+
} else {
|
|
150
|
+
lines.push("- No naming conventions detected.");
|
|
151
|
+
}
|
|
152
|
+
lines.push("");
|
|
153
|
+
lines.push("## Import Style");
|
|
154
|
+
if (sections.imports.length > 0) {
|
|
155
|
+
for (const item of sections.imports) lines.push(`- ${item}`);
|
|
156
|
+
} else {
|
|
157
|
+
lines.push("- No import patterns detected.");
|
|
158
|
+
}
|
|
159
|
+
lines.push("");
|
|
160
|
+
lines.push("## Testing");
|
|
161
|
+
if (sections.testing.length > 0) {
|
|
162
|
+
for (const item of sections.testing) lines.push(`- ${item}`);
|
|
163
|
+
} else {
|
|
164
|
+
lines.push("- No test files detected.");
|
|
165
|
+
}
|
|
166
|
+
lines.push("");
|
|
167
|
+
lines.push("## Error Handling");
|
|
168
|
+
if (sections.errorHandling.length > 0) {
|
|
169
|
+
for (const item of sections.errorHandling) lines.push(`- ${item}`);
|
|
170
|
+
} else {
|
|
171
|
+
lines.push("- No error handling patterns detected.");
|
|
172
|
+
}
|
|
173
|
+
lines.push("");
|
|
174
|
+
lines.push("## Comments");
|
|
175
|
+
if (sections.comments.length > 0) {
|
|
176
|
+
for (const item of sections.comments) lines.push(`- ${item}`);
|
|
177
|
+
} else {
|
|
178
|
+
lines.push("- No comment patterns detected.");
|
|
179
|
+
}
|
|
180
|
+
lines.push(AUTO_END);
|
|
181
|
+
return lines.join("\n");
|
|
182
|
+
}
|
|
183
|
+
function writeConventions(rootDir, sections) {
|
|
184
|
+
const conventionsPath = import_node_path15.default.join(rootDir, ".claude", "rules", "conventions.md");
|
|
185
|
+
import_node_fs14.default.mkdirSync(import_node_path15.default.dirname(conventionsPath), { recursive: true });
|
|
186
|
+
const autoBlock = buildAutoBlock(sections);
|
|
187
|
+
if (!import_node_fs14.default.existsSync(conventionsPath)) {
|
|
188
|
+
const content = [
|
|
189
|
+
"# Conventions",
|
|
190
|
+
"",
|
|
191
|
+
"> Auto-generated by `clawstrap analyze`. Do not edit the AUTO block manually.",
|
|
192
|
+
"",
|
|
193
|
+
autoBlock,
|
|
194
|
+
"",
|
|
195
|
+
"<!-- Add manual conventions below this line -->",
|
|
196
|
+
""
|
|
197
|
+
].join("\n");
|
|
198
|
+
import_node_fs14.default.writeFileSync(conventionsPath, content, "utf-8");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const existing = import_node_fs14.default.readFileSync(conventionsPath, "utf-8");
|
|
202
|
+
const startIdx = existing.indexOf(AUTO_START);
|
|
203
|
+
const endIdx = existing.indexOf(AUTO_END);
|
|
204
|
+
if (startIdx === -1 || endIdx === -1) {
|
|
205
|
+
const updated = existing.trimEnd() + "\n\n" + autoBlock + "\n";
|
|
206
|
+
import_node_fs14.default.writeFileSync(conventionsPath, updated, "utf-8");
|
|
207
|
+
} else {
|
|
208
|
+
const before = existing.slice(0, startIdx);
|
|
209
|
+
const after = existing.slice(endIdx + AUTO_END.length);
|
|
210
|
+
const updated = before + autoBlock + after;
|
|
211
|
+
import_node_fs14.default.writeFileSync(conventionsPath, updated, "utf-8");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
var import_node_fs14, import_node_path15, AUTO_START, AUTO_END;
|
|
215
|
+
var init_writers = __esm({
|
|
216
|
+
"src/watch/writers.ts"() {
|
|
217
|
+
"use strict";
|
|
218
|
+
import_node_fs14 = __toESM(require("fs"), 1);
|
|
219
|
+
import_node_path15 = __toESM(require("path"), 1);
|
|
220
|
+
init_dedup();
|
|
221
|
+
AUTO_START = "<!-- CLAWSTRAP:AUTO -->";
|
|
222
|
+
AUTO_END = "<!-- CLAWSTRAP:END -->";
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
26
226
|
// src/index.ts
|
|
27
227
|
var import_commander = require("commander");
|
|
28
228
|
|
|
@@ -58,6 +258,18 @@ var ClawstrapConfigSchema = import_zod.z.object({
|
|
|
58
258
|
qualityLevel: import_zod.z.enum(QUALITY_LEVELS),
|
|
59
259
|
sessionHandoff: import_zod.z.boolean(),
|
|
60
260
|
sdd: import_zod.z.boolean().default(false),
|
|
261
|
+
watch: import_zod.z.object({
|
|
262
|
+
adapter: import_zod.z.enum(["claude-local", "claude-api", "ollama", "codex-local"]).default("claude-local"),
|
|
263
|
+
scan: import_zod.z.object({
|
|
264
|
+
intervalDays: import_zod.z.number().default(7)
|
|
265
|
+
}).default({}),
|
|
266
|
+
silent: import_zod.z.boolean().default(false)
|
|
267
|
+
}).optional(),
|
|
268
|
+
watchState: import_zod.z.object({
|
|
269
|
+
lastGitCommit: import_zod.z.string().optional(),
|
|
270
|
+
lastScanAt: import_zod.z.string().optional(),
|
|
271
|
+
lastTranscriptAt: import_zod.z.string().optional()
|
|
272
|
+
}).optional(),
|
|
61
273
|
lastExport: LastExportSchema
|
|
62
274
|
});
|
|
63
275
|
|
|
@@ -239,7 +451,7 @@ var governance_file_md_default = "# {%governanceFile%} \u2014 Master Governance
|
|
|
239
451
|
var getting_started_md_default = "# Getting Started \u2014 {%workspaceName%}\n> Generated by Clawstrap v{%clawstrapVersion%} on {%generatedDate%}\n\nWelcome to your AI agent workspace. This workspace is configured for\n**{%workloadLabel%}** workflows.\n\n---\n\n## Quick Start\n\n1. **Read `{%governanceFile%}`** \u2014 this is your master governance file. It loads\n automatically every session and defines the rules your AI assistant follows.\n\n2. **Check `{%systemDir%}/rules/`** \u2014 these rule files also load every session.\n They contain detailed procedures for context discipline, approval workflows,\n and quality gates.\n\n3. **Start working** \u2014 open a Claude Code session in this directory. The\n governance files will load automatically.\n\n---\n\n## What's in This Workspace\n\n```\n{%governanceFile%} \u2190 Master governance (always loaded)\nGETTING_STARTED.md \u2190 You are here\n.clawstrap.json \u2190 Your workspace configuration\n\n{%systemDir%}/\n rules/ \u2190 Auto-loaded rules (every session)\n skills/ \u2190 Skill definitions (loaded when triggered)\n{%#if hasSubagents%} agents/ \u2190 Agent definitions\n{%/if%} gotcha-log.md \u2190 Incident log\n future-considerations.md \u2190 Deferred ideas\n{%#if sessionHandoff%} memory/ \u2190 Persistent memory across sessions\n{%/if%}\nprojects/\n _template/ \u2190 Template for new projects\n\ntmp/ \u2190 Temporary files (gitignored)\nresearch/ \u2190 Reference material\ncontext/ \u2190 Session checkpoints\nartifacts/ \u2190 Durable output\n{%#if sddEnabled%}specs/\n _template.md \u2190 Spec template (copy for each feature)\n{%/if%}```\n\n---\n\n## Key Principles\n\nThis workspace enforces five principles:\n\n1. **If it's not on disk, it didn't happen.** Context windows compress. Sessions\n end. The only thing that persists is files. Flush work {%flushCadence%}.\n\n2. **Approval-first, always.** Plan work, get approval, then execute. No\n speculative actions.\n\n3. **Quality is a structural gate.** Quality checks happen during work, not after.\n{%#if hasQualityGates%} See `{%systemDir%}/rules/quality-gates.md` for the full procedure.{%/if%}\n\n4. **Disagreements reveal ambiguity.** When agents disagree, the rules are\n ambiguous. Escalate and clarify \u2014 don't suppress.\n\n5. **One decision at a time.** Binary decisions made sequentially compound into\n reliable outcomes.\n{%#if hasSubagents%}\n\n---\n\n## Working with Multiple Agents\n\nYour workspace is configured for multi-agent workflows. Key rules:\n\n- **Subagents write to `tmp/`** \u2014 they return file paths and one-line summaries,\n never raw data in conversation.\n- **The main session is an orchestrator** \u2014 it tracks progress, launches agents,\n and checks output files. It does not hold subagent data in memory.\n- **Agent definitions live in `{%systemDir%}/agents/`** \u2014 each file becomes the\n system prompt for a subagent. Governance is baked into the definition.\n{%/if%}\n{%#if sessionHandoff%}\n\n---\n\n## Session Handoff\n\nThis workspace uses session handoff checklists. At the end of every session:\n\n1. Save all work to SSOT files\n2. Sync derived files\n3. Check SSOT integrity\n4. Update progress tracker\n5. Write next-session plan\n6. Run QC on session work\n\nThis ensures no context is lost between sessions.\n{%/if%}\n{%#if isResearch%}\n\n---\n\n## Research Tips\n\n- Write findings to disk immediately \u2014 don't accumulate in conversation\n- Cite sources for every claim\n- Separate facts from interpretation\n- Use `research/` for reference material, `artifacts/` for synthesis output\n{%/if%}\n{%#if isContent%}\n\n---\n\n## Content Workflow Tips\n\n- Every piece follows: draft \u2192 review \u2192 approve\n- Track revision feedback in dedicated files\n- Never finalize without explicit approval\n- Use `artifacts/` for final deliverables\n{%/if%}\n{%#if isDataProcessing%}\n\n---\n\n## Data Processing Tips\n\n- Define batch size before starting any processing run\n- Validate schema on every write\n- Checkpoint every 5 batch items\n- Log all transformations for auditability\n{%/if%}\n\n---\n\n{%#if sddEnabled%}\n\n---\n\n## Spec-Driven Development\n\nThis workspace enforces a spec-first workflow. Before any implementation:\n\n1. **Type `/spec`** to start a new spec interactively \u2014 Claude will guide you\n2. Or copy `specs/_template.md` manually, fill it in, and get approval\n3. Only implement after the spec is approved\n\nYour specs live in `specs/`. Each approved spec is the contract between you and\nClaude \u2014 implementation follows the spec, not the conversation.\n\nSee `{%systemDir%}/rules/sdd.md` for the full rules and exemptions.\n{%/if%}\n\n---\n\n*This workspace was generated by [Clawstrap](https://github.com/clawstrap/clawstrap).\nEdit any file to fit your needs \u2014 there's no lock-in.*\n";
|
|
240
452
|
|
|
241
453
|
// src/templates/gitignore.tmpl
|
|
242
|
-
var gitignore_default = "# Dependencies\nnode_modules/\n\n# Temporary files (subagent output, session data)\ntmp/\n\n# Secrets\n.env\n.env.*\ncredentials.json\n*.pem\n*.key\n\n# OS\n.DS_Store\nThumbs.db\n\n# Editor\n*.swp\n*.swo\n*~\n";
|
|
454
|
+
var gitignore_default = "# Dependencies\nnode_modules/\n\n# Temporary files (subagent output, session data)\ntmp/\n\n# Secrets\n.env\n.env.*\ncredentials.json\n*.pem\n*.key\n\n# OS\n.DS_Store\nThumbs.db\n\n# Editor\n*.swp\n*.swo\n*~\n.clawstrap.watch.pid\ntmp/sessions/\n";
|
|
243
455
|
|
|
244
456
|
// src/templates/rules/context-discipline.md.tmpl
|
|
245
457
|
var context_discipline_md_default = "# Rule: Context Discipline\n> **Scope**: All sessions | **Generated**: {%generatedDate%}\n\n## Flush Cadence\n\nFlush working state to file {%flushCadence%}:\n- Write current state to a context checkpoint file\n- Include: what's done, what's next, accumulated results\n- Path: `context/checkpoint-{date}-{task}.md`\n{%#if hasSubagents%}\n\n## Subagent Output Rules\n\nSubagents MUST:\n1. Write all output to `tmp/{task}/{name}.json` (or `.md`)\n2. Return a one-line receipt: `\"Done. N items. File: {path}. Summary: {1 line}.\"`\n3. NOT dump raw results into the conversation\n\nMain session MUST:\n- Only read the subagent file if needed for the NEXT step\n- Never hold subagent output in conversation memory\n\n## Thin Orchestrator Principle\n\nDuring batch work, the main session is an orchestrator only:\n\n**DO**:\n- Track queue / done / next\n- Launch agents with correct prompts\n- Check output files for completeness\n- Update SSOT data files\n- Run QC gates\n\n**DO NOT**:\n- Read raw extraction data into main context\n- Classify results (agents do that)\n- Hold full subagent output in conversation\n{%/if%}\n\n## Before Batch Work\n\nAlways write an execution plan to `tmp/{task}/plan.md` before starting.\nThis file must survive context loss and be readable by any future session.\n\n## On User Correction\n\n1. FIRST write the correction to its durable home (memory/rule/skill file)\n2. THEN apply the correction to current work\n\nThis ensures the learning persists even if the session ends unexpectedly.\n";
|
|
@@ -771,8 +983,43 @@ Error: project "${name}" already exists at projects/${name}/
|
|
|
771
983
|
}
|
|
772
984
|
|
|
773
985
|
// src/status.ts
|
|
986
|
+
var import_node_fs8 = __toESM(require("fs"), 1);
|
|
987
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
988
|
+
|
|
989
|
+
// src/watch/pid.ts
|
|
774
990
|
var import_node_fs7 = __toESM(require("fs"), 1);
|
|
775
991
|
var import_node_path8 = __toESM(require("path"), 1);
|
|
992
|
+
var PID_FILE = ".clawstrap.watch.pid";
|
|
993
|
+
function pidPath(rootDir) {
|
|
994
|
+
return import_node_path8.default.join(rootDir, PID_FILE);
|
|
995
|
+
}
|
|
996
|
+
function writePid(rootDir, pid) {
|
|
997
|
+
import_node_fs7.default.writeFileSync(pidPath(rootDir), String(pid), "utf-8");
|
|
998
|
+
}
|
|
999
|
+
function readPid(rootDir) {
|
|
1000
|
+
const p = pidPath(rootDir);
|
|
1001
|
+
if (!import_node_fs7.default.existsSync(p)) return null;
|
|
1002
|
+
const raw = import_node_fs7.default.readFileSync(p, "utf-8").trim();
|
|
1003
|
+
const pid = parseInt(raw, 10);
|
|
1004
|
+
return isNaN(pid) ? null : pid;
|
|
1005
|
+
}
|
|
1006
|
+
function clearPid(rootDir) {
|
|
1007
|
+
const p = pidPath(rootDir);
|
|
1008
|
+
if (import_node_fs7.default.existsSync(p)) import_node_fs7.default.unlinkSync(p);
|
|
1009
|
+
}
|
|
1010
|
+
function isDaemonRunning(rootDir) {
|
|
1011
|
+
const pid = readPid(rootDir);
|
|
1012
|
+
if (pid === null) return false;
|
|
1013
|
+
try {
|
|
1014
|
+
process.kill(pid, 0);
|
|
1015
|
+
return true;
|
|
1016
|
+
} catch {
|
|
1017
|
+
clearPid(rootDir);
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// src/status.ts
|
|
776
1023
|
var WORKLOAD_LABELS3 = {
|
|
777
1024
|
research: "Research & Analysis",
|
|
778
1025
|
content: "Content & Writing",
|
|
@@ -780,22 +1027,22 @@ var WORKLOAD_LABELS3 = {
|
|
|
780
1027
|
custom: "General Purpose"
|
|
781
1028
|
};
|
|
782
1029
|
function countEntries(dir, exclude = []) {
|
|
783
|
-
if (!
|
|
784
|
-
return
|
|
1030
|
+
if (!import_node_fs8.default.existsSync(dir)) return 0;
|
|
1031
|
+
return import_node_fs8.default.readdirSync(dir, { withFileTypes: true }).filter((e) => !exclude.includes(e.name) && !e.name.startsWith(".")).length;
|
|
785
1032
|
}
|
|
786
1033
|
function countSkills(skillsDir) {
|
|
787
|
-
if (!
|
|
788
|
-
return
|
|
789
|
-
(e) => e.isDirectory() &&
|
|
1034
|
+
if (!import_node_fs8.default.existsSync(skillsDir)) return 0;
|
|
1035
|
+
return import_node_fs8.default.readdirSync(skillsDir, { withFileTypes: true }).filter(
|
|
1036
|
+
(e) => e.isDirectory() && import_node_fs8.default.existsSync(import_node_path9.default.join(skillsDir, e.name, "SKILL.md"))
|
|
790
1037
|
).length;
|
|
791
1038
|
}
|
|
792
1039
|
async function showStatus() {
|
|
793
1040
|
const { config, vars, rootDir } = loadWorkspace();
|
|
794
1041
|
const systemDir = String(vars.systemDir);
|
|
795
|
-
const agentsDir =
|
|
796
|
-
const skillsDir =
|
|
797
|
-
const rulesDir =
|
|
798
|
-
const projectsDir =
|
|
1042
|
+
const agentsDir = import_node_path9.default.join(rootDir, systemDir, "agents");
|
|
1043
|
+
const skillsDir = import_node_path9.default.join(rootDir, systemDir, "skills");
|
|
1044
|
+
const rulesDir = import_node_path9.default.join(rootDir, systemDir, "rules");
|
|
1045
|
+
const projectsDir = import_node_path9.default.join(rootDir, "projects");
|
|
799
1046
|
const agentCount = countEntries(agentsDir, ["_template.md"]);
|
|
800
1047
|
const skillCount = countSkills(skillsDir);
|
|
801
1048
|
const projectCount = countEntries(projectsDir, ["_template"]);
|
|
@@ -829,32 +1076,48 @@ Last Export:`);
|
|
|
829
1076
|
console.log(` Date: ${exportDate}`);
|
|
830
1077
|
console.log(` Output: ${config.lastExport.outputDir}`);
|
|
831
1078
|
}
|
|
1079
|
+
const watchRunning = isDaemonRunning(rootDir);
|
|
1080
|
+
console.log(`
|
|
1081
|
+
Watch:`);
|
|
1082
|
+
console.log(` Status: ${watchRunning ? "running" : "stopped"}`);
|
|
1083
|
+
if (config.watch) {
|
|
1084
|
+
console.log(` Adapter: ${config.watch.adapter}`);
|
|
1085
|
+
}
|
|
1086
|
+
if (config.watchState?.lastGitCommit) {
|
|
1087
|
+
console.log(` Last git: ${config.watchState.lastGitCommit.slice(0, 8)}`);
|
|
1088
|
+
}
|
|
1089
|
+
if (config.watchState?.lastScanAt) {
|
|
1090
|
+
console.log(` Last scan: ${config.watchState.lastScanAt.split("T")[0]}`);
|
|
1091
|
+
}
|
|
1092
|
+
if (config.watchState?.lastTranscriptAt) {
|
|
1093
|
+
console.log(` Last transcript: ${config.watchState.lastTranscriptAt.replace("T", " ").slice(0, 16)}`);
|
|
1094
|
+
}
|
|
832
1095
|
console.log();
|
|
833
1096
|
}
|
|
834
1097
|
|
|
835
1098
|
// src/export-paperclip.ts
|
|
836
|
-
var
|
|
837
|
-
var
|
|
1099
|
+
var import_node_fs13 = __toESM(require("fs"), 1);
|
|
1100
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
838
1101
|
var import_prompts7 = require("@inquirer/prompts");
|
|
839
1102
|
|
|
840
1103
|
// src/export-paperclip/translate-agents.ts
|
|
841
|
-
var
|
|
842
|
-
var
|
|
1104
|
+
var import_node_fs9 = __toESM(require("fs"), 1);
|
|
1105
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
843
1106
|
function slugToName(slug) {
|
|
844
1107
|
return slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
845
1108
|
}
|
|
846
1109
|
function translateAgents(rootDir, systemDir, workspaceName, skillSlugs) {
|
|
847
|
-
const agentsDir =
|
|
1110
|
+
const agentsDir = import_node_path10.default.join(rootDir, systemDir, "agents");
|
|
848
1111
|
const agents = [];
|
|
849
1112
|
const workerNames = [];
|
|
850
1113
|
const workerAgents = [];
|
|
851
|
-
if (
|
|
852
|
-
for (const entry of
|
|
1114
|
+
if (import_node_fs9.default.existsSync(agentsDir)) {
|
|
1115
|
+
for (const entry of import_node_fs9.default.readdirSync(agentsDir)) {
|
|
853
1116
|
if (entry === "primary-agent.md" || entry === "_template.md" || entry.startsWith(".") || !entry.endsWith(".md")) {
|
|
854
1117
|
continue;
|
|
855
1118
|
}
|
|
856
1119
|
const slug = entry.replace(/\.md$/, "");
|
|
857
|
-
const rawBody =
|
|
1120
|
+
const rawBody = import_node_fs9.default.readFileSync(import_node_path10.default.join(agentsDir, entry), "utf-8");
|
|
858
1121
|
const roleMatch = rawBody.match(/^>\s*\*\*Purpose\*\*:\s*(.+)/m);
|
|
859
1122
|
const description = roleMatch ? roleMatch[1].trim() : "";
|
|
860
1123
|
const name = slugToName(slug);
|
|
@@ -964,8 +1227,8 @@ When your work is complete, hand off to the **CEO** for review routing. If a rev
|
|
|
964
1227
|
}
|
|
965
1228
|
|
|
966
1229
|
// src/export-paperclip/translate-governance.ts
|
|
967
|
-
var
|
|
968
|
-
var
|
|
1230
|
+
var import_node_fs10 = __toESM(require("fs"), 1);
|
|
1231
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
969
1232
|
var GOVERNANCE_TIERS = {
|
|
970
1233
|
solo: {
|
|
971
1234
|
tier: "light",
|
|
@@ -999,39 +1262,39 @@ function getGovernanceConfig(qualityLevel) {
|
|
|
999
1262
|
}
|
|
1000
1263
|
|
|
1001
1264
|
// src/export-paperclip/translate-skills.ts
|
|
1002
|
-
var
|
|
1003
|
-
var
|
|
1265
|
+
var import_node_fs11 = __toESM(require("fs"), 1);
|
|
1266
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
1004
1267
|
function translateSkills(rootDir, systemDir) {
|
|
1005
|
-
const skillsDir =
|
|
1268
|
+
const skillsDir = import_node_path12.default.join(rootDir, systemDir, "skills");
|
|
1006
1269
|
const skills = [];
|
|
1007
|
-
if (!
|
|
1008
|
-
for (const entry of
|
|
1270
|
+
if (!import_node_fs11.default.existsSync(skillsDir)) return skills;
|
|
1271
|
+
for (const entry of import_node_fs11.default.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
1009
1272
|
if (!entry.isDirectory()) continue;
|
|
1010
|
-
const skillMdPath =
|
|
1011
|
-
if (!
|
|
1273
|
+
const skillMdPath = import_node_path12.default.join(skillsDir, entry.name, "SKILL.md");
|
|
1274
|
+
if (!import_node_fs11.default.existsSync(skillMdPath)) continue;
|
|
1012
1275
|
skills.push({
|
|
1013
1276
|
name: entry.name,
|
|
1014
1277
|
sourcePath: `${systemDir}/skills/${entry.name}/SKILL.md`,
|
|
1015
|
-
content:
|
|
1278
|
+
content: import_node_fs11.default.readFileSync(skillMdPath, "utf-8")
|
|
1016
1279
|
});
|
|
1017
1280
|
}
|
|
1018
1281
|
return skills;
|
|
1019
1282
|
}
|
|
1020
1283
|
|
|
1021
1284
|
// src/export-paperclip/translate-goals.ts
|
|
1022
|
-
var
|
|
1023
|
-
var
|
|
1285
|
+
var import_node_fs12 = __toESM(require("fs"), 1);
|
|
1286
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
1024
1287
|
function translateGoals(rootDir) {
|
|
1025
|
-
const projectsDir =
|
|
1288
|
+
const projectsDir = import_node_path13.default.join(rootDir, "projects");
|
|
1026
1289
|
const goals = [];
|
|
1027
|
-
if (!
|
|
1028
|
-
for (const entry of
|
|
1290
|
+
if (!import_node_fs12.default.existsSync(projectsDir)) return goals;
|
|
1291
|
+
for (const entry of import_node_fs12.default.readdirSync(projectsDir, { withFileTypes: true })) {
|
|
1029
1292
|
if (!entry.isDirectory() || entry.name === "_template" || entry.name.startsWith(".")) {
|
|
1030
1293
|
continue;
|
|
1031
1294
|
}
|
|
1032
|
-
const readmePath =
|
|
1033
|
-
if (!
|
|
1034
|
-
const content =
|
|
1295
|
+
const readmePath = import_node_path13.default.join(projectsDir, entry.name, "README.md");
|
|
1296
|
+
if (!import_node_fs12.default.existsSync(readmePath)) continue;
|
|
1297
|
+
const content = import_node_fs12.default.readFileSync(readmePath, "utf-8");
|
|
1035
1298
|
const descMatch = content.match(
|
|
1036
1299
|
/## What This Project Is\s*\n+([\s\S]*?)(?=\n---|\n##|$)/
|
|
1037
1300
|
);
|
|
@@ -1059,7 +1322,7 @@ async function exportPaperclip(options) {
|
|
|
1059
1322
|
default: `Governed AI workspace for ${String(vars.workloadLabel).toLowerCase()}`
|
|
1060
1323
|
});
|
|
1061
1324
|
const companySlug = toSlug(companyName);
|
|
1062
|
-
const outDir =
|
|
1325
|
+
const outDir = import_node_path14.default.resolve(
|
|
1063
1326
|
options.out ?? `${config.workspaceName}-paperclip`
|
|
1064
1327
|
);
|
|
1065
1328
|
const skills = translateSkills(rootDir, systemDir);
|
|
@@ -1089,7 +1352,7 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
|
|
|
1089
1352
|
console.log("\nValidation passed. Run without --validate to export.\n");
|
|
1090
1353
|
return;
|
|
1091
1354
|
}
|
|
1092
|
-
if (
|
|
1355
|
+
if (import_node_fs13.default.existsSync(outDir)) {
|
|
1093
1356
|
const proceed = await (0, import_prompts7.confirm)({
|
|
1094
1357
|
message: `Output directory already exists at ${outDir}. Overwrite?`,
|
|
1095
1358
|
default: false
|
|
@@ -1098,10 +1361,10 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
|
|
|
1098
1361
|
console.log("Aborted.\n");
|
|
1099
1362
|
return;
|
|
1100
1363
|
}
|
|
1101
|
-
|
|
1364
|
+
import_node_fs13.default.rmSync(outDir, { recursive: true, force: true });
|
|
1102
1365
|
}
|
|
1103
1366
|
console.log("\nExporting to Paperclip format (agentcompanies/v1)...\n");
|
|
1104
|
-
|
|
1367
|
+
import_node_fs13.default.mkdirSync(outDir, { recursive: true });
|
|
1105
1368
|
const goalsYaml = goals.length > 0 ? goals.map((g) => ` - ${g.description.split("\n")[0]}`).join("\n") : ` - ${mission}`;
|
|
1106
1369
|
const pipelineLines = agents.map((a, i) => {
|
|
1107
1370
|
return `${i + 1}. **${a.name}** ${a.title.toLowerCase()}`;
|
|
@@ -1131,17 +1394,17 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
|
|
|
1131
1394
|
`Generated with [Clawstrap](https://github.com/peppinho89/clawstrap) v${CLI_VERSION}`,
|
|
1132
1395
|
""
|
|
1133
1396
|
].join("\n");
|
|
1134
|
-
|
|
1397
|
+
import_node_fs13.default.writeFileSync(import_node_path14.default.join(outDir, "COMPANY.md"), companyMd, "utf-8");
|
|
1135
1398
|
console.log(" \u2713 COMPANY.md");
|
|
1136
|
-
|
|
1137
|
-
|
|
1399
|
+
import_node_fs13.default.writeFileSync(
|
|
1400
|
+
import_node_path14.default.join(outDir, ".paperclip.yaml"),
|
|
1138
1401
|
"schema: paperclip/v1\n",
|
|
1139
1402
|
"utf-8"
|
|
1140
1403
|
);
|
|
1141
1404
|
console.log(" \u2713 .paperclip.yaml");
|
|
1142
1405
|
for (const agent of agents) {
|
|
1143
|
-
const agentDir =
|
|
1144
|
-
|
|
1406
|
+
const agentDir = import_node_path14.default.join(outDir, "agents", agent.slug);
|
|
1407
|
+
import_node_fs13.default.mkdirSync(agentDir, { recursive: true });
|
|
1145
1408
|
const frontmatterLines = [
|
|
1146
1409
|
"---",
|
|
1147
1410
|
`name: ${agent.name}`,
|
|
@@ -1156,12 +1419,12 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
|
|
|
1156
1419
|
}
|
|
1157
1420
|
frontmatterLines.push("---");
|
|
1158
1421
|
const agentMd = frontmatterLines.join("\n") + "\n\n" + agent.body + "\n";
|
|
1159
|
-
|
|
1422
|
+
import_node_fs13.default.writeFileSync(import_node_path14.default.join(agentDir, "AGENTS.md"), agentMd, "utf-8");
|
|
1160
1423
|
console.log(` \u2713 agents/${agent.slug}/AGENTS.md`);
|
|
1161
1424
|
}
|
|
1162
1425
|
if (nonCeoAgents.length > 0) {
|
|
1163
|
-
const teamDir =
|
|
1164
|
-
|
|
1426
|
+
const teamDir = import_node_path14.default.join(outDir, "teams", "engineering");
|
|
1427
|
+
import_node_fs13.default.mkdirSync(teamDir, { recursive: true });
|
|
1165
1428
|
const includesList = nonCeoAgents.map((a) => ` - ../../agents/${a.slug}/AGENTS.md`).join("\n");
|
|
1166
1429
|
const teamMd = [
|
|
1167
1430
|
"---",
|
|
@@ -1178,13 +1441,13 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
|
|
|
1178
1441
|
`The engineering team at ${companyName}. Led by the CEO, who scopes and delegates work to specialists.`,
|
|
1179
1442
|
""
|
|
1180
1443
|
].join("\n");
|
|
1181
|
-
|
|
1444
|
+
import_node_fs13.default.writeFileSync(import_node_path14.default.join(teamDir, "TEAM.md"), teamMd, "utf-8");
|
|
1182
1445
|
console.log(" \u2713 teams/engineering/TEAM.md");
|
|
1183
1446
|
}
|
|
1184
1447
|
for (const skill of skills) {
|
|
1185
|
-
const skillDir =
|
|
1186
|
-
|
|
1187
|
-
|
|
1448
|
+
const skillDir = import_node_path14.default.join(outDir, "skills", skill.name);
|
|
1449
|
+
import_node_fs13.default.mkdirSync(skillDir, { recursive: true });
|
|
1450
|
+
import_node_fs13.default.writeFileSync(import_node_path14.default.join(skillDir, "SKILL.md"), skill.content, "utf-8");
|
|
1188
1451
|
console.log(` \u2713 skills/${skill.name}/SKILL.md`);
|
|
1189
1452
|
}
|
|
1190
1453
|
const importScript = [
|
|
@@ -1200,26 +1463,26 @@ ${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
|
|
|
1200
1463
|
'echo "Done. Open your Paperclip dashboard to review."',
|
|
1201
1464
|
""
|
|
1202
1465
|
].join("\n");
|
|
1203
|
-
const importPath =
|
|
1204
|
-
|
|
1205
|
-
|
|
1466
|
+
const importPath = import_node_path14.default.join(outDir, "import.sh");
|
|
1467
|
+
import_node_fs13.default.writeFileSync(importPath, importScript, "utf-8");
|
|
1468
|
+
import_node_fs13.default.chmodSync(importPath, 493);
|
|
1206
1469
|
console.log(" \u2713 import.sh");
|
|
1207
1470
|
const updatedConfig = {
|
|
1208
1471
|
...config,
|
|
1209
1472
|
lastExport: {
|
|
1210
1473
|
format: "paperclip",
|
|
1211
1474
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1212
|
-
outputDir:
|
|
1475
|
+
outputDir: import_node_path14.default.relative(rootDir, outDir) || outDir
|
|
1213
1476
|
}
|
|
1214
1477
|
};
|
|
1215
|
-
|
|
1216
|
-
|
|
1478
|
+
import_node_fs13.default.writeFileSync(
|
|
1479
|
+
import_node_path14.default.join(rootDir, ".clawstrap.json"),
|
|
1217
1480
|
JSON.stringify(updatedConfig, null, 2) + "\n",
|
|
1218
1481
|
"utf-8"
|
|
1219
1482
|
);
|
|
1220
1483
|
console.log(
|
|
1221
1484
|
`
|
|
1222
|
-
Exported to ${
|
|
1485
|
+
Exported to ${import_node_path14.default.relative(process.cwd(), outDir) || outDir}`
|
|
1223
1486
|
);
|
|
1224
1487
|
console.log(
|
|
1225
1488
|
`${agents.length} agent(s), ${skills.length} skill(s), ${goals.length} goal(s)`
|
|
@@ -1228,9 +1491,737 @@ Exported to ${import_node_path13.default.relative(process.cwd(), outDir) || outD
|
|
|
1228
1491
|
`);
|
|
1229
1492
|
}
|
|
1230
1493
|
|
|
1494
|
+
// src/watch.ts
|
|
1495
|
+
var import_node_path20 = __toESM(require("path"), 1);
|
|
1496
|
+
var import_node_child_process4 = require("child_process");
|
|
1497
|
+
var import_node_fs19 = __toESM(require("fs"), 1);
|
|
1498
|
+
|
|
1499
|
+
// src/watch/git.ts
|
|
1500
|
+
var import_node_child_process = require("child_process");
|
|
1501
|
+
var import_node_fs15 = __toESM(require("fs"), 1);
|
|
1502
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
1503
|
+
init_writers();
|
|
1504
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
1505
|
+
"fix",
|
|
1506
|
+
"add",
|
|
1507
|
+
"update",
|
|
1508
|
+
"remove",
|
|
1509
|
+
"feat",
|
|
1510
|
+
"chore",
|
|
1511
|
+
"the",
|
|
1512
|
+
"a",
|
|
1513
|
+
"an",
|
|
1514
|
+
"and",
|
|
1515
|
+
"or",
|
|
1516
|
+
"in",
|
|
1517
|
+
"on",
|
|
1518
|
+
"at",
|
|
1519
|
+
"to",
|
|
1520
|
+
"for",
|
|
1521
|
+
"of",
|
|
1522
|
+
"is",
|
|
1523
|
+
"was",
|
|
1524
|
+
"be",
|
|
1525
|
+
"it",
|
|
1526
|
+
"as",
|
|
1527
|
+
"with",
|
|
1528
|
+
"by",
|
|
1529
|
+
"this",
|
|
1530
|
+
"that"
|
|
1531
|
+
]);
|
|
1532
|
+
function parseGitLog(output) {
|
|
1533
|
+
const entries = [];
|
|
1534
|
+
const lines = output.split("\n");
|
|
1535
|
+
let current = null;
|
|
1536
|
+
for (const line of lines) {
|
|
1537
|
+
if (line.includes("|||")) {
|
|
1538
|
+
if (current) entries.push(current);
|
|
1539
|
+
const parts = line.split("|||");
|
|
1540
|
+
current = {
|
|
1541
|
+
sha: parts[0]?.trim() ?? "",
|
|
1542
|
+
subject: parts[1]?.trim() ?? "",
|
|
1543
|
+
author: parts[2]?.trim() ?? "",
|
|
1544
|
+
date: parts[3]?.trim() ?? "",
|
|
1545
|
+
files: []
|
|
1546
|
+
};
|
|
1547
|
+
} else if (line.trim() && current) {
|
|
1548
|
+
current.files.push(line.trim());
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
if (current) entries.push(current);
|
|
1552
|
+
return entries.filter((e) => e.sha.length > 0);
|
|
1553
|
+
}
|
|
1554
|
+
function getTopDirs(entries) {
|
|
1555
|
+
const dirCount = /* @__PURE__ */ new Map();
|
|
1556
|
+
for (const entry of entries) {
|
|
1557
|
+
const seenDirs = /* @__PURE__ */ new Set();
|
|
1558
|
+
for (const file of entry.files) {
|
|
1559
|
+
const dir = import_node_path16.default.dirname(file);
|
|
1560
|
+
if (dir !== "." && !seenDirs.has(dir)) {
|
|
1561
|
+
seenDirs.add(dir);
|
|
1562
|
+
dirCount.set(dir, (dirCount.get(dir) ?? 0) + 1);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
return Array.from(dirCount.entries()).sort((a, b) => b[1] - a[1]).slice(0, 3).map(([dir]) => dir);
|
|
1567
|
+
}
|
|
1568
|
+
function getCoChangingFiles(entries, minCommits = 3) {
|
|
1569
|
+
const pairCount = /* @__PURE__ */ new Map();
|
|
1570
|
+
for (const entry of entries) {
|
|
1571
|
+
const files = entry.files.slice().sort();
|
|
1572
|
+
for (let i = 0; i < files.length; i++) {
|
|
1573
|
+
for (let j = i + 1; j < files.length; j++) {
|
|
1574
|
+
const key = `${files[i]}|||${files[j]}`;
|
|
1575
|
+
pairCount.set(key, (pairCount.get(key) ?? 0) + 1);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
return Array.from(pairCount.entries()).filter(([, count]) => count >= minCommits).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([key]) => key.split("|||"));
|
|
1580
|
+
}
|
|
1581
|
+
function getRecurringWords(entries) {
|
|
1582
|
+
const wordCount = /* @__PURE__ */ new Map();
|
|
1583
|
+
for (const entry of entries) {
|
|
1584
|
+
const words = entry.subject.toLowerCase().split(/\s+/).map((w) => w.replace(/[^a-z0-9]/g, "")).filter((w) => w.length > 2 && !STOPWORDS.has(w));
|
|
1585
|
+
for (const word of new Set(words)) {
|
|
1586
|
+
wordCount.set(word, (wordCount.get(word) ?? 0) + 1);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
return Array.from(wordCount.entries()).filter(([, count]) => count >= 2).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([word]) => word);
|
|
1590
|
+
}
|
|
1591
|
+
async function runGitObserver(rootDir, sinceCommit) {
|
|
1592
|
+
if (!import_node_fs15.default.existsSync(import_node_path16.default.join(rootDir, ".git"))) {
|
|
1593
|
+
return null;
|
|
1594
|
+
}
|
|
1595
|
+
let headSha;
|
|
1596
|
+
try {
|
|
1597
|
+
headSha = (0, import_node_child_process.execSync)(`git -C "${rootDir}" rev-parse HEAD`, {
|
|
1598
|
+
encoding: "utf-8"
|
|
1599
|
+
}).trim();
|
|
1600
|
+
} catch {
|
|
1601
|
+
return null;
|
|
1602
|
+
}
|
|
1603
|
+
if (!headSha) return null;
|
|
1604
|
+
if (sinceCommit && sinceCommit === headSha) {
|
|
1605
|
+
return { lastCommit: headSha, entriesWritten: 0 };
|
|
1606
|
+
}
|
|
1607
|
+
let logOutput;
|
|
1608
|
+
try {
|
|
1609
|
+
const rangeArg = sinceCommit ? `${sinceCommit}..HEAD` : "";
|
|
1610
|
+
const cmd = `git -C "${rootDir}" log ${rangeArg} --pretty=format:"%H|||%s|||%ae|||%ad" --date=short --name-only`;
|
|
1611
|
+
logOutput = (0, import_node_child_process.execSync)(cmd, { encoding: "utf-8" }).trim();
|
|
1612
|
+
} catch {
|
|
1613
|
+
return null;
|
|
1614
|
+
}
|
|
1615
|
+
if (!logOutput) {
|
|
1616
|
+
return { lastCommit: headSha, entriesWritten: 0 };
|
|
1617
|
+
}
|
|
1618
|
+
const entries = parseGitLog(logOutput);
|
|
1619
|
+
if (entries.length === 0) {
|
|
1620
|
+
return { lastCommit: headSha, entriesWritten: 0 };
|
|
1621
|
+
}
|
|
1622
|
+
const memoryEntries = [];
|
|
1623
|
+
const coChangingPairs = getCoChangingFiles(entries, 3);
|
|
1624
|
+
if (coChangingPairs.length > 0) {
|
|
1625
|
+
const pairs = coChangingPairs.map(([a, b]) => `${a} <-> ${b}`).join(", ");
|
|
1626
|
+
memoryEntries.push(`Co-changing file pairs (frequently modified together): ${pairs}`);
|
|
1627
|
+
}
|
|
1628
|
+
const topDirs = getTopDirs(entries);
|
|
1629
|
+
if (topDirs.length > 0) {
|
|
1630
|
+
memoryEntries.push(`Top high-churn directories (most commits): ${topDirs.join(", ")}`);
|
|
1631
|
+
}
|
|
1632
|
+
const recurringWords = getRecurringWords(entries);
|
|
1633
|
+
if (recurringWords.length > 0) {
|
|
1634
|
+
memoryEntries.push(`Recurring themes in recent commits: ${recurringWords.join(", ")}`);
|
|
1635
|
+
}
|
|
1636
|
+
const dateRange = entries.length > 0 ? `from ${entries[entries.length - 1]?.date ?? "?"} to ${entries[0]?.date ?? "?"}` : "";
|
|
1637
|
+
memoryEntries.push(
|
|
1638
|
+
`Git history analyzed: ${entries.length} commit(s) ${dateRange}. Authors: ${[...new Set(entries.map((e) => e.author))].join(", ")}`
|
|
1639
|
+
);
|
|
1640
|
+
if (memoryEntries.length > 0) {
|
|
1641
|
+
appendToMemory(rootDir, memoryEntries, "git");
|
|
1642
|
+
}
|
|
1643
|
+
return { lastCommit: headSha, entriesWritten: memoryEntries.length };
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// src/watch/scan.ts
|
|
1647
|
+
var import_node_fs16 = __toESM(require("fs"), 1);
|
|
1648
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
1649
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "tmp", "dist", ".claude"]);
|
|
1650
|
+
var CODE_EXTS = /* @__PURE__ */ new Set([".ts", ".js", ".tsx", ".jsx"]);
|
|
1651
|
+
function walkDir(dir, maxDepth = 10, depth = 0) {
|
|
1652
|
+
if (depth > maxDepth) return [];
|
|
1653
|
+
let results = [];
|
|
1654
|
+
let entries;
|
|
1655
|
+
try {
|
|
1656
|
+
entries = import_node_fs16.default.readdirSync(dir, { withFileTypes: true });
|
|
1657
|
+
} catch {
|
|
1658
|
+
return [];
|
|
1659
|
+
}
|
|
1660
|
+
for (const entry of entries) {
|
|
1661
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
1662
|
+
const fullPath = import_node_path17.default.join(dir, entry.name);
|
|
1663
|
+
if (entry.isDirectory()) {
|
|
1664
|
+
results = results.concat(walkDir(fullPath, maxDepth, depth + 1));
|
|
1665
|
+
} else if (entry.isFile()) {
|
|
1666
|
+
results.push(fullPath);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
return results;
|
|
1670
|
+
}
|
|
1671
|
+
function detectNamingCase(name) {
|
|
1672
|
+
if (/^[a-z][a-z0-9]*(-[a-z0-9]+)+$/.test(name)) return "kebab-case";
|
|
1673
|
+
if (/^[A-Z][A-Z0-9_]*$/.test(name)) return "other";
|
|
1674
|
+
if (/^[A-Z][a-zA-Z0-9]+$/.test(name)) return "PascalCase";
|
|
1675
|
+
if (/^[a-z][a-zA-Z0-9]*[A-Z][a-zA-Z0-9]*$/.test(name)) return "camelCase";
|
|
1676
|
+
if (/^[a-z][a-z0-9]*(_[a-z0-9]+)+$/.test(name)) return "snake_case";
|
|
1677
|
+
return "other";
|
|
1678
|
+
}
|
|
1679
|
+
function analyzeNaming(files) {
|
|
1680
|
+
const counts = {
|
|
1681
|
+
"kebab-case": 0,
|
|
1682
|
+
camelCase: 0,
|
|
1683
|
+
snake_case: 0,
|
|
1684
|
+
PascalCase: 0,
|
|
1685
|
+
other: 0
|
|
1686
|
+
};
|
|
1687
|
+
const examples = {
|
|
1688
|
+
"kebab-case": [],
|
|
1689
|
+
camelCase: [],
|
|
1690
|
+
snake_case: [],
|
|
1691
|
+
PascalCase: [],
|
|
1692
|
+
other: []
|
|
1693
|
+
};
|
|
1694
|
+
for (const file of files) {
|
|
1695
|
+
const base = import_node_path17.default.basename(file, import_node_path17.default.extname(file));
|
|
1696
|
+
const style2 = detectNamingCase(base);
|
|
1697
|
+
counts[style2]++;
|
|
1698
|
+
if (examples[style2].length < 3) examples[style2].push(base);
|
|
1699
|
+
}
|
|
1700
|
+
const dominant = Object.entries(counts).sort((a, b) => b[1] - a[1]).filter(([key]) => key !== "other")[0];
|
|
1701
|
+
if (!dominant || dominant[1] === 0) {
|
|
1702
|
+
return ["No dominant file naming convention detected."];
|
|
1703
|
+
}
|
|
1704
|
+
const [style, count] = dominant;
|
|
1705
|
+
const exampleList = examples[style].slice(0, 3).join(", ");
|
|
1706
|
+
const result = [`Dominant file naming: ${style} (${count} files). Examples: ${exampleList}`];
|
|
1707
|
+
const total = Object.values(counts).reduce((a, b) => a + b, 0) - counts.other;
|
|
1708
|
+
if (total > 0) {
|
|
1709
|
+
const pct = Math.round(count / total * 100);
|
|
1710
|
+
result.push(`${style} used in ${pct}% of named files.`);
|
|
1711
|
+
}
|
|
1712
|
+
return result;
|
|
1713
|
+
}
|
|
1714
|
+
function analyzeImports(files) {
|
|
1715
|
+
const sample = files.filter((f) => CODE_EXTS.has(import_node_path17.default.extname(f))).slice(0, 20);
|
|
1716
|
+
let relativeCount = 0;
|
|
1717
|
+
let absoluteCount = 0;
|
|
1718
|
+
let barrelCount = 0;
|
|
1719
|
+
for (const file of sample) {
|
|
1720
|
+
let content;
|
|
1721
|
+
try {
|
|
1722
|
+
content = import_node_fs16.default.readFileSync(file, "utf-8");
|
|
1723
|
+
} catch {
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1726
|
+
const importLines = content.split("\n").filter((l) => /^import\s/.test(l));
|
|
1727
|
+
for (const line of importLines) {
|
|
1728
|
+
if (/from\s+['"]\.\.?\//.test(line)) relativeCount++;
|
|
1729
|
+
else if (/from\s+['"]/.test(line)) absoluteCount++;
|
|
1730
|
+
}
|
|
1731
|
+
const base = import_node_path17.default.basename(file, import_node_path17.default.extname(file));
|
|
1732
|
+
if (base === "index") barrelCount++;
|
|
1733
|
+
}
|
|
1734
|
+
const results = [];
|
|
1735
|
+
const total = relativeCount + absoluteCount;
|
|
1736
|
+
if (total > 0) {
|
|
1737
|
+
const relPct = Math.round(relativeCount / total * 100);
|
|
1738
|
+
results.push(
|
|
1739
|
+
`Import style: ${relPct}% relative (./), ${100 - relPct}% absolute/alias. Analyzed ${sample.length} files.`
|
|
1740
|
+
);
|
|
1741
|
+
} else {
|
|
1742
|
+
results.push("No import statements found in sampled files.");
|
|
1743
|
+
}
|
|
1744
|
+
if (barrelCount > 0) {
|
|
1745
|
+
results.push(`Barrel exports detected: ${barrelCount} index file(s) found.`);
|
|
1746
|
+
}
|
|
1747
|
+
return results;
|
|
1748
|
+
}
|
|
1749
|
+
function analyzeTesting(files) {
|
|
1750
|
+
const testPatterns = [];
|
|
1751
|
+
let hasTestExt = false;
|
|
1752
|
+
let hasSpecExt = false;
|
|
1753
|
+
let hasTestsDir = false;
|
|
1754
|
+
for (const file of files) {
|
|
1755
|
+
const base = import_node_path17.default.basename(file);
|
|
1756
|
+
if (/\.test\.(ts|js|tsx|jsx)$/.test(base)) hasTestExt = true;
|
|
1757
|
+
if (/\.spec\.(ts|js|tsx|jsx)$/.test(base)) hasSpecExt = true;
|
|
1758
|
+
if (file.includes("/__tests__/") || file.includes("\\__tests__\\")) hasTestsDir = true;
|
|
1759
|
+
}
|
|
1760
|
+
if (hasTestExt) testPatterns.push("*.test.ts/js");
|
|
1761
|
+
if (hasSpecExt) testPatterns.push("*.spec.ts/js");
|
|
1762
|
+
if (hasTestsDir) testPatterns.push("__tests__/ directories");
|
|
1763
|
+
if (testPatterns.length === 0) {
|
|
1764
|
+
return ["No test files detected."];
|
|
1765
|
+
}
|
|
1766
|
+
return [`Test patterns found: ${testPatterns.join(", ")}`];
|
|
1767
|
+
}
|
|
1768
|
+
function analyzeErrorHandling(files) {
|
|
1769
|
+
const sample = files.filter((f) => CODE_EXTS.has(import_node_path17.default.extname(f))).slice(0, 20);
|
|
1770
|
+
let tryCatchCount = 0;
|
|
1771
|
+
let resultTypeCount = 0;
|
|
1772
|
+
for (const file of sample) {
|
|
1773
|
+
let content;
|
|
1774
|
+
try {
|
|
1775
|
+
content = import_node_fs16.default.readFileSync(file, "utf-8");
|
|
1776
|
+
} catch {
|
|
1777
|
+
continue;
|
|
1778
|
+
}
|
|
1779
|
+
const tryCatches = (content.match(/\btry\s*\{/g) ?? []).length;
|
|
1780
|
+
const resultTypes = (content.match(/Result<|Either</g) ?? []).length;
|
|
1781
|
+
tryCatchCount += tryCatches;
|
|
1782
|
+
resultTypeCount += resultTypes;
|
|
1783
|
+
}
|
|
1784
|
+
const results = [];
|
|
1785
|
+
if (tryCatchCount > 0 && resultTypeCount === 0) {
|
|
1786
|
+
results.push(`Error handling: try/catch dominant (${tryCatchCount} occurrences in ${sample.length} files).`);
|
|
1787
|
+
} else if (resultTypeCount > 0 && tryCatchCount === 0) {
|
|
1788
|
+
results.push(`Error handling: Result/Either type pattern dominant (${resultTypeCount} occurrences).`);
|
|
1789
|
+
} else if (tryCatchCount > 0 && resultTypeCount > 0) {
|
|
1790
|
+
const dominant = tryCatchCount >= resultTypeCount ? "try/catch" : "Result/Either";
|
|
1791
|
+
results.push(
|
|
1792
|
+
`Error handling: mixed \u2014 ${tryCatchCount} try/catch and ${resultTypeCount} Result/Either. Dominant: ${dominant}.`
|
|
1793
|
+
);
|
|
1794
|
+
} else {
|
|
1795
|
+
results.push(`No explicit error handling patterns detected in ${sample.length} sampled files.`);
|
|
1796
|
+
}
|
|
1797
|
+
return results;
|
|
1798
|
+
}
|
|
1799
|
+
function analyzeComments(files) {
|
|
1800
|
+
const sample = files.filter((f) => CODE_EXTS.has(import_node_path17.default.extname(f))).slice(0, 20);
|
|
1801
|
+
let jsdocCount = 0;
|
|
1802
|
+
let inlineCount = 0;
|
|
1803
|
+
let totalLines = 0;
|
|
1804
|
+
for (const file of sample) {
|
|
1805
|
+
let content;
|
|
1806
|
+
try {
|
|
1807
|
+
content = import_node_fs16.default.readFileSync(file, "utf-8");
|
|
1808
|
+
} catch {
|
|
1809
|
+
continue;
|
|
1810
|
+
}
|
|
1811
|
+
const lines = content.split("\n");
|
|
1812
|
+
totalLines += lines.length;
|
|
1813
|
+
jsdocCount += (content.match(/\/\*\*/g) ?? []).length;
|
|
1814
|
+
inlineCount += lines.filter((l) => /^\s*\/\//.test(l)).length;
|
|
1815
|
+
}
|
|
1816
|
+
const commentDensity = totalLines > 0 ? (jsdocCount + inlineCount) / totalLines : 0;
|
|
1817
|
+
let density;
|
|
1818
|
+
if (commentDensity > 0.15) density = "heavy";
|
|
1819
|
+
else if (commentDensity > 0.05) density = "moderate";
|
|
1820
|
+
else density = "minimal";
|
|
1821
|
+
return [
|
|
1822
|
+
`Comment density: ${density} (${jsdocCount} JSDoc blocks, ${inlineCount} inline comments across ${sample.length} files).`
|
|
1823
|
+
];
|
|
1824
|
+
}
|
|
1825
|
+
async function runScan(rootDir) {
|
|
1826
|
+
const allFiles = walkDir(rootDir);
|
|
1827
|
+
return {
|
|
1828
|
+
naming: analyzeNaming(allFiles),
|
|
1829
|
+
imports: analyzeImports(allFiles),
|
|
1830
|
+
testing: analyzeTesting(allFiles),
|
|
1831
|
+
errorHandling: analyzeErrorHandling(allFiles),
|
|
1832
|
+
comments: analyzeComments(allFiles)
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
// src/watch.ts
|
|
1837
|
+
init_writers();
|
|
1838
|
+
|
|
1839
|
+
// src/watch/daemon.ts
|
|
1840
|
+
var import_node_fs18 = __toESM(require("fs"), 1);
|
|
1841
|
+
var import_node_path19 = __toESM(require("path"), 1);
|
|
1842
|
+
init_writers();
|
|
1843
|
+
|
|
1844
|
+
// src/watch/transcripts.ts
|
|
1845
|
+
var import_node_fs17 = __toESM(require("fs"), 1);
|
|
1846
|
+
var import_node_path18 = __toESM(require("path"), 1);
|
|
1847
|
+
async function processTranscript(filePath, adapter) {
|
|
1848
|
+
let content;
|
|
1849
|
+
try {
|
|
1850
|
+
content = import_node_fs17.default.readFileSync(filePath, "utf-8");
|
|
1851
|
+
} catch {
|
|
1852
|
+
return null;
|
|
1853
|
+
}
|
|
1854
|
+
const prompt = `Extract structured information from this session summary. Return ONLY valid JSON with no markdown or explanation.
|
|
1855
|
+
|
|
1856
|
+
Session content:
|
|
1857
|
+
${content}
|
|
1858
|
+
|
|
1859
|
+
Return JSON with exactly these keys:
|
|
1860
|
+
{
|
|
1861
|
+
"decisions": ["what approach was chosen and why"],
|
|
1862
|
+
"corrections": ["what the agent got wrong and how it was fixed"],
|
|
1863
|
+
"deferredIdeas": ["ideas mentioned but not acted on"],
|
|
1864
|
+
"openThreads": ["unresolved questions or next steps"]
|
|
1865
|
+
}
|
|
1866
|
+
Each item must be a concise one-sentence string. Arrays may be empty.`;
|
|
1867
|
+
let response;
|
|
1868
|
+
try {
|
|
1869
|
+
response = await adapter.complete(prompt);
|
|
1870
|
+
} catch {
|
|
1871
|
+
return null;
|
|
1872
|
+
}
|
|
1873
|
+
try {
|
|
1874
|
+
const cleaned = response.replace(/^```(?:json)?\s*/m, "").replace(/\s*```\s*$/m, "").trim();
|
|
1875
|
+
const parsed = JSON.parse(cleaned);
|
|
1876
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1877
|
+
const obj = parsed;
|
|
1878
|
+
const toArray = (v) => {
|
|
1879
|
+
if (!Array.isArray(v)) return [];
|
|
1880
|
+
return v.filter((item) => typeof item === "string");
|
|
1881
|
+
};
|
|
1882
|
+
return {
|
|
1883
|
+
decisions: toArray(obj["decisions"]),
|
|
1884
|
+
corrections: toArray(obj["corrections"]),
|
|
1885
|
+
deferredIdeas: toArray(obj["deferredIdeas"]),
|
|
1886
|
+
openThreads: toArray(obj["openThreads"])
|
|
1887
|
+
};
|
|
1888
|
+
} catch {
|
|
1889
|
+
return null;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
function watchTranscriptDir(rootDir, onNewFile) {
|
|
1893
|
+
const sessionsDir = import_node_path18.default.join(rootDir, "tmp", "sessions");
|
|
1894
|
+
import_node_fs17.default.mkdirSync(sessionsDir, { recursive: true });
|
|
1895
|
+
const watcher = import_node_fs17.default.watch(sessionsDir, (event, filename) => {
|
|
1896
|
+
if (event !== "rename" || !filename) return;
|
|
1897
|
+
if (!filename.endsWith(".md")) return;
|
|
1898
|
+
const filePath = import_node_path18.default.join(sessionsDir, filename);
|
|
1899
|
+
if (!import_node_fs17.default.existsSync(filePath)) return;
|
|
1900
|
+
onNewFile(filePath).catch(() => {
|
|
1901
|
+
});
|
|
1902
|
+
});
|
|
1903
|
+
return () => {
|
|
1904
|
+
watcher.close();
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
// src/watch/adapters/claude-local.ts
|
|
1909
|
+
var import_node_child_process2 = require("child_process");
|
|
1910
|
+
var ClaudeLocalAdapter = class {
|
|
1911
|
+
async complete(prompt) {
|
|
1912
|
+
const escaped = prompt.replace(/'/g, "'\\''");
|
|
1913
|
+
try {
|
|
1914
|
+
const result = (0, import_node_child_process2.execSync)(`claude -p '${escaped}'`, {
|
|
1915
|
+
encoding: "utf-8",
|
|
1916
|
+
timeout: 6e4,
|
|
1917
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1918
|
+
});
|
|
1919
|
+
return result.trim();
|
|
1920
|
+
} catch (err) {
|
|
1921
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1922
|
+
if (msg.includes("ENOENT")) {
|
|
1923
|
+
throw new Error("Claude Code CLI not found. Install it or use a different adapter.");
|
|
1924
|
+
}
|
|
1925
|
+
throw err;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
};
|
|
1929
|
+
|
|
1930
|
+
// src/watch/adapters/claude-api.ts
|
|
1931
|
+
var API_URL = "https://api.anthropic.com/v1/messages";
|
|
1932
|
+
var DEFAULT_MODEL = "claude-haiku-4-5-20251001";
|
|
1933
|
+
var ClaudeApiAdapter = class {
|
|
1934
|
+
apiKey;
|
|
1935
|
+
model;
|
|
1936
|
+
constructor(apiKey, model) {
|
|
1937
|
+
this.apiKey = apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
|
|
1938
|
+
this.model = model ?? DEFAULT_MODEL;
|
|
1939
|
+
if (!this.apiKey) {
|
|
1940
|
+
throw new Error("ANTHROPIC_API_KEY not set. Set it or use a different adapter.");
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
async complete(prompt) {
|
|
1944
|
+
const response = await fetch(API_URL, {
|
|
1945
|
+
method: "POST",
|
|
1946
|
+
headers: {
|
|
1947
|
+
"x-api-key": this.apiKey,
|
|
1948
|
+
"anthropic-version": "2023-06-01",
|
|
1949
|
+
"content-type": "application/json"
|
|
1950
|
+
},
|
|
1951
|
+
body: JSON.stringify({
|
|
1952
|
+
model: this.model,
|
|
1953
|
+
max_tokens: 1024,
|
|
1954
|
+
messages: [{ role: "user", content: prompt }]
|
|
1955
|
+
})
|
|
1956
|
+
});
|
|
1957
|
+
if (!response.ok) {
|
|
1958
|
+
throw new Error(`Anthropic API error: ${response.status} ${response.statusText}`);
|
|
1959
|
+
}
|
|
1960
|
+
const data = await response.json();
|
|
1961
|
+
const text = data.content.find((c) => c.type === "text")?.text ?? "";
|
|
1962
|
+
return text.trim();
|
|
1963
|
+
}
|
|
1964
|
+
};
|
|
1965
|
+
|
|
1966
|
+
// src/watch/adapters/ollama.ts
|
|
1967
|
+
var DEFAULT_OLLAMA_URL = "http://localhost:11434/api/generate";
|
|
1968
|
+
var DEFAULT_MODEL2 = "llama3.2";
|
|
1969
|
+
var OllamaAdapter = class {
|
|
1970
|
+
model;
|
|
1971
|
+
url;
|
|
1972
|
+
constructor(model, url) {
|
|
1973
|
+
this.model = model ?? DEFAULT_MODEL2;
|
|
1974
|
+
this.url = url ?? DEFAULT_OLLAMA_URL;
|
|
1975
|
+
}
|
|
1976
|
+
async complete(prompt) {
|
|
1977
|
+
const response = await fetch(this.url, {
|
|
1978
|
+
method: "POST",
|
|
1979
|
+
headers: { "content-type": "application/json" },
|
|
1980
|
+
body: JSON.stringify({
|
|
1981
|
+
model: this.model,
|
|
1982
|
+
prompt,
|
|
1983
|
+
stream: false
|
|
1984
|
+
})
|
|
1985
|
+
});
|
|
1986
|
+
if (!response.ok) {
|
|
1987
|
+
throw new Error(`Ollama error: ${response.status}. Is Ollama running?`);
|
|
1988
|
+
}
|
|
1989
|
+
const data = await response.json();
|
|
1990
|
+
return data.response.trim();
|
|
1991
|
+
}
|
|
1992
|
+
};
|
|
1993
|
+
|
|
1994
|
+
// src/watch/adapters/codex-local.ts
|
|
1995
|
+
var import_node_child_process3 = require("child_process");
|
|
1996
|
+
var CodexLocalAdapter = class {
|
|
1997
|
+
async complete(prompt) {
|
|
1998
|
+
const escaped = prompt.replace(/'/g, "'\\''");
|
|
1999
|
+
try {
|
|
2000
|
+
const result = (0, import_node_child_process3.execSync)(`codex '${escaped}'`, {
|
|
2001
|
+
encoding: "utf-8",
|
|
2002
|
+
timeout: 6e4,
|
|
2003
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2004
|
+
});
|
|
2005
|
+
return result.trim();
|
|
2006
|
+
} catch (err) {
|
|
2007
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2008
|
+
if (msg.includes("ENOENT")) {
|
|
2009
|
+
throw new Error("Codex CLI not found. Install it or use a different adapter.");
|
|
2010
|
+
}
|
|
2011
|
+
throw err;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
};
|
|
2015
|
+
|
|
2016
|
+
// src/watch/adapters/index.ts
|
|
2017
|
+
function createAdapter(config) {
|
|
2018
|
+
const type = config.watch?.adapter ?? "claude-local";
|
|
2019
|
+
switch (type) {
|
|
2020
|
+
case "claude-local":
|
|
2021
|
+
return new ClaudeLocalAdapter();
|
|
2022
|
+
case "claude-api":
|
|
2023
|
+
return new ClaudeApiAdapter();
|
|
2024
|
+
case "ollama":
|
|
2025
|
+
return new OllamaAdapter();
|
|
2026
|
+
case "codex-local":
|
|
2027
|
+
return new CodexLocalAdapter();
|
|
2028
|
+
default:
|
|
2029
|
+
return new ClaudeLocalAdapter();
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
// 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");
|
|
2038
|
+
const cleanup = [];
|
|
2039
|
+
const shutdown = () => {
|
|
2040
|
+
cleanup.forEach((fn) => fn());
|
|
2041
|
+
clearPid(rootDir);
|
|
2042
|
+
process.exit(0);
|
|
2043
|
+
};
|
|
2044
|
+
process.on("SIGTERM", shutdown);
|
|
2045
|
+
process.on("SIGINT", shutdown);
|
|
2046
|
+
log("[clawstrap watch] daemon started");
|
|
2047
|
+
const sinceCommit = config.watchState?.lastGitCommit ?? null;
|
|
2048
|
+
const gitResult = await runGitObserver(rootDir, sinceCommit);
|
|
2049
|
+
if (gitResult) {
|
|
2050
|
+
updateWatchState(rootDir, { lastGitCommit: gitResult.lastCommit });
|
|
2051
|
+
log(`[clawstrap watch] git: ${gitResult.entriesWritten} entries written`);
|
|
2052
|
+
}
|
|
2053
|
+
const adapter = createAdapter(config);
|
|
2054
|
+
const stopTranscripts = watchTranscriptDir(rootDir, async (filePath) => {
|
|
2055
|
+
log(`[clawstrap watch] transcript: processing ${import_node_path19.default.basename(filePath)}`);
|
|
2056
|
+
const result = await processTranscript(filePath, adapter);
|
|
2057
|
+
if (result) {
|
|
2058
|
+
const { appendToMemory: appendToMemory2, appendToGotchaLog: appendToGotchaLog2, appendToFutureConsiderations: appendToFutureConsiderations2 } = await Promise.resolve().then(() => (init_writers(), writers_exports));
|
|
2059
|
+
if (result.decisions.length) appendToMemory2(rootDir, result.decisions, "session");
|
|
2060
|
+
if (result.corrections.length) appendToGotchaLog2(rootDir, result.corrections);
|
|
2061
|
+
if (result.deferredIdeas.length) appendToFutureConsiderations2(rootDir, result.deferredIdeas);
|
|
2062
|
+
updateWatchState(rootDir, { lastTranscriptAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2063
|
+
log(
|
|
2064
|
+
`[clawstrap watch] transcript: decisions=${result.decisions.length} corrections=${result.corrections.length}`
|
|
2065
|
+
);
|
|
2066
|
+
}
|
|
2067
|
+
});
|
|
2068
|
+
cleanup.push(stopTranscripts);
|
|
2069
|
+
const intervalDays = config.watch?.scan?.intervalDays ?? 7;
|
|
2070
|
+
const lastScan = config.watchState?.lastScanAt ? new Date(config.watchState.lastScanAt) : null;
|
|
2071
|
+
const msSinceLastScan = lastScan ? Date.now() - lastScan.getTime() : Infinity;
|
|
2072
|
+
const scanIntervalMs = intervalDays * 24 * 60 * 60 * 1e3;
|
|
2073
|
+
const doScan = async () => {
|
|
2074
|
+
log("[clawstrap watch] scan: running convention scan...");
|
|
2075
|
+
const sections = await runScan(rootDir);
|
|
2076
|
+
writeConventions(rootDir, sections);
|
|
2077
|
+
updateWatchState(rootDir, { lastScanAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2078
|
+
log("[clawstrap watch] scan: conventions.md updated");
|
|
2079
|
+
};
|
|
2080
|
+
if (msSinceLastScan >= scanIntervalMs) {
|
|
2081
|
+
await doScan();
|
|
2082
|
+
}
|
|
2083
|
+
const scanTimer = setInterval(doScan, scanIntervalMs);
|
|
2084
|
+
cleanup.push(() => clearInterval(scanTimer));
|
|
2085
|
+
log("[clawstrap watch] watching for changes...");
|
|
2086
|
+
await new Promise(() => {
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
function updateWatchState(rootDir, updates) {
|
|
2090
|
+
const configPath = import_node_path19.default.join(rootDir, ".clawstrap.json");
|
|
2091
|
+
try {
|
|
2092
|
+
const raw = JSON.parse(import_node_fs18.default.readFileSync(configPath, "utf-8"));
|
|
2093
|
+
raw["watchState"] = { ...raw["watchState"] ?? {}, ...updates };
|
|
2094
|
+
import_node_fs18.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
|
|
2095
|
+
} catch {
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
// src/watch.ts
|
|
2100
|
+
async function watch(options) {
|
|
2101
|
+
const { config, rootDir } = loadWorkspace();
|
|
2102
|
+
if (options._daemon) {
|
|
2103
|
+
await runDaemon(rootDir, config);
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2106
|
+
if (options.stop) {
|
|
2107
|
+
const pid = readPid(rootDir);
|
|
2108
|
+
if (!pid || !isDaemonRunning(rootDir)) {
|
|
2109
|
+
console.log("\nNo daemon running.\n");
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
2112
|
+
process.kill(pid, "SIGTERM");
|
|
2113
|
+
clearPid(rootDir);
|
|
2114
|
+
console.log(`
|
|
2115
|
+
Daemon stopped (pid ${pid}).
|
|
2116
|
+
`);
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
if (options.once) {
|
|
2120
|
+
console.log("\nRunning all observers once...\n");
|
|
2121
|
+
const gitResult = await runGitObserver(rootDir, config.watchState?.lastGitCommit ?? null);
|
|
2122
|
+
if (gitResult) {
|
|
2123
|
+
persistWatchState(rootDir, { lastGitCommit: gitResult.lastCommit });
|
|
2124
|
+
console.log(` \u2713 git: ${gitResult.entriesWritten} entries`);
|
|
2125
|
+
}
|
|
2126
|
+
const sections = await runScan(rootDir);
|
|
2127
|
+
writeConventions(rootDir, sections);
|
|
2128
|
+
persistWatchState(rootDir, { lastScanAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2129
|
+
console.log(" \u2713 scan: conventions.md updated");
|
|
2130
|
+
console.log("\nDone.\n");
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
if (isDaemonRunning(rootDir)) {
|
|
2134
|
+
const pid = readPid(rootDir);
|
|
2135
|
+
console.log(`
|
|
2136
|
+
Daemon already running (pid ${pid}). Use --stop to stop it.
|
|
2137
|
+
`);
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
injectWatchHook(rootDir, config);
|
|
2141
|
+
const self = process.argv[1];
|
|
2142
|
+
const child = (0, import_node_child_process4.spawn)(process.execPath, [self, "watch", "--_daemon"], {
|
|
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
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
function persistWatchState(rootDir, updates) {
|
|
2162
|
+
const configPath = import_node_path20.default.join(rootDir, ".clawstrap.json");
|
|
2163
|
+
try {
|
|
2164
|
+
const raw = JSON.parse(import_node_fs19.default.readFileSync(configPath, "utf-8"));
|
|
2165
|
+
raw.watchState = { ...raw.watchState ?? {}, ...updates };
|
|
2166
|
+
import_node_fs19.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
|
|
2167
|
+
} catch {
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
function injectWatchHook(rootDir, config) {
|
|
2171
|
+
const governanceFile = import_node_path20.default.join(rootDir, "CLAUDE.md");
|
|
2172
|
+
if (!import_node_fs19.default.existsSync(governanceFile)) return;
|
|
2173
|
+
const content = import_node_fs19.default.readFileSync(governanceFile, "utf-8");
|
|
2174
|
+
if (content.includes("<!-- CLAWSTRAP:WATCH -->")) return;
|
|
2175
|
+
const _config = config;
|
|
2176
|
+
void _config;
|
|
2177
|
+
const hook = `
|
|
2178
|
+
<!-- CLAWSTRAP:WATCH -->
|
|
2179
|
+
## Session Watch Hook
|
|
2180
|
+
|
|
2181
|
+
\`clawstrap watch\` is active. At every session end, write a session summary to
|
|
2182
|
+
\`tmp/sessions/YYYY-MM-DD-HHmm.md\` using this format:
|
|
2183
|
+
|
|
2184
|
+
\`\`\`
|
|
2185
|
+
## Decisions
|
|
2186
|
+
- [what approach was chosen and why]
|
|
2187
|
+
|
|
2188
|
+
## Corrections
|
|
2189
|
+
- [what the agent got wrong and how it was fixed]
|
|
2190
|
+
|
|
2191
|
+
## Deferred Ideas
|
|
2192
|
+
- [mentioned but not acted on]
|
|
2193
|
+
|
|
2194
|
+
## Open Threads
|
|
2195
|
+
- [unresolved questions or next steps]
|
|
2196
|
+
\`\`\`
|
|
2197
|
+
|
|
2198
|
+
The watch daemon picks this up automatically and updates MEMORY.md and gotcha-log.md.
|
|
2199
|
+
`;
|
|
2200
|
+
import_node_fs19.default.appendFileSync(governanceFile, hook, "utf-8");
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
// src/analyze.ts
|
|
2204
|
+
var import_node_fs20 = __toESM(require("fs"), 1);
|
|
2205
|
+
var import_node_path21 = __toESM(require("path"), 1);
|
|
2206
|
+
init_writers();
|
|
2207
|
+
async function analyze() {
|
|
2208
|
+
const { rootDir } = loadWorkspace();
|
|
2209
|
+
console.log("\nScanning codebase conventions...\n");
|
|
2210
|
+
const sections = await runScan(rootDir);
|
|
2211
|
+
writeConventions(rootDir, sections);
|
|
2212
|
+
const configPath = import_node_path21.default.join(rootDir, ".clawstrap.json");
|
|
2213
|
+
try {
|
|
2214
|
+
const raw = JSON.parse(import_node_fs20.default.readFileSync(configPath, "utf-8"));
|
|
2215
|
+
raw["watchState"] = { ...raw["watchState"] ?? {}, lastScanAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2216
|
+
import_node_fs20.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
|
|
2217
|
+
} catch {
|
|
2218
|
+
}
|
|
2219
|
+
console.log(" \u2713 .claude/rules/conventions.md updated\n");
|
|
2220
|
+
}
|
|
2221
|
+
|
|
1231
2222
|
// src/index.ts
|
|
1232
2223
|
var program = new import_commander.Command();
|
|
1233
|
-
program.name("clawstrap").description("Scaffold a production-ready AI agent workspace").version("1.
|
|
2224
|
+
program.name("clawstrap").description("Scaffold a production-ready AI agent workspace").version("1.4.0");
|
|
1234
2225
|
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) => {
|
|
1235
2226
|
await init(directory, options);
|
|
1236
2227
|
});
|
|
@@ -1258,4 +2249,10 @@ Unknown format: ${options.format}. Supported: paperclip
|
|
|
1258
2249
|
}
|
|
1259
2250
|
await exportPaperclip(options);
|
|
1260
2251
|
});
|
|
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)").option("--_daemon", void 0).action(async (options) => {
|
|
2253
|
+
await watch(options);
|
|
2254
|
+
});
|
|
2255
|
+
program.command("analyze").description("Run codebase convention scan immediately").action(async () => {
|
|
2256
|
+
await analyze();
|
|
2257
|
+
});
|
|
1261
2258
|
program.parse();
|