@hasna/hooks 0.2.20 → 0.3.1

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.
Files changed (69) hide show
  1. package/README.md +46 -2
  2. package/bin/index.js +1205 -93
  3. package/dist/db/migrations/002_session_events.d.ts +10 -0
  4. package/dist/db/migrations/003_user_prompt_submit_event.d.ts +10 -0
  5. package/dist/db/pg-migrations.d.ts +1 -1
  6. package/dist/db/remote-storage.d.ts +9 -0
  7. package/dist/db/schema.d.ts +4 -3
  8. package/dist/db/storage-sync.d.ts +57 -0
  9. package/dist/index.d.ts +16 -3
  10. package/dist/index.js +1092 -67
  11. package/dist/lib/installer.d.ts +26 -5
  12. package/dist/lib/registry.d.ts +5 -1
  13. package/dist/storage.d.ts +4 -0
  14. package/dist/storage.js +696 -0
  15. package/hooks/codewith-native-common.test.ts +61 -0
  16. package/hooks/codewith-native-common.ts +478 -0
  17. package/hooks/hook-agent-rules-version-check/LICENSE +191 -0
  18. package/hooks/hook-agent-rules-version-check/README.md +50 -0
  19. package/hooks/hook-agent-rules-version-check/package.json +53 -0
  20. package/hooks/hook-agent-rules-version-check/src/hook.test.ts +104 -0
  21. package/hooks/hook-agent-rules-version-check/src/hook.ts +215 -0
  22. package/hooks/hook-agent-rules-version-check/tsconfig.json +25 -0
  23. package/hooks/hook-announce-start/README.md +7 -5
  24. package/hooks/hook-announce-start/package.json +2 -2
  25. package/hooks/hook-announce-start/src/hook.ts +33 -14
  26. package/hooks/hook-fleet-blockers-gate/LICENSE +191 -0
  27. package/hooks/hook-fleet-blockers-gate/README.md +39 -0
  28. package/hooks/hook-fleet-blockers-gate/package.json +52 -0
  29. package/hooks/hook-fleet-blockers-gate/src/hook.test.ts +106 -0
  30. package/hooks/hook-fleet-blockers-gate/src/hook.ts +242 -0
  31. package/hooks/hook-fleet-blockers-gate/tsconfig.json +25 -0
  32. package/hooks/hook-fleet-catchup/LICENSE +191 -0
  33. package/hooks/hook-fleet-catchup/README.md +40 -0
  34. package/hooks/hook-fleet-catchup/package.json +52 -0
  35. package/hooks/hook-fleet-catchup/src/hook.test.ts +156 -0
  36. package/hooks/hook-fleet-catchup/src/hook.ts +283 -0
  37. package/hooks/hook-fleet-catchup/tsconfig.json +25 -0
  38. package/hooks/hook-knowledge-context/README.md +53 -0
  39. package/hooks/hook-knowledge-context/package.json +52 -0
  40. package/hooks/hook-knowledge-context/src/hook.test.ts +168 -0
  41. package/hooks/hook-knowledge-context/src/hook.ts +355 -0
  42. package/hooks/hook-knowledge-context/tsconfig.json +25 -0
  43. package/hooks/pre-bash/README.md +17 -0
  44. package/hooks/pre-bash/package.json +12 -0
  45. package/hooks/pre-bash/src/hook.ts +103 -0
  46. package/hooks/prompt-guard/README.md +17 -0
  47. package/hooks/prompt-guard/package.json +12 -0
  48. package/hooks/prompt-guard/src/hook.ts +76 -0
  49. package/hooks/session-start/README.md +17 -0
  50. package/hooks/session-start/package.json +12 -0
  51. package/hooks/session-start/src/hook.ts +150 -0
  52. package/hooks/stop-sync/README.md +17 -0
  53. package/hooks/stop-sync/package.json +12 -0
  54. package/hooks/stop-sync/src/hook.ts +69 -0
  55. package/hooks/worktree-guard/README.md +17 -0
  56. package/hooks/worktree-guard/package.json +12 -0
  57. package/hooks/worktree-guard/src/hook.ts +88 -0
  58. package/package.json +14 -6
  59. package/dist/cli/components/App.d.ts +0 -7
  60. package/dist/cli/components/CategorySelect.d.ts +0 -7
  61. package/dist/cli/components/DataTable.d.ts +0 -11
  62. package/dist/cli/components/Header.d.ts +0 -7
  63. package/dist/cli/components/HookSelect.d.ts +0 -11
  64. package/dist/cli/components/InstallProgress.d.ts +0 -9
  65. package/dist/cli/components/SearchView.d.ts +0 -9
  66. package/dist/cli/index.d.ts +0 -2
  67. package/dist/lib/db-writer.d.ts +0 -9
  68. package/dist/mcp/http.d.ts +0 -16
  69. package/dist/mcp/server.d.ts +0 -17
package/bin/index.js CHANGED
@@ -861,7 +861,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
861
861
  this._exitCallback = (err) => {
862
862
  if (err.code !== "commander.executeSubCommandAsync") {
863
863
  throw err;
864
- }
864
+ } else {}
865
865
  };
866
866
  }
867
867
  return this;
@@ -1890,6 +1890,9 @@ var require_commander = __commonJS((exports) => {
1890
1890
  function getHooksByCategory(category) {
1891
1891
  return HOOKS.filter((h) => h.category === category);
1892
1892
  }
1893
+ function getHookEvents(hook) {
1894
+ return hook.events && hook.events.length > 0 ? hook.events : [hook.event];
1895
+ }
1893
1896
  function searchHooks(query) {
1894
1897
  const q = query.toLowerCase();
1895
1898
  return HOOKS.filter((h) => h.name.toLowerCase().includes(q) || h.displayName.toLowerCase().includes(q) || h.description.toLowerCase().includes(q) || h.tags.some((t) => t.includes(q)));
@@ -1942,6 +1945,16 @@ var init_registry = __esm(() => {
1942
1945
  matcher: "Write|Edit|NotebookEdit",
1943
1946
  tags: ["git", "snapshot", "rollback", "backup"]
1944
1947
  },
1948
+ {
1949
+ name: "worktree-guard",
1950
+ displayName: "Worktree Guard",
1951
+ description: "Warns on feature work outside managed repos worktrees and blocks commit/push from shared checkouts",
1952
+ version: "0.1.0",
1953
+ category: "Git Safety",
1954
+ event: "PreToolUse",
1955
+ matcher: "Bash|Write|Edit|MultiEdit|NotebookEdit",
1956
+ tags: ["git", "worktree", "repos", "multi-agent", "safety"]
1957
+ },
1945
1958
  {
1946
1959
  name: "checktests",
1947
1960
  displayName: "Check Tests",
@@ -2022,6 +2035,16 @@ var init_registry = __esm(() => {
2022
2035
  matcher: "Bash",
2023
2036
  tags: ["npm", "packages", "typosquatting", "supply-chain"]
2024
2037
  },
2038
+ {
2039
+ name: "pre-bash",
2040
+ displayName: "Pre Bash",
2041
+ description: "Codewith Bash gate for staged secrets scans before commit/push and comms rechecks before risky operations",
2042
+ version: "0.1.0",
2043
+ category: "Security",
2044
+ event: "PreToolUse",
2045
+ matcher: "Bash",
2046
+ tags: ["codewith", "bash", "secrets", "gitleaks", "risky-ops"]
2047
+ },
2025
2048
  {
2026
2049
  name: "phonenotify",
2027
2050
  displayName: "Phone Notify",
@@ -2062,6 +2085,17 @@ var init_registry = __esm(() => {
2062
2085
  matcher: "",
2063
2086
  tags: ["context", "compaction", "state", "backup"]
2064
2087
  },
2088
+ {
2089
+ name: "knowledge-context",
2090
+ displayName: "Knowledge Context",
2091
+ description: "Injects deterministic Knowledge context packs into Codewith SessionStart, UserPromptSubmit, and SubagentStart",
2092
+ version: "0.1.0",
2093
+ category: "Context Management",
2094
+ event: "SessionStart",
2095
+ events: ["SessionStart", "UserPromptSubmit", "SubagentStart"],
2096
+ matcher: "",
2097
+ tags: ["knowledge", "context", "codewith", "deterministic", "session-start", "prompt", "subagent"]
2098
+ },
2065
2099
  {
2066
2100
  name: "autoformat",
2067
2101
  displayName: "Auto Format",
@@ -2132,6 +2166,16 @@ var init_registry = __esm(() => {
2132
2166
  matcher: "",
2133
2167
  tags: ["prompt", "injection", "security", "validation", "guard"]
2134
2168
  },
2169
+ {
2170
+ name: "prompt-guard",
2171
+ displayName: "Prompt Guard (Codewith)",
2172
+ description: "Codewith UserPromptSubmit guard for obvious pasted prompt-injection and fake policy/freeze instructions",
2173
+ version: "0.1.0",
2174
+ category: "Permissions",
2175
+ event: "UserPromptSubmit",
2176
+ matcher: "",
2177
+ tags: ["codewith", "prompt", "injection", "policy", "freeze"]
2178
+ },
2135
2179
  {
2136
2180
  name: "desktopnotify",
2137
2181
  displayName: "Desktop Notify",
@@ -2222,6 +2266,26 @@ var init_registry = __esm(() => {
2222
2266
  matcher: "",
2223
2267
  tags: ["tasks", "completion", "gate", "quality", "agent-teams"]
2224
2268
  },
2269
+ {
2270
+ name: "session-start",
2271
+ displayName: "Session Start",
2272
+ description: "Codewith SessionStart digest for blockers, announcements, identity registration, and heartbeat context",
2273
+ version: "0.1.0",
2274
+ category: "Agent Teams",
2275
+ event: "SessionStart",
2276
+ matcher: "",
2277
+ tags: ["codewith", "session", "context", "conversations", "heartbeat"]
2278
+ },
2279
+ {
2280
+ name: "stop-sync",
2281
+ displayName: "Stop Sync",
2282
+ description: "Codewith Stop turn-end heartbeat and best-effort task evidence helper",
2283
+ version: "0.1.0",
2284
+ category: "Agent Teams",
2285
+ event: "Stop",
2286
+ matcher: "",
2287
+ tags: ["codewith", "stop", "heartbeat", "todos", "turn-end"]
2288
+ },
2225
2289
  {
2226
2290
  name: "typecheck-gate",
2227
2291
  displayName: "Typecheck Gate",
@@ -2275,10 +2339,10 @@ var init_registry = __esm(() => {
2275
2339
  {
2276
2340
  name: "announce-start",
2277
2341
  displayName: "Announce Start",
2278
- description: "On session start, auto-registers agent, reads messages, and announces to space",
2279
- version: "0.1.0",
2342
+ description: "On SessionStart, auto-registers agent, reads messages, and announces to space",
2343
+ version: "0.2.0",
2280
2344
  category: "Agent Teams",
2281
- event: "Notification",
2345
+ event: "SessionStart",
2282
2346
  matcher: "",
2283
2347
  tags: ["announcement", "start", "register", "messages", "agent-teams"]
2284
2348
  },
@@ -2301,6 +2365,36 @@ var init_registry = __esm(() => {
2301
2365
  event: "Notification",
2302
2366
  matcher: "",
2303
2367
  tags: ["messages", "dm", "context", "inject", "agent-teams"]
2368
+ },
2369
+ {
2370
+ name: "fleet-catchup",
2371
+ displayName: "Fleet Catchup",
2372
+ description: "On SessionStart, injects unread blockers, channel notifications, and the announcements digest into context (deterministic CLI reads, fail-open)",
2373
+ version: "0.1.0",
2374
+ category: "Agent Teams",
2375
+ event: "SessionStart",
2376
+ matcher: "",
2377
+ tags: ["fleet", "catchup", "blockers", "announcements", "context", "agent-teams"]
2378
+ },
2379
+ {
2380
+ name: "agent-rules-version-check",
2381
+ displayName: "Agent Rules Version Check",
2382
+ description: "On SessionStart, compares the rendered hasna:agent-operating-rules sentinel version against configs state and warns on mismatch",
2383
+ version: "0.1.0",
2384
+ category: "Agent Teams",
2385
+ event: "SessionStart",
2386
+ matcher: "",
2387
+ tags: ["fleet", "rules", "version", "sentinel", "configs", "drift", "agent-teams"]
2388
+ },
2389
+ {
2390
+ name: "fleet-blockers-gate",
2391
+ displayName: "Fleet Blockers Gate",
2392
+ description: "Before tool calls, blocks mutating tools while an unread [FREEZE] blocker is active (TTL-cached conversations check, fail-open <500ms)",
2393
+ version: "0.1.0",
2394
+ category: "Agent Teams",
2395
+ event: "PreToolUse",
2396
+ matcher: "",
2397
+ tags: ["fleet", "freeze", "blockers", "gate", "safety", "agent-teams"]
2304
2398
  }
2305
2399
  ];
2306
2400
  });
@@ -3954,28 +4048,39 @@ function normalizeHookName(name) {
3954
4048
  return name.startsWith("hook-") ? name : `hook-${name}`;
3955
4049
  }
3956
4050
  function shortHookName(name) {
3957
- return normalizeHookName(name).replace("hook-", "");
4051
+ return name.startsWith("hook-") ? name.slice("hook-".length) : name;
3958
4052
  }
3959
4053
  function removeHookEntriesByName(entries, hookName) {
3960
4054
  return entries.filter((entry) => !entry.hooks?.some((h) => {
3961
- const match = h.command?.match(/^hooks run (\w+)/);
4055
+ const match = h.command?.match(/^hooks run ([\w-]+)/);
3962
4056
  return match && match[1] === hookName;
3963
4057
  }));
3964
4058
  }
3965
4059
  function getTargetSettingsDir(target) {
4060
+ if (target === "codewith")
4061
+ return ".codewith";
3966
4062
  if (target === "gemini")
3967
4063
  return ".gemini";
3968
4064
  return ".claude";
3969
4065
  }
3970
- function getSettingsPath(scope = "global", target = "claude") {
4066
+ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath) {
4067
+ if (target === "codewith" && codewithConfigPath)
4068
+ return codewithConfigPath;
4069
+ if (target === "codewith" && process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH) {
4070
+ return process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH;
4071
+ }
3971
4072
  const dir = getTargetSettingsDir(target);
3972
4073
  if (scope === "project") {
3973
- return join2(process.cwd(), dir, "settings.json");
4074
+ return target === "codewith" ? join2(process.cwd(), dir, "config.toml") : join2(process.cwd(), dir, "settings.json");
3974
4075
  }
3975
- return join2(homedir2(), dir, "settings.json");
4076
+ return target === "codewith" ? join2(homedir2(), dir, "config.toml") : join2(homedir2(), dir, "settings.json");
3976
4077
  }
3977
4078
  function getHookPath(name) {
3978
- return join2(HOOKS_DIR, normalizeHookName(name));
4079
+ const shortName = shortHookName(name);
4080
+ const direct = join2(HOOKS_DIR, shortName);
4081
+ if (existsSync2(direct))
4082
+ return direct;
4083
+ return join2(HOOKS_DIR, normalizeHookName(shortName));
3979
4084
  }
3980
4085
  function hookExists(name) {
3981
4086
  return existsSync2(getHookPath(name));
@@ -4001,18 +4106,118 @@ function writeSettings(settings, scope = "global", target = "claude") {
4001
4106
  `);
4002
4107
  }
4003
4108
  function getTargetEventName(internalEvent, target) {
4004
- return EVENT_MAP[target]?.[internalEvent] || internalEvent;
4109
+ return EVENT_MAP[target]?.[internalEvent] ?? null;
4110
+ }
4111
+ function isEventSupported(internalEvent, target) {
4112
+ return getTargetEventName(internalEvent, target) !== null;
4113
+ }
4114
+ function tomlString(value) {
4115
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
4116
+ }
4117
+ function codewithMatcher(matcher) {
4118
+ if (!matcher)
4119
+ return;
4120
+ if (matcher.startsWith("^"))
4121
+ return matcher;
4122
+ if (matcher.includes("|"))
4123
+ return `^(${matcher})$`;
4124
+ return `^${matcher}$`;
4125
+ }
4126
+ function codewithTimeout(name) {
4127
+ switch (name) {
4128
+ case "knowledge-context":
4129
+ return 2;
4130
+ case "session-start":
4131
+ return 8;
4132
+ case "pre-bash":
4133
+ return 20;
4134
+ case "prompt-guard":
4135
+ return 3;
4136
+ case "worktree-guard":
4137
+ case "stop-sync":
4138
+ return 5;
4139
+ default:
4140
+ return 10;
4141
+ }
4142
+ }
4143
+ function codewithStatusMessage(name) {
4144
+ switch (name) {
4145
+ case "knowledge-context":
4146
+ return "Loading Knowledge context";
4147
+ case "session-start":
4148
+ return "Checking Hasna session context";
4149
+ case "pre-bash":
4150
+ return "Checking Bash safety";
4151
+ case "prompt-guard":
4152
+ return "Checking prompt safety";
4153
+ case "worktree-guard":
4154
+ return "Checking worktree safety";
4155
+ case "stop-sync":
4156
+ return "Syncing turn-end heartbeat";
4157
+ default:
4158
+ return `Running ${name}`;
4159
+ }
4160
+ }
4161
+ function buildCodewithTomlFragment(name, profile) {
4162
+ const shortName = shortHookName(name);
4163
+ const meta = getHook(shortName);
4164
+ if (!meta)
4165
+ throw new Error(`Hook '${shortName}' not found`);
4166
+ const command = profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`;
4167
+ const matcher = codewithMatcher(meta.matcher);
4168
+ const fragments = [];
4169
+ for (const event of getHookEvents(meta)) {
4170
+ const eventKey = getTargetEventName(event, "codewith");
4171
+ if (!eventKey) {
4172
+ throw new Error(`Hook '${shortName}' uses event '${event}', which is not supported by the Codewith target`);
4173
+ }
4174
+ const lines = [
4175
+ `[[hooks.${eventKey}]]`
4176
+ ];
4177
+ if (matcher)
4178
+ lines.push(`matcher = ${tomlString(matcher)}`);
4179
+ lines.push("", `[[hooks.${eventKey}.hooks]]`, `type = "command"`, `command = ${tomlString(command)}`, `timeout = ${codewithTimeout(shortName)}`, `statusMessage = ${tomlString(codewithStatusMessage(shortName))}`);
4180
+ fragments.push(lines.join(`
4181
+ `));
4182
+ }
4183
+ return `${fragments.join(`
4184
+
4185
+ `)}
4186
+ `;
4187
+ }
4188
+ function readCodewithConfig(scope, configPath) {
4189
+ const path = getSettingsPath(scope, "codewith", configPath);
4190
+ try {
4191
+ return existsSync2(path) ? readFileSync(path, "utf-8") : "";
4192
+ } catch {
4193
+ return "";
4194
+ }
4195
+ }
4196
+ function appendCodewithFragment(fragment, scope, configPath) {
4197
+ const path = getSettingsPath(scope, "codewith", configPath);
4198
+ const dir = dirname(path);
4199
+ if (!existsSync2(dir))
4200
+ mkdirSync(dir, { recursive: true });
4201
+ const existing = existsSync2(path) ? readFileSync(path, "utf-8") : "";
4202
+ const sep = existing.trim() ? `
4203
+
4204
+ ` : "";
4205
+ writeFileSync(path, `${existing.replace(/\s*$/, "")}${sep}${fragment}`);
4206
+ return path;
4005
4207
  }
4006
4208
  function detectConflict(name, scope, target) {
4007
4209
  const meta = getHook(name);
4008
4210
  if (!meta || !meta.matcher)
4009
4211
  return;
4212
+ const events = new Set(getHookEvents(meta));
4010
4213
  const registered = getRegisteredHooksForTarget(scope, target);
4011
4214
  for (const existingName of registered) {
4012
4215
  if (existingName === name)
4013
4216
  continue;
4014
4217
  const existing = getHook(existingName);
4015
- if (!existing || existing.event !== meta.event || !existing.matcher)
4218
+ if (!existing || !existing.matcher)
4219
+ continue;
4220
+ if (!getHookEvents(existing).some((event) => events.has(event)))
4016
4221
  continue;
4017
4222
  const a = meta.matcher.toLowerCase();
4018
4223
  const b = existing.matcher.toLowerCase();
@@ -4022,11 +4227,60 @@ function detectConflict(name, scope, target) {
4022
4227
  }
4023
4228
  return;
4024
4229
  }
4025
- function installForTarget(name, scope, overwrite, target, profile) {
4230
+ function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath) {
4026
4231
  const shortName = shortHookName(name);
4027
4232
  if (!hookExists(shortName)) {
4028
4233
  return { hook: shortName, success: false, error: `Hook '${shortName}' not found`, target };
4029
4234
  }
4235
+ if (target === "codewith") {
4236
+ try {
4237
+ const fragment = buildCodewithTomlFragment(shortName, profile);
4238
+ if (codewithMode !== "write") {
4239
+ return {
4240
+ hook: shortName,
4241
+ success: true,
4242
+ scope,
4243
+ target,
4244
+ fragment,
4245
+ applied: false,
4246
+ note: "Codewith install is fragment-only by default; open-configs should own applying this TOML."
4247
+ };
4248
+ }
4249
+ if (!codewithConfigPath) {
4250
+ return {
4251
+ hook: shortName,
4252
+ success: false,
4253
+ error: "Direct Codewith writes require an explicit --codewith-config path; refusing to write default ~/.codewith/config.toml.",
4254
+ scope,
4255
+ target,
4256
+ fragment,
4257
+ applied: false
4258
+ };
4259
+ }
4260
+ const existing = readCodewithConfig(scope, codewithConfigPath);
4261
+ if (!overwrite && new RegExp(`command\\s*=\\s*["']hooks run ${shortName}(?:\\s|["'])`).test(existing)) {
4262
+ return { hook: shortName, success: false, error: "Already installed. Use --overwrite to append another fragment.", scope, target };
4263
+ }
4264
+ const path = appendCodewithFragment(fragment, scope, codewithConfigPath);
4265
+ return {
4266
+ hook: shortName,
4267
+ success: true,
4268
+ scope,
4269
+ target,
4270
+ fragment,
4271
+ applied: true,
4272
+ configPath: path,
4273
+ note: "Direct Codewith config write was explicitly requested; prefer open-configs for managed machines."
4274
+ };
4275
+ } catch (error) {
4276
+ return {
4277
+ hook: shortName,
4278
+ success: false,
4279
+ error: error instanceof Error ? error.message : "Unknown error",
4280
+ target
4281
+ };
4282
+ }
4283
+ }
4030
4284
  const registered = getRegisteredHooksForTarget(scope, target);
4031
4285
  if (registered.includes(shortName) && !overwrite) {
4032
4286
  return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope, target };
@@ -4045,35 +4299,92 @@ function installForTarget(name, scope, overwrite, target, profile) {
4045
4299
  }
4046
4300
  }
4047
4301
  function installHook(name, options = {}) {
4048
- const { scope = "global", overwrite = false, target = "claude", profile } = options;
4302
+ const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath } = options;
4049
4303
  if (target === "all") {
4304
+ const shortName = shortHookName(name);
4305
+ const meta = getHook(shortName);
4306
+ if (meta) {
4307
+ const unsupportedTargets = ["claude", "gemini", "codewith"].filter((agentTarget) => getHookEvents(meta).some((event) => !isEventSupported(event, agentTarget)));
4308
+ if (unsupportedTargets.length > 0) {
4309
+ return {
4310
+ hook: shortName,
4311
+ success: false,
4312
+ error: `Event(s) '${getHookEvents(meta).join(", ")}' are not supported by target(s): ${unsupportedTargets.join(", ")}`,
4313
+ target: "all"
4314
+ };
4315
+ }
4316
+ }
4050
4317
  const claudeResult = installForTarget(name, scope, overwrite, "claude", profile);
4051
- installForTarget(name, scope, overwrite, "gemini", profile);
4318
+ if (!claudeResult.success) {
4319
+ return {
4320
+ ...claudeResult,
4321
+ error: `Failed for target 'claude': ${claudeResult.error}`,
4322
+ target: "all"
4323
+ };
4324
+ }
4325
+ const geminiResult = installForTarget(name, scope, overwrite, "gemini", profile);
4326
+ if (!geminiResult.success) {
4327
+ return {
4328
+ ...geminiResult,
4329
+ error: `Failed for target 'gemini': ${geminiResult.error}`,
4330
+ target: "all"
4331
+ };
4332
+ }
4333
+ const codewithResult = installForTarget(name, scope, overwrite, "codewith", profile, codewithMode, codewithConfigPath);
4334
+ if (!codewithResult.success) {
4335
+ return {
4336
+ ...codewithResult,
4337
+ error: `Failed for target 'codewith': ${codewithResult.error}`,
4338
+ target: "all"
4339
+ };
4340
+ }
4052
4341
  return { ...claudeResult, target: "all" };
4053
4342
  }
4054
- return installForTarget(name, scope, overwrite, target, profile);
4343
+ return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath);
4055
4344
  }
4056
4345
  function registerHook(name, scope = "global", target = "claude", profile) {
4057
4346
  const meta = getHook(name);
4058
4347
  if (!meta)
4059
4348
  return;
4349
+ const eventKeys = getHookEvents(meta).map((event) => {
4350
+ const eventKey = getTargetEventName(event, target);
4351
+ if (eventKey === null) {
4352
+ throw new Error(`Event '${event}' is not supported by target '${target}'`);
4353
+ }
4354
+ return eventKey;
4355
+ });
4356
+ const uniqueEventKeys = [...new Set(eventKeys)];
4357
+ if (uniqueEventKeys.length === 0) {
4358
+ throw new Error(`Hook '${name}' has no installable events for target '${target}'`);
4359
+ }
4060
4360
  const settings = readSettings(scope, target);
4061
4361
  if (!settings.hooks)
4062
4362
  settings.hooks = {};
4063
- const eventKey = getTargetEventName(meta.event, target);
4064
- if (!settings.hooks[eventKey])
4065
- settings.hooks[eventKey] = [];
4066
- settings.hooks[eventKey] = removeHookEntriesByName(settings.hooks[eventKey], name);
4363
+ removeHookFromAllEvents(settings, name);
4067
4364
  const hookCommand = profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`;
4068
- const entry = {
4069
- hooks: [{ type: "command", command: hookCommand }]
4070
- };
4071
- if (meta.matcher) {
4072
- entry.matcher = meta.matcher;
4365
+ for (const eventKey of uniqueEventKeys) {
4366
+ if (!settings.hooks[eventKey])
4367
+ settings.hooks[eventKey] = [];
4368
+ const entry = {
4369
+ hooks: [{ type: "command", command: hookCommand }]
4370
+ };
4371
+ if (meta.matcher) {
4372
+ entry.matcher = meta.matcher;
4373
+ }
4374
+ settings.hooks[eventKey].push(entry);
4073
4375
  }
4074
- settings.hooks[eventKey].push(entry);
4075
4376
  writeSettings(settings, scope, target);
4076
4377
  }
4378
+ function removeHookFromAllEvents(settings, name) {
4379
+ if (!settings.hooks)
4380
+ return;
4381
+ for (const key of Object.keys(settings.hooks)) {
4382
+ settings.hooks[key] = removeHookEntriesByName(settings.hooks[key], name);
4383
+ if (settings.hooks[key].length === 0) {
4384
+ delete settings.hooks[key];
4385
+ }
4386
+ }
4387
+ }
4077
4388
  function unregisterHook(name, scope = "global", target = "claude") {
4078
4389
  const meta = getHook(name);
4079
4390
  if (!meta)
@@ -4081,19 +4392,23 @@ function unregisterHook(name, scope = "global", target = "claude") {
4081
4392
  const settings = readSettings(scope, target);
4082
4393
  if (!settings.hooks)
4083
4394
  return;
4084
- const eventKey = getTargetEventName(meta.event, target);
4085
- if (!settings.hooks[eventKey])
4086
- return;
4087
- settings.hooks[eventKey] = removeHookEntriesByName(settings.hooks[eventKey], name);
4088
- if (settings.hooks[eventKey].length === 0) {
4089
- delete settings.hooks[eventKey];
4090
- }
4395
+ removeHookFromAllEvents(settings, name);
4091
4396
  if (Object.keys(settings.hooks).length === 0) {
4092
4397
  delete settings.hooks;
4093
4398
  }
4094
4399
  writeSettings(settings, scope, target);
4095
4400
  }
4096
4401
  function getRegisteredHooksForTarget(scope = "global", target = "claude") {
4402
+ if (target === "codewith") {
4403
+ const config = readCodewithConfig(scope);
4404
+ const registered2 = [];
4405
+ const re = /command\s*=\s*["']hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?["']/g;
4406
+ let match;
4407
+ while (match = re.exec(config)) {
4408
+ registered2.push(match[1]);
4409
+ }
4410
+ return [...new Set(registered2)];
4411
+ }
4097
4412
  const settings = readSettings(scope, target);
4098
4413
  if (!settings.hooks)
4099
4414
  return [];
@@ -4101,8 +4416,8 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
4101
4416
  for (const eventKey of Object.keys(settings.hooks)) {
4102
4417
  for (const entry of settings.hooks[eventKey]) {
4103
4418
  for (const hook of entry.hooks || []) {
4104
- const newMatch = hook.command?.match(/^hooks run (\w+)(?:\s+--profile\s+\w+)?$/);
4105
- const oldMatch = hook.command?.match(/^hook-(\w+)$/);
4419
+ const newMatch = hook.command?.match(/^hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?$/);
4420
+ const oldMatch = hook.command?.match(/^hook-([\w-]+)$/);
4106
4421
  const match = newMatch || oldMatch;
4107
4422
  if (match) {
4108
4423
  registered.push(match[1]);
@@ -4122,6 +4437,9 @@ function removeHook(name, scope = "global", target = "claude") {
4122
4437
  const geminiRemoved = removeHookForTarget(shortName, scope, "gemini");
4123
4438
  return claudeRemoved || geminiRemoved;
4124
4439
  }
4440
+ if (target === "codewith") {
4441
+ return false;
4442
+ }
4125
4443
  return removeHookForTarget(shortName, scope, target);
4126
4444
  }
4127
4445
  function removeHookForTarget(name, scope, target) {
@@ -4142,13 +4460,31 @@ var init_installer = __esm(() => {
4142
4460
  PreToolUse: "PreToolUse",
4143
4461
  PostToolUse: "PostToolUse",
4144
4462
  Stop: "Stop",
4145
- Notification: "Notification"
4463
+ Notification: "Notification",
4464
+ SessionStart: "SessionStart",
4465
+ SessionEnd: "SessionEnd",
4466
+ UserPromptSubmit: null,
4467
+ SubagentStart: null
4146
4468
  },
4147
4469
  gemini: {
4148
4470
  PreToolUse: "BeforeTool",
4149
4471
  PostToolUse: "AfterTool",
4150
4472
  Stop: "AfterAgent",
4151
- Notification: "Notification"
4473
+ Notification: "Notification",
4474
+ SessionStart: null,
4475
+ SessionEnd: null,
4476
+ UserPromptSubmit: null,
4477
+ SubagentStart: null
4478
+ },
4479
+ codewith: {
4480
+ PreToolUse: "PreToolUse",
4481
+ PostToolUse: "PostToolUse",
4482
+ Stop: "Stop",
4483
+ Notification: null,
4484
+ SessionStart: "SessionStart",
4485
+ SessionEnd: null,
4486
+ UserPromptSubmit: "UserPromptSubmit",
4487
+ SubagentStart: "SubagentStart"
4152
4488
  }
4153
4489
  };
4154
4490
  getInstalledHooks = getRegisteredHooks;
@@ -4266,7 +4602,7 @@ var CREATE_HOOK_EVENTS_TABLE = `
4266
4602
  timestamp TEXT NOT NULL,
4267
4603
  session_id TEXT NOT NULL,
4268
4604
  hook_name TEXT NOT NULL,
4269
- event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification')),
4605
+ event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit')),
4270
4606
  tool_name TEXT,
4271
4607
  tool_input TEXT,
4272
4608
  result TEXT CHECK (result IN ('continue', 'block', NULL)),
@@ -4297,6 +4633,64 @@ var init_001_initial = __esm(() => {
4297
4633
  init_schema();
4298
4634
  });
4299
4635
 
4636
+ // src/db/migrations/002_session_events.ts
4637
+ function up2(db) {
4638
+ const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("hook_events");
4639
+ if (!row?.sql)
4640
+ return;
4641
+ if (row.sql.includes("SessionStart"))
4642
+ return;
4643
+ db.exec("BEGIN");
4644
+ try {
4645
+ db.exec("ALTER TABLE hook_events RENAME TO hook_events_old");
4646
+ db.exec(CREATE_HOOK_EVENTS_TABLE);
4647
+ db.exec(`INSERT INTO hook_events
4648
+ (id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata)
4649
+ SELECT id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata
4650
+ FROM hook_events_old`);
4651
+ db.exec("DROP TABLE hook_events_old");
4652
+ for (const idx of CREATE_INDEXES) {
4653
+ db.exec(idx);
4654
+ }
4655
+ db.exec("COMMIT");
4656
+ } catch (err) {
4657
+ db.exec("ROLLBACK");
4658
+ throw err;
4659
+ }
4660
+ }
4661
+ var init_002_session_events = __esm(() => {
4662
+ init_schema();
4663
+ });
4664
+
4665
+ // src/db/migrations/003_user_prompt_submit_event.ts
4666
+ function up3(db) {
4667
+ const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("hook_events");
4668
+ if (!row?.sql)
4669
+ return;
4670
+ if (row.sql.includes("UserPromptSubmit"))
4671
+ return;
4672
+ db.exec("BEGIN");
4673
+ try {
4674
+ db.exec("ALTER TABLE hook_events RENAME TO hook_events_old");
4675
+ db.exec(CREATE_HOOK_EVENTS_TABLE);
4676
+ db.exec(`INSERT INTO hook_events
4677
+ (id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata)
4678
+ SELECT id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata
4679
+ FROM hook_events_old`);
4680
+ db.exec("DROP TABLE hook_events_old");
4681
+ for (const idx of CREATE_INDEXES) {
4682
+ db.exec(idx);
4683
+ }
4684
+ db.exec("COMMIT");
4685
+ } catch (err) {
4686
+ db.exec("ROLLBACK");
4687
+ throw err;
4688
+ }
4689
+ }
4690
+ var init_003_user_prompt_submit_event = __esm(() => {
4691
+ init_schema();
4692
+ });
4693
+
4300
4694
  // src/db/migrations/index.ts
4301
4695
  function ensureMigrationsTable(db) {
4302
4696
  db.exec(`
@@ -4326,7 +4720,13 @@ function runMigrations(db) {
4326
4720
  var MIGRATIONS;
4327
4721
  var init_migrations = __esm(() => {
4328
4722
  init_001_initial();
4329
- MIGRATIONS = [{ version: "001_initial", up }];
4723
+ init_002_session_events();
4724
+ init_003_user_prompt_submit_event();
4725
+ MIGRATIONS = [
4726
+ { version: "001_initial", up },
4727
+ { version: "002_session_events", up: up2 },
4728
+ { version: "003_user_prompt_submit_event", up: up3 }
4729
+ ];
4330
4730
  });
4331
4731
 
4332
4732
  // src/db/legacy-import.ts
@@ -4538,6 +4938,396 @@ var init_db = __esm(() => {
4538
4938
  init_legacy_import();
4539
4939
  });
4540
4940
 
4941
+ // src/db/pg-migrations.ts
4942
+ var PG_MIGRATIONS;
4943
+ var init_pg_migrations = __esm(() => {
4944
+ PG_MIGRATIONS = [
4945
+ `CREATE TABLE IF NOT EXISTS hook_events (
4946
+ id TEXT PRIMARY KEY,
4947
+ timestamp TEXT NOT NULL,
4948
+ session_id TEXT NOT NULL,
4949
+ hook_name TEXT NOT NULL,
4950
+ event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit')),
4951
+ tool_name TEXT,
4952
+ tool_input TEXT,
4953
+ result TEXT CHECK (result IN ('continue', 'block', NULL)),
4954
+ error TEXT,
4955
+ duration_ms INTEGER,
4956
+ project_dir TEXT,
4957
+ metadata TEXT
4958
+ )`,
4959
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_timestamp ON hook_events (timestamp)`,
4960
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_session_id ON hook_events (session_id)`,
4961
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_hook_name ON hook_events (hook_name)`,
4962
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_event_type ON hook_events (event_type)`,
4963
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_errors ON hook_events (timestamp) WHERE error IS NOT NULL`,
4964
+ `CREATE TABLE IF NOT EXISTS schema_migrations (
4965
+ version TEXT PRIMARY KEY,
4966
+ applied_at TEXT NOT NULL
4967
+ )`,
4968
+ `CREATE TABLE IF NOT EXISTS _meta (
4969
+ key TEXT PRIMARY KEY,
4970
+ value TEXT NOT NULL
4971
+ )`,
4972
+ `CREATE TABLE IF NOT EXISTS feedback (
4973
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
4974
+ message TEXT NOT NULL,
4975
+ email TEXT,
4976
+ category TEXT DEFAULT 'general',
4977
+ version TEXT,
4978
+ machine_id TEXT,
4979
+ created_at TEXT NOT NULL DEFAULT NOW()::text
4980
+ )`,
4981
+ `ALTER TABLE hook_events DROP CONSTRAINT IF EXISTS hook_events_event_type_check`,
4982
+ `ALTER TABLE hook_events ADD CONSTRAINT hook_events_event_type_check
4983
+ CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit'))`
4984
+ ];
4985
+ });
4986
+
4987
+ // src/db/remote-storage.ts
4988
+ import pg from "pg";
4989
+ function translatePlaceholders(sql) {
4990
+ let index = 0;
4991
+ return sql.replace(/\?/g, () => `$${++index}`);
4992
+ }
4993
+ function normalizeParams(params) {
4994
+ const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
4995
+ return flat.map((value) => value === undefined ? null : value);
4996
+ }
4997
+ function sslConfigFor(connectionString) {
4998
+ return connectionString.includes("sslmode=require") || connectionString.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
4999
+ }
5000
+
5001
+ class PgAdapterAsync {
5002
+ pool;
5003
+ constructor(connectionString) {
5004
+ this.pool = new pg.Pool({ connectionString, ssl: sslConfigFor(connectionString) });
5005
+ }
5006
+ async run(sql, ...params) {
5007
+ const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
5008
+ return { changes: result.rowCount ?? 0 };
5009
+ }
5010
+ async all(sql, ...params) {
5011
+ const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
5012
+ return result.rows;
5013
+ }
5014
+ async close() {
5015
+ await this.pool.end();
5016
+ }
5017
+ }
5018
+ var init_remote_storage = () => {};
5019
+
5020
+ // src/db/storage-sync.ts
5021
+ function readEnv(name) {
5022
+ const value = process.env[name]?.trim();
5023
+ return value || undefined;
5024
+ }
5025
+ function normalizeStorageMode(value) {
5026
+ const normalized = value?.trim().toLowerCase();
5027
+ if (normalized === "local" || normalized === "hybrid" || normalized === "remote")
5028
+ return normalized;
5029
+ return;
5030
+ }
5031
+ function getStorageDatabaseEnvName() {
5032
+ for (const name of STORAGE_DATABASE_ENV) {
5033
+ if (readEnv(name))
5034
+ return name;
5035
+ }
5036
+ return null;
5037
+ }
5038
+ function getStorageDatabaseEnv() {
5039
+ const name = getStorageDatabaseEnvName();
5040
+ return name ? { name } : null;
5041
+ }
5042
+ function getStorageDatabaseUrl() {
5043
+ const env = getStorageDatabaseEnv();
5044
+ return env ? readEnv(env.name) ?? null : null;
5045
+ }
5046
+ function getStorageMode() {
5047
+ const mode = normalizeStorageMode(readEnv(HOOKS_STORAGE_MODE_ENV)) ?? normalizeStorageMode(readEnv(HOOKS_STORAGE_MODE_FALLBACK_ENV));
5048
+ if (mode)
5049
+ return mode;
5050
+ return getStorageDatabaseUrl() ? "hybrid" : "local";
5051
+ }
5052
+ async function getStoragePg() {
5053
+ const url = getStorageDatabaseUrl();
5054
+ if (!url) {
5055
+ throw new Error("Missing HASNA_HOOKS_DATABASE_URL or HOOKS_DATABASE_URL");
5056
+ }
5057
+ return new PgAdapterAsync(url);
5058
+ }
5059
+ async function runStorageMigrations(remote) {
5060
+ await remote.run("CREATE EXTENSION IF NOT EXISTS pgcrypto");
5061
+ for (const sql of PG_MIGRATIONS)
5062
+ await remote.run(sql);
5063
+ }
5064
+ async function storagePush(options) {
5065
+ const remote = await getStoragePg();
5066
+ const db = getDb();
5067
+ try {
5068
+ await runStorageMigrations(remote);
5069
+ const results = [];
5070
+ for (const table of resolveTables(options?.tables)) {
5071
+ results.push(await pushTable(db, remote, table));
5072
+ }
5073
+ recordSyncMeta(db, "push", results);
5074
+ return results;
5075
+ } finally {
5076
+ await remote.close();
5077
+ }
5078
+ }
5079
+ async function storagePull(options) {
5080
+ const remote = await getStoragePg();
5081
+ const db = getDb();
5082
+ try {
5083
+ await runStorageMigrations(remote);
5084
+ const results = [];
5085
+ for (const table of resolveTables(options?.tables)) {
5086
+ results.push(await pullTable(remote, db, table));
5087
+ }
5088
+ recordSyncMeta(db, "pull", results);
5089
+ return results;
5090
+ } finally {
5091
+ await remote.close();
5092
+ }
5093
+ }
5094
+ async function storageSync(options) {
5095
+ const pull = await storagePull(options);
5096
+ const push = await storagePush(options);
5097
+ return { pull, push };
5098
+ }
5099
+ function getSyncMetaAll() {
5100
+ const db = getDb();
5101
+ ensureSyncMetaTable(db);
5102
+ return db.query("SELECT table_name, last_synced_at, direction FROM _hooks_sync_meta ORDER BY table_name, direction").all();
5103
+ }
5104
+ function getStorageStatus() {
5105
+ const activeEnv = getStorageDatabaseEnv();
5106
+ return {
5107
+ configured: Boolean(activeEnv),
5108
+ mode: getStorageMode(),
5109
+ env: STORAGE_DATABASE_ENV,
5110
+ activeEnv: activeEnv?.name ?? null,
5111
+ service: "hooks",
5112
+ tables: STORAGE_TABLES,
5113
+ sync: getSyncMetaAll()
5114
+ };
5115
+ }
5116
+ function resolveTables(tables) {
5117
+ if (!tables || tables.length === 0)
5118
+ return [...STORAGE_TABLES];
5119
+ const allowed = new Set(STORAGE_TABLES);
5120
+ const requested = tables.map((table) => table.trim()).filter(Boolean);
5121
+ const invalid = requested.filter((table) => !allowed.has(table));
5122
+ if (invalid.length > 0)
5123
+ throw new Error(`Unknown hooks sync table(s): ${invalid.join(", ")}`);
5124
+ return requested;
5125
+ }
5126
+ function parseStorageTables(value) {
5127
+ if (!value)
5128
+ return;
5129
+ return resolveTables(Array.isArray(value) ? value : value.split(","));
5130
+ }
5131
+ async function pushTable(db, remote, table) {
5132
+ const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
5133
+ try {
5134
+ if (!tableExists(db, table))
5135
+ return result;
5136
+ const rows = db.query(`SELECT * FROM ${quoteIdent(table)}`).all();
5137
+ result.rowsRead = rows.length;
5138
+ if (rows.length === 0)
5139
+ return result;
5140
+ const remoteColumns = await getRemoteColumns(remote, table);
5141
+ const columns = filterRemoteColumns(remoteColumns, Object.keys(rows[0]));
5142
+ result.rowsWritten = await upsertPg(remote, table, columns, rows, remoteColumns);
5143
+ } catch (error) {
5144
+ result.errors.push(error instanceof Error ? error.message : String(error));
5145
+ }
5146
+ return result;
5147
+ }
5148
+ async function pullTable(remote, db, table) {
5149
+ const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
5150
+ try {
5151
+ if (!tableExists(db, table))
5152
+ return result;
5153
+ const rows = await remote.all(`SELECT * FROM ${quoteIdent(table)}`);
5154
+ result.rowsRead = rows.length;
5155
+ if (rows.length === 0)
5156
+ return result;
5157
+ const columns = filterLocalColumns(db, table, Object.keys(rows[0]));
5158
+ result.rowsWritten = upsertSqlite(db, table, columns, rows);
5159
+ } catch (error) {
5160
+ result.errors.push(error instanceof Error ? error.message : String(error));
5161
+ }
5162
+ return result;
5163
+ }
5164
+ async function getRemoteColumns(remote, table) {
5165
+ const rows = await remote.all("SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = 'public' AND table_name = ?", table);
5166
+ return new Map(rows.map((row) => [row.column_name, row.data_type]));
5167
+ }
5168
+ function filterRemoteColumns(remoteColumns, columns) {
5169
+ if (remoteColumns.size === 0)
5170
+ return columns;
5171
+ return columns.filter((column) => remoteColumns.has(column));
5172
+ }
5173
+ function filterLocalColumns(db, table, columns) {
5174
+ const rows = db.query(`PRAGMA table_info(${quoteIdent(table)})`).all();
5175
+ const allowed = new Set(rows.map((row) => row.name));
5176
+ return columns.filter((column) => allowed.has(column));
5177
+ }
5178
+ async function upsertPg(remote, table, columns, rows, remoteColumns) {
5179
+ if (columns.length === 0)
5180
+ return 0;
5181
+ const primaryKeys = PRIMARY_KEYS[table];
5182
+ const columnList = columns.map(quoteIdent).join(", ");
5183
+ const placeholders = columns.map(() => "?").join(", ");
5184
+ const keyList = primaryKeys.map(quoteIdent).join(", ");
5185
+ const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
5186
+ const fallbackKey = primaryKeys[0];
5187
+ const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = EXCLUDED.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = EXCLUDED.${quoteIdent(fallbackKey)}`;
5188
+ for (const row of rows) {
5189
+ await remote.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders})
5190
+ ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`, ...columns.map((column) => coerceForPg(row[column], remoteColumns.get(column))));
5191
+ }
5192
+ return rows.length;
5193
+ }
5194
+ function upsertSqlite(db, table, columns, rows) {
5195
+ if (columns.length === 0)
5196
+ return 0;
5197
+ const primaryKeys = PRIMARY_KEYS[table];
5198
+ const columnList = columns.map(quoteIdent).join(", ");
5199
+ const placeholders = columns.map(() => "?").join(", ");
5200
+ const keyList = primaryKeys.map(quoteIdent).join(", ");
5201
+ const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
5202
+ const fallbackKey = primaryKeys[0];
5203
+ const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = excluded.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = excluded.${quoteIdent(fallbackKey)}`;
5204
+ const statement = db.query(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders})
5205
+ ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`);
5206
+ const insert = db.transaction((batch) => {
5207
+ for (const row of batch)
5208
+ statement.run(...columns.map((column) => coerceForSqlite(row[column])));
5209
+ });
5210
+ insert(rows);
5211
+ return rows.length;
5212
+ }
5213
+ function recordSyncMeta(db, direction, results) {
5214
+ ensureSyncMetaTable(db);
5215
+ const now2 = new Date().toISOString();
5216
+ const statement = db.query(`
5217
+ INSERT INTO _hooks_sync_meta (table_name, last_synced_at, direction)
5218
+ VALUES (?, ?, ?)
5219
+ ON CONFLICT(table_name, direction) DO UPDATE SET last_synced_at = excluded.last_synced_at
5220
+ `);
5221
+ for (const result of results) {
5222
+ if (result.errors.length > 0)
5223
+ continue;
5224
+ statement.run(result.table, now2, direction);
5225
+ }
5226
+ }
5227
+ function ensureSyncMetaTable(db) {
5228
+ db.exec(`
5229
+ CREATE TABLE IF NOT EXISTS _hooks_sync_meta (
5230
+ table_name TEXT NOT NULL,
5231
+ last_synced_at TEXT,
5232
+ direction TEXT NOT NULL CHECK(direction IN ('push', 'pull')),
5233
+ PRIMARY KEY (table_name, direction)
5234
+ )
5235
+ `);
5236
+ }
5237
+ function tableExists(db, table) {
5238
+ const row = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
5239
+ return Boolean(row);
5240
+ }
5241
+ function quoteIdent(identifier) {
5242
+ return `"${identifier.replace(/"/g, '""')}"`;
5243
+ }
5244
+ function coerceForPg(value, dataType) {
5245
+ if (value === undefined || value === null)
5246
+ return null;
5247
+ if (dataType === "boolean") {
5248
+ if (typeof value === "boolean")
5249
+ return value;
5250
+ if (typeof value === "number")
5251
+ return value !== 0;
5252
+ if (typeof value === "string")
5253
+ return value === "1" || value.toLowerCase() === "true";
5254
+ }
5255
+ if (value instanceof Date)
5256
+ return value.toISOString();
5257
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array)
5258
+ return value;
5259
+ if (typeof value === "object")
5260
+ return JSON.stringify(value);
5261
+ return value;
5262
+ }
5263
+ function coerceForSqlite(value) {
5264
+ if (value === undefined || value === null)
5265
+ return null;
5266
+ if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
5267
+ return value;
5268
+ if (value instanceof Date)
5269
+ return value.toISOString();
5270
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array)
5271
+ return value;
5272
+ if (typeof value === "object")
5273
+ return JSON.stringify(value);
5274
+ return String(value);
5275
+ }
5276
+ var STORAGE_TABLES, HOOKS_STORAGE_TABLES, HOOKS_STORAGE_ENV = "HASNA_HOOKS_DATABASE_URL", HOOKS_STORAGE_FALLBACK_ENV = "HOOKS_DATABASE_URL", HOOKS_STORAGE_MODE_ENV = "HASNA_HOOKS_STORAGE_MODE", HOOKS_STORAGE_MODE_FALLBACK_ENV = "HOOKS_STORAGE_MODE", STORAGE_DATABASE_ENV, STORAGE_MODE_ENV, PRIMARY_KEYS;
5277
+ var init_storage_sync = __esm(() => {
5278
+ init_db();
5279
+ init_pg_migrations();
5280
+ init_remote_storage();
5281
+ STORAGE_TABLES = [
5282
+ "hook_events",
5283
+ "schema_migrations",
5284
+ "_meta",
5285
+ "feedback"
5286
+ ];
5287
+ HOOKS_STORAGE_TABLES = STORAGE_TABLES;
5288
+ STORAGE_DATABASE_ENV = [HOOKS_STORAGE_ENV, HOOKS_STORAGE_FALLBACK_ENV];
5289
+ STORAGE_MODE_ENV = [HOOKS_STORAGE_MODE_ENV, HOOKS_STORAGE_MODE_FALLBACK_ENV];
5290
+ PRIMARY_KEYS = {
5291
+ hook_events: ["id"],
5292
+ schema_migrations: ["version"],
5293
+ _meta: ["key"],
5294
+ feedback: ["id"]
5295
+ };
5296
+ });
5297
+
5298
+ // src/storage.ts
5299
+ var exports_storage = {};
5300
+ __export(exports_storage, {
5301
+ storageSync: () => storageSync,
5302
+ storagePush: () => storagePush,
5303
+ storagePull: () => storagePull,
5304
+ runStorageMigrations: () => runStorageMigrations,
5305
+ resolveTables: () => resolveTables,
5306
+ parseStorageTables: () => parseStorageTables,
5307
+ getSyncMetaAll: () => getSyncMetaAll,
5308
+ getStorageStatus: () => getStorageStatus,
5309
+ getStoragePg: () => getStoragePg,
5310
+ getStorageMode: () => getStorageMode,
5311
+ getStorageDatabaseUrl: () => getStorageDatabaseUrl,
5312
+ getStorageDatabaseEnvName: () => getStorageDatabaseEnvName,
5313
+ getStorageDatabaseEnv: () => getStorageDatabaseEnv,
5314
+ STORAGE_TABLES: () => STORAGE_TABLES,
5315
+ STORAGE_MODE_ENV: () => STORAGE_MODE_ENV,
5316
+ STORAGE_DATABASE_ENV: () => STORAGE_DATABASE_ENV,
5317
+ PgAdapterAsync: () => PgAdapterAsync,
5318
+ PG_MIGRATIONS: () => PG_MIGRATIONS,
5319
+ HOOKS_STORAGE_TABLES: () => HOOKS_STORAGE_TABLES,
5320
+ HOOKS_STORAGE_MODE_FALLBACK_ENV: () => HOOKS_STORAGE_MODE_FALLBACK_ENV,
5321
+ HOOKS_STORAGE_MODE_ENV: () => HOOKS_STORAGE_MODE_ENV,
5322
+ HOOKS_STORAGE_FALLBACK_ENV: () => HOOKS_STORAGE_FALLBACK_ENV,
5323
+ HOOKS_STORAGE_ENV: () => HOOKS_STORAGE_ENV
5324
+ });
5325
+ var init_storage = __esm(() => {
5326
+ init_storage_sync();
5327
+ init_remote_storage();
5328
+ init_pg_migrations();
5329
+ });
5330
+
4541
5331
  // src/mcp/server.ts
4542
5332
  var exports_server = {};
4543
5333
  __export(exports_server, {
@@ -4671,7 +5461,7 @@ function createHooksServer() {
4671
5461
  const settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
4672
5462
  const eventHooks = settings.hooks?.[meta.event] || [];
4673
5463
  const found = eventHooks.some((entry) => entry.hooks?.some((h) => {
4674
- const match = h.command?.match(/^hooks run (\w+)/);
5464
+ const match = h.command?.match(/^hooks run ([\w-]+)/);
4675
5465
  return match && match[1] === name;
4676
5466
  }));
4677
5467
  if (!found) {
@@ -4714,8 +5504,10 @@ function createHooksServer() {
4714
5504
  events: {
4715
5505
  PreToolUse: "Fires before a tool executes. Can block the operation.",
4716
5506
  PostToolUse: "Fires after a tool executes. Runs asynchronously.",
4717
- Stop: "Fires when a session ends. Useful for notifications.",
4718
- Notification: "Fires on notification events like context compaction."
5507
+ Stop: "Fires when the agent finishes responding. Useful for notifications.",
5508
+ Notification: "Fires on notification events like context compaction.",
5509
+ SessionStart: "Fires when a session starts or resumes. Can inject context.",
5510
+ SessionEnd: "Fires when a session terminates. Useful for cleanup and final announcements."
4719
5511
  },
4720
5512
  commands: {
4721
5513
  install: "hooks install <name>",
@@ -5129,6 +5921,10 @@ function createHooksServer() {
5129
5921
  }]
5130
5922
  };
5131
5923
  });
5924
+ server.tool("storage_status", "Show hooks storage sync configuration and local sync history.", {}, async () => ({ content: [{ type: "text", text: JSON.stringify(getStorageStatus()) }] }));
5925
+ server.tool("storage_push", "Push local hook data to storage PostgreSQL.", { tables: z.array(z.string()).optional() }, async (params) => ({ content: [{ type: "text", text: JSON.stringify(await storagePush(params.tables ? { tables: params.tables } : undefined)) }] }));
5926
+ server.tool("storage_pull", "Pull hook data from storage PostgreSQL to local SQLite.", { tables: z.array(z.string()).optional() }, async (params) => ({ content: [{ type: "text", text: JSON.stringify(await storagePull(params.tables ? { tables: params.tables } : undefined)) }] }));
5927
+ server.tool("storage_sync", "Bidirectional hooks storage sync: pull then push.", { tables: z.array(z.string()).optional() }, async (params) => ({ content: [{ type: "text", text: JSON.stringify(await storageSync(params.tables ? { tables: params.tables } : undefined)) }] }));
5132
5928
  server.tool("send_feedback", "Send feedback about this service", {
5133
5929
  message: z.string(),
5134
5930
  email: z.string().optional(),
@@ -5236,6 +6032,7 @@ var init_server = __esm(() => {
5236
6032
  init_registry();
5237
6033
  init_installer();
5238
6034
  init_profiles();
6035
+ init_storage();
5239
6036
  __dirname3 = dirname2(fileURLToPath2(import.meta.url));
5240
6037
  pkg = { name: "@hasna/hooks", version: "0.0.0" };
5241
6038
  try {
@@ -5326,29 +6123,83 @@ function getPathValue(input, path) {
5326
6123
  return;
5327
6124
  }, input);
5328
6125
  }
5329
- function wildcardToRegExp(pattern) {
5330
- const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
5331
- return new RegExp(`^${escaped}$`);
6126
+ function getFieldValues(input, path) {
6127
+ const values = [];
6128
+ const push = (value) => {
6129
+ if (!values.some((item) => Object.is(item, value)))
6130
+ values.push(value);
6131
+ };
6132
+ if (path.includes(".") && path in input)
6133
+ push(input[path]);
6134
+ const nestedValue = getPathValue(input, path);
6135
+ if (nestedValue !== undefined || !path.includes("."))
6136
+ push(nestedValue);
6137
+ return values;
5332
6138
  }
5333
- function matchString(value, matcher) {
6139
+ function wildcardToRegExp(pattern, options = {}) {
6140
+ let body = "";
6141
+ for (let index = 0;index < pattern.length; index += 1) {
6142
+ const char = pattern[index];
6143
+ if (char === "*") {
6144
+ if (pattern[index + 1] === "*") {
6145
+ body += ".*";
6146
+ index += 1;
6147
+ } else {
6148
+ body += options.segmentSafe ? "[^/]*" : ".*";
6149
+ }
6150
+ } else {
6151
+ body += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
6152
+ }
6153
+ }
6154
+ return new RegExp(`^${body}$`);
6155
+ }
6156
+ function matchString(value, matcher, options = {}) {
5334
6157
  if (matcher === undefined)
5335
6158
  return true;
5336
6159
  if (value === undefined)
5337
6160
  return false;
5338
6161
  const matchers = Array.isArray(matcher) ? matcher : [matcher];
5339
- return matchers.some((item) => wildcardToRegExp(item).test(value));
6162
+ return matchers.some((item) => wildcardToRegExp(item, options).test(value));
5340
6163
  }
5341
6164
  function matchRecord(input, matcher) {
5342
6165
  if (!matcher)
5343
6166
  return true;
5344
6167
  return Object.entries(matcher).every(([path, expected]) => {
5345
- const actual = getPathValue(input, path);
5346
- if (typeof expected === "string" || Array.isArray(expected)) {
5347
- return matchString(actual === undefined ? undefined : String(actual), expected);
5348
- }
5349
- return actual === expected;
6168
+ const actualValues = getFieldValues(input, path);
6169
+ return matchField(actualValues, expected, path);
5350
6170
  });
5351
6171
  }
6172
+ function matchField(actualValues, expected, path) {
6173
+ if (isNegativeMatcher(expected)) {
6174
+ return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
6175
+ }
6176
+ return actualValues.some((actual) => matchPositiveField(actual, expected, path));
6177
+ }
6178
+ function matchPositiveField(actual, expected, path) {
6179
+ if (typeof expected === "string" || Array.isArray(expected)) {
6180
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
6181
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
6182
+ }));
6183
+ }
6184
+ if (Array.isArray(actual)) {
6185
+ return actual.some((item) => item === expected);
6186
+ }
6187
+ return actual === expected;
6188
+ }
6189
+ function stringCandidates(actual) {
6190
+ if (actual === undefined)
6191
+ return [];
6192
+ if (Array.isArray(actual)) {
6193
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
6194
+ }
6195
+ return [String(actual)];
6196
+ }
6197
+ function isPrimitiveFieldValue(value) {
6198
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
6199
+ }
6200
+ function isNegativeMatcher(value) {
6201
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
6202
+ }
5352
6203
  function eventMatchesFilter(event, filter) {
5353
6204
  return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
5354
6205
  }
@@ -5364,6 +6215,13 @@ var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
5364
6215
  function getEventsDataDir(override) {
5365
6216
  return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
5366
6217
  }
6218
+ function getActiveEventsDirEnv() {
6219
+ if (process.env[HASNA_EVENTS_DIR_ENV])
6220
+ return HASNA_EVENTS_DIR_ENV;
6221
+ if (process.env[HASNA_EVENTS_HOME_ENV])
6222
+ return HASNA_EVENTS_HOME_ENV;
6223
+ return null;
6224
+ }
5367
6225
 
5368
6226
  class JsonEventsStore {
5369
6227
  dataDir;
@@ -5476,6 +6334,52 @@ class JsonEventsStore {
5476
6334
  });
5477
6335
  }
5478
6336
  }
6337
+ async function getEventsStatus(dataDir) {
6338
+ const store = new JsonEventsStore(dataDir);
6339
+ await store.init();
6340
+ const [channels, events, deliveries] = await Promise.all([
6341
+ store.listChannels(),
6342
+ store.listEvents(),
6343
+ store.listDeliveries()
6344
+ ]);
6345
+ const transports = channels.reduce((counts, channel) => {
6346
+ counts[channel.transport] = (counts[channel.transport] ?? 0) + 1;
6347
+ return counts;
6348
+ }, {});
6349
+ return {
6350
+ service: "events",
6351
+ schemaVersion: "1.0",
6352
+ dataDir: store.dataDir,
6353
+ env: {
6354
+ primary: HASNA_EVENTS_DIR_ENV,
6355
+ fallback: HASNA_EVENTS_HOME_ENV,
6356
+ active: getActiveEventsDirEnv()
6357
+ },
6358
+ files: {
6359
+ channels: statusFile(store.dataDir, "channels.json", channels.length),
6360
+ events: statusFile(store.dataDir, "events.json", events.length),
6361
+ deliveries: statusFile(store.dataDir, "deliveries.json", deliveries.length)
6362
+ },
6363
+ counts: {
6364
+ channels: channels.length,
6365
+ enabledChannels: channels.filter((channel) => channel.enabled).length,
6366
+ disabledChannels: channels.filter((channel) => !channel.enabled).length,
6367
+ events: events.length,
6368
+ deliveries: deliveries.length
6369
+ },
6370
+ transports,
6371
+ safety: {
6372
+ includesEventPayloads: false,
6373
+ includesWebhookSecrets: false,
6374
+ listOutputsRedactSecrets: true,
6375
+ statusOutputIsMetadataOnly: true
6376
+ }
6377
+ };
6378
+ }
6379
+ function statusFile(dataDir, fileName, records) {
6380
+ const path = join(dataDir, fileName);
6381
+ return { path, exists: existsSync(path), records };
6382
+ }
5479
6383
  var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
5480
6384
  function buildSignatureBase(timestamp, body) {
5481
6385
  return `${timestamp}.${body}`;
@@ -5701,7 +6605,7 @@ class EventsClient {
5701
6605
  }
5702
6606
  return deliveries;
5703
6607
  }
5704
- async testChannel(id, input = {}) {
6608
+ async matchChannel(id, input = {}) {
5705
6609
  const channel = await this.store.getChannel(id);
5706
6610
  if (!channel)
5707
6611
  throw new Error(`Channel not found: ${id}`);
@@ -5718,6 +6622,34 @@ class EventsClient {
5718
6622
  time: input.time,
5719
6623
  id: input.id
5720
6624
  });
6625
+ const matched = channelMatchesEvent(channel, event);
6626
+ return {
6627
+ channelId: channel.id,
6628
+ matched,
6629
+ event,
6630
+ filters: channel.filters,
6631
+ reason: matched ? undefined : channel.enabled ? "event did not match channel filters" : "channel is disabled"
6632
+ };
6633
+ }
6634
+ async testChannel(id, input = {}, options = {}) {
6635
+ const channel = await this.store.getChannel(id);
6636
+ if (!channel)
6637
+ throw new Error(`Channel not found: ${id}`);
6638
+ const match = await this.matchChannel(id, input);
6639
+ const event = match.event;
6640
+ if (options.honorFilters && !match.matched) {
6641
+ const timestamp = new Date().toISOString();
6642
+ const result2 = createDeliveryResult(event, channel, [{
6643
+ attempt: 1,
6644
+ status: "skipped",
6645
+ startedAt: timestamp,
6646
+ completedAt: timestamp,
6647
+ error: match.reason
6648
+ }]);
6649
+ result2.metadata = { reason: "filter_mismatch" };
6650
+ await this.store.appendDelivery(result2);
6651
+ return result2;
6652
+ }
5721
6653
  const eventForChannel = await this.applyRedaction(event, channel);
5722
6654
  const result = await this.deliverWithRetry(eventForChannel, channel);
5723
6655
  await this.store.appendDelivery(result);
@@ -5828,6 +6760,76 @@ function normalizeRetryPolicy(policy) {
5828
6760
  multiplier: Math.max(1, policy?.multiplier ?? 2)
5829
6761
  };
5830
6762
  }
6763
+ function parseFieldMatchers(values, label, typed = false) {
6764
+ if (!values?.length)
6765
+ return;
6766
+ const result = {};
6767
+ for (const value of values) {
6768
+ const parsed = parseMatcherExpression(value, label);
6769
+ const path = parsed.path;
6770
+ if (path in result)
6771
+ throw new Error(`Duplicate ${label} filter path: ${path}`);
6772
+ const matcherValue = typed ? parseTypedMatcherValue(parsed.rawValue, label) : parsed.rawValue;
6773
+ result[path] = parsed.negated ? { not: matcherValue } : matcherValue;
6774
+ }
6775
+ return result;
6776
+ }
6777
+ function parseFilterOptions(options) {
6778
+ const filter2 = {};
6779
+ if (options.source)
6780
+ filter2.source = options.source;
6781
+ if (options.type)
6782
+ filter2.type = options.type;
6783
+ if (options.subject)
6784
+ filter2.subject = options.subject;
6785
+ if (options.severity)
6786
+ filter2.severity = options.severity;
6787
+ const data = mergeMatchers(parseFieldMatchers(options.data, "data"), parseFieldMatchers(options.dataJson, "data-json", true));
6788
+ const metadata = mergeMatchers(parseFieldMatchers(options.metadata, "metadata"), parseFieldMatchers(options.metadataJson, "metadata-json", true));
6789
+ if (Object.keys(data).length > 0)
6790
+ filter2.data = data;
6791
+ if (Object.keys(metadata).length > 0)
6792
+ filter2.metadata = metadata;
6793
+ return Object.keys(filter2).length > 0 ? [filter2] : undefined;
6794
+ }
6795
+ function mergeMatchers(...records) {
6796
+ const result = {};
6797
+ for (const record of records) {
6798
+ if (!record)
6799
+ continue;
6800
+ for (const [path, value] of Object.entries(record)) {
6801
+ if (path in result)
6802
+ throw new Error(`Duplicate filter path: ${path}`);
6803
+ result[path] = value;
6804
+ }
6805
+ }
6806
+ return result;
6807
+ }
6808
+ function parseTypedMatcherValue(value, label) {
6809
+ const parsed = JSON.parse(value);
6810
+ if (parsed === null || typeof parsed === "string" || typeof parsed === "number" || typeof parsed === "boolean" || Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
6811
+ return parsed;
6812
+ }
6813
+ throw new Error(`${label} filter JSON values must be string, string[], number, boolean, or null`);
6814
+ }
6815
+ function parseMatcherExpression(value, label) {
6816
+ const negativeSeparator = value.indexOf("!=");
6817
+ if (negativeSeparator > 0) {
6818
+ return {
6819
+ path: value.slice(0, negativeSeparator),
6820
+ rawValue: value.slice(negativeSeparator + 2),
6821
+ negated: true
6822
+ };
6823
+ }
6824
+ const separator = value.indexOf("=");
6825
+ if (separator <= 0)
6826
+ throw new Error(`Invalid ${label} filter, expected path=value or path!=value: ${value}`);
6827
+ return {
6828
+ path: value.slice(0, separator),
6829
+ rawValue: value.slice(separator + 1),
6830
+ negated: false
6831
+ };
6832
+ }
5831
6833
  function parseJsonObject(value, fallback) {
5832
6834
  if (!value)
5833
6835
  return fallback;
@@ -5849,18 +6851,6 @@ function parseHeaders(values) {
5849
6851
  }
5850
6852
  return headers;
5851
6853
  }
5852
- function parseFilter(options) {
5853
- const filter2 = {};
5854
- if (options.source)
5855
- filter2.source = options.source;
5856
- if (options.type)
5857
- filter2.type = options.type;
5858
- if (options.subject)
5859
- filter2.subject = options.subject;
5860
- if (options.severity)
5861
- filter2.severity = options.severity;
5862
- return Object.keys(filter2).length > 0 ? [filter2] : undefined;
5863
- }
5864
6854
  function createClient(options) {
5865
6855
  if (options.createClient)
5866
6856
  return options.createClient();
@@ -5878,16 +6868,16 @@ function hasJsonOption(options) {
5878
6868
  function wantsJson(actionOptions, command) {
5879
6869
  return hasJsonOption(actionOptions) || hasJsonOption(command);
5880
6870
  }
5881
- function registerWebhookCommands(program, options) {
5882
- const webhooks = program.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
5883
- webhooks.command("add").description("Add or replace a webhook or command subscription").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Subscription/channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions, command) => {
6871
+ function registerChannelCommands(program, options) {
6872
+ const channels = program.command(options.channelsCommandName ?? "channels").description("Manage Hasna event channels");
6873
+ channels.command("add").description("Add or replace a channel").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--data <path=value...>", "Event data field filter; string values, path!=value negatives, array-member matching, dot paths, * segment wildcard, ** recursive wildcard", collectValues, []).option("--metadata <path=value...>", "Event metadata field filter; string values, path!=value negatives, array-member matching, dot paths, * segment wildcard, ** recursive wildcard", collectValues, []).option("--data-json <path=json...>", "Event data field filter with typed JSON value; path!=json negatives supported", collectValues, []).option("--metadata-json <path=json...>", "Event metadata field filter with typed JSON value; path!=json negatives supported", collectValues, []).option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions, command) => {
5884
6874
  const timestamp = new Date().toISOString();
5885
6875
  const channel = {
5886
6876
  id: actionOptions.id,
5887
6877
  name: actionOptions.name,
5888
6878
  enabled: !actionOptions.disabled,
5889
6879
  transport: actionOptions.transport,
5890
- filters: parseFilter(actionOptions),
6880
+ filters: parseFilterOptions(actionOptions),
5891
6881
  retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
5892
6882
  redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
5893
6883
  createdAt: timestamp,
@@ -5903,35 +6893,51 @@ function registerWebhookCommands(program, options) {
5903
6893
  const saved = await createClient(options).addChannel(channel);
5904
6894
  print(sanitizeChannelForOutput(saved), wantsJson(actionOptions, command), `Added ${saved.transport} channel ${saved.id}`);
5905
6895
  });
5906
- webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
5907
- const channels = await createClient(options).listChannels();
6896
+ channels.command("list").description("List configured channels").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
6897
+ const channels2 = await createClient(options).listChannels();
5908
6898
  if (wantsJson(actionOptions, command)) {
5909
- console.log(JSON.stringify(sanitizeChannelsForOutput(channels), null, 2));
6899
+ console.log(JSON.stringify(sanitizeChannelsForOutput(channels2), null, 2));
5910
6900
  return;
5911
6901
  }
5912
- if (!channels.length) {
6902
+ if (!channels2.length) {
5913
6903
  console.log("No channels configured.");
5914
6904
  return;
5915
6905
  }
5916
- for (const channel of channels) {
6906
+ for (const channel of channels2) {
5917
6907
  console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
5918
6908
  }
5919
6909
  });
5920
- webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
6910
+ channels.command("status").description("Show events channel storage status").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
6911
+ const status = await getEventsStatus(options.dataDir);
6912
+ print(status, wantsJson(actionOptions, command), `events dataDir: ${status.dataDir}`);
6913
+ });
6914
+ channels.command("remove").description("Remove a channel").argument("<id>", "Channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
5921
6915
  const removed = await createClient(options).removeChannel(id);
5922
6916
  print({ removed }, wantsJson(actionOptions, command), removed ? `Removed ${id}` : `Channel not found: ${id}`);
5923
6917
  });
5924
- webhooks.command("test").description("Send a test event to one subscription").argument("<id>", "Subscription/channel identifier").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
6918
+ channels.command("test").description("Send a test event to one channel").argument("<id>", "Channel identifier").option("--source <source>", "Event source override").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--honor-filters", "Skip delivery when the sample event does not match channel filters", false).option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
5925
6919
  const result = await createClient(options).testChannel(id, {
5926
- source: options.source,
6920
+ source: actionOptions.source ?? options.source,
5927
6921
  type: actionOptions.type,
5928
6922
  subject: actionOptions.subject ?? id,
5929
6923
  message: actionOptions.message,
5930
- data: parseJsonObject(actionOptions.data, { test: true })
5931
- });
6924
+ data: parseJsonObject(actionOptions.data, { test: true }),
6925
+ metadata: parseJsonObject(actionOptions.metadata, {})
6926
+ }, { honorFilters: actionOptions.honorFilters });
5932
6927
  print(result, wantsJson(actionOptions, command), `${result.status}: ${result.channelId}`);
5933
6928
  });
5934
- return webhooks;
6929
+ channels.command("match").description("Check whether a sample event matches one channel without delivering").argument("<id>", "Channel identifier").option("--source <source>", "Event source override").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events match preview").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
6930
+ const result = await createClient(options).matchChannel(id, {
6931
+ source: actionOptions.source ?? options.source,
6932
+ type: actionOptions.type,
6933
+ subject: actionOptions.subject ?? id,
6934
+ message: actionOptions.message,
6935
+ data: parseJsonObject(actionOptions.data, { test: true }),
6936
+ metadata: parseJsonObject(actionOptions.metadata, {})
6937
+ });
6938
+ print(result, wantsJson(actionOptions, command), `${result.matched ? "matched" : "skipped"}: ${result.channelId}`);
6939
+ });
6940
+ return channels;
5935
6941
  }
5936
6942
  function registerEventCommands(program, options) {
5937
6943
  const events = program.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
@@ -5979,7 +6985,7 @@ function registerEventCommands(program, options) {
5979
6985
  return events;
5980
6986
  }
5981
6987
  function registerEventsCommands(program, options) {
5982
- registerWebhookCommands(program, options);
6988
+ registerChannelCommands(program, options);
5983
6989
  registerEventCommands(program, options);
5984
6990
  }
5985
6991
  function parseNumber(value) {
@@ -7204,6 +8210,8 @@ function resolveScope(options) {
7204
8210
  function resolveTarget(options) {
7205
8211
  if (options.target === "gemini")
7206
8212
  return "gemini";
8213
+ if (options.target === "codewith")
8214
+ return "codewith";
7207
8215
  if (options.target === "all")
7208
8216
  return "all";
7209
8217
  return "claude";
@@ -7302,7 +8310,7 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
7302
8310
  process.stderr.write(stderr);
7303
8311
  process.exit(exitCode);
7304
8312
  });
7305
- program2.command("install").alias("add").argument("[hooks...]", "Hooks to install").option("-o, --overwrite", "Overwrite existing hooks", false).option("-a, --all", "Install all available hooks", false).option("-c, --category <category>", "Install all hooks in a category").option("-g, --global", "Install globally (~/.claude/settings.json)", false).option("-p, --project", "Install for current project (.claude/settings.json)", false).option("-t, --target <target>", "Agent target: claude, gemini, all (default: claude)", "claude").option("--profile <id>", "Agent profile ID to scope hooks to").option("--dry-run", "Preview what would be installed without writing to settings", false).option("-j, --json", "Output as JSON", false).description("Install one or more hooks").action((hooks, options) => {
8313
+ program2.command("install").alias("add").argument("[hooks...]", "Hooks to install").option("-o, --overwrite", "Overwrite existing hooks", false).option("-a, --all", "Install all available hooks", false).option("-c, --category <category>", "Install all hooks in a category").option("-g, --global", "Install globally (~/.claude/settings.json)", false).option("-p, --project", "Install for current project (.claude/settings.json)", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, all (default: claude)", "claude").option("--profile <id>", "Agent profile ID to scope hooks to").option("--dry-run", "Preview what would be installed without writing to settings", false).option("--apply-codewith", "Explicitly append Codewith TOML to a config file (prefer open-configs for managed configs)", false).option("--codewith-config <path>", "Explicit Codewith config path required with --apply-codewith").option("-j, --json", "Output as JSON", false).description("Install one or more hooks").action((hooks, options) => {
7306
8314
  const scope = resolveScope(options);
7307
8315
  const target = resolveTarget(options);
7308
8316
  let toInstall = hooks;
@@ -7325,11 +8333,20 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
7325
8333
  render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
7326
8334
  return;
7327
8335
  }
8336
+ if (options.applyCodewith && (target === "codewith" || target === "all") && !options.codewithConfig) {
8337
+ const message = "--apply-codewith requires --codewith-config <path>; refusing to write default ~/.codewith/config.toml.";
8338
+ if (options.json) {
8339
+ console.log(JSON.stringify({ error: message, scope, target, applied: false }));
8340
+ } else {
8341
+ console.log(chalk2.red(message));
8342
+ }
8343
+ return;
8344
+ }
7328
8345
  if (options.dryRun) {
7329
8346
  const known = toInstall.filter((n) => getHook(n));
7330
8347
  const unknown = toInstall.filter((n) => !getHook(n));
7331
8348
  if (options.json) {
7332
- console.log(JSON.stringify({ dryRun: true, would_install: known, unknown, scope, target }));
8349
+ console.log(JSON.stringify({ dryRun: true, would_install: known, unknown, scope, target, mode: target === "codewith" ? "fragment" : "write" }));
7333
8350
  return;
7334
8351
  }
7335
8352
  console.log(chalk2.bold(`
@@ -7356,21 +8373,30 @@ Dry run \u2014 would install (${scope}, ${target}):
7356
8373
  results.push({ hook: name, success: false, error: `Hook '${name}' not found${hint}` });
7357
8374
  continue;
7358
8375
  }
7359
- const result = installHook(name, { scope, overwrite: options.overwrite, target, profile: options.profile });
8376
+ const result = installHook(name, {
8377
+ scope,
8378
+ overwrite: options.overwrite,
8379
+ target,
8380
+ profile: options.profile,
8381
+ codewithMode: options.applyCodewith ? "write" : "fragment",
8382
+ codewithConfigPath: options.codewithConfig
8383
+ });
7360
8384
  results.push(result);
7361
8385
  }
7362
8386
  if (options.json) {
7363
8387
  console.log(JSON.stringify({
7364
8388
  installed: results.filter((r) => r.success).map((r) => r.hook),
7365
8389
  failed: results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error })),
8390
+ fragments: results.filter((r) => r.success && r.fragment).map((r) => ({ hook: r.hook, fragment: r.fragment, applied: r.applied, configPath: r.configPath, note: r.note })),
7366
8391
  total: results.length,
7367
8392
  success: results.filter((r) => r.success).length,
7368
8393
  scope,
7369
- target
8394
+ target,
8395
+ applied: results.some((r) => r.applied)
7370
8396
  }));
7371
8397
  return;
7372
8398
  }
7373
- const settingsFile = scope === "project" ? ".claude/settings.json" : "~/.claude/settings.json";
8399
+ const settingsFile = target === "codewith" ? options.applyCodewith ? options.codewithConfig : "TOML fragment only (open-configs should apply)" : scope === "project" ? ".claude/settings.json" : "~/.claude/settings.json";
7374
8400
  console.log(chalk2.bold(`
7375
8401
  Installing hooks (${scope}, ${target})...
7376
8402
  `));
@@ -7384,6 +8410,14 @@ Installing hooks (${scope}, ${target})...
7384
8410
  if (result.conflict) {
7385
8411
  console.log(chalk2.yellow(` \u26A0 Warning: ${result.conflict}`));
7386
8412
  }
8413
+ if (result.fragment && target === "codewith") {
8414
+ console.log(chalk2.dim(" Codewith TOML fragment:"));
8415
+ console.log(chalk2.cyan(result.fragment.trimEnd().split(`
8416
+ `).map((line) => ` ${line}`).join(`
8417
+ `)));
8418
+ if (result.note)
8419
+ console.log(chalk2.yellow(` \u26A0 ${result.note}`));
8420
+ }
7387
8421
  } else {
7388
8422
  console.log(chalk2.red(`\u2717 ${result.hook}: ${result.error}`));
7389
8423
  }
@@ -7391,10 +8425,10 @@ Installing hooks (${scope}, ${target})...
7391
8425
  console.log(chalk2.dim(`
7392
8426
  Registered in ${settingsFile}`));
7393
8427
  });
7394
- program2.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available hooks", false).option("-i, --installed", "Show only installed hooks", false).option("-r, --registered", "Show registered hooks", false).option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-t, --target <target>", "Agent target: claude, gemini (default: claude)", "claude").option("-j, --json", "Output as JSON", false).description("List available or installed hooks").action((options) => {
8428
+ program2.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available hooks", false).option("-i, --installed", "Show only installed hooks", false).option("-r, --registered", "Show registered hooks", false).option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith (default: claude)", "claude").option("-j, --json", "Output as JSON", false).description("List available or installed hooks").action((options) => {
7395
8429
  const scope = resolveScope(options);
7396
8430
  if (options.registered || options.installed) {
7397
- const target = options.target === "gemini" ? "gemini" : "claude";
8431
+ const target = options.target === "gemini" ? "gemini" : options.target === "codewith" ? "codewith" : "claude";
7398
8432
  const registered = getRegisteredHooksForTarget(scope, target);
7399
8433
  if (options.json) {
7400
8434
  console.log(JSON.stringify(registered.map((name) => {
@@ -7478,7 +8512,7 @@ Found ${results.length} hook(s):
7478
8512
  console.log(` ${h.description}`);
7479
8513
  }
7480
8514
  });
7481
- program2.command("remove").alias("rm").argument("<hook>", "Hook to remove").option("-g, --global", "Remove from global settings", false).option("-p, --project", "Remove from project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, all (default: claude)", "claude").option("-j, --json", "Output as JSON", false).description("Remove an installed hook").action((hook, options) => {
8515
+ program2.command("remove").alias("rm").argument("<hook>", "Hook to remove").option("-g, --global", "Remove from global settings", false).option("-p, --project", "Remove from project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, all (default: claude)", "claude").option("-j, --json", "Output as JSON", false).description("Remove an installed hook").action((hook, options) => {
7482
8516
  const scope = resolveScope(options);
7483
8517
  const target = resolveTarget(options);
7484
8518
  if (!getHook(hook)) {
@@ -7589,7 +8623,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
7589
8623
  const settings = JSON.parse(readFileSync5(settingsPath, "utf-8"));
7590
8624
  const eventHooks = settings.hooks?.[meta.event] || [];
7591
8625
  const found = eventHooks.some((entry) => entry.hooks?.some((h) => {
7592
- const match = h.command?.match(/^hooks run (\w+)/);
8626
+ const match = h.command?.match(/^hooks run ([\w-]+)/);
7593
8627
  return match && match[1] === name;
7594
8628
  }));
7595
8629
  if (!found) {
@@ -7720,14 +8754,18 @@ ${meta.displayName} v${meta.version}
7720
8754
  const generalDocs = {
7721
8755
  overview: "Hooks are scripts that run at specific points in an AI coding agent session. Install @hasna/hooks globally, then register hooks \u2014 no files are copied to your project.",
7722
8756
  events: {
8757
+ SessionStart: "Fires when a session starts or resumes. Codewith can inject context via hookSpecificOutput.additionalContext.",
8758
+ UserPromptSubmit: "Codewith-native event when a user prompt is submitted; can block obvious injection attempts.",
7723
8759
  PreToolUse: 'Fires before a tool executes. Can block the operation by returning { "decision": "block" }.',
7724
8760
  PostToolUse: "Fires after a tool executes. Runs asynchronously, cannot block.",
7725
- Stop: "Fires when a session ends. Useful for notifications and cleanup.",
7726
- Notification: "Fires on notification events like context compaction."
8761
+ Stop: "Fires at turn end in Codewith and when other agents finish responding. Useful for notifications and cleanup.",
8762
+ Notification: "Fires on notification events like context compaction.",
8763
+ SessionEnd: "Fires when a session terminates. Useful for cleanup and final announcements."
7727
8764
  },
7728
8765
  installation: {
7729
8766
  global: "hooks install gitguard",
7730
8767
  project: "hooks install gitguard --project",
8768
+ codewith: "hooks install session-start --target codewith # emits TOML for open-configs to apply",
7731
8769
  category: 'hooks install --category "Git Safety"',
7732
8770
  all: "hooks install --all"
7733
8771
  },
@@ -7743,7 +8781,7 @@ ${meta.displayName} v${meta.version}
7743
8781
  },
7744
8782
  howItWorks: {
7745
8783
  install: "bun install -g @hasna/hooks",
7746
- register: "hooks install gitguard \u2192 writes to ~/.claude/settings.json",
8784
+ register: "hooks install gitguard \u2192 writes to ~/.claude/settings.json; hooks install session-start --target codewith emits a TOML fragment",
7747
8785
  execution: "Agent runs 'hooks run gitguard' \u2192 executes hook from global package",
7748
8786
  noFileCopy: "No files are copied to your project. Hooks run from the global @hasna/hooks package."
7749
8787
  }
@@ -8050,6 +9088,80 @@ logCmd.command("clear").description("Delete hook event logs").option("--hook <na
8050
9088
  }
8051
9089
  console.log(chalk2.green(`\u2713 Cleared ${count} event(s).`));
8052
9090
  });
9091
+ var storageCmd = program2.command("storage").description("Sync local hook data with storage PostgreSQL");
9092
+ storageCmd.command("status").description("Show storage sync status").option("-j, --json", "Output as JSON", false).action(async (options) => {
9093
+ const { getStorageStatus: getStorageStatus2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
9094
+ const status = getStorageStatus2();
9095
+ if (options.json) {
9096
+ console.log(JSON.stringify(status, null, 2));
9097
+ return;
9098
+ }
9099
+ console.log(chalk2.bold(`
9100
+ Storage Status
9101
+ `));
9102
+ console.log(` Configured: ${status.configured ? chalk2.green(`yes (${status.activeEnv})`) : chalk2.red("no")}`);
9103
+ console.log(` Mode: ${status.mode}`);
9104
+ console.log(` Tables: ${status.tables.join(", ")}`);
9105
+ console.log(` Sync rows: ${status.sync.length}`);
9106
+ });
9107
+ storageCmd.command("push").description("Push local hook data to storage PostgreSQL").option("-t, --tables <tables>", "Comma-separated table names").option("-j, --json", "Output as JSON", false).action(async (options) => {
9108
+ try {
9109
+ const { parseStorageTables: parseStorageTables2, storagePush: storagePush2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
9110
+ const results = await storagePush2({ tables: parseStorageTables2(options.tables) });
9111
+ if (options.json) {
9112
+ console.log(JSON.stringify(results, null, 2));
9113
+ return;
9114
+ }
9115
+ const written = results.reduce((sum, result) => sum + result.rowsWritten, 0);
9116
+ console.log(chalk2.green(`\u2713 Pushed ${written} row(s)`));
9117
+ } catch (error) {
9118
+ const message = error instanceof Error ? error.message : String(error);
9119
+ if (options.json)
9120
+ console.log(JSON.stringify({ error: message }));
9121
+ else
9122
+ console.error(chalk2.red(`\u2717 ${message}`));
9123
+ process.exitCode = 1;
9124
+ }
9125
+ });
9126
+ storageCmd.command("pull").description("Pull hook data from storage PostgreSQL to local SQLite").option("-t, --tables <tables>", "Comma-separated table names").option("-j, --json", "Output as JSON", false).action(async (options) => {
9127
+ try {
9128
+ const { parseStorageTables: parseStorageTables2, storagePull: storagePull2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
9129
+ const results = await storagePull2({ tables: parseStorageTables2(options.tables) });
9130
+ if (options.json) {
9131
+ console.log(JSON.stringify(results, null, 2));
9132
+ return;
9133
+ }
9134
+ const written = results.reduce((sum, result) => sum + result.rowsWritten, 0);
9135
+ console.log(chalk2.green(`\u2713 Pulled ${written} row(s)`));
9136
+ } catch (error) {
9137
+ const message = error instanceof Error ? error.message : String(error);
9138
+ if (options.json)
9139
+ console.log(JSON.stringify({ error: message }));
9140
+ else
9141
+ console.error(chalk2.red(`\u2717 ${message}`));
9142
+ process.exitCode = 1;
9143
+ }
9144
+ });
9145
+ storageCmd.command("sync").description("Bidirectional storage sync: pull then push").option("-t, --tables <tables>", "Comma-separated table names").option("-j, --json", "Output as JSON", false).action(async (options) => {
9146
+ try {
9147
+ const { parseStorageTables: parseStorageTables2, storageSync: storageSync2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
9148
+ const result = await storageSync2({ tables: parseStorageTables2(options.tables) });
9149
+ if (options.json) {
9150
+ console.log(JSON.stringify(result, null, 2));
9151
+ return;
9152
+ }
9153
+ const pulled = result.pull.reduce((sum, entry) => sum + entry.rowsWritten, 0);
9154
+ const pushed = result.push.reduce((sum, entry) => sum + entry.rowsWritten, 0);
9155
+ console.log(chalk2.green(`\u2713 Synced ${pulled} pulled row(s), ${pushed} pushed row(s)`));
9156
+ } catch (error) {
9157
+ const message = error instanceof Error ? error.message : String(error);
9158
+ if (options.json)
9159
+ console.log(JSON.stringify({ error: message }));
9160
+ else
9161
+ console.error(chalk2.red(`\u2717 ${message}`));
9162
+ process.exitCode = 1;
9163
+ }
9164
+ });
8053
9165
  program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process per agent)", false).option("--sse", "Use legacy SSE transport (port 39427)", false).option("--http", "Use Streamable HTTP transport (explicit; this is also the default)", false).option("-p, --port <port>", "Port for HTTP/SSE transport (defaults to 8847 for HTTP, 39427 for SSE)").description("Start MCP server for AI agent integration (default: shared Streamable HTTP)").action(async (options) => {
8054
9166
  if (options.stdio) {
8055
9167
  const { startStdioServer: startStdioServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));