cclaw-cli 0.1.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/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +101 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +70 -0
- package/dist/constants.d.ts +12 -0
- package/dist/constants.js +50 -0
- package/dist/content/agents.d.ts +39 -0
- package/dist/content/agents.js +244 -0
- package/dist/content/autoplan.d.ts +7 -0
- package/dist/content/autoplan.js +297 -0
- package/dist/content/contracts.d.ts +2 -0
- package/dist/content/contracts.js +50 -0
- package/dist/content/examples.d.ts +2 -0
- package/dist/content/examples.js +327 -0
- package/dist/content/hooks.d.ts +16 -0
- package/dist/content/hooks.js +753 -0
- package/dist/content/learnings.d.ts +5 -0
- package/dist/content/learnings.js +265 -0
- package/dist/content/meta-skill.d.ts +10 -0
- package/dist/content/meta-skill.js +137 -0
- package/dist/content/observe.d.ts +21 -0
- package/dist/content/observe.js +1110 -0
- package/dist/content/session-hooks.d.ts +7 -0
- package/dist/content/session-hooks.js +137 -0
- package/dist/content/skills.d.ts +3 -0
- package/dist/content/skills.js +257 -0
- package/dist/content/stage-schema.d.ts +78 -0
- package/dist/content/stage-schema.js +1453 -0
- package/dist/content/subagents.d.ts +13 -0
- package/dist/content/subagents.js +616 -0
- package/dist/content/templates.d.ts +3 -0
- package/dist/content/templates.js +272 -0
- package/dist/content/utility-skills.d.ts +12 -0
- package/dist/content/utility-skills.js +467 -0
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +610 -0
- package/dist/flow-state.d.ts +19 -0
- package/dist/flow-state.js +41 -0
- package/dist/fs-utils.d.ts +5 -0
- package/dist/fs-utils.js +28 -0
- package/dist/gitignore.d.ts +3 -0
- package/dist/gitignore.js +43 -0
- package/dist/harness-adapters.d.ts +12 -0
- package/dist/harness-adapters.js +175 -0
- package/dist/install.d.ts +9 -0
- package/dist/install.js +562 -0
- package/dist/learnings-summarizer.d.ts +25 -0
- package/dist/learnings-summarizer.js +201 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.js +6 -0
- package/dist/policy.d.ts +6 -0
- package/dist/policy.js +179 -0
- package/dist/runs.d.ts +18 -0
- package/dist/runs.js +446 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.js +12 -0
- package/package.json +47 -0
package/dist/install.js
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { COMMAND_FILE_ORDER, REQUIRED_DIRS, RUNTIME_ROOT, UTILITY_COMMANDS } from "./constants.js";
|
|
4
|
+
import { writeConfig, createDefaultConfig, readConfig, configPath } from "./config.js";
|
|
5
|
+
import { commandContract } from "./content/contracts.js";
|
|
6
|
+
import { autoplanSkillMarkdown, autoplanCommandContract } from "./content/autoplan.js";
|
|
7
|
+
import { learnSkillMarkdown, learnCommandContract } from "./content/learnings.js";
|
|
8
|
+
import { subagentDrivenDevSkill, parallelAgentsSkill } from "./content/subagents.js";
|
|
9
|
+
import { sessionHooksSkillMarkdown } from "./content/session-hooks.js";
|
|
10
|
+
import { sessionStartScript, stopCheckpointScript, opencodePluginJs, claudeHooksJson, cursorHooksJson, codexHooksJson } from "./content/hooks.js";
|
|
11
|
+
import { contextMonitorScript, observeScript, promptGuardScript, summarizeObservationsRuntimeModule, summarizeObservationsScript } from "./content/observe.js";
|
|
12
|
+
import { META_SKILL_NAME, usingCclawSkillMarkdown } from "./content/meta-skill.js";
|
|
13
|
+
import { ARTIFACT_TEMPLATES, RULEBOOK_MARKDOWN, buildRulesJson } from "./content/templates.js";
|
|
14
|
+
import { stageSkillFolder, stageSkillMarkdown } from "./content/skills.js";
|
|
15
|
+
import { UTILITY_SKILL_FOLDERS, UTILITY_SKILL_MAP } from "./content/utility-skills.js";
|
|
16
|
+
import { createInitialFlowState } from "./flow-state.js";
|
|
17
|
+
import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
|
|
18
|
+
import { ensureGitignore, removeGitignorePatterns } from "./gitignore.js";
|
|
19
|
+
import { HARNESS_ADAPTERS, syncHarnessShims, removeCclawFromAgentsMd } from "./harness-adapters.js";
|
|
20
|
+
import { ensureRunSystem, readFlowState } from "./runs.js";
|
|
21
|
+
function runtimePath(projectRoot, ...segments) {
|
|
22
|
+
return path.join(projectRoot, RUNTIME_ROOT, ...segments);
|
|
23
|
+
}
|
|
24
|
+
async function ensureStructure(projectRoot) {
|
|
25
|
+
for (const dir of REQUIRED_DIRS) {
|
|
26
|
+
await ensureDir(path.join(projectRoot, dir));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function writeCommandContracts(projectRoot) {
|
|
30
|
+
for (const stage of COMMAND_FILE_ORDER) {
|
|
31
|
+
await writeFileSafe(runtimePath(projectRoot, "commands", `${stage}.md`), commandContract(stage));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function writeArtifactTemplates(projectRoot) {
|
|
35
|
+
for (const [fileName, content] of Object.entries(ARTIFACT_TEMPLATES)) {
|
|
36
|
+
await writeFileSafe(runtimePath(projectRoot, "templates", fileName), content);
|
|
37
|
+
const artifactPath = runtimePath(projectRoot, "artifacts", fileName);
|
|
38
|
+
if (!(await exists(artifactPath))) {
|
|
39
|
+
await writeFileSafe(artifactPath, content);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function writeSkills(projectRoot) {
|
|
44
|
+
for (const stage of COMMAND_FILE_ORDER) {
|
|
45
|
+
const folder = stageSkillFolder(stage);
|
|
46
|
+
await writeFileSafe(runtimePath(projectRoot, "skills", folder, "SKILL.md"), stageSkillMarkdown(stage));
|
|
47
|
+
}
|
|
48
|
+
// Utility skills (not flow stages)
|
|
49
|
+
await writeFileSafe(runtimePath(projectRoot, "skills", "learnings", "SKILL.md"), learnSkillMarkdown());
|
|
50
|
+
await writeFileSafe(runtimePath(projectRoot, "skills", "autoplan", "SKILL.md"), autoplanSkillMarkdown());
|
|
51
|
+
await writeFileSafe(runtimePath(projectRoot, "skills", "subagent-dev", "SKILL.md"), subagentDrivenDevSkill());
|
|
52
|
+
await writeFileSafe(runtimePath(projectRoot, "skills", "parallel-dispatch", "SKILL.md"), parallelAgentsSkill());
|
|
53
|
+
await writeFileSafe(runtimePath(projectRoot, "skills", "session", "SKILL.md"), sessionHooksSkillMarkdown());
|
|
54
|
+
await writeFileSafe(runtimePath(projectRoot, "skills", META_SKILL_NAME, "SKILL.md"), usingCclawSkillMarkdown());
|
|
55
|
+
for (const folder of UTILITY_SKILL_FOLDERS) {
|
|
56
|
+
const generator = UTILITY_SKILL_MAP[folder];
|
|
57
|
+
await writeFileSafe(runtimePath(projectRoot, "skills", folder, "SKILL.md"), generator());
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function writeUtilityCommands(projectRoot) {
|
|
61
|
+
await writeFileSafe(runtimePath(projectRoot, "commands", "learn.md"), learnCommandContract());
|
|
62
|
+
await writeFileSafe(runtimePath(projectRoot, "commands", "autoplan.md"), autoplanCommandContract());
|
|
63
|
+
}
|
|
64
|
+
function toObject(value) {
|
|
65
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Removes // and /* *\/ comments only outside JSON strings (double-quoted).
|
|
72
|
+
* Used for recovering user-edited hook JSON without corrupting string contents.
|
|
73
|
+
*/
|
|
74
|
+
function stripJsonCommentsOutsideStrings(input) {
|
|
75
|
+
let out = "";
|
|
76
|
+
let i = 0;
|
|
77
|
+
let inString = false;
|
|
78
|
+
let escape = false;
|
|
79
|
+
while (i < input.length) {
|
|
80
|
+
const c = input[i];
|
|
81
|
+
if (inString) {
|
|
82
|
+
out += c;
|
|
83
|
+
if (escape) {
|
|
84
|
+
escape = false;
|
|
85
|
+
}
|
|
86
|
+
else if (c === "\\") {
|
|
87
|
+
escape = true;
|
|
88
|
+
}
|
|
89
|
+
else if (c === '"') {
|
|
90
|
+
inString = false;
|
|
91
|
+
}
|
|
92
|
+
i += 1;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (c === '"') {
|
|
96
|
+
inString = true;
|
|
97
|
+
out += c;
|
|
98
|
+
i += 1;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const next = input[i + 1];
|
|
102
|
+
if (c === "/" && next === "/") {
|
|
103
|
+
while (i < input.length && input[i] !== "\n" && input[i] !== "\r")
|
|
104
|
+
i += 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (c === "/" && next === "*") {
|
|
108
|
+
i += 2;
|
|
109
|
+
while (i < input.length - 1 && !(input[i] === "*" && input[i + 1] === "/"))
|
|
110
|
+
i += 1;
|
|
111
|
+
i = Math.min(i + 2, input.length);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
out += c;
|
|
115
|
+
i += 1;
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
function normalizeJsonLike(raw) {
|
|
120
|
+
return stripJsonCommentsOutsideStrings(raw).replace(/,\s*([}\]])/gu, "$1");
|
|
121
|
+
}
|
|
122
|
+
function tryParseHookDocument(raw) {
|
|
123
|
+
try {
|
|
124
|
+
return { parsed: JSON.parse(raw), recovered: false };
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// continue with relaxed parse
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
return { parsed: JSON.parse(normalizeJsonLike(raw)), recovered: true };
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function backupFileNameForHook(projectRoot, hookFilePath) {
|
|
137
|
+
const rel = path.relative(projectRoot, hookFilePath).replace(/[\\/]/gu, "__");
|
|
138
|
+
const ts = new Date().toISOString().replace(/[:.]/gu, "-");
|
|
139
|
+
return `${rel}.${ts}.bak`;
|
|
140
|
+
}
|
|
141
|
+
async function pruneOldHookBackups(backupsDir, maxBackups = 20) {
|
|
142
|
+
let entries = [];
|
|
143
|
+
try {
|
|
144
|
+
entries = await fs.readdir(backupsDir);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
entries = [];
|
|
148
|
+
}
|
|
149
|
+
if (entries.length <= maxBackups)
|
|
150
|
+
return;
|
|
151
|
+
const withStats = await Promise.all(entries.map(async (entry) => {
|
|
152
|
+
const fullPath = path.join(backupsDir, entry);
|
|
153
|
+
const stat = await fs.stat(fullPath);
|
|
154
|
+
return { fullPath, mtimeMs: stat.mtimeMs };
|
|
155
|
+
}));
|
|
156
|
+
withStats.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
157
|
+
const stale = withStats.slice(maxBackups);
|
|
158
|
+
await Promise.all(stale.map(async (item) => {
|
|
159
|
+
await fs.rm(item.fullPath, { force: true });
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
async function backupHookFile(projectRoot, hookFilePath, rawContent) {
|
|
163
|
+
const backupsDir = runtimePath(projectRoot, "backups", "hooks");
|
|
164
|
+
await ensureDir(backupsDir);
|
|
165
|
+
const fileName = backupFileNameForHook(projectRoot, hookFilePath);
|
|
166
|
+
const backupPath = path.join(backupsDir, fileName);
|
|
167
|
+
await writeFileSafe(backupPath, rawContent);
|
|
168
|
+
await pruneOldHookBackups(backupsDir);
|
|
169
|
+
return backupPath;
|
|
170
|
+
}
|
|
171
|
+
function mergeHookDocuments(existingDoc, generatedDoc) {
|
|
172
|
+
const generatedRoot = toObject(generatedDoc) ?? {};
|
|
173
|
+
const generatedHooks = toObject(generatedRoot.hooks) ?? {};
|
|
174
|
+
const strippedExisting = stripManagedHookCommands(existingDoc).updated;
|
|
175
|
+
const existingRoot = toObject(strippedExisting) ?? {};
|
|
176
|
+
const existingHooks = toObject(existingRoot.hooks) ?? {};
|
|
177
|
+
const mergedHooks = { ...existingHooks };
|
|
178
|
+
for (const [eventName, generatedEntries] of Object.entries(generatedHooks)) {
|
|
179
|
+
const existingEntries = existingHooks[eventName];
|
|
180
|
+
if (Array.isArray(generatedEntries)) {
|
|
181
|
+
const preservedEntries = Array.isArray(existingEntries) ? existingEntries : [];
|
|
182
|
+
mergedHooks[eventName] = [...generatedEntries, ...preservedEntries];
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
// Defensive: malformed generated event payload must not wipe user hooks.
|
|
186
|
+
if (Array.isArray(existingEntries)) {
|
|
187
|
+
mergedHooks[eventName] = existingEntries;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
mergedHooks[eventName] = generatedEntries;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const mergedRoot = {
|
|
194
|
+
...existingRoot,
|
|
195
|
+
hooks: mergedHooks
|
|
196
|
+
};
|
|
197
|
+
for (const [key, value] of Object.entries(generatedRoot)) {
|
|
198
|
+
if (key === "hooks")
|
|
199
|
+
continue;
|
|
200
|
+
if (!(key in mergedRoot)) {
|
|
201
|
+
mergedRoot[key] = value;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return mergedRoot;
|
|
205
|
+
}
|
|
206
|
+
async function writeMergedHookJson(projectRoot, hookFilePath, generatedJson) {
|
|
207
|
+
let existingDoc = {};
|
|
208
|
+
if (await exists(hookFilePath)) {
|
|
209
|
+
try {
|
|
210
|
+
const raw = await fs.readFile(hookFilePath, "utf8");
|
|
211
|
+
const parsed = tryParseHookDocument(raw);
|
|
212
|
+
if (parsed) {
|
|
213
|
+
existingDoc = parsed.parsed;
|
|
214
|
+
if (parsed.recovered) {
|
|
215
|
+
await backupHookFile(projectRoot, hookFilePath, raw);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
await backupHookFile(projectRoot, hookFilePath, raw);
|
|
220
|
+
existingDoc = {};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
existingDoc = {};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const generatedDoc = JSON.parse(generatedJson);
|
|
228
|
+
const mergedDoc = mergeHookDocuments(existingDoc, generatedDoc);
|
|
229
|
+
await writeFileSafe(hookFilePath, `${JSON.stringify(mergedDoc, null, 2)}\n`);
|
|
230
|
+
}
|
|
231
|
+
async function writeHooks(projectRoot, harnesses) {
|
|
232
|
+
const hooksDir = runtimePath(projectRoot, "hooks");
|
|
233
|
+
await ensureDir(hooksDir);
|
|
234
|
+
await writeFileSafe(path.join(hooksDir, "session-start.sh"), sessionStartScript());
|
|
235
|
+
await writeFileSafe(path.join(hooksDir, "stop-checkpoint.sh"), stopCheckpointScript());
|
|
236
|
+
await writeFileSafe(path.join(hooksDir, "prompt-guard.sh"), promptGuardScript());
|
|
237
|
+
await writeFileSafe(path.join(hooksDir, "context-monitor.sh"), contextMonitorScript());
|
|
238
|
+
await writeFileSafe(path.join(hooksDir, "observe.sh"), observeScript());
|
|
239
|
+
await writeFileSafe(path.join(hooksDir, "summarize-observations.sh"), summarizeObservationsScript());
|
|
240
|
+
await writeFileSafe(path.join(hooksDir, "summarize-observations.mjs"), summarizeObservationsRuntimeModule());
|
|
241
|
+
await writeFileSafe(path.join(hooksDir, "opencode-plugin.mjs"), opencodePluginJs());
|
|
242
|
+
try {
|
|
243
|
+
for (const script of [
|
|
244
|
+
"session-start.sh",
|
|
245
|
+
"stop-checkpoint.sh",
|
|
246
|
+
"prompt-guard.sh",
|
|
247
|
+
"context-monitor.sh",
|
|
248
|
+
"observe.sh",
|
|
249
|
+
"summarize-observations.sh",
|
|
250
|
+
"summarize-observations.mjs",
|
|
251
|
+
"opencode-plugin.mjs"
|
|
252
|
+
]) {
|
|
253
|
+
await fs.chmod(path.join(hooksDir, script), 0o755);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// chmod may fail on some filesystems
|
|
258
|
+
}
|
|
259
|
+
for (const harness of harnesses) {
|
|
260
|
+
if (harness === "claude") {
|
|
261
|
+
const dir = path.join(projectRoot, ".claude/hooks");
|
|
262
|
+
await ensureDir(dir);
|
|
263
|
+
await writeMergedHookJson(projectRoot, path.join(dir, "hooks.json"), claudeHooksJson());
|
|
264
|
+
}
|
|
265
|
+
else if (harness === "cursor") {
|
|
266
|
+
const cursorDir = path.join(projectRoot, ".cursor");
|
|
267
|
+
await ensureDir(cursorDir);
|
|
268
|
+
await writeMergedHookJson(projectRoot, path.join(cursorDir, "hooks.json"), cursorHooksJson());
|
|
269
|
+
}
|
|
270
|
+
else if (harness === "codex") {
|
|
271
|
+
const dir = path.join(projectRoot, ".codex");
|
|
272
|
+
await ensureDir(dir);
|
|
273
|
+
await writeMergedHookJson(projectRoot, path.join(dir, "hooks.json"), codexHooksJson());
|
|
274
|
+
}
|
|
275
|
+
// OpenCode: plugin.mjs is in .cclaw/hooks/ — user registers in opencode.json
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async function ensureLearningsStore(projectRoot) {
|
|
279
|
+
const storePath = runtimePath(projectRoot, "learnings.jsonl");
|
|
280
|
+
if (!(await exists(storePath))) {
|
|
281
|
+
await writeFileSafe(storePath, "");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async function ensureSessionStateFiles(projectRoot) {
|
|
285
|
+
const stateDir = runtimePath(projectRoot, "state");
|
|
286
|
+
await ensureDir(stateDir);
|
|
287
|
+
const activityPath = path.join(stateDir, "stage-activity.jsonl");
|
|
288
|
+
if (!(await exists(activityPath))) {
|
|
289
|
+
await writeFileSafe(activityPath, "");
|
|
290
|
+
}
|
|
291
|
+
const checkpointPath = path.join(stateDir, "checkpoint.json");
|
|
292
|
+
if (!(await exists(checkpointPath))) {
|
|
293
|
+
const flow = await readFlowState(projectRoot);
|
|
294
|
+
const initialCheckpoint = {
|
|
295
|
+
stage: flow.currentStage,
|
|
296
|
+
runId: flow.activeRunId,
|
|
297
|
+
status: "not_started",
|
|
298
|
+
lastCompletedStep: "",
|
|
299
|
+
remainingSteps: [],
|
|
300
|
+
blockers: [],
|
|
301
|
+
timestamp: new Date().toISOString()
|
|
302
|
+
};
|
|
303
|
+
await writeFileSafe(checkpointPath, `${JSON.stringify(initialCheckpoint, null, 2)}\n`);
|
|
304
|
+
}
|
|
305
|
+
const suggestionMemoryPath = path.join(stateDir, "suggestion-memory.json");
|
|
306
|
+
if (!(await exists(suggestionMemoryPath))) {
|
|
307
|
+
const suggestionMemory = {
|
|
308
|
+
enabled: true,
|
|
309
|
+
mutedStages: [],
|
|
310
|
+
lastSuggestedStage: "",
|
|
311
|
+
lastSuggestedAt: ""
|
|
312
|
+
};
|
|
313
|
+
await writeFileSafe(suggestionMemoryPath, `${JSON.stringify(suggestionMemory, null, 2)}\n`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async function writeRulebook(projectRoot) {
|
|
317
|
+
await writeFileSafe(runtimePath(projectRoot, "rules", "RULES.md"), RULEBOOK_MARKDOWN);
|
|
318
|
+
await writeFileSafe(runtimePath(projectRoot, "rules", "rules.json"), `${JSON.stringify(buildRulesJson(), null, 2)}\n`);
|
|
319
|
+
}
|
|
320
|
+
async function writeState(projectRoot, forceReset = false) {
|
|
321
|
+
const statePath = runtimePath(projectRoot, "state", "flow-state.json");
|
|
322
|
+
if (!forceReset && (await exists(statePath))) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const state = createInitialFlowState();
|
|
326
|
+
await writeFileSafe(statePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
327
|
+
}
|
|
328
|
+
async function writeAdapterManifest(projectRoot, harnesses) {
|
|
329
|
+
const manifest = {
|
|
330
|
+
generatedAt: new Date().toISOString(),
|
|
331
|
+
harnesses,
|
|
332
|
+
commandSource: `${RUNTIME_ROOT}/commands/*.md`,
|
|
333
|
+
skillSource: `${RUNTIME_ROOT}/skills/*/SKILL.md`
|
|
334
|
+
};
|
|
335
|
+
await writeFileSafe(runtimePath(projectRoot, "adapters", "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
336
|
+
}
|
|
337
|
+
async function cleanLegacyArtifacts(projectRoot) {
|
|
338
|
+
// Remove deprecated utility skill folders from older releases.
|
|
339
|
+
for (const legacyFolder of [
|
|
340
|
+
"project-learnings",
|
|
341
|
+
"auto-orchestration",
|
|
342
|
+
"subagent-driven-development",
|
|
343
|
+
"dispatching-parallel-agents",
|
|
344
|
+
"session-guidelines",
|
|
345
|
+
"security-review",
|
|
346
|
+
"documentation",
|
|
347
|
+
"browser-qa-testing"
|
|
348
|
+
]) {
|
|
349
|
+
try {
|
|
350
|
+
await fs.rm(runtimePath(projectRoot, "skills", legacyFolder), {
|
|
351
|
+
recursive: true,
|
|
352
|
+
force: true
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
// best-effort cleanup
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// Remove legacy duplicate security agent file when present.
|
|
360
|
+
try {
|
|
361
|
+
await fs.rm(runtimePath(projectRoot, "agents", "securityer.md"), { force: true });
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
// best-effort cleanup
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async function cleanStaleFiles(projectRoot) {
|
|
368
|
+
const expectedShimFiles = new Set([
|
|
369
|
+
...COMMAND_FILE_ORDER.map((stage) => `cc-${stage}.md`),
|
|
370
|
+
...UTILITY_COMMANDS.map((cmd) => `cc-${cmd}.md`)
|
|
371
|
+
]);
|
|
372
|
+
for (const adapter of Object.values(HARNESS_ADAPTERS)) {
|
|
373
|
+
const commandDir = path.join(projectRoot, adapter.commandDir);
|
|
374
|
+
if (!(await exists(commandDir)))
|
|
375
|
+
continue;
|
|
376
|
+
let entries = [];
|
|
377
|
+
try {
|
|
378
|
+
entries = await fs.readdir(commandDir);
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
entries = [];
|
|
382
|
+
}
|
|
383
|
+
for (const entry of entries) {
|
|
384
|
+
if (!/^cc-.*\.md$/u.test(entry))
|
|
385
|
+
continue;
|
|
386
|
+
if (expectedShimFiles.has(entry))
|
|
387
|
+
continue;
|
|
388
|
+
await fs.rm(path.join(commandDir, entry), { force: true });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// Keep user-owned custom assets under .cclaw/agents and .cclaw/skills.
|
|
392
|
+
// Legacy managed removals happen in cleanLegacyArtifacts() with explicit paths.
|
|
393
|
+
}
|
|
394
|
+
async function materializeRuntime(projectRoot, harnesses, forceStateReset) {
|
|
395
|
+
await ensureStructure(projectRoot);
|
|
396
|
+
await cleanLegacyArtifacts(projectRoot);
|
|
397
|
+
await cleanStaleFiles(projectRoot);
|
|
398
|
+
await writeCommandContracts(projectRoot);
|
|
399
|
+
await writeUtilityCommands(projectRoot);
|
|
400
|
+
await writeSkills(projectRoot);
|
|
401
|
+
await writeArtifactTemplates(projectRoot);
|
|
402
|
+
await writeRulebook(projectRoot);
|
|
403
|
+
await writeState(projectRoot, forceStateReset);
|
|
404
|
+
await ensureRunSystem(projectRoot);
|
|
405
|
+
await ensureSessionStateFiles(projectRoot);
|
|
406
|
+
await writeAdapterManifest(projectRoot, harnesses);
|
|
407
|
+
await ensureLearningsStore(projectRoot);
|
|
408
|
+
await writeHooks(projectRoot, harnesses);
|
|
409
|
+
await syncHarnessShims(projectRoot, harnesses);
|
|
410
|
+
await ensureGitignore(projectRoot);
|
|
411
|
+
}
|
|
412
|
+
export async function initCclaw(options) {
|
|
413
|
+
const config = createDefaultConfig(options.harnesses);
|
|
414
|
+
await writeConfig(options.projectRoot, config);
|
|
415
|
+
await materializeRuntime(options.projectRoot, config.harnesses, true);
|
|
416
|
+
}
|
|
417
|
+
export async function syncCclaw(projectRoot) {
|
|
418
|
+
const config = await readConfig(projectRoot);
|
|
419
|
+
if (!(await exists(configPath(projectRoot)))) {
|
|
420
|
+
await writeConfig(projectRoot, createDefaultConfig(config.harnesses));
|
|
421
|
+
}
|
|
422
|
+
await materializeRuntime(projectRoot, config.harnesses, false);
|
|
423
|
+
}
|
|
424
|
+
export async function upgradeCclaw(projectRoot) {
|
|
425
|
+
const config = await readConfig(projectRoot);
|
|
426
|
+
const upgradedConfig = createDefaultConfig(config.harnesses);
|
|
427
|
+
await writeConfig(projectRoot, upgradedConfig);
|
|
428
|
+
await materializeRuntime(projectRoot, upgradedConfig.harnesses, false);
|
|
429
|
+
}
|
|
430
|
+
function stripManagedHookCommands(value) {
|
|
431
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
432
|
+
return { updated: value, changed: false };
|
|
433
|
+
}
|
|
434
|
+
const root = { ...value };
|
|
435
|
+
const hooks = root.hooks;
|
|
436
|
+
if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
|
|
437
|
+
return { updated: root, changed: false };
|
|
438
|
+
}
|
|
439
|
+
let changed = false;
|
|
440
|
+
const cleanedHooks = {};
|
|
441
|
+
for (const [eventName, entries] of Object.entries(hooks)) {
|
|
442
|
+
if (!Array.isArray(entries)) {
|
|
443
|
+
cleanedHooks[eventName] = entries;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
const cleanedEntries = entries.flatMap((entry) => {
|
|
447
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
448
|
+
return [entry];
|
|
449
|
+
}
|
|
450
|
+
const obj = entry;
|
|
451
|
+
if (typeof obj.command === "string" && isManagedRuntimeHookCommand(obj.command)) {
|
|
452
|
+
changed = true;
|
|
453
|
+
return [];
|
|
454
|
+
}
|
|
455
|
+
if (Array.isArray(obj.hooks)) {
|
|
456
|
+
const nested = obj.hooks.filter((nestedHook) => {
|
|
457
|
+
if (!nestedHook || typeof nestedHook !== "object" || Array.isArray(nestedHook))
|
|
458
|
+
return true;
|
|
459
|
+
const nestedObj = nestedHook;
|
|
460
|
+
return !(typeof nestedObj.command === "string" && isManagedRuntimeHookCommand(nestedObj.command));
|
|
461
|
+
});
|
|
462
|
+
if (nested.length !== obj.hooks.length) {
|
|
463
|
+
changed = true;
|
|
464
|
+
}
|
|
465
|
+
if (nested.length === 0) {
|
|
466
|
+
changed = true;
|
|
467
|
+
return [];
|
|
468
|
+
}
|
|
469
|
+
return [{ ...obj, hooks: nested }];
|
|
470
|
+
}
|
|
471
|
+
return [entry];
|
|
472
|
+
});
|
|
473
|
+
if (cleanedEntries.length > 0) {
|
|
474
|
+
cleanedHooks[eventName] = cleanedEntries;
|
|
475
|
+
}
|
|
476
|
+
else if (entries.length > 0) {
|
|
477
|
+
changed = true;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (!changed) {
|
|
481
|
+
return { updated: root, changed: false };
|
|
482
|
+
}
|
|
483
|
+
root.hooks = cleanedHooks;
|
|
484
|
+
return { updated: root, changed: true };
|
|
485
|
+
}
|
|
486
|
+
function isManagedRuntimeHookCommand(command) {
|
|
487
|
+
const normalized = command.trim().replace(/\s+/gu, " ");
|
|
488
|
+
return /(^|\s)(?:bash\s+)?(?:\.\/)?\.cclaw\/hooks\/(?:session-start|stop-checkpoint|prompt-guard|context-monitor|observe|summarize-observations)\.sh(?:\s|$)/u.test(normalized);
|
|
489
|
+
}
|
|
490
|
+
async function removeManagedHookEntries(hookFilePath) {
|
|
491
|
+
if (!(await exists(hookFilePath)))
|
|
492
|
+
return;
|
|
493
|
+
let parsed = null;
|
|
494
|
+
try {
|
|
495
|
+
const raw = await fs.readFile(hookFilePath, "utf8");
|
|
496
|
+
const recovered = tryParseHookDocument(raw);
|
|
497
|
+
parsed = recovered?.parsed ?? null;
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (parsed === null)
|
|
503
|
+
return;
|
|
504
|
+
const { updated, changed } = stripManagedHookCommands(parsed);
|
|
505
|
+
if (!changed)
|
|
506
|
+
return;
|
|
507
|
+
const root = updated;
|
|
508
|
+
const hooks = root.hooks;
|
|
509
|
+
const hasHooks = typeof hooks === "object" &&
|
|
510
|
+
hooks !== null &&
|
|
511
|
+
!Array.isArray(hooks) &&
|
|
512
|
+
Object.keys(hooks).length > 0;
|
|
513
|
+
if (!hasHooks) {
|
|
514
|
+
const onlyHooksShell = Object.keys(root).every((key) => key === "hooks" || key === "version");
|
|
515
|
+
if (onlyHooksShell) {
|
|
516
|
+
await fs.rm(hookFilePath, { force: true });
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
root.hooks = {};
|
|
520
|
+
}
|
|
521
|
+
await writeFileSafe(hookFilePath, `${JSON.stringify(root, null, 2)}\n`);
|
|
522
|
+
}
|
|
523
|
+
export async function uninstallCclaw(projectRoot) {
|
|
524
|
+
const fullRuntimePath = path.join(projectRoot, RUNTIME_ROOT);
|
|
525
|
+
try {
|
|
526
|
+
await fs.rm(fullRuntimePath, { recursive: true, force: true });
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
// path not present
|
|
530
|
+
}
|
|
531
|
+
await removeCclawFromAgentsMd(projectRoot);
|
|
532
|
+
await removeGitignorePatterns(projectRoot);
|
|
533
|
+
// Clean hook files
|
|
534
|
+
const hookFiles = [
|
|
535
|
+
".claude/hooks/hooks.json",
|
|
536
|
+
".cursor/hooks.json",
|
|
537
|
+
".codex/hooks.json"
|
|
538
|
+
];
|
|
539
|
+
for (const hf of hookFiles) {
|
|
540
|
+
await removeManagedHookEntries(path.join(projectRoot, hf));
|
|
541
|
+
}
|
|
542
|
+
const commandDirs = [
|
|
543
|
+
".claude/commands",
|
|
544
|
+
".cursor/commands",
|
|
545
|
+
".opencode/commands",
|
|
546
|
+
".codex/commands"
|
|
547
|
+
];
|
|
548
|
+
for (const relDir of commandDirs) {
|
|
549
|
+
const fullDir = path.join(projectRoot, relDir);
|
|
550
|
+
try {
|
|
551
|
+
const entries = await fs.readdir(fullDir);
|
|
552
|
+
for (const entry of entries) {
|
|
553
|
+
if (/^cc-.*\.md$/u.test(entry)) {
|
|
554
|
+
await fs.rm(path.join(fullDir, entry), { force: true });
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
// directory not present
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface ObservationRecord {
|
|
2
|
+
ts?: string;
|
|
3
|
+
event?: string;
|
|
4
|
+
tool?: string;
|
|
5
|
+
phase?: string;
|
|
6
|
+
stage?: string;
|
|
7
|
+
runId?: string;
|
|
8
|
+
data?: unknown;
|
|
9
|
+
}
|
|
10
|
+
export type LearningSource = "observed" | "user-stated" | "inferred";
|
|
11
|
+
export type LearningType = "pitfall" | "pattern" | "preference";
|
|
12
|
+
export interface LearningRecord {
|
|
13
|
+
ts: string;
|
|
14
|
+
skill: string;
|
|
15
|
+
type: LearningType;
|
|
16
|
+
key: string;
|
|
17
|
+
insight: string;
|
|
18
|
+
confidence: number;
|
|
19
|
+
source: LearningSource;
|
|
20
|
+
}
|
|
21
|
+
export interface SummarizeOutcome {
|
|
22
|
+
candidates: LearningRecord[];
|
|
23
|
+
appendable: LearningRecord[];
|
|
24
|
+
}
|
|
25
|
+
export declare function summarizeObservationLearnings(observationJsonl: string, existingLearningsJsonl: string, timestamp: string): SummarizeOutcome;
|