@pushary/agent-hooks 0.68.0 → 0.69.0
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 +89 -0
- package/data/cursor-plugin/scripts/pushary-gate.mjs +36 -10
- package/data/vscode-plugin/scripts/pushary-gate.mjs +32 -9
- package/dist/bin/pushary-claude.js +21 -11
- package/dist/bin/pushary-clean.js +58 -20
- package/dist/bin/pushary-codex-hook.js +38 -23
- package/dist/bin/pushary-codex.js +1 -1
- package/dist/bin/pushary-connect.js +1 -1
- package/dist/bin/pushary-doctor.js +139 -26
- package/dist/bin/pushary-gemini-hook.js +32 -16
- package/dist/bin/pushary-hook.js +3 -3
- package/dist/bin/pushary-notification-hook.js +1 -1
- package/dist/bin/pushary-permission-denied-hook.js +3 -3
- package/dist/bin/pushary-permission-hook.js +3 -3
- package/dist/bin/pushary-post-hook.js +1 -1
- package/dist/bin/pushary-prompt-hook.js +1 -1
- package/dist/bin/pushary-session-end-hook.js +1 -1
- package/dist/bin/pushary-session-start-hook.js +1 -1
- package/dist/bin/pushary-setup.js +16 -6
- package/dist/bin/pushary-status.js +1 -1
- package/dist/bin/pushary-stop-hook.js +1 -1
- package/dist/bin/pushary-stopfailure-hook.js +1 -1
- package/dist/chunk-3USMXNVB.js +64 -0
- package/dist/{chunk-SONXGCZY.js → chunk-C5I2RAMT.js} +45 -27
- package/dist/{chunk-XOUYM27W.js → chunk-H46LSQRT.js} +182 -103
- package/dist/{chunk-KB4ODLGB.js → chunk-I4BYZIX4.js} +2 -0
- package/dist/{chunk-YMFZUXL5.js → chunk-P7VBAYQO.js} +25 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +3 -3
- package/package.json +1 -1
- package/dist/chunk-YHG74UFF.js +0 -12
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isModeStateDegraded,
|
|
3
|
+
resolveAutoResolveOrigin,
|
|
4
|
+
resolvePolicyAcross
|
|
5
|
+
} from "./chunk-H46LSQRT.js";
|
|
6
|
+
import {
|
|
7
|
+
evaluateScope,
|
|
8
|
+
scopeChangeReason,
|
|
9
|
+
scopeRequiresHuman
|
|
10
|
+
} from "./chunk-DQAN3JQP.js";
|
|
11
|
+
|
|
12
|
+
// src/answer.ts
|
|
13
|
+
var denyReasonFrom = (value, note) => {
|
|
14
|
+
const trimmed = note?.trim();
|
|
15
|
+
if (trimmed) return `Denied from your phone: ${trimmed}`;
|
|
16
|
+
return value && value !== "no" && value !== "yes" ? `Denied from your phone: ${value}` : "Denied via push notification";
|
|
17
|
+
};
|
|
18
|
+
var isDeferAnswer = (value) => value === "defer";
|
|
19
|
+
|
|
20
|
+
// src/gate.ts
|
|
21
|
+
var KILL_REASON = "Stopped by user, this agent was halted from Pushary";
|
|
22
|
+
var strictestScopeVerdict = (scope, paths) => {
|
|
23
|
+
if (paths.length === 0) return evaluateScope(scope, void 0);
|
|
24
|
+
let worst = { outcome: "in_scope" };
|
|
25
|
+
for (const path of paths) {
|
|
26
|
+
const verdict = evaluateScope(scope, path);
|
|
27
|
+
if (verdict.outcome === "off_limits") return verdict;
|
|
28
|
+
if (verdict.outcome === "out_of_scope") worst = verdict;
|
|
29
|
+
}
|
|
30
|
+
return worst;
|
|
31
|
+
};
|
|
32
|
+
var survivesDegraded = (config, toolName, toolInputs, cwd, repoKey) => toolInputs.length === 1 && resolveAutoResolveOrigin(config, toolName, toolInputs[0], cwd, repoKey) === "safe_readonly";
|
|
33
|
+
var resolveGate = (input) => {
|
|
34
|
+
const { modeState, config, toolName, toolInputs, cwd, repoKey } = input;
|
|
35
|
+
if (modeState.kill) return { kind: "kill", reason: KILL_REASON };
|
|
36
|
+
const policy = resolvePolicyAcross(config, toolName, modeState.mode, toolInputs, cwd, repoKey);
|
|
37
|
+
const autoApproves = policy.timeoutSeconds === 0 && policy.timeoutAction === "approve";
|
|
38
|
+
const autoDenies = policy.timeoutSeconds === 0 && policy.timeoutAction === "deny";
|
|
39
|
+
if (autoDenies) return { kind: "deny", policy, reason: `Denied by policy for ${policy.tool}` };
|
|
40
|
+
if (isModeStateDegraded(modeState)) {
|
|
41
|
+
if (autoApproves) {
|
|
42
|
+
return survivesDegraded(config, toolName, toolInputs, cwd, repoKey) ? { kind: "allow", policy } : { kind: "defer", policy };
|
|
43
|
+
}
|
|
44
|
+
return { kind: "gate", policy };
|
|
45
|
+
}
|
|
46
|
+
const scopeVerdict = modeState.scope ? strictestScopeVerdict(modeState.scope, input.scopePaths ?? []) : { outcome: "no_contract" };
|
|
47
|
+
if (autoApproves) {
|
|
48
|
+
if (!scopeRequiresHuman(scopeVerdict)) return { kind: "allow", policy };
|
|
49
|
+
}
|
|
50
|
+
const scopeReason = scopeChangeReason(scopeVerdict);
|
|
51
|
+
return {
|
|
52
|
+
kind: "gate",
|
|
53
|
+
policy,
|
|
54
|
+
scopeReason,
|
|
55
|
+
scopePath: scopeReason && "path" in scopeVerdict ? scopeVerdict.path : void 0
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
denyReasonFrom,
|
|
61
|
+
isDeferAnswer,
|
|
62
|
+
KILL_REASON,
|
|
63
|
+
resolveGate
|
|
64
|
+
};
|
|
@@ -3,8 +3,9 @@ import {
|
|
|
3
3
|
} from "./chunk-CAJZAFVS.js";
|
|
4
4
|
import {
|
|
5
5
|
denyReasonFrom,
|
|
6
|
-
isDeferAnswer
|
|
7
|
-
|
|
6
|
+
isDeferAnswer,
|
|
7
|
+
resolveGate
|
|
8
|
+
} from "./chunk-3USMXNVB.js";
|
|
8
9
|
import {
|
|
9
10
|
DEFAULT_SESSION,
|
|
10
11
|
askUser,
|
|
@@ -19,13 +20,12 @@ import {
|
|
|
19
20
|
readLastPrompt,
|
|
20
21
|
readLastUserPrompt,
|
|
21
22
|
repoKeyFor,
|
|
22
|
-
resolvePolicy,
|
|
23
23
|
savePendingQuestion,
|
|
24
24
|
scopePathFor,
|
|
25
25
|
sendNotification,
|
|
26
26
|
throttlePass,
|
|
27
27
|
waitForAnswer
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-H46LSQRT.js";
|
|
29
29
|
import {
|
|
30
30
|
isGatingMoment,
|
|
31
31
|
recordKeylessMoment
|
|
@@ -33,11 +33,8 @@ import {
|
|
|
33
33
|
import {
|
|
34
34
|
buildDecisionEpisodeFeatures,
|
|
35
35
|
effectiveWaitSeconds,
|
|
36
|
-
evaluateScope,
|
|
37
36
|
hookWaitClamped,
|
|
38
|
-
hookWaitDeadline
|
|
39
|
-
scopeChangeReason,
|
|
40
|
-
scopeRequiresHuman
|
|
37
|
+
hookWaitDeadline
|
|
41
38
|
} from "./chunk-DQAN3JQP.js";
|
|
42
39
|
import {
|
|
43
40
|
getMachineId
|
|
@@ -383,6 +380,18 @@ var handleKeyless = (input) => {
|
|
|
383
380
|
}
|
|
384
381
|
return void 0;
|
|
385
382
|
};
|
|
383
|
+
var claudeGateInput = (modeState, config, input) => {
|
|
384
|
+
const scopePath = modeState.scope ? scopePathFor(input.tool_name, input.tool_input, input.cwd) : void 0;
|
|
385
|
+
return {
|
|
386
|
+
modeState,
|
|
387
|
+
config,
|
|
388
|
+
toolName: input.tool_name,
|
|
389
|
+
toolInputs: [input.tool_input ?? {}],
|
|
390
|
+
scopePaths: scopePath ? [scopePath] : [],
|
|
391
|
+
cwd: input.cwd,
|
|
392
|
+
repoKey: repoKeyFor(input.cwd)
|
|
393
|
+
};
|
|
394
|
+
};
|
|
386
395
|
var handlePreToolUse = async (input) => {
|
|
387
396
|
if (input.tool_name.startsWith("mcp__pushary__")) return void 0;
|
|
388
397
|
let apiKey;
|
|
@@ -394,26 +403,26 @@ var handlePreToolUse = async (input) => {
|
|
|
394
403
|
try {
|
|
395
404
|
const modeState = await fetchModeState(apiKey, input.session_id);
|
|
396
405
|
const policy = await getPolicy(apiKey, modeState.policyVersion, "claude_code", true);
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
406
|
+
const verdict = resolveGate(claudeGateInput(modeState, policy, input));
|
|
407
|
+
if (verdict.kind === "kill") return deny(verdict.reason);
|
|
400
408
|
if (input.tool_name === "AskUserQuestion") {
|
|
401
409
|
return handleAskUserQuestion(apiKey, input);
|
|
402
410
|
}
|
|
403
411
|
if (shouldDeferToNativeMode(input.permission_mode, input.tool_name)) {
|
|
404
412
|
return void 0;
|
|
405
413
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
414
|
+
switch (verdict.kind) {
|
|
415
|
+
case "allow":
|
|
416
|
+
return allow();
|
|
417
|
+
case "deny":
|
|
418
|
+
return deny(verdict.reason);
|
|
419
|
+
// State we could not establish, with an auto-approve on the table. No
|
|
420
|
+
// opinion, so Claude Code's own flow runs as if the hook were absent.
|
|
421
|
+
case "defer":
|
|
422
|
+
return void 0;
|
|
423
|
+
default:
|
|
424
|
+
return dispatchModeHandler(apiKey, input, verdict.policy, getMachineId(), modeState.trainingConsent, verdict.scopeReason, verdict.scopePath);
|
|
415
425
|
}
|
|
416
|
-
return dispatchModeHandler(apiKey, input, toolPolicy, getMachineId(), modeState.trainingConsent, scopeReason, scopePath);
|
|
417
426
|
} catch {
|
|
418
427
|
return void 0;
|
|
419
428
|
}
|
|
@@ -452,13 +461,22 @@ var handlePermissionRequest = async (input) => {
|
|
|
452
461
|
try {
|
|
453
462
|
const modeState = await fetchModeState(apiKey, input.session_id);
|
|
454
463
|
const policy = await getPolicy(apiKey, modeState.policyVersion, "claude_code", true);
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
464
|
+
const verdict = resolveGate(claudeGateInput(modeState, policy, input));
|
|
465
|
+
switch (verdict.kind) {
|
|
466
|
+
case "kill":
|
|
467
|
+
return permReqDeny(verdict.reason);
|
|
468
|
+
case "allow":
|
|
469
|
+
return permReqAllow();
|
|
470
|
+
case "deny":
|
|
471
|
+
return permReqDeny(verdict.reason);
|
|
472
|
+
// No opinion, so Claude Code's own dialog shows.
|
|
473
|
+
case "defer":
|
|
474
|
+
return void 0;
|
|
475
|
+
default:
|
|
476
|
+
return toPermissionRequestOutput(
|
|
477
|
+
await dispatchModeHandler(apiKey, input, verdict.policy, getMachineId(), modeState.trainingConsent, verdict.scopeReason, verdict.scopePath)
|
|
478
|
+
);
|
|
460
479
|
}
|
|
461
|
-
return toPermissionRequestOutput(await dispatchModeHandler(apiKey, input, toolPolicy, getMachineId(), modeState.trainingConsent));
|
|
462
480
|
} catch {
|
|
463
481
|
return void 0;
|
|
464
482
|
}
|
|
@@ -54,6 +54,7 @@ var isWaitForAnswerResponse = (data) => {
|
|
|
54
54
|
|
|
55
55
|
// src/policy.ts
|
|
56
56
|
var CACHE_TTL_MS = 60 * 1e3;
|
|
57
|
+
var STALE_CACHE_MAX_MS = 12 * 60 * 60 * 1e3;
|
|
57
58
|
var policyCacheFile = (apiKey, agent, repoAware = false) => {
|
|
58
59
|
const parts = [apiKey, agent, repoAware ? "repoAware" : void 0].filter(Boolean);
|
|
59
60
|
const hash = createHash("sha256").update(parts.join(":")).digest("hex").slice(0, 12);
|
|
@@ -88,18 +89,19 @@ var getPolicy = async (apiKey, expectedVersion, agent, repoAware = false) => {
|
|
|
88
89
|
const cached = JSON.parse(stat);
|
|
89
90
|
if (!isPolicyConfig(cached)) throw new Error("Corrupted cache");
|
|
90
91
|
const versionStale = expectedVersion != null && (cached._policyVersion ?? null) !== expectedVersion;
|
|
91
|
-
const
|
|
92
|
+
const age = cached._cachedAt ? Date.now() - cached._cachedAt : 0;
|
|
93
|
+
const ttlFresh = !cached._cachedAt || age < CACHE_TTL_MS;
|
|
92
94
|
if (ttlFresh && !versionStale) {
|
|
93
95
|
return cached;
|
|
94
96
|
}
|
|
95
|
-
staleCache = cached;
|
|
97
|
+
if (age < STALE_CACHE_MAX_MS) staleCache = cached;
|
|
96
98
|
} catch {
|
|
97
99
|
}
|
|
98
100
|
}
|
|
99
101
|
try {
|
|
100
102
|
const policy = await fetchPolicy(apiKey, agent, repoAware);
|
|
101
103
|
try {
|
|
102
|
-
writeFileSync(path, JSON.stringify({ ...policy, _cachedAt: Date.now(), _policyVersion: expectedVersion ?? null }), "utf-8");
|
|
104
|
+
writeFileSync(path, JSON.stringify({ ...policy, _cachedAt: Date.now(), _policyVersion: expectedVersion ?? null }), { encoding: "utf-8", mode: 384 });
|
|
103
105
|
} catch {
|
|
104
106
|
}
|
|
105
107
|
return policy;
|
|
@@ -178,6 +180,57 @@ var resolvePolicy = (config, toolName, modeOverride, toolInput, cwd, repoKey) =>
|
|
|
178
180
|
}
|
|
179
181
|
return base;
|
|
180
182
|
};
|
|
183
|
+
var restraintRank = (policy) => {
|
|
184
|
+
if (policy.timeoutSeconds === 0 && policy.timeoutAction === "deny") return 4;
|
|
185
|
+
if (policy.timeoutSeconds === 0 && policy.timeoutAction === "approve") return 0;
|
|
186
|
+
if (policy.mode === "push_only") return 3;
|
|
187
|
+
return policy.mode === "terminal_only" || policy.mode === "notify_only" ? 1 : 2;
|
|
188
|
+
};
|
|
189
|
+
var strictestPolicy = (a, b) => restraintRank(b) > restraintRank(a) ? b : a;
|
|
190
|
+
var resolvePolicyAcross = (config, toolName, modeOverride, toolInputs, cwd, repoKey) => {
|
|
191
|
+
if (toolInputs.length === 0) return resolvePolicy(config, toolName, modeOverride, void 0, cwd, repoKey);
|
|
192
|
+
return toolInputs.map((input) => resolvePolicy(config, toolName, modeOverride, input, cwd, repoKey)).reduce(strictestPolicy);
|
|
193
|
+
};
|
|
194
|
+
var isModeStateDegraded = (state) => state?.degraded === true;
|
|
195
|
+
var MODE_GRACE_MS = 60 * 1e3;
|
|
196
|
+
var modeStateCacheFile = (apiKey) => {
|
|
197
|
+
const hash = createHash("sha256").update(`mode:${apiKey}`).digest("hex").slice(0, 12);
|
|
198
|
+
return join(tmpdir(), `pushary-mode-${hash}.json`);
|
|
199
|
+
};
|
|
200
|
+
var readLastGoodMode = (apiKey) => {
|
|
201
|
+
try {
|
|
202
|
+
const path = modeStateCacheFile(apiKey);
|
|
203
|
+
if (!existsSync(path)) return null;
|
|
204
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
205
|
+
if (!parsed || typeof parsed.at !== "number" || !parsed.state) return null;
|
|
206
|
+
return parsed;
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
var writeLastGoodMode = (apiKey, state) => {
|
|
212
|
+
try {
|
|
213
|
+
writeFileSync(modeStateCacheFile(apiKey), JSON.stringify({ state, at: Date.now() }), { encoding: "utf-8", mode: 384 });
|
|
214
|
+
} catch {
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
var unknownMode = () => ({
|
|
218
|
+
mode: null,
|
|
219
|
+
kill: false,
|
|
220
|
+
policyVersion: null,
|
|
221
|
+
relayUrl: null,
|
|
222
|
+
trainingConsent: false,
|
|
223
|
+
scope: null,
|
|
224
|
+
degraded: true
|
|
225
|
+
});
|
|
226
|
+
var degradedFallback = (apiKey, authoritative = false) => {
|
|
227
|
+
if (authoritative) return unknownMode();
|
|
228
|
+
const last = readLastGoodMode(apiKey);
|
|
229
|
+
if (last && Date.now() - last.at < MODE_GRACE_MS) {
|
|
230
|
+
return { ...last.state, degraded: false };
|
|
231
|
+
}
|
|
232
|
+
return unknownMode();
|
|
233
|
+
};
|
|
181
234
|
var toPolicyVersion = (value) => typeof value === "string" || typeof value === "number" ? String(value) : null;
|
|
182
235
|
var fetchModeState = async (apiKey, sessionId) => {
|
|
183
236
|
try {
|
|
@@ -187,10 +240,12 @@ var fetchModeState = async (apiKey, sessionId) => {
|
|
|
187
240
|
headers: { "Authorization": `Bearer ${apiKey}` },
|
|
188
241
|
signal: AbortSignal.timeout(3e3)
|
|
189
242
|
});
|
|
190
|
-
if (!response.ok)
|
|
243
|
+
if (!response.ok) {
|
|
244
|
+
return degradedFallback(apiKey, response.status === 401 || response.status === 403);
|
|
245
|
+
}
|
|
191
246
|
const data = await response.json();
|
|
192
247
|
const mode = data.override?.mode;
|
|
193
|
-
|
|
248
|
+
const state = {
|
|
194
249
|
mode: isApprovalMode(mode) ? mode : null,
|
|
195
250
|
kill: data.kill === true,
|
|
196
251
|
policyVersion: toPolicyVersion(data.policyVersion),
|
|
@@ -198,10 +253,13 @@ var fetchModeState = async (apiKey, sessionId) => {
|
|
|
198
253
|
trainingConsent: data.trainingConsent === true,
|
|
199
254
|
// Validated rather than cast: a malformed contract must read as no contract,
|
|
200
255
|
// never as one that silently matches nothing.
|
|
201
|
-
scope: isScopeContract(data.scope) ? data.scope : null
|
|
256
|
+
scope: isScopeContract(data.scope) ? data.scope : null,
|
|
257
|
+
degraded: false
|
|
202
258
|
};
|
|
259
|
+
writeLastGoodMode(apiKey, state);
|
|
260
|
+
return state;
|
|
203
261
|
} catch {
|
|
204
|
-
return
|
|
262
|
+
return degradedFallback(apiKey);
|
|
205
263
|
}
|
|
206
264
|
};
|
|
207
265
|
var fetchModeOverride = async (apiKey) => (await fetchModeState(apiKey)).mode;
|
|
@@ -275,7 +333,7 @@ var TOOL_TARGET_MAX_LENGTH = 80;
|
|
|
275
333
|
var deriveCommandHead = (command) => {
|
|
276
334
|
if (typeof command !== "string") return void 0;
|
|
277
335
|
const head = command.trim().split(/\s+/).slice(0, 2).join(" ");
|
|
278
|
-
return head ? head.slice(0, TOOL_TARGET_MAX_LENGTH) : void 0;
|
|
336
|
+
return head ? redactSecrets(head).slice(0, TOOL_TARGET_MAX_LENGTH) : void 0;
|
|
279
337
|
};
|
|
280
338
|
var deriveToolTarget = (toolName, toolInput) => {
|
|
281
339
|
if (toolName === "Bash" || toolName === "PowerShell" || toolName === "Monitor") {
|
|
@@ -300,7 +358,7 @@ var deriveReceiptCommandHead = (command) => {
|
|
|
300
358
|
const [first, second] = tokens;
|
|
301
359
|
const keep = first && second && SCRIPT_RUNNERS.has(first.toLowerCase()) && RUN_SUBCOMMANDS.has(second.toLowerCase()) ? 3 : 2;
|
|
302
360
|
const head = tokens.slice(0, keep).join(" ");
|
|
303
|
-
return head ? head.slice(0, TOOL_TARGET_MAX_LENGTH) : void 0;
|
|
361
|
+
return head ? redactSecrets(head).slice(0, TOOL_TARGET_MAX_LENGTH) : void 0;
|
|
304
362
|
};
|
|
305
363
|
var RECEIPT_TARGET_MAX_LENGTH = 256;
|
|
306
364
|
var EXIT_CODE_FAILURE = /^Error: Exit code \d+/;
|
|
@@ -335,7 +393,9 @@ var deriveReceiptMeta = (toolName, toolInput, toolResult, cwd) => {
|
|
|
335
393
|
if (typeof filePath !== "string" || !filePath) return void 0;
|
|
336
394
|
return {
|
|
337
395
|
kind: toolName === "Write" ? "write" : "edit",
|
|
338
|
-
|
|
396
|
+
// A path is an unlikely place for a credential but not an impossible one
|
|
397
|
+
// (a token in a temp directory name), and this field is persisted.
|
|
398
|
+
target: redactSecrets(relativizeReceiptPath(filePath, cwd)).slice(0, RECEIPT_TARGET_MAX_LENGTH),
|
|
339
399
|
ok
|
|
340
400
|
};
|
|
341
401
|
}
|
|
@@ -441,9 +501,93 @@ var removePendingSession = (sessionId) => {
|
|
|
441
501
|
}
|
|
442
502
|
};
|
|
443
503
|
|
|
504
|
+
// src/repo.ts
|
|
505
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
506
|
+
import { basename, dirname, isAbsolute as isAbsolute2, join as join3, resolve } from "path";
|
|
507
|
+
var MAX_PARENT_WALK = 64;
|
|
508
|
+
var GIT_FILE_PREFIX = "gitdir:";
|
|
509
|
+
var findGitDir = (startDir) => {
|
|
510
|
+
let current = resolve(startDir);
|
|
511
|
+
for (let depth = 0; depth < MAX_PARENT_WALK; depth += 1) {
|
|
512
|
+
const candidate = join3(current, ".git");
|
|
513
|
+
try {
|
|
514
|
+
if (existsSync3(candidate)) {
|
|
515
|
+
if (statSync2(candidate).isDirectory()) return candidate;
|
|
516
|
+
const pointer = readFileSync2(candidate, "utf-8").trim();
|
|
517
|
+
if (pointer.startsWith(GIT_FILE_PREFIX)) {
|
|
518
|
+
const target = pointer.slice(GIT_FILE_PREFIX.length).trim();
|
|
519
|
+
return isAbsolute2(target) ? target : resolve(current, target);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
} catch {
|
|
523
|
+
}
|
|
524
|
+
const parent = dirname(current);
|
|
525
|
+
if (parent === current) return void 0;
|
|
526
|
+
current = parent;
|
|
527
|
+
}
|
|
528
|
+
return void 0;
|
|
529
|
+
};
|
|
530
|
+
var configPathFor = (gitDir) => {
|
|
531
|
+
const index = gitDir.replace(/\\/g, "/").indexOf("/worktrees/");
|
|
532
|
+
return index === -1 ? join3(gitDir, "config") : join3(gitDir.slice(0, index), "config");
|
|
533
|
+
};
|
|
534
|
+
var ORIGIN_SECTION = /^\[remote "origin"\]/;
|
|
535
|
+
var SECTION_HEADER = /^\[/;
|
|
536
|
+
var URL_LINE = /^url\s*=\s*(.+)$/;
|
|
537
|
+
var readOriginRemote = (gitDir) => {
|
|
538
|
+
try {
|
|
539
|
+
const configPath = configPathFor(gitDir);
|
|
540
|
+
if (!existsSync3(configPath)) return void 0;
|
|
541
|
+
let inOrigin = false;
|
|
542
|
+
for (const rawLine of readFileSync2(configPath, "utf-8").split("\n")) {
|
|
543
|
+
const line = rawLine.trim();
|
|
544
|
+
if (SECTION_HEADER.test(line)) {
|
|
545
|
+
inOrigin = ORIGIN_SECTION.test(line);
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
if (!inOrigin) continue;
|
|
549
|
+
const url = URL_LINE.exec(line);
|
|
550
|
+
if (url) return url[1]?.trim();
|
|
551
|
+
}
|
|
552
|
+
} catch {
|
|
553
|
+
}
|
|
554
|
+
return void 0;
|
|
555
|
+
};
|
|
556
|
+
var deriveRepoKey = (cwd) => {
|
|
557
|
+
const dir = cwd && cwd.length > 0 ? cwd : process.cwd();
|
|
558
|
+
try {
|
|
559
|
+
const gitDir = findGitDir(dir);
|
|
560
|
+
if (gitDir) {
|
|
561
|
+
const remote = readOriginRemote(gitDir);
|
|
562
|
+
const normalized = remote ? normalizeRepoRemote(remote) : void 0;
|
|
563
|
+
if (normalized) return normalized;
|
|
564
|
+
const root = gitDir.endsWith(".git") ? dirname(gitDir) : dir;
|
|
565
|
+
return localRepoKey(basename(root));
|
|
566
|
+
}
|
|
567
|
+
return localRepoKey(basename(resolve(dir)));
|
|
568
|
+
} catch {
|
|
569
|
+
return void 0;
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
var repoKeyCache = /* @__PURE__ */ new Map();
|
|
573
|
+
var REPO_KEY_ENV = "PUSHARY_REPO_KEY";
|
|
574
|
+
var repoKeyFor = (cwd) => {
|
|
575
|
+
const override = process.env[REPO_KEY_ENV];
|
|
576
|
+
if (typeof override === "string") {
|
|
577
|
+
const trimmed = override.trim();
|
|
578
|
+
if (trimmed.toLowerCase() === "off") return void 0;
|
|
579
|
+
if (trimmed.length > 0) return trimmed.toLowerCase();
|
|
580
|
+
}
|
|
581
|
+
const dir = cwd && cwd.length > 0 ? cwd : process.cwd();
|
|
582
|
+
if (repoKeyCache.has(dir)) return repoKeyCache.get(dir);
|
|
583
|
+
const derived = deriveRepoKey(dir);
|
|
584
|
+
repoKeyCache.set(dir, derived);
|
|
585
|
+
return derived;
|
|
586
|
+
};
|
|
587
|
+
|
|
444
588
|
// src/usage.ts
|
|
445
|
-
import { closeSync, mkdirSync as mkdirSync2, openSync, readFileSync as
|
|
446
|
-
import { join as
|
|
589
|
+
import { closeSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync3, readSync, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
590
|
+
import { join as join4 } from "path";
|
|
447
591
|
import { tmpdir as tmpdir3 } from "os";
|
|
448
592
|
var DEFAULT_PRICES = [
|
|
449
593
|
{ match: "opus-4-1", in: 15, out: 75 },
|
|
@@ -479,12 +623,12 @@ var estimateCostUsd = (usage, model) => {
|
|
|
479
623
|
const perTokenOut = price.out / 1e6;
|
|
480
624
|
return usage.inputTokens * perTokenIn + usage.outputTokens * perTokenOut + usage.cacheCreationTokens * perTokenIn * CACHE_WRITE_MULTIPLIER + usage.cacheReadTokens * perTokenIn * CACHE_READ_MULTIPLIER;
|
|
481
625
|
};
|
|
482
|
-
var stateDir = () => process.env.PUSHARY_USAGE_DIR?.trim() ||
|
|
483
|
-
var stateFile = (sessionId) =>
|
|
626
|
+
var stateDir = () => process.env.PUSHARY_USAGE_DIR?.trim() || join4(tmpdir3(), "pushary-usage");
|
|
627
|
+
var stateFile = (sessionId) => join4(stateDir(), sessionId.replace(/[^a-zA-Z0-9_-]/g, "_"));
|
|
484
628
|
var emptyState = () => ({ offset: 0, tokensIn: 0, tokensOut: 0, costUsd: 0, recentIds: [] });
|
|
485
629
|
var readState = (path) => {
|
|
486
630
|
try {
|
|
487
|
-
const parsed = JSON.parse(
|
|
631
|
+
const parsed = JSON.parse(readFileSync3(path, "utf-8"));
|
|
488
632
|
if (typeof parsed.offset === "number" && parsed.offset >= 0 && typeof parsed.tokensIn === "number" && typeof parsed.tokensOut === "number" && typeof parsed.costUsd === "number" && Array.isArray(parsed.recentIds)) {
|
|
489
633
|
return {
|
|
490
634
|
offset: parsed.offset,
|
|
@@ -559,7 +703,7 @@ var extractUserText = (content) => {
|
|
|
559
703
|
};
|
|
560
704
|
var readLastUserPrompt = (transcriptPath) => {
|
|
561
705
|
try {
|
|
562
|
-
const size =
|
|
706
|
+
const size = statSync3(transcriptPath).size;
|
|
563
707
|
const start = Math.max(0, size - USER_PROMPT_TAIL_BYTES);
|
|
564
708
|
const lines = readRange(transcriptPath, start, size).toString("utf-8").split("\n");
|
|
565
709
|
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
@@ -582,7 +726,7 @@ var readLastUserPrompt = (transcriptPath) => {
|
|
|
582
726
|
};
|
|
583
727
|
var readNewUsage = (transcriptPath, sessionId) => {
|
|
584
728
|
try {
|
|
585
|
-
const size =
|
|
729
|
+
const size = statSync3(transcriptPath).size;
|
|
586
730
|
const path = stateFile(sessionId);
|
|
587
731
|
let state = readState(path);
|
|
588
732
|
if (size < state.offset) state = { ...emptyState(), recentIds: state.recentIds };
|
|
@@ -613,7 +757,7 @@ var readNewUsage = (transcriptPath, sessionId) => {
|
|
|
613
757
|
};
|
|
614
758
|
|
|
615
759
|
// src/codex-adapter.ts
|
|
616
|
-
import { resolve } from "path";
|
|
760
|
+
import { resolve as resolve2 } from "path";
|
|
617
761
|
var CODEX_AGENT = { type: "codex", label: "Codex" };
|
|
618
762
|
var codexAllow = () => ({ kind: "allow" });
|
|
619
763
|
var codexDeny = (reason) => ({ kind: "deny", reason });
|
|
@@ -679,10 +823,25 @@ var toPolicyLookup = (toolName, toolInput, cwd) => {
|
|
|
679
823
|
const command = toolInput.command;
|
|
680
824
|
const files = parseApplyPatchFiles(command);
|
|
681
825
|
if (files.length === 1) {
|
|
682
|
-
return { tool: "Edit", input: { file_path: cwd ?
|
|
826
|
+
return { tool: "Edit", input: { file_path: cwd ? resolve2(cwd, files[0]) : files[0] } };
|
|
683
827
|
}
|
|
684
828
|
return { tool: "Edit", input: typeof command === "string" ? { file_path: command } : {} };
|
|
685
829
|
};
|
|
830
|
+
var toPolicyLookups = (toolName, toolInput, cwd) => {
|
|
831
|
+
if (toolName !== "apply_patch") {
|
|
832
|
+
const single = toPolicyLookup(toolName, toolInput, cwd);
|
|
833
|
+
return { tool: single.tool, inputs: [single.input] };
|
|
834
|
+
}
|
|
835
|
+
const files = parseApplyPatchFiles(toolInput.command);
|
|
836
|
+
if (files.length === 0) {
|
|
837
|
+
const single = toPolicyLookup(toolName, toolInput, cwd);
|
|
838
|
+
return { tool: single.tool, inputs: [single.input] };
|
|
839
|
+
}
|
|
840
|
+
return {
|
|
841
|
+
tool: "Edit",
|
|
842
|
+
inputs: files.map((file) => ({ file_path: cwd ? resolve2(cwd, file) : file }))
|
|
843
|
+
};
|
|
844
|
+
};
|
|
686
845
|
var permissionTimeoutDecision = (timeoutAction) => {
|
|
687
846
|
if (timeoutAction === "approve") return codexAllow();
|
|
688
847
|
if (timeoutAction === "deny") return codexDeny("No response within timeout");
|
|
@@ -690,90 +849,6 @@ var permissionTimeoutDecision = (timeoutAction) => {
|
|
|
690
849
|
};
|
|
691
850
|
var preToolUseTimeoutDecision = (timeoutAction, denyReason = "No response within timeout") => timeoutAction === "deny" ? codexDeny(denyReason) : codexPass();
|
|
692
851
|
|
|
693
|
-
// src/repo.ts
|
|
694
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
|
|
695
|
-
import { basename, dirname, isAbsolute as isAbsolute2, join as join4, resolve as resolve2 } from "path";
|
|
696
|
-
var MAX_PARENT_WALK = 64;
|
|
697
|
-
var GIT_FILE_PREFIX = "gitdir:";
|
|
698
|
-
var findGitDir = (startDir) => {
|
|
699
|
-
let current = resolve2(startDir);
|
|
700
|
-
for (let depth = 0; depth < MAX_PARENT_WALK; depth += 1) {
|
|
701
|
-
const candidate = join4(current, ".git");
|
|
702
|
-
try {
|
|
703
|
-
if (existsSync3(candidate)) {
|
|
704
|
-
if (statSync3(candidate).isDirectory()) return candidate;
|
|
705
|
-
const pointer = readFileSync3(candidate, "utf-8").trim();
|
|
706
|
-
if (pointer.startsWith(GIT_FILE_PREFIX)) {
|
|
707
|
-
const target = pointer.slice(GIT_FILE_PREFIX.length).trim();
|
|
708
|
-
return isAbsolute2(target) ? target : resolve2(current, target);
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
} catch {
|
|
712
|
-
}
|
|
713
|
-
const parent = dirname(current);
|
|
714
|
-
if (parent === current) return void 0;
|
|
715
|
-
current = parent;
|
|
716
|
-
}
|
|
717
|
-
return void 0;
|
|
718
|
-
};
|
|
719
|
-
var configPathFor = (gitDir) => {
|
|
720
|
-
const index = gitDir.replace(/\\/g, "/").indexOf("/worktrees/");
|
|
721
|
-
return index === -1 ? join4(gitDir, "config") : join4(gitDir.slice(0, index), "config");
|
|
722
|
-
};
|
|
723
|
-
var ORIGIN_SECTION = /^\[remote "origin"\]/;
|
|
724
|
-
var SECTION_HEADER = /^\[/;
|
|
725
|
-
var URL_LINE = /^url\s*=\s*(.+)$/;
|
|
726
|
-
var readOriginRemote = (gitDir) => {
|
|
727
|
-
try {
|
|
728
|
-
const configPath = configPathFor(gitDir);
|
|
729
|
-
if (!existsSync3(configPath)) return void 0;
|
|
730
|
-
let inOrigin = false;
|
|
731
|
-
for (const rawLine of readFileSync3(configPath, "utf-8").split("\n")) {
|
|
732
|
-
const line = rawLine.trim();
|
|
733
|
-
if (SECTION_HEADER.test(line)) {
|
|
734
|
-
inOrigin = ORIGIN_SECTION.test(line);
|
|
735
|
-
continue;
|
|
736
|
-
}
|
|
737
|
-
if (!inOrigin) continue;
|
|
738
|
-
const url = URL_LINE.exec(line);
|
|
739
|
-
if (url) return url[1]?.trim();
|
|
740
|
-
}
|
|
741
|
-
} catch {
|
|
742
|
-
}
|
|
743
|
-
return void 0;
|
|
744
|
-
};
|
|
745
|
-
var deriveRepoKey = (cwd) => {
|
|
746
|
-
const dir = cwd && cwd.length > 0 ? cwd : process.cwd();
|
|
747
|
-
try {
|
|
748
|
-
const gitDir = findGitDir(dir);
|
|
749
|
-
if (gitDir) {
|
|
750
|
-
const remote = readOriginRemote(gitDir);
|
|
751
|
-
const normalized = remote ? normalizeRepoRemote(remote) : void 0;
|
|
752
|
-
if (normalized) return normalized;
|
|
753
|
-
const root = gitDir.endsWith(".git") ? dirname(gitDir) : dir;
|
|
754
|
-
return localRepoKey(basename(root));
|
|
755
|
-
}
|
|
756
|
-
return localRepoKey(basename(resolve2(dir)));
|
|
757
|
-
} catch {
|
|
758
|
-
return void 0;
|
|
759
|
-
}
|
|
760
|
-
};
|
|
761
|
-
var repoKeyCache = /* @__PURE__ */ new Map();
|
|
762
|
-
var REPO_KEY_ENV = "PUSHARY_REPO_KEY";
|
|
763
|
-
var repoKeyFor = (cwd) => {
|
|
764
|
-
const override = process.env[REPO_KEY_ENV];
|
|
765
|
-
if (typeof override === "string") {
|
|
766
|
-
const trimmed = override.trim();
|
|
767
|
-
if (trimmed.toLowerCase() === "off") return void 0;
|
|
768
|
-
if (trimmed.length > 0) return trimmed.toLowerCase();
|
|
769
|
-
}
|
|
770
|
-
const dir = cwd && cwd.length > 0 ? cwd : process.cwd();
|
|
771
|
-
if (repoKeyCache.has(dir)) return repoKeyCache.get(dir);
|
|
772
|
-
const derived = deriveRepoKey(dir);
|
|
773
|
-
repoKeyCache.set(dir, derived);
|
|
774
|
-
return derived;
|
|
775
|
-
};
|
|
776
|
-
|
|
777
852
|
// src/throttle.ts
|
|
778
853
|
import { join as join5 } from "path";
|
|
779
854
|
import { tmpdir as tmpdir4 } from "os";
|
|
@@ -1235,7 +1310,10 @@ var handleStopFailure = async (input, agent = CLAUDE_CODE_AGENT) => {
|
|
|
1235
1310
|
|
|
1236
1311
|
export {
|
|
1237
1312
|
getPolicy,
|
|
1313
|
+
resolveAutoResolveOrigin,
|
|
1238
1314
|
resolvePolicy,
|
|
1315
|
+
resolvePolicyAcross,
|
|
1316
|
+
isModeStateDegraded,
|
|
1239
1317
|
fetchModeState,
|
|
1240
1318
|
fetchModeOverride,
|
|
1241
1319
|
askUser,
|
|
@@ -1260,6 +1338,7 @@ export {
|
|
|
1260
1338
|
toCodexWire,
|
|
1261
1339
|
describeApplyPatch,
|
|
1262
1340
|
toPolicyLookup,
|
|
1341
|
+
toPolicyLookups,
|
|
1263
1342
|
permissionTimeoutDecision,
|
|
1264
1343
|
preToolUseTimeoutDecision,
|
|
1265
1344
|
readLastPrompt,
|
|
@@ -533,6 +533,7 @@ var cancelPairing = async (pairId) => {
|
|
|
533
533
|
} catch {
|
|
534
534
|
}
|
|
535
535
|
};
|
|
536
|
+
var PAIR_MAX_CONSECUTIVE_FAILURES = 8;
|
|
536
537
|
var claimPairing = async (pairId) => {
|
|
537
538
|
try {
|
|
538
539
|
const res = await fetch(`${apiBase()}/api/mobile/pair/claim?id=${encodeURIComponent(pairId)}`, {
|
|
@@ -544,6 +545,17 @@ var claimPairing = async (pairId) => {
|
|
|
544
545
|
return null;
|
|
545
546
|
}
|
|
546
547
|
};
|
|
548
|
+
var ackPairing = async (pairId) => {
|
|
549
|
+
try {
|
|
550
|
+
await fetch(`${apiBase()}/api/mobile/pair/ack`, {
|
|
551
|
+
method: "POST",
|
|
552
|
+
headers: { "Content-Type": "application/json" },
|
|
553
|
+
body: JSON.stringify({ pairId }),
|
|
554
|
+
signal: AbortSignal.timeout(5e3)
|
|
555
|
+
});
|
|
556
|
+
} catch {
|
|
557
|
+
}
|
|
558
|
+
};
|
|
547
559
|
var buildPairLinks = (pairId, publicKeyB64, baseUrl = apiBase()) => {
|
|
548
560
|
const id = encodeURIComponent(pairId);
|
|
549
561
|
const pk = encodeURIComponent(publicKeyB64);
|
|
@@ -593,10 +605,22 @@ var connectViaAppPairing = async (options = {}) => {
|
|
|
593
605
|
void Promise.allSettled([Promise.resolve(flushed), cancelPairing(pairId)]).finally(() => process.exit(EXIT_ABORTED));
|
|
594
606
|
};
|
|
595
607
|
process.once("SIGINT", onInterrupt);
|
|
608
|
+
let consecutiveFailures = 0;
|
|
596
609
|
try {
|
|
597
610
|
while (Date.now() < deadline) {
|
|
598
611
|
if (cancelled) return null;
|
|
599
612
|
const claim = await claimPairing(pairId);
|
|
613
|
+
if (claim === null) {
|
|
614
|
+
consecutiveFailures += 1;
|
|
615
|
+
if (consecutiveFailures >= PAIR_MAX_CONSECUTIVE_FAILURES) {
|
|
616
|
+
stop(yellow("!"), "Can't reach pushary.com to finish pairing. Check your connection, then re-run setup.");
|
|
617
|
+
await cancelPairing(pairId);
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
await sleep(PAIR_POLL_INTERVAL_MS);
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
consecutiveFailures = 0;
|
|
600
624
|
if (claim?.status === "authorized") {
|
|
601
625
|
let apiKey;
|
|
602
626
|
try {
|
|
@@ -606,6 +630,7 @@ var connectViaAppPairing = async (options = {}) => {
|
|
|
606
630
|
await cancelPairing(pairId);
|
|
607
631
|
return null;
|
|
608
632
|
}
|
|
633
|
+
await ackPairing(pairId);
|
|
609
634
|
stop(check, "App connected");
|
|
610
635
|
return { apiKey, handle: claim.siteSlug };
|
|
611
636
|
}
|