cclaw-cli 0.46.3 → 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.
@@ -11,10 +11,11 @@ export interface HookRuntimeOptions {
11
11
  export declare const RUNTIME_SHELL_DETECT_ROOT = "HARNESS=\"codex\"\nif [ -n \"${CLAUDE_PROJECT_DIR:-}\" ]; then\n HARNESS=\"claude\"\nelif [ -n \"${CURSOR_PROJECT_DIR:-}\" ] || [ -n \"${CURSOR_PROJECT_ROOT:-}\" ]; then\n HARNESS=\"cursor\"\nelif [ -n \"${OPENCODE_PROJECT_DIR:-}\" ] || [ -n \"${OPENCODE_PROJECT_ROOT:-}\" ]; then\n HARNESS=\"opencode\"\nfi\n\nROOT=\"\"\nfor candidate in \"${CCLAW_PROJECT_ROOT:-}\" \"${CLAUDE_PROJECT_DIR:-}\" \"${CURSOR_PROJECT_DIR:-}\" \"${CURSOR_PROJECT_ROOT:-}\" \"${OPENCODE_PROJECT_DIR:-}\" \"${OPENCODE_PROJECT_ROOT:-}\" \"${PWD:-}\"; do\n if [ -n \"$candidate\" ] && [ -d \"$candidate/.cclaw\" ]; then\n ROOT=\"$candidate\"\n break\n fi\ndone\nif [ -z \"$ROOT\" ]; then\n ROOT=\"${CCLAW_PROJECT_ROOT:-${CLAUDE_PROJECT_DIR:-${CURSOR_PROJECT_DIR:-${CURSOR_PROJECT_ROOT:-${OPENCODE_PROJECT_DIR:-${OPENCODE_PROJECT_ROOT:-${PWD}}}}}}}\"\nfi";
12
12
  export declare function sessionStartScript(_options?: HookRuntimeOptions): string;
13
13
  export declare function stopCheckpointScript(): string;
14
+ export declare function runHookDispatcherScript(): string;
14
15
  export declare function stageCompleteScript(): string;
15
16
  export declare function preCompactScript(): string;
16
17
  export { claudeHooksJsonWithObservation as claudeHooksJson } from "./observe.js";
17
18
  export { cursorHooksJsonWithObservation as cursorHooksJson } from "./observe.js";
18
19
  export { codexHooksJsonWithObservation as codexHooksJson } from "./observe.js";
19
- export declare function opencodePluginJs(_options?: HookRuntimeOptions): string;
20
+ export { opencodePluginJs } from "./opencode-plugin.js";
20
21
  export declare function hooksAgentsMdBlock(): string;
@@ -769,6 +769,56 @@ case "$HARNESS" in
769
769
  esac
770
770
  `;
771
771
  }
772
+ export function runHookDispatcherScript() {
773
+ return `#!/usr/bin/env bash
774
+ # cclaw hook dispatcher — generated by cclaw sync
775
+ # Single entrypoint used by harness hook JSON wiring.
776
+ set -euo pipefail
777
+
778
+ ${DETECT_ROOT}
779
+
780
+ if [ "$#" -lt 1 ]; then
781
+ printf 'Usage: bash ${RUNTIME_ROOT}/hooks/run-hook.cmd <session-start|stop-checkpoint|pre-compact|prompt-guard|workflow-guard|context-monitor>\\n' >&2
782
+ exit 1
783
+ fi
784
+
785
+ HOOK_NAME="$1"
786
+ shift || true
787
+
788
+ case "$HOOK_NAME" in
789
+ session-start|session-start.sh)
790
+ HOOK_FILE="session-start.sh"
791
+ ;;
792
+ stop-checkpoint|stop-checkpoint.sh)
793
+ HOOK_FILE="stop-checkpoint.sh"
794
+ ;;
795
+ pre-compact|pre-compact.sh)
796
+ HOOK_FILE="pre-compact.sh"
797
+ ;;
798
+ prompt-guard|prompt-guard.sh)
799
+ HOOK_FILE="prompt-guard.sh"
800
+ ;;
801
+ workflow-guard|workflow-guard.sh)
802
+ HOOK_FILE="workflow-guard.sh"
803
+ ;;
804
+ context-monitor|context-monitor.sh)
805
+ HOOK_FILE="context-monitor.sh"
806
+ ;;
807
+ *)
808
+ printf '[cclaw] run-hook: unsupported hook "%s".\\n' "$HOOK_NAME" >&2
809
+ exit 1
810
+ ;;
811
+ esac
812
+
813
+ HOOK_PATH="$ROOT/${RUNTIME_ROOT}/hooks/$HOOK_FILE"
814
+ if [ ! -f "$HOOK_PATH" ]; then
815
+ printf '[cclaw] run-hook: hook script not found at %s\\n' "$HOOK_PATH" >&2
816
+ exit 1
817
+ fi
818
+
819
+ exec bash "$HOOK_PATH" "$@"
820
+ `;
821
+ }
772
822
  export function stageCompleteScript() {
773
823
  return `#!/usr/bin/env bash
774
824
  # cclaw stage-complete helper — generated by cclaw sync
@@ -952,293 +1002,7 @@ export { codexHooksJsonWithObservation as codexHooksJson } from "./observe.js";
952
1002
  // ---------------------------------------------------------------------------
953
1003
  // OpenCode plugin — JS module
954
1004
  // ---------------------------------------------------------------------------
955
- export function opencodePluginJs(_options = {}) {
956
- return `// cclaw OpenCode plugin — generated by cclaw sync
957
- import { existsSync, mkdirSync, readFileSync } from "node:fs";
958
- import { join } from "node:path";
959
-
960
- export default function cclawPlugin(ctx) {
961
- const root = ctx.directory || process.cwd();
962
- const runtimeDir = join(root, "${RUNTIME_ROOT}");
963
- const stateDir = join(runtimeDir, "state");
964
- const flowStatePath = join(stateDir, "flow-state.json");
965
- const checkpointPath = join(stateDir, "checkpoint.json");
966
- const activityPath = join(stateDir, "stage-activity.jsonl");
967
- const contextWarningsPath = join(stateDir, "context-warnings.jsonl");
968
- const contextModePath = join(stateDir, "context-mode.json");
969
- const contextsDir = join(runtimeDir, "contexts");
970
- const sessionDigestPath = join(stateDir, "session-digest.md");
971
- const knowledgePath = join(runtimeDir, "knowledge.jsonl");
972
- const knowledgeDigestPath = join(stateDir, "knowledge-digest.md");
973
- const metaSkillPath = join(runtimeDir, "skills/${META_SKILL_NAME}/SKILL.md");
974
-
975
- function ensureRuntimeDirs() {
976
- try {
977
- mkdirSync(runtimeDir, { recursive: true });
978
- } catch {
979
- // ignore
980
- }
981
- try {
982
- mkdirSync(stateDir, { recursive: true });
983
- } catch {
984
- // ignore
985
- }
986
- }
987
-
988
- function readFlowState() {
989
- try {
990
- const raw = readFileSync(flowStatePath, "utf8");
991
- const state = JSON.parse(raw);
992
- return {
993
- stage: typeof state.currentStage === "string" ? state.currentStage : "none",
994
- completed: Array.isArray(state.completedStages) ? state.completedStages.length : 0,
995
- activeRunId: typeof state.activeRunId === "string" ? state.activeRunId : "none"
996
- };
997
- } catch {
998
- return { stage: "none", completed: 0, activeRunId: "none" };
999
- }
1000
- }
1001
-
1002
- function readFileText(filePath) {
1003
- try {
1004
- return readFileSync(filePath, "utf8");
1005
- } catch {
1006
- return "";
1007
- }
1008
- }
1009
-
1010
- function readTailLines(filePath, maxLines) {
1011
- const text = readFileText(filePath).trim();
1012
- if (!text) return [];
1013
- return text.split(/\\r?\\n/).slice(-maxLines);
1014
- }
1015
-
1016
- function readCheckpointSummary() {
1017
- try {
1018
- const raw = readFileText(checkpointPath);
1019
- if (!raw) return "";
1020
- const cp = JSON.parse(raw);
1021
- return \`Checkpoint: stage=\${cp.stage || "none"}, status=\${cp.status || "unknown"}, run=\${cp.runId || "none"}, at=\${cp.timestamp || "unknown"}\`;
1022
- } catch {
1023
- return "";
1024
- }
1025
- }
1026
-
1027
- function readContextMode() {
1028
- let mode = "default";
1029
- try {
1030
- const parsed = JSON.parse(readFileText(contextModePath));
1031
- if (parsed && typeof parsed.activeMode === "string" && parsed.activeMode.trim().length > 0) {
1032
- mode = parsed.activeMode.trim();
1033
- }
1034
- } catch {
1035
- // keep default
1036
- }
1037
- const guidePath = join(contextsDir, mode + ".md");
1038
- const guide = existsSync(guidePath) ? "${RUNTIME_ROOT}/contexts/" + mode + ".md" : "";
1039
- return { mode, guide };
1040
- }
1041
-
1042
- function readRecentActivity() {
1043
- try {
1044
- const lines = readTailLines(activityPath, 5);
1045
- if (lines.length === 0) return [];
1046
- return lines
1047
- .map((line) => {
1048
- try {
1049
- return JSON.parse(line);
1050
- } catch {
1051
- return null;
1052
- }
1053
- })
1054
- .filter(Boolean)
1055
- .map((entry) => \`- \${entry.ts || "unknown"} [\${entry.phase || "unknown"}] \${entry.tool || "unknown"} (stage=\${entry.stage || "unknown"}, run=\${entry.runId || "none"})\`);
1056
- } catch {
1057
- return [];
1058
- }
1059
- }
1060
-
1061
- function readLatestContextWarning() {
1062
- try {
1063
- const line = readTailLines(contextWarningsPath, 1)[0];
1064
- if (!line) return "";
1065
- try {
1066
- const parsed = JSON.parse(line);
1067
- if (parsed && typeof parsed.note === "string") return parsed.note;
1068
- } catch {
1069
- // non-json fallback
1070
- }
1071
- return line;
1072
- } catch {
1073
- return "";
1074
- }
1075
- }
1076
-
1077
- function readKnowledgeDigest() {
1078
- const digest = readFileText(knowledgeDigestPath).trim();
1079
- if (!digest) {
1080
- return readTailLines(knowledgePath, 12);
1081
- }
1082
- return digest
1083
- .split(/\\r?\\n/)
1084
- .map((line) => line.trim())
1085
- .filter((line) => line.length > 0)
1086
- .filter((line) => !line.startsWith("#"));
1087
- }
1088
-
1089
- function buildBootstrap() {
1090
- const flow = readFlowState();
1091
- const parts = [
1092
- \`cclaw loaded. Flow: stage=\${flow.stage} (\${flow.completed}/8 completed, run=\${flow.activeRunId}). Active artifacts: ${RUNTIME_ROOT}/artifacts/\`
1093
- ];
1094
- const contextMode = readContextMode();
1095
- parts.push(
1096
- contextMode.guide
1097
- ? \`Context mode: \${contextMode.mode} (guide: \${contextMode.guide})\`
1098
- : \`Context mode: \${contextMode.mode}\`
1099
- );
1100
-
1101
- const checkpoint = readCheckpointSummary();
1102
- if (checkpoint) parts.push(checkpoint);
1103
-
1104
- const digest = readFileText(sessionDigestPath).trim();
1105
- if (digest) parts.push("Last session:", digest);
1106
-
1107
- const activity = readRecentActivity();
1108
- if (activity.length > 0) parts.push("Recent stage activity:", ...activity);
1109
-
1110
- const warning = readLatestContextWarning();
1111
- if (warning) parts.push("Latest context warning:", warning);
1112
-
1113
- const knowledge = readKnowledgeDigest();
1114
- if (knowledge.length > 0) parts.push("Knowledge digest (top relevant entries):", ...knowledge);
1115
-
1116
- parts.push(
1117
- "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."
1118
- );
1119
-
1120
- const meta = readFileText(metaSkillPath).trim();
1121
- if (meta) parts.push("", meta);
1122
- return parts.join("\\n");
1123
- }
1124
-
1125
- let bootstrapCache = "";
1126
-
1127
- function refreshBootstrapCache() {
1128
- bootstrapCache = buildBootstrap();
1129
- }
1130
-
1131
- function getBootstrap() {
1132
- if (!bootstrapCache) refreshBootstrapCache();
1133
- return bootstrapCache;
1134
- }
1135
-
1136
- async function runHookScript(scriptFileName, payload = {}) {
1137
- const { spawnSync } = await import("node:child_process");
1138
- const scriptPath = join(root, "${RUNTIME_ROOT}/hooks/" + scriptFileName);
1139
- const input = typeof payload === "string" ? payload : JSON.stringify(payload ?? {});
1140
- try {
1141
- const result = spawnSync("bash", [scriptPath], {
1142
- cwd: root,
1143
- timeout: 20000,
1144
- stdio: ["pipe", "ignore", "ignore"],
1145
- input
1146
- });
1147
- return typeof result.status === "number" ? result.status === 0 : false;
1148
- } catch {
1149
- return false;
1150
- }
1151
- }
1152
-
1153
- function normalizeToolPayload(input, output) {
1154
- if (typeof output === "undefined") return input ?? {};
1155
- return { input: input ?? {}, output: output ?? {} };
1156
- }
1157
-
1158
- function resolveEventType(payload) {
1159
- if (typeof payload === "string") return payload;
1160
- if (payload && typeof payload === "object") {
1161
- if (typeof payload.type === "string") return payload.type;
1162
- if (typeof payload.name === "string") return payload.name;
1163
- if (payload.event && typeof payload.event === "object") {
1164
- if (typeof payload.event.type === "string") return payload.event.type;
1165
- if (typeof payload.event.name === "string") return payload.event.name;
1166
- }
1167
- }
1168
- return "";
1169
- }
1170
-
1171
- function resolveEventData(payload) {
1172
- if (payload && typeof payload === "object" && payload.event && typeof payload.event === "object") {
1173
- return payload.event;
1174
- }
1175
- return payload;
1176
- }
1177
-
1178
- ensureRuntimeDirs();
1179
-
1180
- return {
1181
- event: async (payload) => {
1182
- const eventType = resolveEventType(payload);
1183
- const eventData = resolveEventData(payload);
1184
- if (
1185
- eventType === "session.created" ||
1186
- eventType === "session.resumed" ||
1187
- eventType === "session.compacted" ||
1188
- eventType === "session.cleared"
1189
- ) {
1190
- // Avoid writing directly to stdout in lifecycle hooks because it can
1191
- // interfere with OpenCode TUI rendering. Bootstrap is injected via
1192
- // the system transform hook instead.
1193
- refreshBootstrapCache();
1194
- }
1195
- if (eventType === "session.idle") {
1196
- await runHookScript("stop-checkpoint.sh", { loop_count: 0 });
1197
- }
1198
- if (eventType === "tool.execute.before") {
1199
- const toolPayload = normalizeToolPayload(eventData, undefined);
1200
- const promptOk = await runHookScript("prompt-guard.sh", toolPayload);
1201
- const workflowOk = await runHookScript("workflow-guard.sh", toolPayload);
1202
- if (!promptOk || !workflowOk) {
1203
- throw new Error(
1204
- "cclaw OpenCode guard blocked tool.execute.before (prompt/workflow guard non-zero exit)."
1205
- );
1206
- }
1207
- }
1208
- if (eventType === "tool.execute.after") {
1209
- const toolPayload = normalizeToolPayload(eventData, undefined);
1210
- await runHookScript("context-monitor.sh", toolPayload);
1211
- }
1212
- },
1213
- "tool.execute.before": async (input, output) => {
1214
- const payload = normalizeToolPayload(input, output);
1215
- const promptOk = await runHookScript("prompt-guard.sh", payload);
1216
- const workflowOk = await runHookScript("workflow-guard.sh", payload);
1217
- if (!promptOk || !workflowOk) {
1218
- throw new Error(
1219
- "cclaw OpenCode guard blocked tool.execute.before (prompt/workflow guard non-zero exit)."
1220
- );
1221
- }
1222
- },
1223
- "tool.execute.after": async (input, output) => {
1224
- const payload = normalizeToolPayload(input, output);
1225
- await runHookScript("context-monitor.sh", payload);
1226
- },
1227
- "experimental.chat.system.transform": (payload) => {
1228
- const bootstrap = getBootstrap();
1229
- if (typeof payload === "string") {
1230
- return payload.includes("cclaw loaded.") ? payload : \`\${payload}\\n\\n\${bootstrap}\`;
1231
- }
1232
- if (payload && typeof payload === "object" && typeof payload.system === "string") {
1233
- if (payload.system.includes("cclaw loaded.")) return payload;
1234
- return { ...payload, system: \`\${payload.system}\\n\\n\${bootstrap}\` };
1235
- }
1236
- return payload;
1237
- }
1238
- };
1239
- }
1240
- `;
1241
- }
1005
+ export { opencodePluginJs } from "./opencode-plugin.js";
1242
1006
  // ---------------------------------------------------------------------------
1243
1007
  // AGENTS.md block for hooks
1244
1008
  // ---------------------------------------------------------------------------
@@ -19,9 +19,6 @@ export declare function observeScript(): string;
19
19
  export declare function contextMonitorScript(): string;
20
20
  export declare function summarizeObservationsRuntimeModule(): string;
21
21
  export declare function summarizeObservationsScript(): string;
22
- /**
23
- * Updated hooks.json generators with PreToolUse/PostToolUse observation.
24
- */
25
22
  export declare function claudeHooksJsonWithObservation(): string;
26
23
  export declare function cursorHooksJsonWithObservation(): string;
27
24
  /**
@@ -1808,6 +1808,9 @@ exit 0
1808
1808
  /**
1809
1809
  * Updated hooks.json generators with PreToolUse/PostToolUse observation.
1810
1810
  */
1811
+ function hookDispatcherCommand(scriptName) {
1812
+ return `bash ${RUNTIME_ROOT}/hooks/run-hook.cmd ${scriptName}`;
1813
+ }
1811
1814
  export function claudeHooksJsonWithObservation() {
1812
1815
  return JSON.stringify({
1813
1816
  cclawHookSchemaVersion: 1,
@@ -1816,30 +1819,30 @@ export function claudeHooksJsonWithObservation() {
1816
1819
  matcher: "startup|resume|clear|compact",
1817
1820
  hooks: [{
1818
1821
  type: "command",
1819
- command: `bash ${RUNTIME_ROOT}/hooks/session-start.sh`
1822
+ command: hookDispatcherCommand("session-start.sh")
1820
1823
  }]
1821
1824
  }],
1822
1825
  PreToolUse: [{
1823
1826
  matcher: "*",
1824
1827
  hooks: [{
1825
1828
  type: "command",
1826
- command: `bash ${RUNTIME_ROOT}/hooks/prompt-guard.sh`
1829
+ command: hookDispatcherCommand("prompt-guard.sh")
1827
1830
  }, {
1828
1831
  type: "command",
1829
- command: `bash ${RUNTIME_ROOT}/hooks/workflow-guard.sh`
1832
+ command: hookDispatcherCommand("workflow-guard.sh")
1830
1833
  }]
1831
1834
  }],
1832
1835
  PostToolUse: [{
1833
1836
  matcher: "*",
1834
1837
  hooks: [{
1835
1838
  type: "command",
1836
- command: `bash ${RUNTIME_ROOT}/hooks/context-monitor.sh`
1839
+ command: hookDispatcherCommand("context-monitor.sh")
1837
1840
  }]
1838
1841
  }],
1839
1842
  Stop: [{
1840
1843
  hooks: [{
1841
1844
  type: "command",
1842
- command: `bash ${RUNTIME_ROOT}/hooks/stop-checkpoint.sh`,
1845
+ command: hookDispatcherCommand("stop-checkpoint.sh"),
1843
1846
  timeout: 10
1844
1847
  }]
1845
1848
  }],
@@ -1847,7 +1850,7 @@ export function claudeHooksJsonWithObservation() {
1847
1850
  matcher: "manual|auto",
1848
1851
  hooks: [{
1849
1852
  type: "command",
1850
- command: `bash ${RUNTIME_ROOT}/hooks/pre-compact.sh`,
1853
+ command: hookDispatcherCommand("pre-compact.sh"),
1851
1854
  timeout: 10
1852
1855
  }]
1853
1856
  }]
@@ -1860,31 +1863,31 @@ export function cursorHooksJsonWithObservation() {
1860
1863
  version: 1,
1861
1864
  hooks: {
1862
1865
  sessionStart: [{
1863
- command: `bash ${RUNTIME_ROOT}/hooks/session-start.sh`
1866
+ command: hookDispatcherCommand("session-start.sh")
1864
1867
  }],
1865
1868
  sessionResume: [{
1866
- command: `bash ${RUNTIME_ROOT}/hooks/session-start.sh`
1869
+ command: hookDispatcherCommand("session-start.sh")
1867
1870
  }],
1868
1871
  sessionClear: [{
1869
- command: `bash ${RUNTIME_ROOT}/hooks/session-start.sh`
1872
+ command: hookDispatcherCommand("session-start.sh")
1870
1873
  }],
1871
1874
  sessionCompact: [{
1872
- command: `bash ${RUNTIME_ROOT}/hooks/pre-compact.sh`
1875
+ command: hookDispatcherCommand("pre-compact.sh")
1873
1876
  }, {
1874
- command: `bash ${RUNTIME_ROOT}/hooks/session-start.sh`
1877
+ command: hookDispatcherCommand("session-start.sh")
1875
1878
  }],
1876
1879
  preToolUse: [{
1877
1880
  matcher: "*",
1878
- command: `bash ${RUNTIME_ROOT}/hooks/prompt-guard.sh`
1881
+ command: hookDispatcherCommand("prompt-guard.sh")
1879
1882
  }, {
1880
1883
  matcher: "*",
1881
- command: `bash ${RUNTIME_ROOT}/hooks/workflow-guard.sh`
1884
+ command: hookDispatcherCommand("workflow-guard.sh")
1882
1885
  }],
1883
1886
  postToolUse: [{
1884
1887
  matcher: "*",
1885
- command: `bash ${RUNTIME_ROOT}/hooks/context-monitor.sh`
1888
+ command: hookDispatcherCommand("context-monitor.sh")
1886
1889
  }],
1887
- stop: [{ command: `bash ${RUNTIME_ROOT}/hooks/stop-checkpoint.sh`, timeout: 10 }]
1890
+ stop: [{ command: hookDispatcherCommand("stop-checkpoint.sh"), timeout: 10 }]
1888
1891
  }
1889
1892
  }, null, 2);
1890
1893
  }
@@ -1915,13 +1918,13 @@ export function codexHooksJsonWithObservation() {
1915
1918
  matcher: "startup|resume",
1916
1919
  hooks: [{
1917
1920
  type: "command",
1918
- command: `bash ${RUNTIME_ROOT}/hooks/session-start.sh`
1921
+ command: hookDispatcherCommand("session-start.sh")
1919
1922
  }]
1920
1923
  }],
1921
1924
  UserPromptSubmit: [{
1922
1925
  hooks: [{
1923
1926
  type: "command",
1924
- command: `bash ${RUNTIME_ROOT}/hooks/prompt-guard.sh`
1927
+ command: hookDispatcherCommand("prompt-guard.sh")
1925
1928
  }, {
1926
1929
  type: "command",
1927
1930
  command: "bash -lc 'if command -v cclaw >/dev/null 2>&1; then cclaw internal verify-current-state --quiet >/dev/null || true; else npx -y cclaw-cli internal verify-current-state --quiet >/dev/null || true; fi'"
@@ -1931,23 +1934,23 @@ export function codexHooksJsonWithObservation() {
1931
1934
  matcher: "Bash",
1932
1935
  hooks: [{
1933
1936
  type: "command",
1934
- command: `bash ${RUNTIME_ROOT}/hooks/prompt-guard.sh`
1937
+ command: hookDispatcherCommand("prompt-guard.sh")
1935
1938
  }, {
1936
1939
  type: "command",
1937
- command: `bash ${RUNTIME_ROOT}/hooks/workflow-guard.sh`
1940
+ command: hookDispatcherCommand("workflow-guard.sh")
1938
1941
  }]
1939
1942
  }],
1940
1943
  PostToolUse: [{
1941
1944
  matcher: "Bash",
1942
1945
  hooks: [{
1943
1946
  type: "command",
1944
- command: `bash ${RUNTIME_ROOT}/hooks/context-monitor.sh`
1947
+ command: hookDispatcherCommand("context-monitor.sh")
1945
1948
  }]
1946
1949
  }],
1947
1950
  Stop: [{
1948
1951
  hooks: [{
1949
1952
  type: "command",
1950
- command: `bash ${RUNTIME_ROOT}/hooks/stop-checkpoint.sh`,
1953
+ command: hookDispatcherCommand("stop-checkpoint.sh"),
1951
1954
  timeout: 10
1952
1955
  }]
1953
1956
  }]
@@ -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/dist/doctor.js CHANGED
@@ -622,6 +622,7 @@ export async function doctorChecks(projectRoot, options = {}) {
622
622
  for (const script of [
623
623
  "session-start.sh",
624
624
  "stop-checkpoint.sh",
625
+ "run-hook.cmd",
625
626
  "prompt-guard.sh",
626
627
  "workflow-guard.sh",
627
628
  "context-monitor.sh"
package/dist/install.js CHANGED
@@ -23,7 +23,7 @@ import { archiveCommandContract, archiveCommandSkillMarkdown } from "./content/a
23
23
  import { rewindCommandContract, rewindCommandSkillMarkdown } from "./content/rewind-command.js";
24
24
  import { subagentDrivenDevSkill, parallelAgentsSkill } from "./content/subagents.js";
25
25
  import { sessionHooksSkillMarkdown } from "./content/session-hooks.js";
26
- import { sessionStartScript, stopCheckpointScript, stageCompleteScript, preCompactScript, opencodePluginJs, claudeHooksJson, codexHooksJson, cursorHooksJson } from "./content/hooks.js";
26
+ import { sessionStartScript, stopCheckpointScript, runHookDispatcherScript, stageCompleteScript, preCompactScript, opencodePluginJs, claudeHooksJson, codexHooksJson, cursorHooksJson } from "./content/hooks.js";
27
27
  import { contextMonitorScript, promptGuardScript, workflowGuardScript } from "./content/observe.js";
28
28
  import { META_SKILL_NAME, usingCclawSkillMarkdown } from "./content/meta-skill.js";
29
29
  import { decisionProtocolMarkdown, completionProtocolMarkdown, ethosProtocolMarkdown } from "./content/protocols.js";
@@ -616,6 +616,7 @@ async function writeHooks(projectRoot, config) {
616
616
  await ensureDir(hooksDir);
617
617
  await writeFileSafe(path.join(hooksDir, "session-start.sh"), sessionStartScript());
618
618
  await writeFileSafe(path.join(hooksDir, "stop-checkpoint.sh"), stopCheckpointScript());
619
+ await writeFileSafe(path.join(hooksDir, "run-hook.cmd"), runHookDispatcherScript());
619
620
  await writeFileSafe(path.join(hooksDir, "stage-complete.sh"), stageCompleteScript());
620
621
  await writeFileSafe(path.join(hooksDir, "pre-compact.sh"), preCompactScript());
621
622
  await writeFileSafe(path.join(hooksDir, "prompt-guard.sh"), promptGuardScript({
@@ -633,6 +634,7 @@ async function writeHooks(projectRoot, config) {
633
634
  for (const script of [
634
635
  "session-start.sh",
635
636
  "stop-checkpoint.sh",
637
+ "run-hook.cmd",
636
638
  "stage-complete.sh",
637
639
  "pre-compact.sh",
638
640
  "prompt-guard.sh",
@@ -1263,7 +1265,8 @@ function stripManagedHookCommands(value) {
1263
1265
  }
1264
1266
  function isManagedRuntimeHookCommand(command) {
1265
1267
  const normalized = command.trim().replace(/\s+/gu, " ");
1266
- if (/(^|\s)(?:bash\s+)?(?:\.\/)?\.cclaw\/hooks\/(?:session-start|stop-checkpoint|pre-compact|prompt-guard|workflow-guard|context-monitor)\.sh(?:\s|$)/u.test(normalized)) {
1268
+ if (/(^|\s)(?:bash\s+)?(?:\.\/)?\.cclaw\/hooks\/(?:session-start|stop-checkpoint|pre-compact|prompt-guard|workflow-guard|context-monitor)\.sh(?:\s|$)/u.test(normalized) ||
1269
+ /(^|\s)(?:bash\s+)?(?:\.\/)?\.cclaw\/hooks\/run-hook\.cmd\s+(?:session-start|stop-checkpoint|pre-compact|prompt-guard|workflow-guard|context-monitor)(?:\.sh)?(?:\s|$)/u.test(normalized)) {
1267
1270
  return true;
1268
1271
  }
1269
1272
  // Codex UserPromptSubmit non-blocking state nudge:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.46.3",
3
+ "version": "0.46.5",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {