@pushary/agent-hooks 0.95.2 → 0.95.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +1 -1
- package/data/cursor-plugin/.cursor-plugin/plugin.json +1 -1
- package/data/cursor-plugin/CHANGELOG.md +5 -0
- package/data/cursor-plugin/README.md +6 -8
- package/data/cursor-plugin/hooks/hooks.json +56 -1
- package/data/cursor-plugin/scripts/pushary-gate.mjs +120 -71
- package/data/vscode-plugin/.claude-plugin/plugin.json +1 -1
- package/data/vscode-plugin/CHANGELOG.md +5 -0
- package/data/vscode-plugin/README.md +7 -15
- package/data/vscode-plugin/hooks/hooks.json +49 -0
- package/data/vscode-plugin/scripts/pushary-gate.mjs +208 -88
- package/dist/bin/pushary-bell-hook.js +20 -22
- package/dist/bin/pushary-bell.js +9 -11
- package/dist/bin/pushary-claude.js +58 -46
- package/dist/bin/pushary-clean.js +21 -20
- package/dist/bin/pushary-codex-bridge.js +120 -25
- package/dist/bin/pushary-codex-hook.js +43 -63
- package/dist/bin/pushary-codex.js +11 -10
- package/dist/bin/pushary-connect.js +8 -10
- package/dist/bin/pushary-cowork.js +5 -5
- package/dist/bin/pushary-daemon.js +26 -28
- package/dist/bin/pushary-disconnect.js +11 -10
- package/dist/bin/pushary-doctor.js +25 -25
- package/dist/bin/pushary-elicitation-hook.js +30 -31
- package/dist/bin/pushary-gemini-bridge.js +9 -11
- package/dist/bin/pushary-gemini-hook.js +45 -79
- package/dist/bin/pushary-hook.js +27 -29
- package/dist/bin/pushary-login.js +57 -31
- package/dist/bin/pushary-logout.js +6 -8
- package/dist/bin/pushary-mode.js +5 -5
- package/dist/bin/pushary-notification-hook.js +22 -24
- package/dist/bin/pushary-opencode-hook.js +38 -74
- package/dist/bin/pushary-permission-denied-hook.js +25 -27
- package/dist/bin/pushary-permission-hook.js +25 -27
- package/dist/bin/pushary-post-hook.js +22 -24
- package/dist/bin/pushary-prompt-hook.js +22 -24
- package/dist/bin/pushary-session-end-hook.js +22 -24
- package/dist/bin/pushary-session-start-hook.js +25 -27
- package/dist/bin/pushary-setup.js +78 -55
- package/dist/bin/pushary-stats.js +4 -4
- package/dist/bin/pushary-status.js +10 -12
- package/dist/bin/pushary-stop-hook.js +22 -24
- package/dist/bin/pushary-stopfailure-hook.js +22 -24
- package/dist/bin/pushary-suggestions.js +2 -2
- package/dist/bin/pushary-transcript-register.js +18 -20
- package/dist/bin/pushary-transcripts.js +7 -9
- package/dist/bin/pushary-upgrade.js +17 -19
- package/dist/bin/pushary-wait.js +5 -5
- package/dist/bin/pushary.js +1 -1
- package/dist/{chunk-7LUFYRH4.js → chunk-2D7BAOMY.js} +7 -7
- package/dist/{chunk-3ZUTNTZA.js → chunk-2ZTL3PGN.js} +1 -1
- package/dist/{chunk-EPFDN2ZT.js → chunk-32RMQO5Z.js} +1 -1
- package/dist/{chunk-EZIEXHYM.js → chunk-3AJIK5AO.js} +5 -2
- package/dist/{chunk-IPHOUBTU.js → chunk-4PX4QK2Z.js} +1 -1
- package/dist/{chunk-UW4LWAWY.js → chunk-4RF52LSU.js} +8 -8
- package/dist/{chunk-UOTSWS5N.js → chunk-67YNQ3QA.js} +43 -98
- package/dist/{chunk-G4TL4YWH.js → chunk-6AEM5RCS.js} +1 -1
- package/dist/{chunk-G24SEVCI.js → chunk-6VANQN5Q.js} +3 -7
- package/dist/{chunk-CKOLDI47.js → chunk-BDI2MXTT.js} +2 -2
- package/dist/{chunk-YKLCWWEI.js → chunk-BMANRGJQ.js} +1 -1
- package/dist/{chunk-2XUPP4WM.js → chunk-CUEZMPLJ.js} +1 -1
- package/dist/{chunk-A4R6BZCK.js → chunk-D62EONG3.js} +1 -1
- package/dist/{chunk-ZDAT5KMU.js → chunk-D6PRC3YQ.js} +1 -1
- package/dist/{chunk-MOY5X6HJ.js → chunk-DLLAWCQB.js} +1 -1
- package/dist/{chunk-DSTLA2TC.js → chunk-GOPIMNVC.js} +1 -1
- package/dist/{chunk-CH6DTGQR.js → chunk-HBHEX3JQ.js} +3 -3
- package/dist/{chunk-DNVDKHQD.js → chunk-HCMM3BOW.js} +226 -58
- package/dist/{chunk-76XICHYT.js → chunk-HGUZOUB2.js} +4 -4
- package/dist/{chunk-E3NF5CL2.js → chunk-HQIVR7MM.js} +6 -3
- package/dist/{chunk-JDFB3YKG.js → chunk-I6GZX3FT.js} +3 -3
- package/dist/{chunk-3OXHYPZG.js → chunk-IM2A5H33.js} +4 -1
- package/dist/{chunk-V27AHID3.js → chunk-IVQMNLSA.js} +3 -14
- package/dist/{chunk-4LL6LG5X.js → chunk-KAVDAZ7A.js} +43 -1
- package/dist/chunk-KBITFQST.js +6 -0
- package/dist/{chunk-AMZJ3CKW.js → chunk-KI4PA7MQ.js} +1 -1
- package/dist/{chunk-IAEXPTXA.js → chunk-N4VNLISN.js} +2 -2
- package/dist/{chunk-VX3BWMC5.js → chunk-OEKFD5BC.js} +1 -1
- package/dist/{chunk-QF4RI5CD.js → chunk-OI3DJA7N.js} +1 -1
- package/dist/{chunk-WB2C7VPF.js → chunk-OW6AZDY2.js} +3 -3
- package/dist/{chunk-4XXUHWIK.js → chunk-QHWXUNMN.js} +1 -1
- package/dist/{chunk-M7WWEFG6.js → chunk-QQGNFHIK.js} +2 -2
- package/dist/{chunk-YJAQRKH5.js → chunk-RLXVW4AM.js} +1 -1
- package/dist/{chunk-GUVCYYDK.js → chunk-S5CMLR35.js} +94 -15
- package/dist/{chunk-EUAQ4YAN.js → chunk-UCS65VVR.js} +1 -1
- package/dist/{chunk-WM5XMCID.js → chunk-VOSGZ6PH.js} +4 -4
- package/dist/{chunk-QUE76J4H.js → chunk-VTO3TNIB.js} +1 -1
- package/dist/{chunk-6FAHPGCZ.js → chunk-WDKHZRAS.js} +183 -25
- package/dist/{chunk-5GLPYGKS.js → chunk-WRDLHR2F.js} +2 -2
- package/dist/{chunk-RVSEGFPD.js → chunk-XLOELLAS.js} +9 -4
- package/dist/chunk-XS33QBJA.js +112 -0
- package/dist/{chunk-FQUV5KTM.js → chunk-Z2UWFEJW.js} +8 -8
- package/dist/src/index.d.ts +8 -1
- package/dist/src/index.js +11 -12
- package/package.json +1 -1
- package/dist/chunk-BC3VCZ3E.js +0 -86
- package/dist/chunk-RER3FRTD.js +0 -265
- package/dist/chunk-X7YWD6IS.js +0 -44
|
@@ -6,6 +6,55 @@
|
|
|
6
6
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pushary-gate.mjs\"",
|
|
7
7
|
"timeout": 60
|
|
8
8
|
}
|
|
9
|
+
],
|
|
10
|
+
"PostToolUse": [
|
|
11
|
+
{
|
|
12
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pushary-gate.mjs\"",
|
|
13
|
+
"timeout": 10,
|
|
14
|
+
"type": "command"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"Stop": [
|
|
18
|
+
{
|
|
19
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pushary-gate.mjs\"",
|
|
20
|
+
"timeout": 10,
|
|
21
|
+
"type": "command"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"SessionStart": [
|
|
25
|
+
{
|
|
26
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pushary-gate.mjs\"",
|
|
27
|
+
"timeout": 10,
|
|
28
|
+
"type": "command"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"UserPromptSubmit": [
|
|
32
|
+
{
|
|
33
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pushary-gate.mjs\"",
|
|
34
|
+
"timeout": 10,
|
|
35
|
+
"type": "command"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"PreCompact": [
|
|
39
|
+
{
|
|
40
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pushary-gate.mjs\"",
|
|
41
|
+
"timeout": 10,
|
|
42
|
+
"type": "command"
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"SubagentStart": [
|
|
46
|
+
{
|
|
47
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pushary-gate.mjs\"",
|
|
48
|
+
"timeout": 10,
|
|
49
|
+
"type": "command"
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"SubagentStop": [
|
|
53
|
+
{
|
|
54
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pushary-gate.mjs\"",
|
|
55
|
+
"timeout": 10,
|
|
56
|
+
"type": "command"
|
|
57
|
+
}
|
|
9
58
|
]
|
|
10
59
|
}
|
|
11
60
|
}
|
|
@@ -1,45 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Pushary
|
|
3
|
-
//
|
|
4
|
-
// Routes risky terminal commands through your Pushary permission policy before
|
|
5
|
-
// they run. What HAPPENS to a matched command is decided by your dashboard
|
|
6
|
-
// policy (the same policy the @pushary/agent-hooks CLI uses for Claude Code), so
|
|
7
|
-
// behavior is consistent across agents.
|
|
8
|
-
//
|
|
9
|
-
// It honors, per tool ("Bash"): auto-approve, the four approval modes
|
|
10
|
-
// (push_only / push_first / notify_only / terminal_only), the timeout action
|
|
11
|
-
// (approve / deny / escalate), a live mode override, and the kill switch, all
|
|
12
|
-
// scoped to the VS Code chat session. Policy is cached in the temp dir for 5
|
|
13
|
-
// minutes with a stale-fallback, and requests retry.
|
|
14
|
-
//
|
|
15
|
-
// Self-contained: no dependencies, uses the global fetch (Node 18+).
|
|
16
|
-
//
|
|
17
|
-
// Contract (https://code.visualstudio.com/docs/agent-customization/hooks):
|
|
18
|
-
// stdin : { "hook_event_name": "PreToolUse", "tool_name": string,
|
|
19
|
-
// "tool_input": object, "cwd": string, "session_id": string, ... }
|
|
20
|
-
// stdout : { "continue": true } (not our business)
|
|
21
|
-
// { "hookSpecificOutput": { "hookEventName": "PreToolUse",
|
|
22
|
-
// "permissionDecision": "allow" | "deny" | "ask",
|
|
23
|
-
// "permissionDecisionReason"?: string } }
|
|
24
|
-
//
|
|
25
|
-
// `hookEventName` is redundant for VS Code but required by Claude Code, which
|
|
26
|
-
// can load this same plugin directory. Emitting it keeps one script valid for
|
|
27
|
-
// both.
|
|
28
|
-
//
|
|
29
|
-
// WHY THIS SCRIPT SELF-FILTERS: VS Code parses a hook's `matcher` but does not
|
|
30
|
-
// enforce it, so PreToolUse fires on EVERY tool call: reads, searches, edits.
|
|
31
|
-
// ../hooks/hooks.json therefore carries no matcher at all, because a matcher
|
|
32
|
-
// there would be inert and would only read as a guarantee it cannot make.
|
|
33
|
-
// RISKY_COMMAND below is the one and only gate, and the pass-through path must
|
|
34
|
-
// stay allocation-light and do no I/O, because it runs before every single tool
|
|
35
|
-
// the agent uses.
|
|
36
|
-
//
|
|
37
|
-
// Failure model: every handled path writes a decision and exits 0. Network and
|
|
38
|
-
// parse errors fall back to "ask" (VS Code's own prompt), so it never silently
|
|
39
|
-
// allows a risky command. A 55s hard guard guarantees output before the hook's
|
|
40
|
-
// 60s timeout fires.
|
|
2
|
+
// Pushary VS Code hooks: server policy, fenced phone approvals, and lifecycle telemetry.
|
|
3
|
+
// Dependency-free for marketplace installs. Regenerate agent-hooks/data after edits.
|
|
41
4
|
|
|
42
|
-
import { createHash } from 'node:crypto'
|
|
5
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
43
6
|
import { homedir, hostname } from 'node:os'
|
|
44
7
|
import { existsSync, readFileSync } from 'node:fs'
|
|
45
8
|
import { basename, dirname, join } from 'node:path'
|
|
@@ -56,9 +19,6 @@ const HARD_GUARD_MS = 55_000 // force a graceful "ask" before the 60s hook timeo
|
|
|
56
19
|
|
|
57
20
|
// Which commands are worth a phone approval. This is the only place the set is
|
|
58
21
|
// defined; edit it here and nowhere else.
|
|
59
|
-
const RISKY_COMMAND =
|
|
60
|
-
/\brm\b|\brmdir\b|\bunlink\b|\bmkfs|\bdd\b|\bshutdown\b|\breboot\b|\bpkill\b|\bkillall\b|\bsystemctl\b|--force\b|force-push|reset --hard|\brebase\b|\bdrop\b|\bDROP\b|\btruncate\b|\bTRUNCATE\b|delete from|DELETE FROM|\bdeploy\b|\bpublish\b|\brelease\b|\bmigrate\b/
|
|
61
|
-
|
|
62
22
|
// VS Code, Copilot CLI and Claude Code each name the terminal tool differently,
|
|
63
23
|
// and the name has changed across VS Code releases. Matching a normalized form
|
|
64
24
|
// of every spelling we have seen is what keeps the gate working after an upgrade
|
|
@@ -78,6 +38,16 @@ const TERMINAL_TOOLS = new Set([
|
|
|
78
38
|
|
|
79
39
|
const normalizeToolName = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, '')
|
|
80
40
|
|
|
41
|
+
const READ_ONLY_TOOLS = new Set([
|
|
42
|
+
'readfile', 'viewimage', 'listdir', 'listdirectory', 'filesearch', 'findfiles', 'grepsearch', 'findtextinfiles',
|
|
43
|
+
'semanticsearch', 'searchcodebase', 'searchworkspacesymbols', 'geterrors', 'getchangedfiles', 'readprojectstructure',
|
|
44
|
+
'getnotebooksummary', 'readnotebookcelloutput', 'testsearch', 'findtestfiles', 'testfailure', 'getvscodeapi',
|
|
45
|
+
'getterminaloutput', 'gettaskoutput', 'terminalselection', 'terminallastcommand',
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
export const isReadOnlyTool = (toolName) =>
|
|
49
|
+
typeof toolName === 'string' && READ_ONLY_TOOLS.has(normalizeToolName(toolName.replace(/^copilot_/, '')))
|
|
50
|
+
|
|
81
51
|
// ── VS Code decisions ─────────────────────────────────────────────────────────
|
|
82
52
|
const PASS = { continue: true }
|
|
83
53
|
const decision = (permissionDecision, permissionDecisionReason) => ({
|
|
@@ -90,7 +60,9 @@ const decision = (permissionDecision, permissionDecisionReason) => ({
|
|
|
90
60
|
const ALLOW = decision('allow')
|
|
91
61
|
const ask = (reason) => decision('ask', reason)
|
|
92
62
|
const deny = (reason) => decision('deny', reason)
|
|
63
|
+
const unresolved = (toolName, reason) => (isReadOnlyTool(toolName) ? PASS : ask(reason))
|
|
93
64
|
|
|
65
|
+
let activeQuestion
|
|
94
66
|
let done = false
|
|
95
67
|
const respond = (result) => {
|
|
96
68
|
if (done) return
|
|
@@ -252,16 +224,17 @@ const GATE_TIMEOUT_MS = 5000
|
|
|
252
224
|
* apply repo-scoped rules, since an unknown repository is not evidence of a
|
|
253
225
|
* match -- which is what this gate already got, by being filtered server-side.
|
|
254
226
|
*/
|
|
255
|
-
const decide = async (apiKey, command, cwd, sessionId) => {
|
|
227
|
+
const decide = async (apiKey, command, cwd, sessionId, ident = {}) => {
|
|
256
228
|
try {
|
|
257
229
|
const response = await fetch(`${BASE_URL}/api/agent/gate`, {
|
|
258
230
|
method: 'POST',
|
|
259
231
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
260
232
|
body: JSON.stringify({
|
|
261
233
|
v: 1,
|
|
262
|
-
source: '
|
|
263
|
-
toolName: 'Bash',
|
|
264
|
-
toolInputs: [{ command }],
|
|
234
|
+
source: 'vscode',
|
|
235
|
+
toolName: ident.toolName ?? 'Bash',
|
|
236
|
+
toolInputs: ident.toolInputs ?? [{ command }],
|
|
237
|
+
repoKey: deriveRepoKey(cwd),
|
|
265
238
|
cwd,
|
|
266
239
|
sessionId,
|
|
267
240
|
}),
|
|
@@ -275,6 +248,91 @@ const decide = async (apiKey, command, cwd, sessionId) => {
|
|
|
275
248
|
}
|
|
276
249
|
}
|
|
277
250
|
|
|
251
|
+
const REPO_KEY_MAX_LENGTH = 200
|
|
252
|
+
const MAX_PARENT_WALK = 64
|
|
253
|
+
|
|
254
|
+
const normalizeRepoRemote = (remoteUrl) => {
|
|
255
|
+
const raw = (remoteUrl || '').trim()
|
|
256
|
+
if (!raw) return undefined
|
|
257
|
+
let hostAndPath
|
|
258
|
+
const scp = /^[A-Za-z0-9._-]+@([A-Za-z0-9._-]+):(.+)$/.exec(raw)
|
|
259
|
+
if (scp) {
|
|
260
|
+
hostAndPath = `${scp[1]}/${scp[2]}`
|
|
261
|
+
} else {
|
|
262
|
+
const url = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/(.+)$/.exec(raw)
|
|
263
|
+
if (!url || /^file:/i.test(raw)) return undefined
|
|
264
|
+
hostAndPath = url[1]
|
|
265
|
+
}
|
|
266
|
+
const slash = hostAndPath.indexOf('/')
|
|
267
|
+
if (slash <= 0) return undefined
|
|
268
|
+
const at = hostAndPath.slice(0, slash).lastIndexOf('@')
|
|
269
|
+
// Credentials must never survive: a remote can carry a token and this value is
|
|
270
|
+
// sent to the server and persisted.
|
|
271
|
+
const host = (at === -1 ? hostAndPath.slice(0, slash) : hostAndPath.slice(at + 1, slash)).replace(/:\d+$/, '')
|
|
272
|
+
const path = hostAndPath.slice(slash + 1).split('/').filter(Boolean).join('/')
|
|
273
|
+
if (!host || !path) return undefined
|
|
274
|
+
return `${host}/${path}`.replace(/\.git$/i, '').toLowerCase().slice(0, REPO_KEY_MAX_LENGTH)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const findGitDir = (startDir) => {
|
|
278
|
+
let current = startDir
|
|
279
|
+
for (let depth = 0; depth < MAX_PARENT_WALK; depth += 1) {
|
|
280
|
+
const candidate = join(current, '.git')
|
|
281
|
+
try {
|
|
282
|
+
if (existsSync(candidate)) {
|
|
283
|
+
const pointer = readFileSync(candidate, 'utf-8').trim()
|
|
284
|
+
// A directory read throws EISDIR, which is the ordinary-clone case.
|
|
285
|
+
if (pointer.startsWith('gitdir:')) {
|
|
286
|
+
const target = pointer.slice('gitdir:'.length).trim()
|
|
287
|
+
return target.startsWith('/') ? target : join(current, target)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if (error?.code === 'EISDIR') return candidate
|
|
292
|
+
}
|
|
293
|
+
const parent = dirname(current)
|
|
294
|
+
if (parent === current) return undefined
|
|
295
|
+
current = parent
|
|
296
|
+
}
|
|
297
|
+
return undefined
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const deriveRepoKey = (cwd) => {
|
|
301
|
+
const override = process.env.PUSHARY_REPO_KEY
|
|
302
|
+
if (typeof override === 'string') {
|
|
303
|
+
const trimmed = override.trim()
|
|
304
|
+
if (trimmed.toLowerCase() === 'off') return undefined
|
|
305
|
+
if (trimmed) return trimmed.toLowerCase()
|
|
306
|
+
}
|
|
307
|
+
const dir = cwd || process.cwd()
|
|
308
|
+
try {
|
|
309
|
+
const gitDir = findGitDir(dir)
|
|
310
|
+
if (gitDir) {
|
|
311
|
+
// A worktree's config lives in the main git directory.
|
|
312
|
+
const wt = gitDir.replace(/\\/g, '/').indexOf('/worktrees/')
|
|
313
|
+
const configPath = wt === -1 ? join(gitDir, 'config') : join(gitDir.slice(0, wt), 'config')
|
|
314
|
+
if (existsSync(configPath)) {
|
|
315
|
+
let inOrigin = false
|
|
316
|
+
for (const rawLine of readFileSync(configPath, 'utf-8').split('\n')) {
|
|
317
|
+
const line = rawLine.trim()
|
|
318
|
+
if (line.startsWith('[')) { inOrigin = /^\[remote "origin"\]/.test(line); continue }
|
|
319
|
+
if (!inOrigin) continue
|
|
320
|
+
const url = /^url\s*=\s*(.+)$/.exec(line)
|
|
321
|
+
if (url) {
|
|
322
|
+
const normalized = normalizeRepoRemote(url[1].trim())
|
|
323
|
+
if (normalized) return normalized
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const root = gitDir.endsWith('.git') ? dirname(gitDir) : dir
|
|
328
|
+
return `local/${basename(root).toLowerCase()}`.slice(0, REPO_KEY_MAX_LENGTH)
|
|
329
|
+
}
|
|
330
|
+
return `local/${basename(dir).toLowerCase()}`.slice(0, REPO_KEY_MAX_LENGTH)
|
|
331
|
+
} catch {
|
|
332
|
+
return undefined
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
278
336
|
// ── action body capture + redaction (inlined mirror of describe.ts, since this
|
|
279
337
|
// dependency-free hook cannot import the workspace) ───────────────────────────
|
|
280
338
|
const ACTION_BODY_MAX = 4000
|
|
@@ -324,13 +382,15 @@ export const extractCommand = (toolInput) => {
|
|
|
324
382
|
return ''
|
|
325
383
|
}
|
|
326
384
|
|
|
327
|
-
//
|
|
328
|
-
//
|
|
385
|
+
// Every named tool reaches policy, as it does through the native bridge. A local
|
|
386
|
+
// allowlist would silently bypass new tools and user-authored standing denials.
|
|
329
387
|
export const shouldGate = (toolName, toolInput) => {
|
|
330
|
-
if (typeof toolName !== 'string' || !
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
388
|
+
if (typeof toolName !== 'string' || !toolName.trim() || toolName.startsWith('mcp__pushary__') || toolName.startsWith('mcp_pushary_')) return null
|
|
389
|
+
if (TERMINAL_TOOLS.has(normalizeToolName(toolName))) {
|
|
390
|
+
const command = extractCommand(toolInput)
|
|
391
|
+
if (command) return command
|
|
392
|
+
}
|
|
393
|
+
return `${toolName}: ${JSON.stringify(toolInput ?? {})}`
|
|
334
394
|
}
|
|
335
395
|
|
|
336
396
|
// ── network diagnosis ────────────────────────────────────────────────────────
|
|
@@ -365,15 +425,16 @@ export const describeNetworkFailure = (error, env = process.env) => {
|
|
|
365
425
|
// this field too, but a credential should never travel to be scrubbed on
|
|
366
426
|
// arrival, and the notify body below was scrubbed nowhere at all.
|
|
367
427
|
const askArgs = (command, project, ident) => ({
|
|
368
|
-
question: `Allow this
|
|
428
|
+
question: `Allow this action?\n\n${redactSecrets(command)}`,
|
|
369
429
|
type: 'confirm',
|
|
370
430
|
context: `VS Code agent wants to run this in ${project}`,
|
|
371
431
|
agentName: ident.agentName,
|
|
372
432
|
sessionId: ident.sessionId,
|
|
373
433
|
machineId: ident.machineId,
|
|
374
|
-
toolName: 'Bash',
|
|
434
|
+
toolName: ident.toolName ?? 'Bash',
|
|
375
435
|
actionBody: deriveActionBody(command),
|
|
376
436
|
wait: false,
|
|
437
|
+
waitEndsAt: new Date(Date.now() + MAX_BLOCK_MS).toISOString(),
|
|
377
438
|
})
|
|
378
439
|
|
|
379
440
|
const pollForAnswer = async (apiKey, correlationId, deadlineMs) => {
|
|
@@ -381,14 +442,12 @@ const pollForAnswer = async (apiKey, correlationId, deadlineMs) => {
|
|
|
381
442
|
const remaining = clamp(deadlineMs - Date.now(), 1_000, WAIT_CHUNK_MS)
|
|
382
443
|
try {
|
|
383
444
|
const answer = await callTool(apiKey, 'wait_for_answer', { correlationId, timeoutMs: remaining })
|
|
384
|
-
if (answer?.
|
|
445
|
+
if (answer?.error) return STOPPED
|
|
446
|
+
if (answer?.answered || stopped(answer) || (answer?.status && answer.status !== 'pending')) return answer
|
|
385
447
|
} catch {
|
|
386
|
-
|
|
387
|
-
await sleep(POLL_GAP_MS)
|
|
388
|
-
continue
|
|
448
|
+
return { answered: false, handoffAction: 'stop' }
|
|
389
449
|
}
|
|
390
|
-
|
|
391
|
-
await sleep(POLL_GAP_MS)
|
|
450
|
+
await sleep(Math.min(POLL_GAP_MS, Math.max(0, deadlineMs - Date.now())))
|
|
392
451
|
}
|
|
393
452
|
return { answered: false }
|
|
394
453
|
}
|
|
@@ -399,24 +458,28 @@ const fromTimeoutAction = (action, deniedReason) =>
|
|
|
399
458
|
const DENIED = 'The user denied this command via a Pushary push approval. Do not run it. Propose an alternative or ask how to proceed.'
|
|
400
459
|
|
|
401
460
|
const fromAnswer = (answer) => {
|
|
461
|
+
activeQuestion = undefined
|
|
402
462
|
if (answer.value === 'defer') return ask()
|
|
403
463
|
return answer.value === 'yes' ? ALLOW : deny(DENIED)
|
|
404
464
|
}
|
|
405
465
|
|
|
466
|
+
const stopped = (result) => result?.handoffAction === 'stop'
|
|
467
|
+
|| ['cancelled', 'unavailable', 'stopped', 'missing'].includes(result?.status)
|
|
468
|
+
const STOPPED = { answered: false, handoffAction: 'stop' }
|
|
469
|
+
const STOP_REASON = 'This approval was cancelled or its state could not be verified. Do not run the action; wait for a new user instruction.'
|
|
470
|
+
|
|
406
471
|
const withdrawQuestion = async (apiKey, correlationId) => {
|
|
407
|
-
const unanswered = { answered: false }
|
|
408
|
-
let cancelled
|
|
409
|
-
try {
|
|
410
|
-
cancelled = await callTool(apiKey, 'cancel_question', { correlationId }, WITHDRAW_TIMEOUT_MS)
|
|
411
|
-
} catch {
|
|
412
|
-
return unanswered
|
|
413
|
-
}
|
|
414
|
-
if (cancelled?.cancelled !== false || cancelled?.status === 'unavailable') return unanswered
|
|
415
472
|
try {
|
|
473
|
+
const cancelled = await callTool(apiKey, 'cancel_question', { correlationId }, WITHDRAW_TIMEOUT_MS)
|
|
474
|
+
if (stopped(cancelled)) return STOPPED
|
|
475
|
+
if (cancelled?.cancelled === true) return { answered: false }
|
|
416
476
|
const answer = await callTool(apiKey, 'wait_for_answer', { correlationId, timeoutMs: 1_000 }, WITHDRAW_TIMEOUT_MS)
|
|
417
|
-
|
|
477
|
+
if (answer?.answered) return answer
|
|
478
|
+
return ['expired', 'missing'].includes(answer?.status) ? { answered: false } : STOPPED
|
|
418
479
|
} catch {
|
|
419
|
-
return
|
|
480
|
+
return STOPPED
|
|
481
|
+
} finally {
|
|
482
|
+
activeQuestion = undefined
|
|
420
483
|
}
|
|
421
484
|
}
|
|
422
485
|
|
|
@@ -428,66 +491,90 @@ const handoffMessage = (asked) =>
|
|
|
428
491
|
// push_only: wait up to the policy timeout, then apply the timeout action.
|
|
429
492
|
const handlePushOnly = async (apiKey, command, project, ident, timeoutSeconds, timeoutAction) => {
|
|
430
493
|
let asked
|
|
494
|
+
const args = { ...askArgs(command, project, ident), requestId: randomUUID() }
|
|
431
495
|
try {
|
|
432
|
-
asked = await withRetry(() => callTool(apiKey, 'ask_user',
|
|
496
|
+
asked = await withRetry(() => callTool(apiKey, 'ask_user', args), 3)
|
|
433
497
|
} catch {
|
|
434
|
-
return
|
|
498
|
+
return deny('Pushary could not create a verifiable approval. Retry the action.')
|
|
435
499
|
}
|
|
436
|
-
if (
|
|
500
|
+
if (stopped(asked)) return deny(STOP_REASON)
|
|
501
|
+
if (asked?.answered) return fromAnswer(asked)
|
|
502
|
+
if (!asked?.correlationId) return deny('Pushary did not return a verifiable approval.')
|
|
503
|
+
activeQuestion = { apiKey, correlationId: asked.correlationId }
|
|
437
504
|
|
|
438
505
|
// Keyboard bypass: the user is at the keyboard, so VS Code's own prompt is the
|
|
439
506
|
// faster channel.
|
|
440
507
|
if (handedOff(asked)) {
|
|
441
508
|
const late = await withdrawQuestion(apiKey, asked.correlationId)
|
|
509
|
+
if (stopped(late)) return deny(STOP_REASON)
|
|
442
510
|
if (late.answered) return fromAnswer(late)
|
|
443
511
|
return ask(handoffMessage(asked))
|
|
444
512
|
}
|
|
445
513
|
if (asked.noDevices) {
|
|
446
514
|
const late = await withdrawQuestion(apiKey, asked.correlationId)
|
|
515
|
+
if (stopped(late)) return deny(STOP_REASON)
|
|
447
516
|
if (late.answered) return fromAnswer(late)
|
|
448
|
-
return
|
|
517
|
+
return ask('No device connected, approve here.')
|
|
449
518
|
}
|
|
450
519
|
|
|
451
520
|
const realMs = timeoutAction === 'wait' ? MAX_BLOCK_MS : Math.max(timeoutSeconds, 1) * 1000
|
|
452
521
|
const cap = Math.min(realMs, MAX_BLOCK_MS)
|
|
453
|
-
const
|
|
522
|
+
const deadline = Date.now() + cap
|
|
523
|
+
const answer = await pollForAnswer(apiKey, asked.correlationId, deadline)
|
|
524
|
+
if (stopped(answer)) {
|
|
525
|
+
await withdrawQuestion(apiKey, asked.correlationId)
|
|
526
|
+
return deny(STOP_REASON)
|
|
527
|
+
}
|
|
454
528
|
if (answer.answered) return fromAnswer(answer)
|
|
455
529
|
|
|
456
530
|
const late = await withdrawQuestion(apiKey, asked.correlationId)
|
|
531
|
+
if (stopped(late)) return deny(STOP_REASON)
|
|
457
532
|
if (late.answered) return fromAnswer(late)
|
|
458
533
|
|
|
459
534
|
// If VS Code's hook limit cut us off before the configured timeout, hand off to
|
|
460
535
|
// VS Code's own prompt rather than misapplying the policy's timeout action.
|
|
461
|
-
if (cap >= realMs) return fromTimeoutAction(timeoutAction, 'No response within the approval timeout; denied per your Pushary policy.')
|
|
536
|
+
if (cap >= realMs && Date.now() >= deadline) return fromTimeoutAction(timeoutAction, 'No response within the approval timeout; denied per your Pushary policy.')
|
|
462
537
|
return ask()
|
|
463
538
|
}
|
|
464
539
|
|
|
465
540
|
// push_first: race the push for a short window, then fall back to VS Code's prompt.
|
|
466
541
|
const handlePushFirst = async (apiKey, command, project, ident, pushFirstSeconds) => {
|
|
467
542
|
let asked
|
|
543
|
+
const args = { ...askArgs(command, project, ident), requestId: randomUUID() }
|
|
468
544
|
try {
|
|
469
|
-
asked = await withRetry(() => callTool(apiKey, 'ask_user',
|
|
545
|
+
asked = await withRetry(() => callTool(apiKey, 'ask_user', args), 3)
|
|
470
546
|
} catch {
|
|
471
|
-
return
|
|
547
|
+
return deny('Pushary could not create a verifiable approval. Retry the action.')
|
|
472
548
|
}
|
|
473
|
-
if (
|
|
549
|
+
if (stopped(asked)) return deny(STOP_REASON)
|
|
550
|
+
if (asked?.answered) return fromAnswer(asked)
|
|
551
|
+
if (!asked?.correlationId) return deny('Pushary did not return a verifiable approval.')
|
|
552
|
+
activeQuestion = { apiKey, correlationId: asked.correlationId }
|
|
474
553
|
|
|
475
554
|
if (handedOff(asked)) {
|
|
476
555
|
const late = await withdrawQuestion(apiKey, asked.correlationId)
|
|
556
|
+
if (stopped(late)) return deny(STOP_REASON)
|
|
477
557
|
if (late.answered) return fromAnswer(late)
|
|
478
558
|
return ask(handoffMessage(asked))
|
|
479
559
|
}
|
|
480
560
|
if (asked.noDevices) {
|
|
481
561
|
const late = await withdrawQuestion(apiKey, asked.correlationId)
|
|
562
|
+
if (stopped(late)) return deny(STOP_REASON)
|
|
482
563
|
if (late.answered) return fromAnswer(late)
|
|
483
564
|
return ask('No device connected, approve here.')
|
|
484
565
|
}
|
|
485
566
|
|
|
486
567
|
const cap = Math.min(Math.max(pushFirstSeconds, 1) * 1000, MAX_BLOCK_MS)
|
|
487
|
-
const
|
|
568
|
+
const deadline = Date.now() + cap
|
|
569
|
+
const answer = await pollForAnswer(apiKey, asked.correlationId, deadline)
|
|
570
|
+
if (stopped(answer)) {
|
|
571
|
+
await withdrawQuestion(apiKey, asked.correlationId)
|
|
572
|
+
return deny(STOP_REASON)
|
|
573
|
+
}
|
|
488
574
|
if (answer.answered) return fromAnswer(answer)
|
|
489
575
|
|
|
490
576
|
const late = await withdrawQuestion(apiKey, asked.correlationId)
|
|
577
|
+
if (stopped(late)) return deny(STOP_REASON)
|
|
491
578
|
if (late.answered) return fromAnswer(late)
|
|
492
579
|
return ask('No answer from your phone in time, so the request was withdrawn there. Approve here.')
|
|
493
580
|
}
|
|
@@ -508,11 +595,38 @@ const handleNotifyOnly = async (apiKey, command, project, ident) => {
|
|
|
508
595
|
return ask()
|
|
509
596
|
}
|
|
510
597
|
|
|
598
|
+
const TELEMETRY_EVENTS = ["PostToolUse", "Stop", "SessionStart", "UserPromptSubmit", "PreCompact", "SubagentStart", "SubagentStop"]
|
|
599
|
+
|
|
600
|
+
const reportTelemetry = async (input, source) => {
|
|
601
|
+
const apiKey = resolveApiKey()
|
|
602
|
+
if (!apiKey) return
|
|
603
|
+
const sentAt = new Date().toISOString()
|
|
604
|
+
const payload = JSON.stringify(input, (_key, value) => typeof value === 'string' ? redactSecretsDeep(value) : value)
|
|
605
|
+
if (Buffer.byteLength(payload) > 262144) return
|
|
606
|
+
const hash = value => createHash('sha256').update(value).digest('hex')
|
|
607
|
+
const sessionId = input.session_id ?? input.sessionId ?? input.conversation_id ?? ''
|
|
608
|
+
const hookId = hash([source, sessionId, input.hook_event_name, sentAt, hash(payload)].join('|')).slice(0, 32)
|
|
609
|
+
try {
|
|
610
|
+
await fetch(`${BASE_URL}/api/agent/hook`, {
|
|
611
|
+
method: 'POST',
|
|
612
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
613
|
+
body: JSON.stringify({ v: 1, machineId: getMachineId(), app: { platform: 'cli', version: 'editor-plugin' },
|
|
614
|
+
envelopes: [{ wire: 1, hookId, source, event: input.hook_event_name, sentAt,
|
|
615
|
+
cwd: input.cwd, repoKey: deriveRepoKey(input.cwd), payload }] }),
|
|
616
|
+
signal: AbortSignal.timeout(5000),
|
|
617
|
+
})
|
|
618
|
+
} catch { /* Telemetry never blocks an agent. */ }
|
|
619
|
+
}
|
|
620
|
+
|
|
511
621
|
const main = async () => {
|
|
512
622
|
// Backstop: if anything hangs, return "ask" rather than letting the hook time
|
|
513
623
|
// out and leave the agent with no decision at all. Scheduled here rather than
|
|
514
624
|
// at module scope so importing this file for its helpers arms nothing.
|
|
515
|
-
setTimeout(() =>
|
|
625
|
+
setTimeout(async () => {
|
|
626
|
+
if (!activeQuestion) return respond(deny('Pushary could not finish approval safely. Retry the action.'))
|
|
627
|
+
const late = await withdrawQuestion(activeQuestion.apiKey, activeQuestion.correlationId)
|
|
628
|
+
respond(late.answered ? fromAnswer(late) : deny('Pushary approval expired or could not be withdrawn safely. Retry the action.'))
|
|
629
|
+
}, HARD_GUARD_MS - 5_000).unref()
|
|
516
630
|
|
|
517
631
|
let input
|
|
518
632
|
try {
|
|
@@ -530,6 +644,12 @@ const main = async () => {
|
|
|
530
644
|
return respond(PASS)
|
|
531
645
|
}
|
|
532
646
|
|
|
647
|
+
input.hook_event_name ??= input.hookEventName
|
|
648
|
+
if (TELEMETRY_EVENTS.includes(input.hook_event_name)) {
|
|
649
|
+
await reportTelemetry(input, 'vscode')
|
|
650
|
+
return respond({})
|
|
651
|
+
}
|
|
652
|
+
|
|
533
653
|
// Fast path. This runs before every tool the agent uses, so it must stay free
|
|
534
654
|
// of disk and network work.
|
|
535
655
|
const command = shouldGate(input.tool_name, input.tool_input)
|
|
@@ -539,20 +659,20 @@ const main = async () => {
|
|
|
539
659
|
if (!apiKey) {
|
|
540
660
|
diag('no API key found (PUSHARY_API_KEY, the plugin .mcp.json, or ~/.pushary/config.json). Run: npx @pushary/agent-hooks setup')
|
|
541
661
|
return respond(
|
|
542
|
-
|
|
662
|
+
unresolved(input.tool_name, 'Pushary is not configured: run `npx @pushary/agent-hooks setup` (get a key at https://pushary.com) to route this approval to your phone.')
|
|
543
663
|
)
|
|
544
664
|
}
|
|
545
665
|
|
|
546
666
|
const project = basename(input.cwd || process.cwd()) || 'workspace'
|
|
547
|
-
const sessionId = typeof input.session_id === 'string' ? input.session_id : undefined
|
|
548
|
-
const ident = { agentName: `VS Code - ${project}`, sessionId, machineId: getMachineId() }
|
|
667
|
+
const sessionId = typeof (input.session_id ?? input.sessionId) === 'string' ? (input.session_id ?? input.sessionId) : undefined
|
|
668
|
+
const ident = { agentName: `VS Code - ${project}`, sessionId, machineId: getMachineId(), toolName: input.tool_name, toolInputs: [input.tool_input ?? {}] }
|
|
549
669
|
|
|
550
670
|
try {
|
|
551
|
-
const verdict = await decide(apiKey, command, input.cwd, sessionId)
|
|
671
|
+
const verdict = await decide(apiKey, command, input.cwd, sessionId, ident)
|
|
552
672
|
|
|
553
673
|
// No verdict, or one that says nothing: VS Code's own prompt decides, exactly
|
|
554
674
|
// as if this gate were not installed. Never a forced denial on an outage.
|
|
555
|
-
if (!verdict || verdict.kind === 'no_opinion') return respond(
|
|
675
|
+
if (!verdict || verdict.kind === 'no_opinion') return respond(unresolved(input.tool_name))
|
|
556
676
|
if (verdict.kind === 'kill') return respond(deny(verdict.reason))
|
|
557
677
|
if (verdict.kind === 'allow') return respond(ALLOW)
|
|
558
678
|
if (verdict.kind === 'deny') return respond(deny(verdict.reason))
|
|
@@ -573,7 +693,7 @@ const main = async () => {
|
|
|
573
693
|
}
|
|
574
694
|
} catch (error) {
|
|
575
695
|
diag(describeNetworkFailure(error))
|
|
576
|
-
return respond(
|
|
696
|
+
return respond(unresolved(input.tool_name))
|
|
577
697
|
}
|
|
578
698
|
}
|
|
579
699
|
|
|
@@ -1,38 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
handleBell
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-VTO3TNIB.js";
|
|
5
5
|
import {
|
|
6
6
|
readHookInput
|
|
7
|
-
} from "../chunk-
|
|
8
|
-
import "../chunk-
|
|
9
|
-
import "../chunk-
|
|
10
|
-
import "../chunk-
|
|
11
|
-
import "../chunk-
|
|
12
|
-
import "../chunk-
|
|
13
|
-
import "../chunk-
|
|
14
|
-
import "../chunk-
|
|
15
|
-
import "../chunk-
|
|
16
|
-
import "../chunk-
|
|
17
|
-
import "../chunk-
|
|
18
|
-
import "../chunk-
|
|
7
|
+
} from "../chunk-OEKFD5BC.js";
|
|
8
|
+
import "../chunk-4RF52LSU.js";
|
|
9
|
+
import "../chunk-BMANRGJQ.js";
|
|
10
|
+
import "../chunk-VOSGZ6PH.js";
|
|
11
|
+
import "../chunk-2ZTL3PGN.js";
|
|
12
|
+
import "../chunk-CUEZMPLJ.js";
|
|
13
|
+
import "../chunk-HBHEX3JQ.js";
|
|
14
|
+
import "../chunk-XLOELLAS.js";
|
|
15
|
+
import "../chunk-2D7BAOMY.js";
|
|
16
|
+
import "../chunk-GOPIMNVC.js";
|
|
17
|
+
import "../chunk-3AJIK5AO.js";
|
|
18
|
+
import "../chunk-4PX4QK2Z.js";
|
|
19
19
|
import "../chunk-EQRS3SGM.js";
|
|
20
|
-
import "../chunk-
|
|
20
|
+
import "../chunk-OI3DJA7N.js";
|
|
21
21
|
import "../chunk-XHKBHWLX.js";
|
|
22
22
|
import "../chunk-L4ZCFMWW.js";
|
|
23
|
-
import "../chunk-
|
|
24
|
-
import "../chunk-
|
|
25
|
-
import "../chunk-
|
|
23
|
+
import "../chunk-IVQMNLSA.js";
|
|
24
|
+
import "../chunk-N4VNLISN.js";
|
|
25
|
+
import "../chunk-OW6AZDY2.js";
|
|
26
26
|
import "../chunk-GMXKITVA.js";
|
|
27
27
|
import "../chunk-DINDL2CF.js";
|
|
28
|
-
import "../chunk-
|
|
29
|
-
import "../chunk-BC3VCZ3E.js";
|
|
28
|
+
import "../chunk-S5CMLR35.js";
|
|
30
29
|
import "../chunk-7QLSKOSU.js";
|
|
31
30
|
import "../chunk-KZERVKTD.js";
|
|
32
|
-
import "../chunk-
|
|
33
|
-
import "../chunk-4LL6LG5X.js";
|
|
31
|
+
import "../chunk-KAVDAZ7A.js";
|
|
34
32
|
import "../chunk-2UMNXADU.js";
|
|
35
|
-
import "../chunk-
|
|
33
|
+
import "../chunk-WDKHZRAS.js";
|
|
36
34
|
|
|
37
35
|
// bin/pushary-bell-hook.ts
|
|
38
36
|
var main = async () => {
|
package/dist/bin/pushary-bell.js
CHANGED
|
@@ -5,41 +5,39 @@ import {
|
|
|
5
5
|
liveAgentCount,
|
|
6
6
|
ring,
|
|
7
7
|
upgradeThreshold
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-VTO3TNIB.js";
|
|
9
9
|
import {
|
|
10
10
|
addBellHooks,
|
|
11
11
|
bellInstalled,
|
|
12
12
|
removeBellHooks
|
|
13
|
-
} from "../chunk-
|
|
14
|
-
import "../chunk-
|
|
13
|
+
} from "../chunk-GOPIMNVC.js";
|
|
14
|
+
import "../chunk-3AJIK5AO.js";
|
|
15
15
|
import "../chunk-L4ZCFMWW.js";
|
|
16
16
|
import {
|
|
17
17
|
claudeSettings
|
|
18
|
-
} from "../chunk-
|
|
18
|
+
} from "../chunk-OW6AZDY2.js";
|
|
19
19
|
import {
|
|
20
20
|
createIo
|
|
21
21
|
} from "../chunk-5RUIFDTP.js";
|
|
22
22
|
import {
|
|
23
23
|
exitOnHelpFlag
|
|
24
|
-
} from "../chunk-
|
|
25
|
-
import "../chunk-
|
|
24
|
+
} from "../chunk-DLLAWCQB.js";
|
|
25
|
+
import "../chunk-D6PRC3YQ.js";
|
|
26
26
|
import {
|
|
27
27
|
parseFlags
|
|
28
28
|
} from "../chunk-GMXKITVA.js";
|
|
29
29
|
import "../chunk-DINDL2CF.js";
|
|
30
|
-
import "../chunk-
|
|
31
|
-
import "../chunk-BC3VCZ3E.js";
|
|
30
|
+
import "../chunk-S5CMLR35.js";
|
|
32
31
|
import "../chunk-7QLSKOSU.js";
|
|
33
32
|
import {
|
|
34
33
|
EXIT
|
|
35
34
|
} from "../chunk-KZERVKTD.js";
|
|
36
|
-
import "../chunk-X7YWD6IS.js";
|
|
37
35
|
import {
|
|
38
36
|
readJsonSafe,
|
|
39
37
|
writeJsonAtomic
|
|
40
|
-
} from "../chunk-
|
|
38
|
+
} from "../chunk-KAVDAZ7A.js";
|
|
41
39
|
import "../chunk-2UMNXADU.js";
|
|
42
|
-
import "../chunk-
|
|
40
|
+
import "../chunk-WDKHZRAS.js";
|
|
43
41
|
|
|
44
42
|
// bin/pushary-bell.ts
|
|
45
43
|
exitOnHelpFlag("bell");
|