@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.
- package/CHANGELOG.md +94 -0
- package/dist/bin/pushary-bell-hook.js +5 -5
- package/dist/bin/pushary-bell.js +15 -14
- package/dist/bin/pushary-claude.js +93 -143
- package/dist/bin/pushary-clean.js +13 -12
- package/dist/bin/pushary-codex-bridge.d.ts +1 -0
- package/dist/bin/pushary-codex-bridge.js +349 -0
- package/dist/bin/pushary-codex-hook.js +18 -14
- package/dist/bin/pushary-codex.js +7 -7
- package/dist/bin/pushary-connect.js +13 -12
- package/dist/bin/pushary-daemon.js +331 -41
- package/dist/bin/pushary-doctor.js +48 -25
- package/dist/bin/pushary-gemini-bridge.d.ts +1 -0
- package/dist/bin/pushary-gemini-bridge.js +299 -0
- package/dist/bin/pushary-gemini-hook.js +28 -21
- package/dist/bin/pushary-hook.js +14 -13
- package/dist/bin/pushary-login.js +15 -14
- package/dist/bin/pushary-logout.js +12 -11
- package/dist/bin/pushary-mode.js +10 -9
- package/dist/bin/pushary-notification-hook.js +5 -5
- package/dist/bin/pushary-permission-denied-hook.js +9 -9
- package/dist/bin/pushary-permission-hook.js +9 -9
- package/dist/bin/pushary-post-hook.js +5 -5
- package/dist/bin/pushary-prompt-hook.js +5 -5
- package/dist/bin/pushary-session-end-hook.js +5 -5
- package/dist/bin/pushary-session-start-hook.js +5 -5
- package/dist/bin/pushary-setup.js +42 -36
- package/dist/bin/pushary-stats.js +6 -5
- package/dist/bin/pushary-status.js +17 -16
- package/dist/bin/pushary-stop-hook.js +5 -5
- package/dist/bin/pushary-stopfailure-hook.js +5 -5
- package/dist/bin/pushary-suggestions.js +7 -6
- package/dist/bin/pushary-upgrade.js +18 -17
- package/dist/bin/pushary-wait.js +10 -9
- package/dist/bin/pushary.js +8 -6
- package/dist/{chunk-SAA3VBDK.js → chunk-437T77TY.js} +3 -3
- package/dist/{chunk-U27BZWFT.js → chunk-6LC4WSCL.js} +1 -1
- package/dist/{chunk-7WWZPII5.js → chunk-7PZPJSWI.js} +1 -1
- package/dist/{chunk-4QQOKEOS.js → chunk-BLDDXHHA.js} +6 -6
- package/dist/{chunk-7PO2VEA4.js → chunk-CCCJ6BPW.js} +40 -34
- package/dist/chunk-CCH775Y6.js +343 -0
- package/dist/chunk-DINDL2CF.js +45 -0
- package/dist/{chunk-TXF4QZSQ.js → chunk-DOXSWT4Q.js} +1 -1
- package/dist/chunk-ERYKGH4J.js +140 -0
- package/dist/{chunk-QR6MVVQ7.js → chunk-F52SIIDG.js} +4 -4
- package/dist/{chunk-26D2YMDU.js → chunk-FBGBJXVC.js} +1 -1
- package/dist/chunk-GCKAWHFP.js +124 -0
- package/dist/{chunk-QF7WTF2C.js → chunk-HZNOAJGB.js} +2 -2
- package/dist/chunk-ILB6UOAF.js +192 -0
- package/dist/{chunk-RRZZ7TFJ.js → chunk-J4KFVOCQ.js} +4 -2
- package/dist/{chunk-WDRRULBH.js → chunk-K7CSRGKZ.js} +4 -4
- package/dist/{chunk-ZUKH2NPJ.js → chunk-MGJBC6DD.js} +1 -44
- package/dist/{chunk-GQLB2GBJ.js → chunk-MHBC2XUP.js} +1 -1
- package/dist/{chunk-YWRJGYUH.js → chunk-MQMPG5X4.js} +28 -2
- package/dist/{chunk-3AGM3TDM.js → chunk-NJ7JT26G.js} +1 -1
- package/dist/chunk-NRGRUOAR.js +25 -0
- package/dist/{chunk-EHLTWCDZ.js → chunk-SEAHGHCE.js} +1 -1
- package/dist/{chunk-NDC3TBHI.js → chunk-UKSQWTBH.js} +1 -1
- package/dist/{chunk-UVAC2KHH.js → chunk-UXWEE3QI.js} +1 -1
- package/dist/{chunk-MUEW424A.js → chunk-VT4IVERX.js} +50 -3
- package/dist/{chunk-DHAZ6SOT.js → chunk-YR5TSPXB.js} +81 -25
- package/dist/src/index.d.ts +6 -3
- package/dist/src/index.js +9 -9
- 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-
|
|
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-
|
|
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:
|
|
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
|
|
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
|
+
};
|