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.
@@ -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 declare function opencodePluginJs(_options?: HookRuntimeOptions): string;
20
+ export { opencodePluginJs } from "./opencode-plugin.js";
21
21
  export declare function hooksAgentsMdBlock(): string;
@@ -1002,293 +1002,7 @@ export { codexHooksJsonWithObservation as codexHooksJson } from "./observe.js";
1002
1002
  // ---------------------------------------------------------------------------
1003
1003
  // OpenCode plugin — JS module
1004
1004
  // ---------------------------------------------------------------------------
1005
- export function opencodePluginJs(_options = {}) {
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;
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.46.4",
3
+ "version": "0.46.6",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {