cclaw-cli 0.46.4 → 0.46.6
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
|
// ---------------------------------------------------------------------------
|
|
@@ -15,7 +15,6 @@ export interface WorkflowGuardOptions {
|
|
|
15
15
|
tddTestGlobs?: string[];
|
|
16
16
|
}
|
|
17
17
|
export declare function workflowGuardScript(options?: WorkflowGuardOptions): string;
|
|
18
|
-
export declare function observeScript(): string;
|
|
19
18
|
export declare function contextMonitorScript(): string;
|
|
20
19
|
export declare function summarizeObservationsRuntimeModule(): string;
|
|
21
20
|
export declare function summarizeObservationsScript(): string;
|
package/dist/content/observe.js
CHANGED
|
@@ -693,260 +693,6 @@ if [ -n "$REASONS" ]; then
|
|
|
693
693
|
printf '[cclaw] %s\n' "$NOTE" >&2
|
|
694
694
|
fi
|
|
695
695
|
|
|
696
|
-
exit 0
|
|
697
|
-
`;
|
|
698
|
-
}
|
|
699
|
-
export function observeScript() {
|
|
700
|
-
return `#!/usr/bin/env bash
|
|
701
|
-
# cclaw observe hook — generated by cclaw sync
|
|
702
|
-
# Captures PreToolUse/PostToolUse events to ${RUNTIME_ROOT}/observations.jsonl
|
|
703
|
-
# Reads hook JSON from stdin, extracts tool + truncated I/O, appends JSONL.
|
|
704
|
-
# Always exits 0 to never block the agent.
|
|
705
|
-
set -uo pipefail
|
|
706
|
-
|
|
707
|
-
# Phase: "pre" or "post" passed as $1 by the hook runner
|
|
708
|
-
PHASE="\${1:-post}"
|
|
709
|
-
|
|
710
|
-
${RUNTIME_SHELL_DETECT_ROOT}
|
|
711
|
-
|
|
712
|
-
OBS_FILE="$ROOT/${RUNTIME_ROOT}/observations.jsonl"
|
|
713
|
-
STATE_FILE="$ROOT/${RUNTIME_ROOT}/state/flow-state.json"
|
|
714
|
-
ACTIVITY_FILE="$ROOT/${RUNTIME_ROOT}/state/stage-activity.jsonl"
|
|
715
|
-
LOCK_DIR="$ROOT/${RUNTIME_ROOT}/state/.observe.lock"
|
|
716
|
-
MAX_LEN=2000
|
|
717
|
-
|
|
718
|
-
# Guard: skip if disabled or observations dir missing
|
|
719
|
-
[ -f "$ROOT/${RUNTIME_ROOT}/.observe-disabled" ] && exit 0
|
|
720
|
-
[ -d "$ROOT/${RUNTIME_ROOT}" ] || exit 0
|
|
721
|
-
mkdir -p "$ROOT/${RUNTIME_ROOT}/state" 2>/dev/null || true
|
|
722
|
-
|
|
723
|
-
escape_json() {
|
|
724
|
-
local str="$1"
|
|
725
|
-
str=\${str//\\\\/\\\\\\\\}
|
|
726
|
-
str=\${str//\\"/\\\\\\"}
|
|
727
|
-
str=\${str//$'\\t'/\\\\t}
|
|
728
|
-
str=\${str//$'\\n'/\\\\n}
|
|
729
|
-
printf '%s' "$str"
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
acquire_lock() {
|
|
733
|
-
local attempt=0
|
|
734
|
-
while ! mkdir "$LOCK_DIR" 2>/dev/null; do
|
|
735
|
-
attempt=$((attempt + 1))
|
|
736
|
-
if [ "$attempt" -ge 200 ]; then
|
|
737
|
-
return 1
|
|
738
|
-
fi
|
|
739
|
-
sleep 0.02
|
|
740
|
-
done
|
|
741
|
-
return 0
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
release_lock() {
|
|
745
|
-
rmdir "$LOCK_DIR" 2>/dev/null || true
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
rotate_file() {
|
|
749
|
-
local file_path="$1"
|
|
750
|
-
local keep_lines="$2"
|
|
751
|
-
if [ ! -f "$file_path" ]; then
|
|
752
|
-
return
|
|
753
|
-
fi
|
|
754
|
-
local line_count
|
|
755
|
-
line_count=$(wc -l < "$file_path" 2>/dev/null | tr -d ' ')
|
|
756
|
-
if [ -z "$line_count" ]; then
|
|
757
|
-
return
|
|
758
|
-
fi
|
|
759
|
-
if [ "$line_count" -gt $((keep_lines * 2)) ]; then
|
|
760
|
-
local tmp_path="\${file_path}.tmp.$$"
|
|
761
|
-
if tail -n "$keep_lines" "$file_path" > "$tmp_path" 2>/dev/null; then
|
|
762
|
-
mv "$tmp_path" "$file_path" 2>/dev/null || rm -f "$tmp_path" 2>/dev/null || true
|
|
763
|
-
fi
|
|
764
|
-
fi
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
sync_run_artifacts() {
|
|
768
|
-
if [ "$PHASE" != "post" ]; then
|
|
769
|
-
return
|
|
770
|
-
fi
|
|
771
|
-
[ -n "$ACTIVE_RUN" ] || return
|
|
772
|
-
if [ "$ACTIVE_RUN" = "none" ]; then
|
|
773
|
-
return
|
|
774
|
-
fi
|
|
775
|
-
|
|
776
|
-
local tool_lower
|
|
777
|
-
tool_lower=$(printf '%s' "$TOOL" | tr '[:upper:]' '[:lower:]')
|
|
778
|
-
case "$tool_lower" in
|
|
779
|
-
write|edit|multiedit|multi_edit|delete|applypatch|runcommand|shell|terminal|execcommand) ;;
|
|
780
|
-
*) return ;;
|
|
781
|
-
esac
|
|
782
|
-
|
|
783
|
-
local active_dir="$ROOT/${RUNTIME_ROOT}/artifacts"
|
|
784
|
-
local run_dir="$ROOT/${RUNTIME_ROOT}/runs/$ACTIVE_RUN/artifacts"
|
|
785
|
-
[ -d "$active_dir" ] || return
|
|
786
|
-
mkdir -p "$run_dir" 2>/dev/null || return
|
|
787
|
-
|
|
788
|
-
for run_file in "$run_dir"/*; do
|
|
789
|
-
[ -e "$run_file" ] || continue
|
|
790
|
-
[ -f "$run_file" ] || continue
|
|
791
|
-
local base_name
|
|
792
|
-
base_name=$(basename "$run_file")
|
|
793
|
-
local active_file="$active_dir/$base_name"
|
|
794
|
-
if [ ! -f "$active_file" ]; then
|
|
795
|
-
rm -f "$run_file" 2>/dev/null || true
|
|
796
|
-
continue
|
|
797
|
-
fi
|
|
798
|
-
if command -v cmp >/dev/null 2>&1 && cmp -s "$active_file" "$run_file" 2>/dev/null; then
|
|
799
|
-
continue
|
|
800
|
-
fi
|
|
801
|
-
cp "$active_file" "$run_file" 2>/dev/null || true
|
|
802
|
-
done
|
|
803
|
-
|
|
804
|
-
for active_file in "$active_dir"/*; do
|
|
805
|
-
[ -e "$active_file" ] || continue
|
|
806
|
-
[ -f "$active_file" ] || continue
|
|
807
|
-
local base_name
|
|
808
|
-
base_name=$(basename "$active_file")
|
|
809
|
-
local run_file="$run_dir/$base_name"
|
|
810
|
-
if [ -f "$run_file" ] && command -v cmp >/dev/null 2>&1 && cmp -s "$active_file" "$run_file" 2>/dev/null; then
|
|
811
|
-
continue
|
|
812
|
-
fi
|
|
813
|
-
cp "$active_file" "$run_file" 2>/dev/null || true
|
|
814
|
-
done
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
# Read stdin (hook JSON)
|
|
818
|
-
INPUT=$(cat 2>/dev/null || echo '{}')
|
|
819
|
-
[ -z "$INPUT" ] && exit 0
|
|
820
|
-
|
|
821
|
-
# Extract fields from hook payload.
|
|
822
|
-
TOOL="unknown"
|
|
823
|
-
PAYLOAD=""
|
|
824
|
-
if command -v jq >/dev/null 2>&1; then
|
|
825
|
-
TOOL=$(echo "$INPUT" | jq -r '.tool_name // .tool // .toolName // .name // .id // .command // .tool.name // .tool.id // .input.tool_name // .input.tool // .input.toolName // .input.name // .input.id // .input.command // .input.tool.name // .input.tool.id // "unknown"' 2>/dev/null || echo "unknown")
|
|
826
|
-
if [ "$PHASE" = "pre" ]; then
|
|
827
|
-
PAYLOAD=$(echo "$INPUT" | jq -r --arg max "$MAX_LEN" '.tool_input // .input // {} | tostring | .[0:($max|tonumber)]' 2>/dev/null || echo "")
|
|
828
|
-
else
|
|
829
|
-
PAYLOAD=$(echo "$INPUT" | jq -r --arg max "$MAX_LEN" '.tool_response // .tool_output // .output // .result_json // "" | tostring | .[0:($max|tonumber)]' 2>/dev/null || echo "")
|
|
830
|
-
fi
|
|
831
|
-
else
|
|
832
|
-
TOOL=$(printf '%s' "$INPUT" | sed -n -E 's/.*"tool_name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\\1/p' | head -1)
|
|
833
|
-
if [ -z "$TOOL" ]; then
|
|
834
|
-
TOOL=$(printf '%s' "$INPUT" | sed -n -E 's/.*"tool"[[:space:]]*:[[:space:]]*"([^"]+)".*/\\1/p' | head -1)
|
|
835
|
-
fi
|
|
836
|
-
[ -n "$TOOL" ] || TOOL="unknown"
|
|
837
|
-
PAYLOAD=$(printf '%s' "$INPUT" | cut -c1-"$MAX_LEN")
|
|
838
|
-
fi
|
|
839
|
-
|
|
840
|
-
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
|
|
841
|
-
STAGE="none"
|
|
842
|
-
ACTIVE_RUN="none"
|
|
843
|
-
if [ -f "$STATE_FILE" ]; then
|
|
844
|
-
if command -v jq >/dev/null 2>&1; then
|
|
845
|
-
STAGE=$(jq -r '.currentStage // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
|
|
846
|
-
ACTIVE_RUN=$(jq -r '.activeRunId // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
|
|
847
|
-
elif command -v python3 >/dev/null 2>&1; then
|
|
848
|
-
STAGE=$(python3 - "$STATE_FILE" <<'PY'
|
|
849
|
-
import json
|
|
850
|
-
import sys
|
|
851
|
-
|
|
852
|
-
stage = "none"
|
|
853
|
-
try:
|
|
854
|
-
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
855
|
-
data = json.load(fh)
|
|
856
|
-
value = data.get("currentStage")
|
|
857
|
-
if isinstance(value, str) and value:
|
|
858
|
-
stage = value
|
|
859
|
-
except Exception:
|
|
860
|
-
pass
|
|
861
|
-
print(stage)
|
|
862
|
-
PY
|
|
863
|
-
)
|
|
864
|
-
ACTIVE_RUN=$(python3 - "$STATE_FILE" <<'PY'
|
|
865
|
-
import json
|
|
866
|
-
import sys
|
|
867
|
-
|
|
868
|
-
run_id = "none"
|
|
869
|
-
try:
|
|
870
|
-
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
871
|
-
data = json.load(fh)
|
|
872
|
-
value = data.get("activeRunId")
|
|
873
|
-
if isinstance(value, str) and value:
|
|
874
|
-
run_id = value
|
|
875
|
-
except Exception:
|
|
876
|
-
pass
|
|
877
|
-
print(run_id)
|
|
878
|
-
PY
|
|
879
|
-
)
|
|
880
|
-
else
|
|
881
|
-
STAGE=$(grep -o '"currentStage"[[:space:]]*:[[:space:]]*"[^"]*"' "$STATE_FILE" 2>/dev/null | head -1 | sed 's/.*"\\([^"]*\\)"$/\\1/' || echo "none")
|
|
882
|
-
ACTIVE_RUN=$(grep -o '"activeRunId"[[:space:]]*:[[:space:]]*"[^"]*"' "$STATE_FILE" 2>/dev/null | head -1 | sed 's/.*"\\([^"]*\\)"$/\\1/' || echo "none")
|
|
883
|
-
fi
|
|
884
|
-
fi
|
|
885
|
-
|
|
886
|
-
# Skip observation of cclaw hooks to avoid recursion
|
|
887
|
-
case "$TOOL" in
|
|
888
|
-
cclaw*|*cclaw-hook*|observe) exit 0 ;;
|
|
889
|
-
esac
|
|
890
|
-
|
|
891
|
-
if [ "$PHASE" = "pre" ]; then
|
|
892
|
-
EVENT="tool_start"
|
|
893
|
-
else
|
|
894
|
-
EVENT="tool_complete"
|
|
895
|
-
fi
|
|
896
|
-
|
|
897
|
-
# Scrub potential secrets (env vars, tokens, keys) — BSD/macOS sed compatible
|
|
898
|
-
PAYLOAD=$(echo "$PAYLOAD" | sed -E 's/[A-Za-z0-9_]*([Kk][Ee][Yy]|[Tt][Oo][Kk][Ee][Nn]|[Ss][Ee][Cc][Rr][Ee][Tt]|[Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd]|[Cc][Rr][Ee][Dd][Ee][Nn][Tt][Ii][Aa][Ll])[A-Za-z0-9_]*[=:][^ ",}]+/[REDACTED]/g' 2>/dev/null || echo "$PAYLOAD")
|
|
899
|
-
|
|
900
|
-
# Build JSONL lines.
|
|
901
|
-
if command -v jq >/dev/null 2>&1; then
|
|
902
|
-
EVENT_JSON=$(jq -n -c \\
|
|
903
|
-
--arg ts "$TS" \\
|
|
904
|
-
--arg event "$EVENT" \\
|
|
905
|
-
--arg tool "$TOOL" \\
|
|
906
|
-
--arg phase "$PHASE" \\
|
|
907
|
-
--arg stage "$STAGE" \\
|
|
908
|
-
--arg runId "$ACTIVE_RUN" \\
|
|
909
|
-
--arg payload "$PAYLOAD" \\
|
|
910
|
-
'{ts:$ts,event:$event,tool:$tool,phase:$phase,stage:$stage,runId:$runId,data:$payload}' 2>/dev/null || echo "")
|
|
911
|
-
ACTIVITY_JSON=$(jq -n -c \\
|
|
912
|
-
--arg ts "$TS" \\
|
|
913
|
-
--arg event "$EVENT" \\
|
|
914
|
-
--arg tool "$TOOL" \\
|
|
915
|
-
--arg phase "$PHASE" \\
|
|
916
|
-
--arg stage "$STAGE" \\
|
|
917
|
-
--arg runId "$ACTIVE_RUN" \\
|
|
918
|
-
--arg schemaVersion "1" \\
|
|
919
|
-
'{ts:$ts,event:$event,tool:$tool,phase:$phase,stage:$stage,runId:$runId,schemaVersion:($schemaVersion|tonumber)}' 2>/dev/null || echo "")
|
|
920
|
-
else
|
|
921
|
-
EVENT_JSON=$(printf '{"ts":"%s","event":"%s","tool":"%s","phase":"%s","stage":"%s","runId":"%s","data":"%s"}' \\
|
|
922
|
-
"$(escape_json "$TS")" \\
|
|
923
|
-
"$(escape_json "$EVENT")" \\
|
|
924
|
-
"$(escape_json "$TOOL")" \\
|
|
925
|
-
"$(escape_json "$PHASE")" \\
|
|
926
|
-
"$(escape_json "$STAGE")" \\
|
|
927
|
-
"$(escape_json "$ACTIVE_RUN")" \\
|
|
928
|
-
"$(escape_json "$PAYLOAD")")
|
|
929
|
-
ACTIVITY_JSON=$(printf '{"ts":"%s","event":"%s","tool":"%s","phase":"%s","stage":"%s","runId":"%s","schemaVersion":1}' \\
|
|
930
|
-
"$(escape_json "$TS")" \\
|
|
931
|
-
"$(escape_json "$EVENT")" \\
|
|
932
|
-
"$(escape_json "$TOOL")" \\
|
|
933
|
-
"$(escape_json "$PHASE")" \\
|
|
934
|
-
"$(escape_json "$STAGE")" \\
|
|
935
|
-
"$(escape_json "$ACTIVE_RUN")")
|
|
936
|
-
fi
|
|
937
|
-
|
|
938
|
-
if acquire_lock; then
|
|
939
|
-
trap release_lock EXIT INT TERM
|
|
940
|
-
[ -n "$EVENT_JSON" ] && printf '%s\\n' "$EVENT_JSON" >> "$OBS_FILE" 2>/dev/null
|
|
941
|
-
[ -n "$ACTIVITY_JSON" ] && printf '%s\\n' "$ACTIVITY_JSON" >> "$ACTIVITY_FILE" 2>/dev/null
|
|
942
|
-
rotate_file "$OBS_FILE" 4000
|
|
943
|
-
rotate_file "$ACTIVITY_FILE" 3000
|
|
944
|
-
release_lock
|
|
945
|
-
trap - EXIT INT TERM
|
|
946
|
-
fi
|
|
947
|
-
|
|
948
|
-
sync_run_artifacts
|
|
949
|
-
|
|
950
696
|
exit 0
|
|
951
697
|
`;
|
|
952
698
|
}
|
|
@@ -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
|
+
}
|