@pushary/agent-hooks 0.83.2 → 0.84.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 (64) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/bin/pushary-bell-hook.js +5 -5
  3. package/dist/bin/pushary-bell.js +15 -14
  4. package/dist/bin/pushary-claude.js +93 -143
  5. package/dist/bin/pushary-clean.js +13 -12
  6. package/dist/bin/pushary-codex-bridge.d.ts +1 -0
  7. package/dist/bin/pushary-codex-bridge.js +349 -0
  8. package/dist/bin/pushary-codex-hook.js +18 -14
  9. package/dist/bin/pushary-codex.js +7 -7
  10. package/dist/bin/pushary-connect.js +13 -12
  11. package/dist/bin/pushary-daemon.js +331 -41
  12. package/dist/bin/pushary-doctor.js +48 -25
  13. package/dist/bin/pushary-gemini-bridge.d.ts +1 -0
  14. package/dist/bin/pushary-gemini-bridge.js +299 -0
  15. package/dist/bin/pushary-gemini-hook.js +28 -21
  16. package/dist/bin/pushary-hook.js +14 -13
  17. package/dist/bin/pushary-login.js +15 -14
  18. package/dist/bin/pushary-logout.js +12 -11
  19. package/dist/bin/pushary-mode.js +10 -9
  20. package/dist/bin/pushary-notification-hook.js +5 -5
  21. package/dist/bin/pushary-permission-denied-hook.js +9 -9
  22. package/dist/bin/pushary-permission-hook.js +9 -9
  23. package/dist/bin/pushary-post-hook.js +5 -5
  24. package/dist/bin/pushary-prompt-hook.js +5 -5
  25. package/dist/bin/pushary-session-end-hook.js +5 -5
  26. package/dist/bin/pushary-session-start-hook.js +5 -5
  27. package/dist/bin/pushary-setup.js +42 -36
  28. package/dist/bin/pushary-stats.js +6 -5
  29. package/dist/bin/pushary-status.js +17 -16
  30. package/dist/bin/pushary-stop-hook.js +5 -5
  31. package/dist/bin/pushary-stopfailure-hook.js +5 -5
  32. package/dist/bin/pushary-suggestions.js +7 -6
  33. package/dist/bin/pushary-upgrade.js +18 -17
  34. package/dist/bin/pushary-wait.js +10 -9
  35. package/dist/bin/pushary.js +8 -6
  36. package/dist/{chunk-SAA3VBDK.js → chunk-437T77TY.js} +3 -3
  37. package/dist/{chunk-U27BZWFT.js → chunk-6LC4WSCL.js} +1 -1
  38. package/dist/{chunk-7WWZPII5.js → chunk-7PZPJSWI.js} +1 -1
  39. package/dist/{chunk-4QQOKEOS.js → chunk-BLDDXHHA.js} +6 -6
  40. package/dist/{chunk-7PO2VEA4.js → chunk-CCCJ6BPW.js} +40 -34
  41. package/dist/chunk-CCH775Y6.js +343 -0
  42. package/dist/chunk-DINDL2CF.js +45 -0
  43. package/dist/{chunk-TXF4QZSQ.js → chunk-DOXSWT4Q.js} +1 -1
  44. package/dist/chunk-ERYKGH4J.js +140 -0
  45. package/dist/{chunk-QR6MVVQ7.js → chunk-F52SIIDG.js} +4 -4
  46. package/dist/{chunk-26D2YMDU.js → chunk-FBGBJXVC.js} +1 -1
  47. package/dist/chunk-GCKAWHFP.js +124 -0
  48. package/dist/{chunk-QF7WTF2C.js → chunk-HZNOAJGB.js} +2 -2
  49. package/dist/chunk-ILB6UOAF.js +192 -0
  50. package/dist/{chunk-RRZZ7TFJ.js → chunk-J4KFVOCQ.js} +4 -2
  51. package/dist/{chunk-WDRRULBH.js → chunk-K7CSRGKZ.js} +4 -4
  52. package/dist/{chunk-ZUKH2NPJ.js → chunk-MGJBC6DD.js} +1 -44
  53. package/dist/{chunk-GQLB2GBJ.js → chunk-MHBC2XUP.js} +1 -1
  54. package/dist/{chunk-YWRJGYUH.js → chunk-MQMPG5X4.js} +28 -2
  55. package/dist/{chunk-3AGM3TDM.js → chunk-NJ7JT26G.js} +1 -1
  56. package/dist/chunk-NRGRUOAR.js +25 -0
  57. package/dist/{chunk-EHLTWCDZ.js → chunk-SEAHGHCE.js} +1 -1
  58. package/dist/{chunk-NDC3TBHI.js → chunk-UKSQWTBH.js} +1 -1
  59. package/dist/{chunk-UVAC2KHH.js → chunk-UXWEE3QI.js} +1 -1
  60. package/dist/{chunk-MUEW424A.js → chunk-VT4IVERX.js} +50 -3
  61. package/dist/{chunk-DHAZ6SOT.js → chunk-YR5TSPXB.js} +81 -25
  62. package/dist/src/index.d.ts +6 -3
  63. package/dist/src/index.js +9 -9
  64. package/package.json +3 -1
@@ -1,3 +1,6 @@
1
+ import {
2
+ getMachineId
3
+ } from "./chunk-RN3NOEJF.js";
1
4
  import {
2
5
  callMcpTool,
3
6
  withRetry
@@ -7,7 +10,11 @@ import {
7
10
  } from "./chunk-SAF6HGAA.js";
8
11
  import {
9
12
  reportedInstallSource
10
- } from "./chunk-SAA3VBDK.js";
13
+ } from "./chunk-437T77TY.js";
14
+ import {
15
+ getApiKey,
16
+ getBaseUrl
17
+ } from "./chunk-2UMNXADU.js";
11
18
  import {
12
19
  ACTION_BODY_MAX,
13
20
  DECISION_LINE_MAX,
@@ -23,14 +30,7 @@ import {
23
30
  redactSecrets,
24
31
  redactSecretsDeep,
25
32
  selectGoverningRule
26
- } from "./chunk-YWRJGYUH.js";
27
- import {
28
- getMachineId
29
- } from "./chunk-RN3NOEJF.js";
30
- import {
31
- getApiKey,
32
- getBaseUrl
33
- } from "./chunk-2UMNXADU.js";
33
+ } from "./chunk-MQMPG5X4.js";
34
34
 
35
35
  // src/policy.ts
36
36
  import { createHash } from "crypto";
@@ -60,19 +60,21 @@ var CACHE_TTL_MS = 60 * 1e3;
60
60
  var STALE_CACHE_MAX_MS = 12 * 60 * 60 * 1e3;
61
61
  var policyRequestFor = (agentType) => ({
62
62
  agent: agentType,
63
- repoAware: agentType === "claude_code"
63
+ repoAware: ["claude_code", "codex", "gemini_cli"].includes(agentType)
64
64
  });
65
65
  var policyCacheFile = (apiKey, agent, repoAware = false) => {
66
66
  const parts = [apiKey, agent, repoAware ? "repoAware" : void 0].filter(Boolean);
67
67
  const hash = createHash("sha256").update(parts.join(":")).digest("hex").slice(0, 12);
68
68
  return join(tmpdir(), `pushary-policy-${hash}.json`);
69
69
  };
70
- var fetchPolicy = async (apiKey, agent, repoAware = false) => {
70
+ var fetchPolicy = async (apiKey, agent, repoAware = false, sessionId) => {
71
71
  return withRetry(async () => {
72
72
  const baseUrl = getBaseUrl();
73
73
  const params = new URLSearchParams();
74
74
  if (agent) params.set("agent", agent);
75
75
  if (repoAware) params.set("repoAware", "1");
76
+ params.set("scopeAware", "1");
77
+ if (sessionId) params.set("sessionId", sessionId);
76
78
  const query = params.toString();
77
79
  const url = query ? `${baseUrl}/api/mcp/policy?${query}` : `${baseUrl}/api/mcp/policy`;
78
80
  const response = await fetch(url, {
@@ -87,7 +89,7 @@ var fetchPolicy = async (apiKey, agent, repoAware = false) => {
87
89
  return raw;
88
90
  }, { maxAttempts: 2 });
89
91
  };
90
- var getPolicy = async (apiKey, expectedVersion, agent, repoAware = false) => {
92
+ var getPolicy = async (apiKey, expectedVersion, agent, repoAware = false, sessionId) => {
91
93
  const path = policyCacheFile(apiKey, agent, repoAware);
92
94
  let staleCache = null;
93
95
  if (existsSync(path)) {
@@ -96,19 +98,20 @@ var getPolicy = async (apiKey, expectedVersion, agent, repoAware = false) => {
96
98
  const cached = JSON.parse(stat);
97
99
  if (!isPolicyConfig(cached)) throw new Error("Corrupted cache");
98
100
  const versionStale = expectedVersion != null && (cached._policyVersion ?? null) !== expectedVersion;
101
+ const sessionStale = (cached._sessionId ?? null) !== (sessionId ?? null);
99
102
  const age = cached._cachedAt ? Date.now() - cached._cachedAt : 0;
100
103
  const ttlFresh = !cached._cachedAt || age < CACHE_TTL_MS;
101
- if (ttlFresh && !versionStale) {
104
+ if (ttlFresh && !versionStale && !sessionStale) {
102
105
  return cached;
103
106
  }
104
- if (age < STALE_CACHE_MAX_MS) staleCache = cached;
107
+ if (!sessionStale && age < STALE_CACHE_MAX_MS) staleCache = cached;
105
108
  } catch {
106
109
  }
107
110
  }
108
111
  try {
109
- const policy = await fetchPolicy(apiKey, agent, repoAware);
112
+ const policy = await fetchPolicy(apiKey, agent, repoAware, sessionId);
110
113
  try {
111
- writeFileSync(path, JSON.stringify({ ...policy, _cachedAt: Date.now(), _policyVersion: expectedVersion ?? null }), { encoding: "utf-8", mode: 384 });
114
+ writeFileSync(path, JSON.stringify({ ...policy, _cachedAt: Date.now(), _policyVersion: expectedVersion ?? null, _sessionId: sessionId ?? null }), { encoding: "utf-8", mode: 384 });
112
115
  } catch {
113
116
  }
114
117
  return policy;
@@ -124,20 +127,20 @@ var autoApprove = (tool) => ({
124
127
  mode: "terminal_only",
125
128
  pushFirstSeconds: 0
126
129
  });
127
- var resolveAutoResolveOrigin = (config, toolName, toolInput, cwd, repoKey) => {
130
+ var resolveAutoResolveOrigin = (config, toolName, toolInput, cwd, repoKey, sessionId) => {
128
131
  const args = toolInput ? policyArgForms(toolName, toolInput, cwd) : [];
129
132
  const arg = args[0];
130
- const match = selectGoverningRule(config.policies, toolName, args, repoKey);
133
+ const match = selectGoverningRule(config.policies, toolName, args, repoKey, sessionId);
131
134
  const governedBySpecificRule = match?.rank === "exact" || match?.rank === "prefix";
132
135
  if (!governedBySpecificRule && toolName === "Bash" && typeof arg === "string" && isSafeReadOnlyCommand(arg)) {
133
136
  return "safe_readonly";
134
137
  }
135
138
  return "policy_timeout";
136
139
  };
137
- var resolvePolicy = (config, toolName, modeOverride, toolInput, cwd, repoKey) => {
140
+ var resolvePolicy = (config, toolName, modeOverride, toolInput, cwd, repoKey, sessionId) => {
138
141
  const args = toolInput ? policyArgForms(toolName, toolInput, cwd) : [];
139
142
  const arg = args[0];
140
- const match = selectGoverningRule(config.policies, toolName, args, repoKey);
143
+ const match = selectGoverningRule(config.policies, toolName, args, repoKey, sessionId);
141
144
  let base = match?.policy ?? config.policies.find((p) => p.tool === "*") ?? {
142
145
  tool: toolName,
143
146
  timeoutSeconds: config.defaultTimeoutSeconds,
@@ -172,9 +175,9 @@ var restraintRank = (policy) => {
172
175
  return policy.mode === "terminal_only" || policy.mode === "notify_only" ? 1 : 2;
173
176
  };
174
177
  var strictestPolicy = (a, b) => restraintRank(b) > restraintRank(a) ? b : a;
175
- var resolvePolicyAcross = (config, toolName, modeOverride, toolInputs, cwd, repoKey) => {
176
- if (toolInputs.length === 0) return resolvePolicy(config, toolName, modeOverride, void 0, cwd, repoKey);
177
- return toolInputs.map((input) => resolvePolicy(config, toolName, modeOverride, input, cwd, repoKey)).reduce(strictestPolicy);
178
+ var resolvePolicyAcross = (config, toolName, modeOverride, toolInputs, cwd, repoKey, sessionId) => {
179
+ if (toolInputs.length === 0) return resolvePolicy(config, toolName, modeOverride, void 0, cwd, repoKey, sessionId);
180
+ return toolInputs.map((input) => resolvePolicy(config, toolName, modeOverride, input, cwd, repoKey, sessionId)).reduce(strictestPolicy);
178
181
  };
179
182
  var isModeStateDegraded = (state) => state?.degraded === true;
180
183
  var MODE_GRACE_MS = 60 * 1e3;
@@ -930,12 +933,13 @@ var notifyLateAnswers = async (apiKey, late, agentName, sessionId) => {
930
933
  };
931
934
  var CLAUDE_CODE_AGENT = { type: "claude_code", label: "Claude Code" };
932
935
  var POLICY_CACHE_TTL_MS = 5 * 60 * 1e3;
933
- var readFreshCachedPolicy = (apiKey, agentType) => {
936
+ var readFreshCachedPolicy = (apiKey, agentType, sessionId) => {
934
937
  const request = policyRequestFor(agentType);
935
938
  const path = policyCacheFile(apiKey, request.agent, request.repoAware);
936
939
  if (!existsSync5(path)) return null;
937
940
  const cached = JSON.parse(readFileSync4(path, "utf-8"));
938
941
  if (!isPolicyConfig(cached)) return null;
942
+ if ((cached._sessionId ?? null) !== (sessionId ?? null)) return null;
939
943
  if (!cached._cachedAt || Date.now() - cached._cachedAt >= POLICY_CACHE_TTL_MS) return null;
940
944
  return cached;
941
945
  };
@@ -943,13 +947,13 @@ var autoDecisionEnabled = () => {
943
947
  const flag = process.env.PUSHARY_AUTO_DECISION_BEACON;
944
948
  return flag === "1" || flag === "true";
945
949
  };
946
- var deriveDecisionSource = (toolName, toolInput, liveMode, agentType, cwd) => {
950
+ var deriveDecisionSource = (toolName, toolInput, liveMode, agentType, cwd, sessionId) => {
947
951
  try {
948
952
  if (liveMode.kill) return "terminal";
949
953
  if (!autoDecisionEnabled()) return void 0;
950
- const policy = readFreshCachedPolicy(getApiKey(), agentType);
954
+ const policy = readFreshCachedPolicy(getApiKey(), agentType, sessionId);
951
955
  if (!policy) return void 0;
952
- const resolved = resolvePolicy(policy, toolName, liveMode.mode, toolInput, cwd, repoKeyFor(cwd));
956
+ const resolved = resolvePolicy(policy, toolName, liveMode.mode, toolInput, cwd, repoKeyFor(cwd), sessionId);
953
957
  if (isAutoApprove(resolved)) return "policy_auto";
954
958
  if (resolved.mode === "push_only" || resolved.mode === "push_first") return "human";
955
959
  return "terminal";
@@ -957,16 +961,16 @@ var deriveDecisionSource = (toolName, toolInput, liveMode, agentType, cwd) => {
957
961
  return void 0;
958
962
  }
959
963
  };
960
- var deriveAutoDecisionBeacon = (toolName, toolInput, liveMode, agentType, cwd) => {
964
+ var deriveAutoDecisionBeacon = (toolName, toolInput, liveMode, agentType, cwd, sessionId) => {
961
965
  try {
962
966
  if (liveMode.kill) return void 0;
963
967
  if (!autoDecisionEnabled()) return void 0;
964
- const policy = readFreshCachedPolicy(getApiKey(), agentType);
968
+ const policy = readFreshCachedPolicy(getApiKey(), agentType, sessionId);
965
969
  if (!policy) return void 0;
966
- const resolved = resolvePolicy(policy, toolName, liveMode.mode, toolInput, cwd, repoKeyFor(cwd));
970
+ const resolved = resolvePolicy(policy, toolName, liveMode.mode, toolInput, cwd, repoKeyFor(cwd), sessionId);
967
971
  if (resolved.timeoutSeconds !== 0 || resolved.timeoutAction !== "approve") return void 0;
968
972
  return {
969
- decisionOrigin: resolveAutoResolveOrigin(policy, toolName, toolInput, cwd, repoKeyFor(cwd)),
973
+ decisionOrigin: resolveAutoResolveOrigin(policy, toolName, toolInput, cwd, repoKeyFor(cwd), sessionId),
970
974
  toolName,
971
975
  toolTarget: deriveToolTarget(toolName, toolInput)
972
976
  };
@@ -1009,7 +1013,7 @@ var reportEvent = async (event, options = {}) => {
1009
1013
  machineId: event.machineId ?? getMachineId(),
1010
1014
  // Lets the phone's Stop end this process instead of only denying its next
1011
1015
  // tool call. Ours alone: never another process, never a parent.
1012
- pid: event.pid ?? process.pid,
1016
+ pid: event.pid,
1013
1017
  repoKey: event.repoKey ?? repoKeyFor(),
1014
1018
  installMode: detectInstallMode(),
1015
1019
  // How the binary was INVOKED this time and which front door produced the
@@ -1031,6 +1035,7 @@ var reportEvent = async (event, options = {}) => {
1031
1035
  }),
1032
1036
  signal: AbortSignal.timeout(options.timeoutMs ?? 1e4)
1033
1037
  });
1038
+ if (options.requireOk && !res.ok) throw new Error(`/api/agent/event rejected (${res.status})`);
1034
1039
  try {
1035
1040
  return await res.json();
1036
1041
  } catch {
@@ -1056,8 +1061,8 @@ var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
1056
1061
  for (const l of late) removePendingQuestion(sessionKey, l.correlationId);
1057
1062
  }
1058
1063
  const isMirrorOnly = NON_GATED_MIRROR_TOOLS.has(input.tool_name);
1059
- const decisionSource = isMirrorOnly ? void 0 : deriveDecisionSource(lookup.tool, lookup.input, liveMode, agent.type, input.cwd);
1060
- const beacon = !isMirrorOnly && decisionSource === "policy_auto" && throttlePass(`autodecision:${sessionKey}:${lookup.tool}`, AUTO_DECISION_WINDOW_MS) ? deriveAutoDecisionBeacon(lookup.tool, lookup.input, liveMode, agent.type, input.cwd) : void 0;
1064
+ const decisionSource = isMirrorOnly ? void 0 : deriveDecisionSource(lookup.tool, lookup.input, liveMode, agent.type, input.cwd, input.session_id);
1065
+ const beacon = !isMirrorOnly && decisionSource === "policy_auto" && throttlePass(`autodecision:${sessionKey}:${lookup.tool}`, AUTO_DECISION_WINDOW_MS) ? deriveAutoDecisionBeacon(lookup.tool, lookup.input, liveMode, agent.type, input.cwd, input.session_id) : void 0;
1061
1066
  const toolReport = reportEvent({
1062
1067
  event: isError ? "tool_error" : "tool_complete",
1063
1068
  agentType: agent.type,
@@ -1313,6 +1318,7 @@ var handleStopFailure = async (input, agent = CLAUDE_CODE_AGENT) => {
1313
1318
  };
1314
1319
 
1315
1320
  export {
1321
+ policyRequestFor,
1316
1322
  getPolicy,
1317
1323
  resolveAutoResolveOrigin,
1318
1324
  resolvePolicy,
@@ -0,0 +1,343 @@
1
+ import {
2
+ needsShell,
3
+ spawnClaude
4
+ } from "./chunk-XHKBHWLX.js";
5
+ import {
6
+ resolveGlobalBinary
7
+ } from "./chunk-VT4IVERX.js";
8
+ import {
9
+ pusharyDir
10
+ } from "./chunk-SEAHGHCE.js";
11
+ import {
12
+ getMachineId
13
+ } from "./chunk-RN3NOEJF.js";
14
+ import {
15
+ getPackageVersion
16
+ } from "./chunk-DINDL2CF.js";
17
+ import {
18
+ readJsonSafe,
19
+ writeJsonAtomic
20
+ } from "./chunk-6MTNS63X.js";
21
+ import {
22
+ getApiKey
23
+ } from "./chunk-2UMNXADU.js";
24
+
25
+ // src/daemon-lifecycle.ts
26
+ import { dirname, join } from "path";
27
+ import { existsSync, mkdirSync, renameSync, unlinkSync, writeFileSync } from "fs";
28
+ import { createHash, randomUUID } from "crypto";
29
+ var daemonNeedsHandoff = (status) => status.kind === "version-mismatch" || status.kind === "configuration-mismatch" || status.kind === "unresponsive" && status.processAlive === true;
30
+ var statePath = (dir) => join(dir, "daemon-state.json");
31
+ var lockPath = (dir) => join(dir, "daemon.lock");
32
+ var targetPath = (dir) => join(dir, "daemon-target.json");
33
+ var DAEMON_LEASE_RENEW_INTERVAL_MS = 3e4;
34
+ var LOCAL_LEASE_TIMEOUT_MS = DAEMON_LEASE_RENEW_INTERVAL_MS * 3;
35
+ var buildDaemonLaunch = (input) => input.siblingEntry ? { command: input.execPath, args: [input.siblingEntry], shell: false } : {
36
+ command: input.daemonBinary,
37
+ args: [],
38
+ shell: needsShell(input.daemonBinary, input.platform ?? process.platform)
39
+ };
40
+ var spawnDetachedDaemon = (plan, options = {}) => {
41
+ const spawnImpl = options.spawnImpl ?? spawnClaude;
42
+ const child = spawnImpl(plan.command, plan.args, {
43
+ detached: true,
44
+ stdio: "ignore",
45
+ windowsHide: true,
46
+ shell: plan.shell
47
+ });
48
+ child.once?.("error", () => {
49
+ });
50
+ child.unref();
51
+ return typeof child.pid === "number" ? child.pid : null;
52
+ };
53
+ var processIsAlive = (pid) => {
54
+ try {
55
+ process.kill(pid, 0);
56
+ return true;
57
+ } catch (err) {
58
+ return err.code === "EPERM";
59
+ }
60
+ };
61
+ var isString = (value) => typeof value === "string" && value.length > 0;
62
+ var isTimestamp = (value) => isString(value) && Number.isFinite(Date.parse(value));
63
+ var currentConfigurationId = () => {
64
+ try {
65
+ return createHash("sha256").update(getApiKey()).digest("hex").slice(0, 16);
66
+ } catch {
67
+ return null;
68
+ }
69
+ };
70
+ var parseDaemonState = (value) => {
71
+ if (typeof value !== "object" || value === null) return null;
72
+ const state = value;
73
+ if (!isString(state.instanceId) || !Number.isInteger(state.pid) || state.pid <= 0 || !isString(state.version) || !(state.configurationId === void 0 || state.configurationId === null || isString(state.configurationId)) || !isString(state.machineId) || !isTimestamp(state.startedAt) || !isTimestamp(state.updatedAt) || !(state.lastSuccessfulHeartbeatAt === null || isTimestamp(state.lastSuccessfulHeartbeatAt))) return null;
74
+ return {
75
+ instanceId: state.instanceId,
76
+ pid: state.pid,
77
+ version: state.version,
78
+ configurationId: state.configurationId ?? null,
79
+ machineId: state.machineId,
80
+ startedAt: state.startedAt,
81
+ updatedAt: state.updatedAt,
82
+ lastSuccessfulHeartbeatAt: state.lastSuccessfulHeartbeatAt
83
+ };
84
+ };
85
+ var parseLockOwner = (value) => {
86
+ if (typeof value !== "object" || value === null) return null;
87
+ const owner = value;
88
+ return isString(owner.instanceId) && Number.isInteger(owner.pid) && owner.pid > 0 ? { instanceId: owner.instanceId, pid: owner.pid } : null;
89
+ };
90
+ var readDaemonStatus = (options = {}) => {
91
+ const dir = options.dir ?? pusharyDir();
92
+ const installedVersion = options.installedVersion ?? getPackageVersion();
93
+ const state = readJsonSafe(statePath(dir));
94
+ if (state.kind === "missing") {
95
+ return {
96
+ kind: "absent",
97
+ installedVersion,
98
+ machineId: null,
99
+ pid: null,
100
+ processAlive: null,
101
+ daemonVersion: null,
102
+ startedAt: null,
103
+ updatedAt: null,
104
+ lastSuccessfulHeartbeatAt: null
105
+ };
106
+ }
107
+ const value = state.kind === "ok" ? parseDaemonState(state.value) : null;
108
+ if (value) {
109
+ const alive = (options.isProcessAlive ?? processIsAlive)(value.pid);
110
+ const fresh = (options.now ?? Date.now)() - Date.parse(value.updatedAt) <= LOCAL_LEASE_TIMEOUT_MS;
111
+ const expectedConfigurationId = options.configurationId === void 0 ? currentConfigurationId() : options.configurationId;
112
+ return {
113
+ kind: !alive || !fresh ? "unresponsive" : value.version !== installedVersion ? "version-mismatch" : expectedConfigurationId !== null && value.configurationId !== null && value.configurationId !== expectedConfigurationId ? "configuration-mismatch" : "running",
114
+ installedVersion,
115
+ machineId: value.machineId,
116
+ pid: value.pid,
117
+ processAlive: alive,
118
+ daemonVersion: value.version,
119
+ startedAt: value.startedAt,
120
+ updatedAt: value.updatedAt,
121
+ lastSuccessfulHeartbeatAt: value.lastSuccessfulHeartbeatAt
122
+ };
123
+ }
124
+ return {
125
+ kind: "invalid",
126
+ installedVersion,
127
+ machineId: null,
128
+ pid: null,
129
+ processAlive: null,
130
+ daemonVersion: null,
131
+ startedAt: null,
132
+ updatedAt: null,
133
+ lastSuccessfulHeartbeatAt: null
134
+ };
135
+ };
136
+ var safeUnlink = (path) => {
137
+ try {
138
+ unlinkSync(path);
139
+ } catch (err) {
140
+ if (err.code !== "ENOENT") throw err;
141
+ }
142
+ };
143
+ var requestDaemonVersion = (version, options = {}) => {
144
+ const dir = options.dir ?? pusharyDir();
145
+ const now = options.now ?? Date.now;
146
+ writeJsonAtomic(targetPath(dir), { version, requestedAt: new Date(now()).toISOString() }, 384);
147
+ };
148
+ var acquireDaemonLease = (options = {}) => {
149
+ const dir = options.dir ?? pusharyDir();
150
+ const installedVersion = options.installedVersion ?? getPackageVersion();
151
+ const machineId = options.machineId ?? getMachineId();
152
+ const pid = options.pid ?? process.pid;
153
+ const now = options.now ?? Date.now;
154
+ const instanceId = randomUUID();
155
+ const startedAt = new Date(now()).toISOString();
156
+ const writeLock = () => {
157
+ mkdirSync(dir, { recursive: true });
158
+ writeFileSync(lockPath(dir), JSON.stringify({ instanceId, pid }), { flag: "wx", mode: 384 });
159
+ };
160
+ try {
161
+ writeLock();
162
+ } catch (err) {
163
+ if (err.code !== "EEXIST") {
164
+ return { kind: "unavailable", detail: err instanceof Error ? err.message : String(err) };
165
+ }
166
+ const status = readDaemonStatus(options);
167
+ if (status.kind === "running" || status.kind === "version-mismatch" || status.kind === "configuration-mismatch") {
168
+ return { kind: "already-running", status };
169
+ }
170
+ const lock = readJsonSafe(lockPath(dir));
171
+ const owner = lock.kind === "ok" ? parseLockOwner(lock.value) : null;
172
+ if (!owner) return { kind: "unavailable", detail: "daemon lock is invalid" };
173
+ if ((options.isProcessAlive ?? processIsAlive)(owner.pid)) {
174
+ return { kind: "unavailable", detail: "daemon owner is still running or starting" };
175
+ }
176
+ const stale = join(dir, `daemon.lock.${instanceId}.stale`);
177
+ let ownsReplacement = false;
178
+ try {
179
+ renameSync(lockPath(dir), stale);
180
+ writeLock();
181
+ ownsReplacement = true;
182
+ safeUnlink(statePath(dir));
183
+ safeUnlink(stale);
184
+ } catch (retryError) {
185
+ if (ownsReplacement) safeUnlink(lockPath(dir));
186
+ try {
187
+ safeUnlink(stale);
188
+ } catch {
189
+ }
190
+ return { kind: "unavailable", detail: retryError instanceof Error ? retryError.message : String(retryError) };
191
+ }
192
+ }
193
+ const state = {
194
+ instanceId,
195
+ pid,
196
+ version: installedVersion,
197
+ configurationId: options.configurationId === void 0 ? currentConfigurationId() : options.configurationId,
198
+ machineId,
199
+ startedAt,
200
+ updatedAt: startedAt,
201
+ lastSuccessfulHeartbeatAt: null
202
+ };
203
+ try {
204
+ writeJsonAtomic(statePath(dir), state, 384);
205
+ } catch (err) {
206
+ safeUnlink(lockPath(dir));
207
+ return { kind: "unavailable", detail: err instanceof Error ? err.message : String(err) };
208
+ }
209
+ let currentState = state;
210
+ const isOwner = () => {
211
+ const owner = readJsonSafe(lockPath(dir));
212
+ return owner.kind === "ok" && owner.value.instanceId === instanceId;
213
+ };
214
+ const updateState = (next) => {
215
+ if (!isOwner()) return;
216
+ try {
217
+ writeJsonAtomic(statePath(dir), next, 384);
218
+ currentState = next;
219
+ } catch {
220
+ }
221
+ };
222
+ return {
223
+ kind: "acquired",
224
+ lease: {
225
+ isOwner,
226
+ renew: (at = now()) => {
227
+ updateState({ ...currentState, updatedAt: new Date(at).toISOString() });
228
+ },
229
+ recordSuccessfulHeartbeat: (at = now()) => {
230
+ const timestamp = new Date(at).toISOString();
231
+ updateState({
232
+ ...currentState,
233
+ updatedAt: timestamp,
234
+ lastSuccessfulHeartbeatAt: timestamp
235
+ });
236
+ },
237
+ shouldYield: () => {
238
+ if (!isOwner()) return true;
239
+ const target = readJsonSafe(targetPath(dir));
240
+ return target.kind === "ok" && (typeof target.value.version === "string" && target.value.version !== installedVersion || isTimestamp(target.value.requestedAt) && Date.parse(target.value.requestedAt) > Date.parse(startedAt));
241
+ },
242
+ release: () => {
243
+ if (!isOwner()) return;
244
+ safeUnlink(statePath(dir));
245
+ safeUnlink(lockPath(dir));
246
+ }
247
+ }
248
+ };
249
+ };
250
+ var hasConfiguration = () => {
251
+ try {
252
+ getApiKey();
253
+ return true;
254
+ } catch {
255
+ return false;
256
+ }
257
+ };
258
+ var wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
259
+ var requestDaemonHandoff = async (initial, options = {}) => {
260
+ const now = options.now ?? Date.now;
261
+ const requestedAt = Math.max(
262
+ now(),
263
+ initial.startedAt === null ? 0 : Date.parse(initial.startedAt) + 1
264
+ );
265
+ requestDaemonVersion(options.installedVersion ?? initial.installedVersion, {
266
+ dir: options.dir,
267
+ now: () => requestedAt
268
+ });
269
+ if (initial.pid === null || initial.processAlive !== true) return initial;
270
+ const sleep = options.sleep ?? wait;
271
+ const pollIntervalMs = options.pollIntervalMs ?? 50;
272
+ const attempts = Math.max(1, Math.ceil((options.waitTimeoutMs ?? 5e3) / pollIntervalMs));
273
+ let status = initial;
274
+ for (let attempt = 0; attempt < attempts; attempt++) {
275
+ await sleep(pollIntervalMs);
276
+ status = readDaemonStatus(options);
277
+ if (status.pid !== initial.pid || status.processAlive !== true) break;
278
+ }
279
+ return status;
280
+ };
281
+ var launchInstalledDaemon = (options) => {
282
+ const entryExists = options.entryExists ?? existsSync;
283
+ const argvEntry = process.argv[1];
284
+ const candidate = options.daemonEntry !== void 0 ? options.daemonEntry : argvEntry ? join(dirname(argvEntry), "pushary-daemon.js") : null;
285
+ const siblingEntry = candidate && entryExists(candidate) ? candidate : null;
286
+ const platform = options.platform ?? process.platform;
287
+ const daemonBinary = options.daemonBinary ?? resolveGlobalBinary("pushary-daemon") ?? (platform === "win32" ? "pushary-daemon.cmd" : "pushary-daemon");
288
+ const plan = buildDaemonLaunch({
289
+ siblingEntry,
290
+ execPath: options.execPath ?? process.execPath,
291
+ daemonBinary,
292
+ platform
293
+ });
294
+ spawnDetachedDaemon(plan, { spawnImpl: options.spawnImpl });
295
+ };
296
+ var ensureDaemonRunning = async (options = {}) => {
297
+ const initial = readDaemonStatus(options);
298
+ if (!(options.configured ?? hasConfiguration)()) {
299
+ return { kind: "not-configured", status: initial, detail: "Pushary is not configured on this machine" };
300
+ }
301
+ if (initial.kind === "running") return { kind: "running", status: initial };
302
+ let handoffStatus;
303
+ try {
304
+ handoffStatus = await requestDaemonHandoff(initial, options);
305
+ } catch (err) {
306
+ return {
307
+ kind: "unavailable",
308
+ status: initial,
309
+ detail: err instanceof Error ? err.message : String(err)
310
+ };
311
+ }
312
+ const sleep = options.sleep ?? wait;
313
+ const pollIntervalMs = options.pollIntervalMs ?? 50;
314
+ const attempts = Math.max(1, Math.ceil((options.waitTimeoutMs ?? 5e3) / pollIntervalMs));
315
+ if (handoffStatus.kind === "running") return { kind: "running", status: handoffStatus };
316
+ if (daemonNeedsHandoff(handoffStatus)) {
317
+ return { kind: "unavailable", status: handoffStatus, detail: "unhealthy daemon did not yield" };
318
+ }
319
+ try {
320
+ (options.spawnDaemon ?? (() => launchInstalledDaemon(options)))();
321
+ } catch (err) {
322
+ return {
323
+ kind: "unavailable",
324
+ status: initial,
325
+ detail: err instanceof Error ? err.message : String(err)
326
+ };
327
+ }
328
+ let status = initial;
329
+ for (let attempt = 0; attempt < attempts; attempt++) {
330
+ status = readDaemonStatus(options);
331
+ if (status.kind === "running") return { kind: "started", status };
332
+ await sleep(pollIntervalMs);
333
+ }
334
+ return { kind: "unavailable", status, detail: `daemon did not become healthy (${status.kind})` };
335
+ };
336
+
337
+ export {
338
+ DAEMON_LEASE_RENEW_INTERVAL_MS,
339
+ readDaemonStatus,
340
+ acquireDaemonLease,
341
+ requestDaemonHandoff,
342
+ ensureDaemonRunning
343
+ };
@@ -0,0 +1,45 @@
1
+ // src/cli/version.ts
2
+ import { readFileSync } from "fs";
3
+ import { dirname, join, parse } from "path";
4
+ import { fileURLToPath } from "url";
5
+ var PACKAGE_NAME = "@pushary/agent-hooks";
6
+ var findOwnPackageJson = (start) => {
7
+ const { root } = parse(start);
8
+ let dir = start;
9
+ while (true) {
10
+ const candidate = join(dir, "package.json");
11
+ try {
12
+ const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
13
+ if (pkg.name === PACKAGE_NAME) return candidate;
14
+ } catch {
15
+ }
16
+ if (dir === root) return void 0;
17
+ const parent = dirname(dir);
18
+ if (parent === dir) return void 0;
19
+ dir = parent;
20
+ }
21
+ };
22
+ var getPackageVersion = () => {
23
+ try {
24
+ const found = findOwnPackageJson(dirname(fileURLToPath(import.meta.url)));
25
+ if (!found) return "0.0.0";
26
+ const pkg = JSON.parse(readFileSync(found, "utf-8"));
27
+ return pkg.version ?? "0.0.0";
28
+ } catch {
29
+ return "0.0.0";
30
+ }
31
+ };
32
+ var isNewerVersion = (candidate, current) => {
33
+ const a = candidate.split(".").map((part) => parseInt(part, 10) || 0);
34
+ const b = current.split(".").map((part) => parseInt(part, 10) || 0);
35
+ for (let i = 0; i < 3; i++) {
36
+ if ((a[i] ?? 0) > (b[i] ?? 0)) return true;
37
+ if ((a[i] ?? 0) < (b[i] ?? 0)) return false;
38
+ }
39
+ return false;
40
+ };
41
+
42
+ export {
43
+ getPackageVersion,
44
+ isNewerVersion
45
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HOOK_BUDGETS
3
- } from "./chunk-YWRJGYUH.js";
3
+ } from "./chunk-MQMPG5X4.js";
4
4
 
5
5
  // src/gemini-config.ts
6
6
  var GEMINI_HOOK_BINARY = "pushary-gemini-hook";