cclaw-cli 0.46.4 → 0.46.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/content/hooks.d.ts
CHANGED
|
@@ -17,5 +17,5 @@ export declare function preCompactScript(): string;
|
|
|
17
17
|
export { claudeHooksJsonWithObservation as claudeHooksJson } from "./observe.js";
|
|
18
18
|
export { cursorHooksJsonWithObservation as cursorHooksJson } from "./observe.js";
|
|
19
19
|
export { codexHooksJsonWithObservation as codexHooksJson } from "./observe.js";
|
|
20
|
-
export
|
|
20
|
+
export { opencodePluginJs } from "./opencode-plugin.js";
|
|
21
21
|
export declare function hooksAgentsMdBlock(): string;
|
package/dist/content/hooks.js
CHANGED
|
@@ -1002,293 +1002,7 @@ export { codexHooksJsonWithObservation as codexHooksJson } from "./observe.js";
|
|
|
1002
1002
|
// ---------------------------------------------------------------------------
|
|
1003
1003
|
// OpenCode plugin — JS module
|
|
1004
1004
|
// ---------------------------------------------------------------------------
|
|
1005
|
-
export
|
|
1006
|
-
return `// cclaw OpenCode plugin — generated by cclaw sync
|
|
1007
|
-
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
1008
|
-
import { join } from "node:path";
|
|
1009
|
-
|
|
1010
|
-
export default function cclawPlugin(ctx) {
|
|
1011
|
-
const root = ctx.directory || process.cwd();
|
|
1012
|
-
const runtimeDir = join(root, "${RUNTIME_ROOT}");
|
|
1013
|
-
const stateDir = join(runtimeDir, "state");
|
|
1014
|
-
const flowStatePath = join(stateDir, "flow-state.json");
|
|
1015
|
-
const checkpointPath = join(stateDir, "checkpoint.json");
|
|
1016
|
-
const activityPath = join(stateDir, "stage-activity.jsonl");
|
|
1017
|
-
const contextWarningsPath = join(stateDir, "context-warnings.jsonl");
|
|
1018
|
-
const contextModePath = join(stateDir, "context-mode.json");
|
|
1019
|
-
const contextsDir = join(runtimeDir, "contexts");
|
|
1020
|
-
const sessionDigestPath = join(stateDir, "session-digest.md");
|
|
1021
|
-
const knowledgePath = join(runtimeDir, "knowledge.jsonl");
|
|
1022
|
-
const knowledgeDigestPath = join(stateDir, "knowledge-digest.md");
|
|
1023
|
-
const metaSkillPath = join(runtimeDir, "skills/${META_SKILL_NAME}/SKILL.md");
|
|
1024
|
-
|
|
1025
|
-
function ensureRuntimeDirs() {
|
|
1026
|
-
try {
|
|
1027
|
-
mkdirSync(runtimeDir, { recursive: true });
|
|
1028
|
-
} catch {
|
|
1029
|
-
// ignore
|
|
1030
|
-
}
|
|
1031
|
-
try {
|
|
1032
|
-
mkdirSync(stateDir, { recursive: true });
|
|
1033
|
-
} catch {
|
|
1034
|
-
// ignore
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
function readFlowState() {
|
|
1039
|
-
try {
|
|
1040
|
-
const raw = readFileSync(flowStatePath, "utf8");
|
|
1041
|
-
const state = JSON.parse(raw);
|
|
1042
|
-
return {
|
|
1043
|
-
stage: typeof state.currentStage === "string" ? state.currentStage : "none",
|
|
1044
|
-
completed: Array.isArray(state.completedStages) ? state.completedStages.length : 0,
|
|
1045
|
-
activeRunId: typeof state.activeRunId === "string" ? state.activeRunId : "none"
|
|
1046
|
-
};
|
|
1047
|
-
} catch {
|
|
1048
|
-
return { stage: "none", completed: 0, activeRunId: "none" };
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
function readFileText(filePath) {
|
|
1053
|
-
try {
|
|
1054
|
-
return readFileSync(filePath, "utf8");
|
|
1055
|
-
} catch {
|
|
1056
|
-
return "";
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
function readTailLines(filePath, maxLines) {
|
|
1061
|
-
const text = readFileText(filePath).trim();
|
|
1062
|
-
if (!text) return [];
|
|
1063
|
-
return text.split(/\\r?\\n/).slice(-maxLines);
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
function readCheckpointSummary() {
|
|
1067
|
-
try {
|
|
1068
|
-
const raw = readFileText(checkpointPath);
|
|
1069
|
-
if (!raw) return "";
|
|
1070
|
-
const cp = JSON.parse(raw);
|
|
1071
|
-
return \`Checkpoint: stage=\${cp.stage || "none"}, status=\${cp.status || "unknown"}, run=\${cp.runId || "none"}, at=\${cp.timestamp || "unknown"}\`;
|
|
1072
|
-
} catch {
|
|
1073
|
-
return "";
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
function readContextMode() {
|
|
1078
|
-
let mode = "default";
|
|
1079
|
-
try {
|
|
1080
|
-
const parsed = JSON.parse(readFileText(contextModePath));
|
|
1081
|
-
if (parsed && typeof parsed.activeMode === "string" && parsed.activeMode.trim().length > 0) {
|
|
1082
|
-
mode = parsed.activeMode.trim();
|
|
1083
|
-
}
|
|
1084
|
-
} catch {
|
|
1085
|
-
// keep default
|
|
1086
|
-
}
|
|
1087
|
-
const guidePath = join(contextsDir, mode + ".md");
|
|
1088
|
-
const guide = existsSync(guidePath) ? "${RUNTIME_ROOT}/contexts/" + mode + ".md" : "";
|
|
1089
|
-
return { mode, guide };
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
function readRecentActivity() {
|
|
1093
|
-
try {
|
|
1094
|
-
const lines = readTailLines(activityPath, 5);
|
|
1095
|
-
if (lines.length === 0) return [];
|
|
1096
|
-
return lines
|
|
1097
|
-
.map((line) => {
|
|
1098
|
-
try {
|
|
1099
|
-
return JSON.parse(line);
|
|
1100
|
-
} catch {
|
|
1101
|
-
return null;
|
|
1102
|
-
}
|
|
1103
|
-
})
|
|
1104
|
-
.filter(Boolean)
|
|
1105
|
-
.map((entry) => \`- \${entry.ts || "unknown"} [\${entry.phase || "unknown"}] \${entry.tool || "unknown"} (stage=\${entry.stage || "unknown"}, run=\${entry.runId || "none"})\`);
|
|
1106
|
-
} catch {
|
|
1107
|
-
return [];
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
function readLatestContextWarning() {
|
|
1112
|
-
try {
|
|
1113
|
-
const line = readTailLines(contextWarningsPath, 1)[0];
|
|
1114
|
-
if (!line) return "";
|
|
1115
|
-
try {
|
|
1116
|
-
const parsed = JSON.parse(line);
|
|
1117
|
-
if (parsed && typeof parsed.note === "string") return parsed.note;
|
|
1118
|
-
} catch {
|
|
1119
|
-
// non-json fallback
|
|
1120
|
-
}
|
|
1121
|
-
return line;
|
|
1122
|
-
} catch {
|
|
1123
|
-
return "";
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
function readKnowledgeDigest() {
|
|
1128
|
-
const digest = readFileText(knowledgeDigestPath).trim();
|
|
1129
|
-
if (!digest) {
|
|
1130
|
-
return readTailLines(knowledgePath, 12);
|
|
1131
|
-
}
|
|
1132
|
-
return digest
|
|
1133
|
-
.split(/\\r?\\n/)
|
|
1134
|
-
.map((line) => line.trim())
|
|
1135
|
-
.filter((line) => line.length > 0)
|
|
1136
|
-
.filter((line) => !line.startsWith("#"));
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
function buildBootstrap() {
|
|
1140
|
-
const flow = readFlowState();
|
|
1141
|
-
const parts = [
|
|
1142
|
-
\`cclaw loaded. Flow: stage=\${flow.stage} (\${flow.completed}/8 completed, run=\${flow.activeRunId}). Active artifacts: ${RUNTIME_ROOT}/artifacts/\`
|
|
1143
|
-
];
|
|
1144
|
-
const contextMode = readContextMode();
|
|
1145
|
-
parts.push(
|
|
1146
|
-
contextMode.guide
|
|
1147
|
-
? \`Context mode: \${contextMode.mode} (guide: \${contextMode.guide})\`
|
|
1148
|
-
: \`Context mode: \${contextMode.mode}\`
|
|
1149
|
-
);
|
|
1150
|
-
|
|
1151
|
-
const checkpoint = readCheckpointSummary();
|
|
1152
|
-
if (checkpoint) parts.push(checkpoint);
|
|
1153
|
-
|
|
1154
|
-
const digest = readFileText(sessionDigestPath).trim();
|
|
1155
|
-
if (digest) parts.push("Last session:", digest);
|
|
1156
|
-
|
|
1157
|
-
const activity = readRecentActivity();
|
|
1158
|
-
if (activity.length > 0) parts.push("Recent stage activity:", ...activity);
|
|
1159
|
-
|
|
1160
|
-
const warning = readLatestContextWarning();
|
|
1161
|
-
if (warning) parts.push("Latest context warning:", warning);
|
|
1162
|
-
|
|
1163
|
-
const knowledge = readKnowledgeDigest();
|
|
1164
|
-
if (knowledge.length > 0) parts.push("Knowledge digest (top relevant entries):", ...knowledge);
|
|
1165
|
-
|
|
1166
|
-
parts.push(
|
|
1167
|
-
"If you discover a non-obvious rule or pattern, append one strict-schema JSON line to .cclaw/knowledge.jsonl using type: rule, pattern, lesson, or compound."
|
|
1168
|
-
);
|
|
1169
|
-
|
|
1170
|
-
const meta = readFileText(metaSkillPath).trim();
|
|
1171
|
-
if (meta) parts.push("", meta);
|
|
1172
|
-
return parts.join("\\n");
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
let bootstrapCache = "";
|
|
1176
|
-
|
|
1177
|
-
function refreshBootstrapCache() {
|
|
1178
|
-
bootstrapCache = buildBootstrap();
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
function getBootstrap() {
|
|
1182
|
-
if (!bootstrapCache) refreshBootstrapCache();
|
|
1183
|
-
return bootstrapCache;
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
async function runHookScript(scriptFileName, payload = {}) {
|
|
1187
|
-
const { spawnSync } = await import("node:child_process");
|
|
1188
|
-
const scriptPath = join(root, "${RUNTIME_ROOT}/hooks/" + scriptFileName);
|
|
1189
|
-
const input = typeof payload === "string" ? payload : JSON.stringify(payload ?? {});
|
|
1190
|
-
try {
|
|
1191
|
-
const result = spawnSync("bash", [scriptPath], {
|
|
1192
|
-
cwd: root,
|
|
1193
|
-
timeout: 20000,
|
|
1194
|
-
stdio: ["pipe", "ignore", "ignore"],
|
|
1195
|
-
input
|
|
1196
|
-
});
|
|
1197
|
-
return typeof result.status === "number" ? result.status === 0 : false;
|
|
1198
|
-
} catch {
|
|
1199
|
-
return false;
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
function normalizeToolPayload(input, output) {
|
|
1204
|
-
if (typeof output === "undefined") return input ?? {};
|
|
1205
|
-
return { input: input ?? {}, output: output ?? {} };
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
function resolveEventType(payload) {
|
|
1209
|
-
if (typeof payload === "string") return payload;
|
|
1210
|
-
if (payload && typeof payload === "object") {
|
|
1211
|
-
if (typeof payload.type === "string") return payload.type;
|
|
1212
|
-
if (typeof payload.name === "string") return payload.name;
|
|
1213
|
-
if (payload.event && typeof payload.event === "object") {
|
|
1214
|
-
if (typeof payload.event.type === "string") return payload.event.type;
|
|
1215
|
-
if (typeof payload.event.name === "string") return payload.event.name;
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
return "";
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
function resolveEventData(payload) {
|
|
1222
|
-
if (payload && typeof payload === "object" && payload.event && typeof payload.event === "object") {
|
|
1223
|
-
return payload.event;
|
|
1224
|
-
}
|
|
1225
|
-
return payload;
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
ensureRuntimeDirs();
|
|
1229
|
-
|
|
1230
|
-
return {
|
|
1231
|
-
event: async (payload) => {
|
|
1232
|
-
const eventType = resolveEventType(payload);
|
|
1233
|
-
const eventData = resolveEventData(payload);
|
|
1234
|
-
if (
|
|
1235
|
-
eventType === "session.created" ||
|
|
1236
|
-
eventType === "session.resumed" ||
|
|
1237
|
-
eventType === "session.compacted" ||
|
|
1238
|
-
eventType === "session.cleared"
|
|
1239
|
-
) {
|
|
1240
|
-
// Avoid writing directly to stdout in lifecycle hooks because it can
|
|
1241
|
-
// interfere with OpenCode TUI rendering. Bootstrap is injected via
|
|
1242
|
-
// the system transform hook instead.
|
|
1243
|
-
refreshBootstrapCache();
|
|
1244
|
-
}
|
|
1245
|
-
if (eventType === "session.idle") {
|
|
1246
|
-
await runHookScript("stop-checkpoint.sh", { loop_count: 0 });
|
|
1247
|
-
}
|
|
1248
|
-
if (eventType === "tool.execute.before") {
|
|
1249
|
-
const toolPayload = normalizeToolPayload(eventData, undefined);
|
|
1250
|
-
const promptOk = await runHookScript("prompt-guard.sh", toolPayload);
|
|
1251
|
-
const workflowOk = await runHookScript("workflow-guard.sh", toolPayload);
|
|
1252
|
-
if (!promptOk || !workflowOk) {
|
|
1253
|
-
throw new Error(
|
|
1254
|
-
"cclaw OpenCode guard blocked tool.execute.before (prompt/workflow guard non-zero exit)."
|
|
1255
|
-
);
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
if (eventType === "tool.execute.after") {
|
|
1259
|
-
const toolPayload = normalizeToolPayload(eventData, undefined);
|
|
1260
|
-
await runHookScript("context-monitor.sh", toolPayload);
|
|
1261
|
-
}
|
|
1262
|
-
},
|
|
1263
|
-
"tool.execute.before": async (input, output) => {
|
|
1264
|
-
const payload = normalizeToolPayload(input, output);
|
|
1265
|
-
const promptOk = await runHookScript("prompt-guard.sh", payload);
|
|
1266
|
-
const workflowOk = await runHookScript("workflow-guard.sh", payload);
|
|
1267
|
-
if (!promptOk || !workflowOk) {
|
|
1268
|
-
throw new Error(
|
|
1269
|
-
"cclaw OpenCode guard blocked tool.execute.before (prompt/workflow guard non-zero exit)."
|
|
1270
|
-
);
|
|
1271
|
-
}
|
|
1272
|
-
},
|
|
1273
|
-
"tool.execute.after": async (input, output) => {
|
|
1274
|
-
const payload = normalizeToolPayload(input, output);
|
|
1275
|
-
await runHookScript("context-monitor.sh", payload);
|
|
1276
|
-
},
|
|
1277
|
-
"experimental.chat.system.transform": (payload) => {
|
|
1278
|
-
const bootstrap = getBootstrap();
|
|
1279
|
-
if (typeof payload === "string") {
|
|
1280
|
-
return payload.includes("cclaw loaded.") ? payload : \`\${payload}\\n\\n\${bootstrap}\`;
|
|
1281
|
-
}
|
|
1282
|
-
if (payload && typeof payload === "object" && typeof payload.system === "string") {
|
|
1283
|
-
if (payload.system.includes("cclaw loaded.")) return payload;
|
|
1284
|
-
return { ...payload, system: \`\${payload.system}\\n\\n\${bootstrap}\` };
|
|
1285
|
-
}
|
|
1286
|
-
return payload;
|
|
1287
|
-
}
|
|
1288
|
-
};
|
|
1289
|
-
}
|
|
1290
|
-
`;
|
|
1291
|
-
}
|
|
1005
|
+
export { opencodePluginJs } from "./opencode-plugin.js";
|
|
1292
1006
|
// ---------------------------------------------------------------------------
|
|
1293
1007
|
// AGENTS.md block for hooks
|
|
1294
1008
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function opencodePluginJs(_options?: Record<string, never>): string;
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { RUNTIME_ROOT } from "../constants.js";
|
|
2
|
+
import { META_SKILL_NAME } from "./meta-skill.js";
|
|
3
|
+
export function opencodePluginJs(_options = {}) {
|
|
4
|
+
return `// cclaw OpenCode plugin — generated by cclaw sync
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
export default function cclawPlugin(ctx) {
|
|
9
|
+
const root = ctx.directory || process.cwd();
|
|
10
|
+
const runtimeDir = join(root, "${RUNTIME_ROOT}");
|
|
11
|
+
const stateDir = join(runtimeDir, "state");
|
|
12
|
+
const flowStatePath = join(stateDir, "flow-state.json");
|
|
13
|
+
const checkpointPath = join(stateDir, "checkpoint.json");
|
|
14
|
+
const activityPath = join(stateDir, "stage-activity.jsonl");
|
|
15
|
+
const contextWarningsPath = join(stateDir, "context-warnings.jsonl");
|
|
16
|
+
const contextModePath = join(stateDir, "context-mode.json");
|
|
17
|
+
const contextsDir = join(runtimeDir, "contexts");
|
|
18
|
+
const sessionDigestPath = join(stateDir, "session-digest.md");
|
|
19
|
+
const knowledgePath = join(runtimeDir, "knowledge.jsonl");
|
|
20
|
+
const knowledgeDigestPath = join(stateDir, "knowledge-digest.md");
|
|
21
|
+
const metaSkillPath = join(runtimeDir, "skills/${META_SKILL_NAME}/SKILL.md");
|
|
22
|
+
|
|
23
|
+
function ensureRuntimeDirs() {
|
|
24
|
+
try {
|
|
25
|
+
mkdirSync(runtimeDir, { recursive: true });
|
|
26
|
+
} catch {
|
|
27
|
+
// ignore
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
mkdirSync(stateDir, { recursive: true });
|
|
31
|
+
} catch {
|
|
32
|
+
// ignore
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readFlowState() {
|
|
37
|
+
try {
|
|
38
|
+
const raw = readFileSync(flowStatePath, "utf8");
|
|
39
|
+
const state = JSON.parse(raw);
|
|
40
|
+
return {
|
|
41
|
+
stage: typeof state.currentStage === "string" ? state.currentStage : "none",
|
|
42
|
+
completed: Array.isArray(state.completedStages) ? state.completedStages.length : 0,
|
|
43
|
+
activeRunId: typeof state.activeRunId === "string" ? state.activeRunId : "none"
|
|
44
|
+
};
|
|
45
|
+
} catch {
|
|
46
|
+
return { stage: "none", completed: 0, activeRunId: "none" };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readFileText(filePath) {
|
|
51
|
+
try {
|
|
52
|
+
return readFileSync(filePath, "utf8");
|
|
53
|
+
} catch {
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readTailLines(filePath, maxLines) {
|
|
59
|
+
const text = readFileText(filePath).trim();
|
|
60
|
+
if (!text) return [];
|
|
61
|
+
return text.split(/\\r?\\n/).slice(-maxLines);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readCheckpointSummary() {
|
|
65
|
+
try {
|
|
66
|
+
const raw = readFileText(checkpointPath);
|
|
67
|
+
if (!raw) return "";
|
|
68
|
+
const cp = JSON.parse(raw);
|
|
69
|
+
return \`Checkpoint: stage=\${cp.stage || "none"}, status=\${cp.status || "unknown"}, run=\${cp.runId || "none"}, at=\${cp.timestamp || "unknown"}\`;
|
|
70
|
+
} catch {
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readContextMode() {
|
|
76
|
+
let mode = "default";
|
|
77
|
+
try {
|
|
78
|
+
const parsed = JSON.parse(readFileText(contextModePath));
|
|
79
|
+
if (parsed && typeof parsed.activeMode === "string" && parsed.activeMode.trim().length > 0) {
|
|
80
|
+
mode = parsed.activeMode.trim();
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// keep default
|
|
84
|
+
}
|
|
85
|
+
const guidePath = join(contextsDir, mode + ".md");
|
|
86
|
+
const guide = existsSync(guidePath) ? "${RUNTIME_ROOT}/contexts/" + mode + ".md" : "";
|
|
87
|
+
return { mode, guide };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readRecentActivity() {
|
|
91
|
+
try {
|
|
92
|
+
const lines = readTailLines(activityPath, 5);
|
|
93
|
+
if (lines.length === 0) return [];
|
|
94
|
+
return lines
|
|
95
|
+
.map((line) => {
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(line);
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
.filter(Boolean)
|
|
103
|
+
.map((entry) => \`- \${entry.ts || "unknown"} [\${entry.phase || "unknown"}] \${entry.tool || "unknown"} (stage=\${entry.stage || "unknown"}, run=\${entry.runId || "none"})\`);
|
|
104
|
+
} catch {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readLatestContextWarning() {
|
|
110
|
+
try {
|
|
111
|
+
const line = readTailLines(contextWarningsPath, 1)[0];
|
|
112
|
+
if (!line) return "";
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(line);
|
|
115
|
+
if (parsed && typeof parsed.note === "string") return parsed.note;
|
|
116
|
+
} catch {
|
|
117
|
+
// non-json fallback
|
|
118
|
+
}
|
|
119
|
+
return line;
|
|
120
|
+
} catch {
|
|
121
|
+
return "";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readKnowledgeDigest() {
|
|
126
|
+
const digest = readFileText(knowledgeDigestPath).trim();
|
|
127
|
+
if (!digest) {
|
|
128
|
+
return readTailLines(knowledgePath, 12);
|
|
129
|
+
}
|
|
130
|
+
return digest
|
|
131
|
+
.split(/\\r?\\n/)
|
|
132
|
+
.map((line) => line.trim())
|
|
133
|
+
.filter((line) => line.length > 0)
|
|
134
|
+
.filter((line) => !line.startsWith("#"));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function buildBootstrap() {
|
|
138
|
+
const flow = readFlowState();
|
|
139
|
+
const parts = [
|
|
140
|
+
\`cclaw loaded. Flow: stage=\${flow.stage} (\${flow.completed}/8 completed, run=\${flow.activeRunId}). Active artifacts: ${RUNTIME_ROOT}/artifacts/\`
|
|
141
|
+
];
|
|
142
|
+
const contextMode = readContextMode();
|
|
143
|
+
parts.push(
|
|
144
|
+
contextMode.guide
|
|
145
|
+
? \`Context mode: \${contextMode.mode} (guide: \${contextMode.guide})\`
|
|
146
|
+
: \`Context mode: \${contextMode.mode}\`
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const checkpoint = readCheckpointSummary();
|
|
150
|
+
if (checkpoint) parts.push(checkpoint);
|
|
151
|
+
|
|
152
|
+
const digest = readFileText(sessionDigestPath).trim();
|
|
153
|
+
if (digest) parts.push("Last session:", digest);
|
|
154
|
+
|
|
155
|
+
const activity = readRecentActivity();
|
|
156
|
+
if (activity.length > 0) parts.push("Recent stage activity:", ...activity);
|
|
157
|
+
|
|
158
|
+
const warning = readLatestContextWarning();
|
|
159
|
+
if (warning) parts.push("Latest context warning:", warning);
|
|
160
|
+
|
|
161
|
+
const knowledge = readKnowledgeDigest();
|
|
162
|
+
if (knowledge.length > 0) parts.push("Knowledge digest (top relevant entries):", ...knowledge);
|
|
163
|
+
|
|
164
|
+
parts.push(
|
|
165
|
+
"If you discover a non-obvious rule or pattern, append one strict-schema JSON line to .cclaw/knowledge.jsonl using type: rule, pattern, lesson, or compound."
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const meta = readFileText(metaSkillPath).trim();
|
|
169
|
+
if (meta) parts.push("", meta);
|
|
170
|
+
return parts.join("\\n");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let bootstrapCache = "";
|
|
174
|
+
|
|
175
|
+
function refreshBootstrapCache() {
|
|
176
|
+
bootstrapCache = buildBootstrap();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function getBootstrap() {
|
|
180
|
+
if (!bootstrapCache) refreshBootstrapCache();
|
|
181
|
+
return bootstrapCache;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function runHookScript(scriptFileName, payload = {}) {
|
|
185
|
+
const { spawnSync } = await import("node:child_process");
|
|
186
|
+
const scriptPath = join(root, "${RUNTIME_ROOT}/hooks/" + scriptFileName);
|
|
187
|
+
const input = typeof payload === "string" ? payload : JSON.stringify(payload ?? {});
|
|
188
|
+
try {
|
|
189
|
+
const result = spawnSync("bash", [scriptPath], {
|
|
190
|
+
cwd: root,
|
|
191
|
+
timeout: 20000,
|
|
192
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
193
|
+
input
|
|
194
|
+
});
|
|
195
|
+
return typeof result.status === "number" ? result.status === 0 : false;
|
|
196
|
+
} catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function normalizeToolPayload(input, output) {
|
|
202
|
+
if (typeof output === "undefined") return input ?? {};
|
|
203
|
+
return { input: input ?? {}, output: output ?? {} };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function resolveEventType(payload) {
|
|
207
|
+
if (typeof payload === "string") return payload;
|
|
208
|
+
if (payload && typeof payload === "object") {
|
|
209
|
+
if (typeof payload.type === "string") return payload.type;
|
|
210
|
+
if (typeof payload.name === "string") return payload.name;
|
|
211
|
+
if (payload.event && typeof payload.event === "object") {
|
|
212
|
+
if (typeof payload.event.type === "string") return payload.event.type;
|
|
213
|
+
if (typeof payload.event.name === "string") return payload.event.name;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return "";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function resolveEventData(payload) {
|
|
220
|
+
if (payload && typeof payload === "object" && payload.event && typeof payload.event === "object") {
|
|
221
|
+
return payload.event;
|
|
222
|
+
}
|
|
223
|
+
return payload;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
ensureRuntimeDirs();
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
event: async (payload) => {
|
|
230
|
+
const eventType = resolveEventType(payload);
|
|
231
|
+
const eventData = resolveEventData(payload);
|
|
232
|
+
if (
|
|
233
|
+
eventType === "session.created" ||
|
|
234
|
+
eventType === "session.resumed" ||
|
|
235
|
+
eventType === "session.compacted" ||
|
|
236
|
+
eventType === "session.cleared"
|
|
237
|
+
) {
|
|
238
|
+
// Avoid writing directly to stdout in lifecycle hooks because it can
|
|
239
|
+
// interfere with OpenCode TUI rendering. Bootstrap is injected via
|
|
240
|
+
// the system transform hook instead.
|
|
241
|
+
refreshBootstrapCache();
|
|
242
|
+
}
|
|
243
|
+
if (eventType === "session.idle") {
|
|
244
|
+
await runHookScript("stop-checkpoint.sh", { loop_count: 0 });
|
|
245
|
+
}
|
|
246
|
+
if (eventType === "tool.execute.before") {
|
|
247
|
+
const toolPayload = normalizeToolPayload(eventData, undefined);
|
|
248
|
+
const promptOk = await runHookScript("prompt-guard.sh", toolPayload);
|
|
249
|
+
const workflowOk = await runHookScript("workflow-guard.sh", toolPayload);
|
|
250
|
+
if (!promptOk || !workflowOk) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
"cclaw OpenCode guard blocked tool.execute.before (prompt/workflow guard non-zero exit)."
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (eventType === "tool.execute.after") {
|
|
257
|
+
const toolPayload = normalizeToolPayload(eventData, undefined);
|
|
258
|
+
await runHookScript("context-monitor.sh", toolPayload);
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
"tool.execute.before": async (input, output) => {
|
|
262
|
+
const payload = normalizeToolPayload(input, output);
|
|
263
|
+
const promptOk = await runHookScript("prompt-guard.sh", payload);
|
|
264
|
+
const workflowOk = await runHookScript("workflow-guard.sh", payload);
|
|
265
|
+
if (!promptOk || !workflowOk) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
"cclaw OpenCode guard blocked tool.execute.before (prompt/workflow guard non-zero exit)."
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
"tool.execute.after": async (input, output) => {
|
|
272
|
+
const payload = normalizeToolPayload(input, output);
|
|
273
|
+
await runHookScript("context-monitor.sh", payload);
|
|
274
|
+
},
|
|
275
|
+
"experimental.chat.system.transform": (payload) => {
|
|
276
|
+
const bootstrap = getBootstrap();
|
|
277
|
+
if (typeof payload === "string") {
|
|
278
|
+
return payload.includes("cclaw loaded.") ? payload : \`\${payload}\\n\\n\${bootstrap}\`;
|
|
279
|
+
}
|
|
280
|
+
if (payload && typeof payload === "object" && typeof payload.system === "string") {
|
|
281
|
+
if (payload.system.includes("cclaw loaded.")) return payload;
|
|
282
|
+
return { ...payload, system: \`\${payload.system}\\n\\n\${bootstrap}\` };
|
|
283
|
+
}
|
|
284
|
+
return payload;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
`;
|
|
289
|
+
}
|