@pushary/agent-hooks 0.89.10 → 0.89.12
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 +69 -0
- package/README.md +3 -2
- package/dist/bin/pushary-bell-hook.js +5 -5
- package/dist/bin/pushary-bell.js +11 -11
- package/dist/bin/pushary-claude.js +125 -36
- package/dist/bin/pushary-clean.js +39 -28
- package/dist/bin/pushary-codex-bridge.js +17 -10
- package/dist/bin/pushary-codex-hook.js +12 -11
- package/dist/bin/pushary-codex.js +8 -8
- package/dist/bin/pushary-connect.js +12 -12
- package/dist/bin/pushary-daemon-supervisor.d.ts +1 -0
- package/dist/bin/pushary-daemon-supervisor.js +27 -0
- package/dist/bin/pushary-daemon.js +22 -22
- package/dist/bin/pushary-disconnect.js +9 -9
- package/dist/bin/pushary-doctor.js +59 -49
- package/dist/bin/pushary-elicitation-hook.js +5 -5
- package/dist/bin/pushary-gemini-bridge.js +17 -9
- package/dist/bin/pushary-gemini-hook.js +11 -10
- package/dist/bin/pushary-hook.js +14 -14
- package/dist/bin/pushary-login.js +11 -11
- package/dist/bin/pushary-logout.js +11 -11
- package/dist/bin/pushary-mode.js +5 -5
- package/dist/bin/pushary-notification-hook.js +8 -8
- package/dist/bin/pushary-opencode-hook.js +11 -10
- package/dist/bin/pushary-permission-denied-hook.js +12 -12
- package/dist/bin/pushary-permission-hook.js +12 -12
- package/dist/bin/pushary-post-hook.js +8 -8
- package/dist/bin/pushary-prompt-hook.js +8 -8
- package/dist/bin/pushary-session-end-hook.js +8 -8
- package/dist/bin/pushary-session-start-hook.js +8 -8
- package/dist/bin/pushary-setup.js +88 -60
- package/dist/bin/pushary-stats.js +4 -4
- package/dist/bin/pushary-status.js +14 -14
- package/dist/bin/pushary-stop-hook.js +8 -8
- package/dist/bin/pushary-stopfailure-hook.js +8 -8
- package/dist/bin/pushary-suggestions.js +2 -2
- package/dist/bin/pushary-upgrade.js +21 -21
- package/dist/bin/pushary-wait.js +5 -5
- package/dist/bin/pushary.js +1 -1
- package/dist/{chunk-253WC6O5.js → chunk-327YMKP5.js} +1 -1
- package/dist/{chunk-6XUABEX2.js → chunk-3OXHYPZG.js} +1 -1
- package/dist/{chunk-MZGWKIUZ.js → chunk-3XSP5SML.js} +7 -3
- package/dist/{chunk-XX2V4QS5.js → chunk-4LL6LG5X.js} +2 -2
- package/dist/{chunk-DKWM5T3P.js → chunk-5NDB3YOE.js} +2 -2
- package/dist/{chunk-TEZ2FZD6.js → chunk-6GU55AQQ.js} +2 -2
- package/dist/{chunk-Q223OM5J.js → chunk-6V7ZDH3A.js} +4 -4
- package/dist/{chunk-OLAGKKSR.js → chunk-6VXNA5JB.js} +16 -6
- package/dist/{chunk-34GN3IPX.js → chunk-7DK3ZH3N.js} +1 -1
- package/dist/{chunk-PZRWFWSR.js → chunk-7HYJPOGG.js} +7 -7
- package/dist/{chunk-TY5HVMXT.js → chunk-DHR4F2CR.js} +1 -1
- package/dist/{chunk-3HFK3UXM.js → chunk-FCUUVHTS.js} +1 -1
- package/dist/{chunk-NCVIWGVA.js → chunk-GKPURU7P.js} +1 -1
- package/dist/{chunk-UXHMWF4H.js → chunk-JHJHCMAF.js} +1 -1
- package/dist/chunk-JJV5Q4KG.js +20 -0
- package/dist/{chunk-TG3T3M4K.js → chunk-JVZX5YRJ.js} +6 -2
- package/dist/{chunk-6EKQZA6R.js → chunk-KVHP5ZME.js} +1 -1
- package/dist/{chunk-X4ZUUFKV.js → chunk-L4ZCFMWW.js} +63 -1
- package/dist/{chunk-2LPEZYMC.js → chunk-MFNQ5IMW.js} +1 -1
- package/dist/{chunk-KVE5RCQL.js → chunk-NBRM3FMD.js} +1 -1
- package/dist/{chunk-FZ73G2RO.js → chunk-NESKLV6R.js} +25 -6
- package/dist/{chunk-GTZLL7F4.js → chunk-OADGBOP7.js} +5 -5
- package/dist/{chunk-GSSFSVAQ.js → chunk-OCMOTJTW.js} +42 -24
- package/dist/{chunk-LURY4WNF.js → chunk-OTPJ4FUR.js} +1 -1
- package/dist/{chunk-QA72MKIT.js → chunk-OVEGEQSB.js} +5 -5
- package/dist/{chunk-3SQYKLBW.js → chunk-PS7DQ2SU.js} +46 -18
- package/dist/{chunk-UDBJHQMG.js → chunk-QFJZHVVL.js} +1 -1
- package/dist/chunk-QZCFVWA7.js +58 -0
- package/dist/{chunk-3XTFX4CL.js → chunk-RLQWNEPM.js} +3 -2
- package/dist/{chunk-3CUDMTL4.js → chunk-RNKPLUWC.js} +10 -3
- package/dist/{chunk-SYUIXXQW.js → chunk-VF3RTVWY.js} +1 -1
- package/dist/{chunk-TRVIRETT.js → chunk-VPLQFXCH.js} +1 -1
- package/dist/{chunk-IUN6VOKV.js → chunk-WDVQ6O4C.js} +2 -42
- package/dist/{chunk-IUWRVTQK.js → chunk-XREW7RQ3.js} +1 -1
- package/dist/{chunk-BLODMQKJ.js → chunk-Y4WVZIEI.js} +1 -1
- package/dist/src/index.d.ts +8 -1
- package/dist/src/index.js +16 -12
- package/package.json +2 -1
- /package/dist/{chunk-X6RXX2NF.js → chunk-3MCEES6Z.js} +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PRETOOLUSE_HANDLED_TOOLS,
|
|
3
3
|
PRETOOLUSE_PHONE_ANSWERED_TOOLS
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-KVHP5ZME.js";
|
|
5
5
|
import {
|
|
6
6
|
defersToNativeMode
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-GKPURU7P.js";
|
|
8
8
|
import {
|
|
9
9
|
isGatingMoment,
|
|
10
10
|
recordKeylessMoment
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-FCUUVHTS.js";
|
|
12
12
|
import {
|
|
13
13
|
denyReasonFrom,
|
|
14
14
|
isDeferAnswer
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
readLastUserPrompt,
|
|
21
21
|
repoKeyFor,
|
|
22
22
|
throttlePass
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-OADGBOP7.js";
|
|
24
24
|
import {
|
|
25
25
|
DEFAULT_SESSION,
|
|
26
26
|
askUser,
|
|
@@ -28,18 +28,19 @@ import {
|
|
|
28
28
|
pollForAnswer,
|
|
29
29
|
savePendingQuestion,
|
|
30
30
|
sendNotification
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-JHJHCMAF.js";
|
|
32
32
|
import {
|
|
33
|
+
UNNAMED_ACTION,
|
|
33
34
|
deriveAction,
|
|
34
35
|
deriveActionBody,
|
|
35
36
|
deriveBlocker,
|
|
36
37
|
deriveToolTarget,
|
|
37
38
|
describeToolCall,
|
|
38
39
|
scopePathFor
|
|
39
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-JVZX5YRJ.js";
|
|
40
41
|
import {
|
|
41
42
|
getMachineId
|
|
42
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-5NDB3YOE.js";
|
|
43
44
|
import {
|
|
44
45
|
getApiKey,
|
|
45
46
|
getBaseUrl
|
|
@@ -54,7 +55,7 @@ import {
|
|
|
54
55
|
parseAgentQuestionAnswers,
|
|
55
56
|
parseAgentQuestions,
|
|
56
57
|
resolveGate
|
|
57
|
-
} from "./chunk-
|
|
58
|
+
} from "./chunk-3MCEES6Z.js";
|
|
58
59
|
|
|
59
60
|
// src/decision-episode.ts
|
|
60
61
|
var EPISODE_PATH = "/api/agent/decision-episode";
|
|
@@ -87,11 +88,20 @@ var allow = () => claudePreToolUseOutput("allow");
|
|
|
87
88
|
var deny = (reason) => claudePreToolUseOutput("deny", reason);
|
|
88
89
|
var allowWithInput = (updatedInput) => claudePreToolUseOutput("allow", void 0, updatedInput);
|
|
89
90
|
var ask = (reason) => claudePreToolUseOutput("ask", reason);
|
|
91
|
+
var storedWaitDeadline = (result, requestedDeadline) => {
|
|
92
|
+
const stored = result.waitEndsAt ? Date.parse(result.waitEndsAt) : Number.NaN;
|
|
93
|
+
return Number.isFinite(stored) ? Math.min(stored, requestedDeadline) : requestedDeadline;
|
|
94
|
+
};
|
|
95
|
+
var returnedAnswer = (result) => result.answered === true ? { answered: true, value: result.value, note: result.note } : null;
|
|
90
96
|
var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, timeoutAction, sessionId, machineId, toolName, toolTarget, decision, signals) => {
|
|
97
|
+
const effectiveWait = effectiveWaitSeconds(timeoutAction, timeoutSeconds, "claude");
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
const requestedDeadline = hookWaitDeadline(START_MS, effectiveWait, "claude", now);
|
|
100
|
+
const clampCut = hookWaitClamped(START_MS, effectiveWait, "claude", now);
|
|
91
101
|
let result;
|
|
92
102
|
try {
|
|
93
103
|
result = await askUser(apiKey, {
|
|
94
|
-
question: `Allow ${description}?`,
|
|
104
|
+
question: `Allow ${description || UNNAMED_ACTION}?`,
|
|
95
105
|
type: "confirm",
|
|
96
106
|
context: `Agent wants to run this in ${projectName}`,
|
|
97
107
|
agentName: `Claude Code - ${projectName}`,
|
|
@@ -99,6 +109,7 @@ var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, ti
|
|
|
99
109
|
machineId,
|
|
100
110
|
toolName,
|
|
101
111
|
toolTarget,
|
|
112
|
+
waitEndsAt: timeoutAction !== "approve" && timeoutAction !== "deny" || clampCut ? new Date(requestedDeadline).toISOString() : void 0,
|
|
102
113
|
...decision
|
|
103
114
|
});
|
|
104
115
|
} catch {
|
|
@@ -117,6 +128,8 @@ var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, ti
|
|
|
117
128
|
return ask(result.suppressed ? "You are at the keyboard, approve here." : "Delivery mode is Terminal, approve here.");
|
|
118
129
|
}
|
|
119
130
|
if (result.noDevices) {
|
|
131
|
+
await cancelQuestion(apiKey, result.correlationId).catch(() => {
|
|
132
|
+
});
|
|
120
133
|
switch (timeoutAction) {
|
|
121
134
|
case "approve":
|
|
122
135
|
return allow();
|
|
@@ -126,12 +139,9 @@ var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, ti
|
|
|
126
139
|
return void 0;
|
|
127
140
|
}
|
|
128
141
|
}
|
|
129
|
-
const
|
|
130
|
-
const now = Date.now();
|
|
131
|
-
const deadline = hookWaitDeadline(START_MS, effectiveWait, "claude", now);
|
|
132
|
-
const clampCut = hookWaitClamped(START_MS, effectiveWait, "claude", now);
|
|
142
|
+
const deadline = storedWaitDeadline(result, requestedDeadline);
|
|
133
143
|
announceWait(effectiveWait, result.answerUrl, sessionId);
|
|
134
|
-
const answer = await pollForAnswer(apiKey, result.correlationId, deadline);
|
|
144
|
+
const answer = returnedAnswer(result) ?? await pollForAnswer(apiKey, result.correlationId, deadline);
|
|
135
145
|
if (answer.answered) {
|
|
136
146
|
if (isDeferAnswer(answer.value)) return ask("Handling on your machine");
|
|
137
147
|
signals.humanDecision = true;
|
|
@@ -147,6 +157,7 @@ var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, ti
|
|
|
147
157
|
case "deny":
|
|
148
158
|
return deny("No response within timeout");
|
|
149
159
|
default:
|
|
160
|
+
savePendingQuestion(sessionId || DEFAULT_SESSION, result.correlationId);
|
|
150
161
|
return ask("No push response, asking in terminal");
|
|
151
162
|
}
|
|
152
163
|
};
|
|
@@ -154,10 +165,11 @@ var handleTerminalOnly = () => {
|
|
|
154
165
|
return ask();
|
|
155
166
|
};
|
|
156
167
|
var handlePushFirst = async (apiKey, description, projectName, pushFirstSeconds, sessionId, machineId, toolName, toolTarget, decision, signals) => {
|
|
168
|
+
const requestedDeadline = hookWaitDeadline(START_MS, pushFirstSeconds, "claude", Date.now());
|
|
157
169
|
let result;
|
|
158
170
|
try {
|
|
159
171
|
result = await askUser(apiKey, {
|
|
160
|
-
question: `Allow ${description}?`,
|
|
172
|
+
question: `Allow ${description || UNNAMED_ACTION}?`,
|
|
161
173
|
type: "confirm",
|
|
162
174
|
context: `Agent wants to run this in ${projectName}`,
|
|
163
175
|
agentName: `Claude Code - ${projectName}`,
|
|
@@ -165,6 +177,7 @@ var handlePushFirst = async (apiKey, description, projectName, pushFirstSeconds,
|
|
|
165
177
|
machineId,
|
|
166
178
|
toolName,
|
|
167
179
|
toolTarget,
|
|
180
|
+
waitEndsAt: new Date(requestedDeadline).toISOString(),
|
|
168
181
|
...decision
|
|
169
182
|
});
|
|
170
183
|
} catch {
|
|
@@ -176,11 +189,13 @@ var handlePushFirst = async (apiKey, description, projectName, pushFirstSeconds,
|
|
|
176
189
|
return ask("You are at the keyboard, approve here.");
|
|
177
190
|
}
|
|
178
191
|
if (result.noDevices) {
|
|
192
|
+
await cancelQuestion(apiKey, result.correlationId).catch(() => {
|
|
193
|
+
});
|
|
179
194
|
return void 0;
|
|
180
195
|
}
|
|
181
196
|
announceWait(pushFirstSeconds, result.answerUrl, sessionId);
|
|
182
|
-
const deadline =
|
|
183
|
-
const answer = await pollForAnswer(apiKey, result.correlationId, deadline, 1500);
|
|
197
|
+
const deadline = storedWaitDeadline(result, requestedDeadline);
|
|
198
|
+
const answer = returnedAnswer(result) ?? await pollForAnswer(apiKey, result.correlationId, deadline, 1500);
|
|
184
199
|
if (answer.answered) {
|
|
185
200
|
if (isDeferAnswer(answer.value)) return ask("Handling on your machine");
|
|
186
201
|
signals.humanDecision = true;
|
|
@@ -330,7 +345,8 @@ var askAsOneCard = async (apiKey, input, parsed, projectName, deadline) => {
|
|
|
330
345
|
sessionId: input.session_id,
|
|
331
346
|
machineId: getMachineId(),
|
|
332
347
|
toolName: "AskUserQuestion",
|
|
333
|
-
toolUseId: input.tool_use_id
|
|
348
|
+
toolUseId: input.tool_use_id,
|
|
349
|
+
waitEndsAt: new Date(deadline).toISOString()
|
|
334
350
|
});
|
|
335
351
|
} catch {
|
|
336
352
|
return void 0;
|
|
@@ -340,7 +356,7 @@ var askAsOneCard = async (apiKey, input, parsed, projectName, deadline) => {
|
|
|
340
356
|
});
|
|
341
357
|
return void 0;
|
|
342
358
|
}
|
|
343
|
-
const answer = await pollForAnswer(apiKey, result.correlationId, deadline);
|
|
359
|
+
const answer = returnedAnswer(result) ?? await pollForAnswer(apiKey, result.correlationId, storedWaitDeadline(result, deadline));
|
|
344
360
|
const values = answer.answered && answer.value ? parseAgentQuestionAnswers(parsed, answer.value) : null;
|
|
345
361
|
if (!values || isDeferAnswer(answer.value)) {
|
|
346
362
|
savePendingQuestion(input.session_id || DEFAULT_SESSION, result.correlationId);
|
|
@@ -366,7 +382,8 @@ var askQuestionByQuestion = async (apiKey, input, parsed, projectName, deadline)
|
|
|
366
382
|
// One AskUserQuestion call raises several questions, and their payload
|
|
367
383
|
// fingerprints are identical (same tool, no target, no body). Without the
|
|
368
384
|
// index they would all collapse onto the first decision.
|
|
369
|
-
toolUseId: input.tool_use_id ? `${input.tool_use_id}#q${index}` : void 0
|
|
385
|
+
toolUseId: input.tool_use_id ? `${input.tool_use_id}#q${index}` : void 0,
|
|
386
|
+
waitEndsAt: new Date(deadline).toISOString()
|
|
370
387
|
});
|
|
371
388
|
} catch {
|
|
372
389
|
return void 0;
|
|
@@ -376,7 +393,7 @@ var askQuestionByQuestion = async (apiKey, input, parsed, projectName, deadline)
|
|
|
376
393
|
});
|
|
377
394
|
return void 0;
|
|
378
395
|
}
|
|
379
|
-
const answer = await pollForAnswer(apiKey, result.correlationId, deadline);
|
|
396
|
+
const answer = returnedAnswer(result) ?? await pollForAnswer(apiKey, result.correlationId, storedWaitDeadline(result, deadline));
|
|
380
397
|
const legacyValue = answer.answered ? answer.value?.trim() : void 0;
|
|
381
398
|
const values = legacyValue ? parseAgentQuestionAnswers([question], legacyValue) ?? parseAgentQuestionAnswers([question], JSON.stringify({
|
|
382
399
|
[question.question]: question.multiSelect ? JSON.stringify([legacyValue]) : legacyValue
|
|
@@ -404,6 +421,7 @@ var PLAN_KEEP_PLANNING = "Keep planning";
|
|
|
404
421
|
var handleExitPlanMode = async (apiKey, input) => {
|
|
405
422
|
const toolInput = input.tool_input ?? {};
|
|
406
423
|
const projectName = basename(input.cwd ?? process.cwd());
|
|
424
|
+
const deadline = hookWaitDeadline(START_MS, effectiveWaitSeconds("wait", 0, "claude"), "claude", Date.now());
|
|
407
425
|
let result;
|
|
408
426
|
try {
|
|
409
427
|
result = await askUser(apiKey, {
|
|
@@ -420,7 +438,8 @@ var handleExitPlanMode = async (apiKey, input) => {
|
|
|
420
438
|
// so approving from a lock screen is a real decision and not a blind yes.
|
|
421
439
|
actionBody: deriveActionBody("ExitPlanMode", toolInput),
|
|
422
440
|
intent: readLastPrompt(input.session_id ?? DEFAULT_SESSION) ?? (input.transcript_path ? readLastUserPrompt(input.transcript_path) : void 0),
|
|
423
|
-
blocker: "Waiting for you to approve the plan"
|
|
441
|
+
blocker: "Waiting for you to approve the plan",
|
|
442
|
+
waitEndsAt: new Date(deadline).toISOString()
|
|
424
443
|
});
|
|
425
444
|
} catch {
|
|
426
445
|
return void 0;
|
|
@@ -430,8 +449,7 @@ var handleExitPlanMode = async (apiKey, input) => {
|
|
|
430
449
|
});
|
|
431
450
|
return void 0;
|
|
432
451
|
}
|
|
433
|
-
const
|
|
434
|
-
const answer = await pollForAnswer(apiKey, result.correlationId, deadline);
|
|
452
|
+
const answer = returnedAnswer(result) ?? await pollForAnswer(apiKey, result.correlationId, storedWaitDeadline(result, deadline));
|
|
435
453
|
const value = answer.answered ? (answer.value ?? "").trim() : "";
|
|
436
454
|
if (!value || isDeferAnswer(answer.value)) {
|
|
437
455
|
savePendingQuestion(input.session_id || DEFAULT_SESSION, result.correlationId);
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
HOOK_BUDGETS,
|
|
3
3
|
HOOK_LOCATOR_BINARY
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-3MCEES6Z.js";
|
|
5
|
+
|
|
6
|
+
// src/removal-scope.ts
|
|
7
|
+
var namesAppLocator = (command) => command.includes(HOOK_LOCATOR_BINARY);
|
|
8
|
+
var isProtectedByScope = (command, scope) => scope?.keepAppWiring === true && namesAppLocator(command);
|
|
5
9
|
|
|
6
10
|
// src/codex-config.ts
|
|
7
11
|
import { createHash } from "crypto";
|
|
8
12
|
import { homedir } from "os";
|
|
9
13
|
import { join } from "path";
|
|
10
14
|
|
|
11
|
-
// src/removal-scope.ts
|
|
12
|
-
var namesAppLocator = (command) => command.includes(HOOK_LOCATOR_BINARY);
|
|
13
|
-
var isProtectedByScope = (command, scope) => scope?.keepAppWiring === true && namesAppLocator(command);
|
|
14
|
-
|
|
15
15
|
// src/canonical-json.ts
|
|
16
16
|
var canonicalJson = (value) => {
|
|
17
17
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
@@ -4,13 +4,13 @@ import {
|
|
|
4
4
|
} from "./chunk-XHKBHWLX.js";
|
|
5
5
|
import {
|
|
6
6
|
resolveGlobalBinary
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-L4ZCFMWW.js";
|
|
8
8
|
import {
|
|
9
9
|
getMachineId
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-5NDB3YOE.js";
|
|
11
11
|
import {
|
|
12
12
|
pusharyDir
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-OVEGEQSB.js";
|
|
14
14
|
import {
|
|
15
15
|
getPackageVersion
|
|
16
16
|
} from "./chunk-DINDL2CF.js";
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
writeFileAtomic,
|
|
20
20
|
writeJsonAtomic,
|
|
21
21
|
writeJsonAtomicDurable
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-4LL6LG5X.js";
|
|
23
23
|
import {
|
|
24
24
|
getApiKey
|
|
25
25
|
} from "./chunk-2UMNXADU.js";
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
import { dirname, join, win32 as win32Path } from "path";
|
|
29
29
|
import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
30
30
|
import { createHash, randomUUID } from "crypto";
|
|
31
|
-
import { homedir } from "os";
|
|
31
|
+
import { homedir, userInfo } from "os";
|
|
32
32
|
import { execFileSync } from "child_process";
|
|
33
33
|
var daemonNeedsHandoff = (status) => status.kind === "version-mismatch" || status.kind === "configuration-mismatch" || status.kind === "unresponsive" && status.processAlive === true;
|
|
34
34
|
var DAEMON_REMOTE_PROOF_MAX_AGE_MS = 18e4;
|
|
@@ -39,9 +39,14 @@ var remoteProofIsFresh = (timestamp, now) => {
|
|
|
39
39
|
return Number.isFinite(age) && age >= -DAEMON_REMOTE_PROOF_CLOCK_SKEW_MS && age < DAEMON_REMOTE_PROOF_MAX_AGE_MS;
|
|
40
40
|
};
|
|
41
41
|
var daemonHasRemoteProof = (status, now = Date.now()) => status.kind === "running" && remoteProofIsFresh(status.lastSuccessfulHeartbeatAt, now) && remoteProofIsFresh(status.lastSuccessfulControlAt, now);
|
|
42
|
+
var readServiceDefinitionFile = (path) => {
|
|
43
|
+
const bytes = readFileSync(path);
|
|
44
|
+
const encoding = bytes.length >= 2 && bytes[0] === 255 && bytes[1] === 254 ? "utf16le" : "utf-8";
|
|
45
|
+
return { contents: bytes.toString(encoding), encoding };
|
|
46
|
+
};
|
|
42
47
|
var runDaemonServiceCommand = (command, args) => execFileSync(command, [...args], {
|
|
43
48
|
encoding: "utf-8",
|
|
44
|
-
stdio: ["ignore", "pipe", "
|
|
49
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
45
50
|
windowsHide: true
|
|
46
51
|
});
|
|
47
52
|
var commandFailureOutput = (error) => {
|
|
@@ -163,7 +168,6 @@ After=network-online.target
|
|
|
163
168
|
[Service]
|
|
164
169
|
Type=simple
|
|
165
170
|
ExecStart=${systemdQuote(input.execPath)} ${systemdQuote(input.daemonEntry)}
|
|
166
|
-
WorkingDirectory=${systemdQuote(input.homeDir)}
|
|
167
171
|
Environment=${systemdQuote(`PATH=${input.pathEnv}`)}
|
|
168
172
|
Restart=on-failure
|
|
169
173
|
RestartSec=10
|
|
@@ -199,12 +203,18 @@ WantedBy=default.target
|
|
|
199
203
|
}
|
|
200
204
|
if (input.platform === "win32") {
|
|
201
205
|
const taskName = "Pushary Daemon";
|
|
206
|
+
const username = process.env.USERNAME?.trim() || userInfo().username;
|
|
207
|
+
const domain = process.env.USERDOMAIN?.trim();
|
|
208
|
+
const userId = input.userId?.trim() || (domain ? `${domain}\\${username}` : username);
|
|
202
209
|
const definitionPath = win32Path.join(input.stateDir, "pushary-daemon-task.xml");
|
|
203
|
-
const
|
|
210
|
+
const supervisorPath = win32Path.join(win32Path.dirname(input.daemonEntry), "pushary-daemon-supervisor.js");
|
|
211
|
+
const supervised = (input.entryExists ?? existsSync)(supervisorPath);
|
|
212
|
+
const args = supervised ? `"${xml(supervisorPath)}" "${xml(input.daemonEntry)}"` : `"${xml(input.daemonEntry)}"`;
|
|
213
|
+
const contents = `\uFEFF<?xml version="1.0" encoding="UTF-16"?>
|
|
204
214
|
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
205
215
|
<RegistrationInfo><Description>Pushary phone-to-agent daemon</Description></RegistrationInfo>
|
|
206
|
-
<Triggers><LogonTrigger><Enabled>true</Enabled></LogonTrigger></Triggers>
|
|
207
|
-
<Principals><Principal id="Author"><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
|
|
216
|
+
<Triggers><LogonTrigger><UserId>${xml(userId)}</UserId><Enabled>true</Enabled></LogonTrigger></Triggers>
|
|
217
|
+
<Principals><Principal id="Author"><UserId>${xml(userId)}</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
|
|
208
218
|
<Settings>
|
|
209
219
|
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
210
220
|
<StartWhenAvailable>true</StartWhenAvailable>
|
|
@@ -216,7 +226,7 @@ WantedBy=default.target
|
|
|
216
226
|
<Actions Context="Author">
|
|
217
227
|
<Exec>
|
|
218
228
|
<Command>${xml(input.execPath)}</Command>
|
|
219
|
-
<Arguments
|
|
229
|
+
<Arguments>${args}</Arguments>
|
|
220
230
|
<WorkingDirectory>${xml(input.homeDir)}</WorkingDirectory>
|
|
221
231
|
</Exec>
|
|
222
232
|
</Actions>
|
|
@@ -225,6 +235,7 @@ WantedBy=default.target
|
|
|
225
235
|
return {
|
|
226
236
|
definitionPath,
|
|
227
237
|
contents,
|
|
238
|
+
encoding: "utf16le",
|
|
228
239
|
install: [
|
|
229
240
|
{ command: "schtasks.exe", args: ["/Create", "/TN", taskName, "/XML", definitionPath, "/F"] },
|
|
230
241
|
{ command: "schtasks.exe", args: ["/Run", "/TN", taskName] }
|
|
@@ -302,7 +313,8 @@ var resolveDaemonServiceDefinition = (options = {}) => {
|
|
|
302
313
|
execPath: options.execPath ?? process.execPath,
|
|
303
314
|
daemonEntry,
|
|
304
315
|
pathEnv: options.pathEnv ?? process.env.PATH ?? "",
|
|
305
|
-
uid: options.uid ?? process.getuid?.()
|
|
316
|
+
uid: options.uid ?? process.getuid?.(),
|
|
317
|
+
entryExists: exists
|
|
306
318
|
});
|
|
307
319
|
};
|
|
308
320
|
var runDaemonServiceSteps = (steps, run) => {
|
|
@@ -317,7 +329,9 @@ var runDaemonServiceSteps = (steps, run) => {
|
|
|
317
329
|
var applyDaemonService = (action, definition, options = {}) => {
|
|
318
330
|
const run = options.run ?? runDaemonServiceCommand;
|
|
319
331
|
try {
|
|
320
|
-
if (action === "install")
|
|
332
|
+
if (action === "install") {
|
|
333
|
+
writeFileAtomic(definition.definitionPath, definition.contents, 384, definition.encoding);
|
|
334
|
+
}
|
|
321
335
|
const steps = action === "uninstall" ? [...definition.stop, ...definition.uninstall] : definition[action];
|
|
322
336
|
runDaemonServiceSteps(steps, run);
|
|
323
337
|
if (action === "uninstall") {
|
|
@@ -674,7 +688,7 @@ var disableDaemonService = async (options = {}) => deactivateDaemonService("unin
|
|
|
674
688
|
var parseDaemonServiceTransaction = (value) => {
|
|
675
689
|
if (typeof value !== "object" || value === null) return null;
|
|
676
690
|
const transaction = value;
|
|
677
|
-
if (transaction.version !== 1 || transaction.phase !== "prepared" && transaction.phase !== "committed" || typeof transaction.definitionPath !== "string" || !(transaction.previousContents === null || typeof transaction.previousContents === "string") || typeof transaction.previousRegistered !== "boolean" || !(transaction.previousEnabled === void 0 || typeof transaction.previousEnabled === "boolean") || typeof transaction.previousRunning !== "boolean") return null;
|
|
691
|
+
if (transaction.version !== 1 || transaction.phase !== "prepared" && transaction.phase !== "committed" || typeof transaction.definitionPath !== "string" || !(transaction.previousContents === null || typeof transaction.previousContents === "string") || !(transaction.previousEncoding === void 0 || typeof transaction.previousEncoding === "string") || typeof transaction.previousRegistered !== "boolean" || !(transaction.previousEnabled === void 0 || typeof transaction.previousEnabled === "boolean") || typeof transaction.previousRunning !== "boolean") return null;
|
|
678
692
|
return {
|
|
679
693
|
...transaction,
|
|
680
694
|
previousEnabled: typeof transaction.previousEnabled === "boolean" ? transaction.previousEnabled : transaction.previousRegistered
|
|
@@ -723,12 +737,13 @@ var probeDaemonServiceState = (definition, run) => {
|
|
|
723
737
|
return { kind: "ok", registered, enabled, running: false };
|
|
724
738
|
}
|
|
725
739
|
};
|
|
740
|
+
var previousEncoding = (snapshot) => snapshot.previousEncoding ?? "utf-8";
|
|
726
741
|
var restoreDaemonServiceSnapshot = (definition, snapshot, run) => {
|
|
727
742
|
const removed = applyDaemonService("uninstall", definition, { run });
|
|
728
743
|
if (removed.kind !== "ok") return removed;
|
|
729
744
|
try {
|
|
730
745
|
if (snapshot.previousContents === null) safeUnlink(definition.definitionPath);
|
|
731
|
-
else writeFileAtomic(definition.definitionPath, snapshot.previousContents, 384);
|
|
746
|
+
else writeFileAtomic(definition.definitionPath, snapshot.previousContents, 384, previousEncoding(snapshot));
|
|
732
747
|
} catch (err) {
|
|
733
748
|
return {
|
|
734
749
|
kind: "failed",
|
|
@@ -767,7 +782,8 @@ var restoreDaemonServiceSnapshot = (definition, snapshot, run) => {
|
|
|
767
782
|
}
|
|
768
783
|
const restored = applyDaemonService("install", {
|
|
769
784
|
...definition,
|
|
770
|
-
contents: snapshot.previousContents
|
|
785
|
+
contents: snapshot.previousContents,
|
|
786
|
+
encoding: previousEncoding(snapshot)
|
|
771
787
|
}, { run });
|
|
772
788
|
if (restored.kind !== "ok") return restored;
|
|
773
789
|
if (!snapshot.previousRunning) applyDaemonService("stop", definition, { run });
|
|
@@ -829,9 +845,12 @@ var replaceDaemonService = async (options = {}) => {
|
|
|
829
845
|
return { kind: "failed", detail: `Could not recover interrupted service replacement: ${recovered.detail}` };
|
|
830
846
|
}
|
|
831
847
|
let previousContents = null;
|
|
848
|
+
let previousDefinitionEncoding = "utf-8";
|
|
832
849
|
try {
|
|
833
850
|
if (existsSync(definition.definitionPath)) {
|
|
834
|
-
|
|
851
|
+
const previous = readServiceDefinitionFile(definition.definitionPath);
|
|
852
|
+
previousContents = previous.contents;
|
|
853
|
+
previousDefinitionEncoding = previous.encoding;
|
|
835
854
|
}
|
|
836
855
|
} catch (err) {
|
|
837
856
|
return { kind: "failed", detail: `Could not snapshot the existing service: ${err instanceof Error ? err.message : String(err)}` };
|
|
@@ -846,6 +865,7 @@ var replaceDaemonService = async (options = {}) => {
|
|
|
846
865
|
phase: "prepared",
|
|
847
866
|
definitionPath: definition.definitionPath,
|
|
848
867
|
previousContents,
|
|
868
|
+
previousEncoding: previousDefinitionEncoding,
|
|
849
869
|
previousRegistered: previousState.registered,
|
|
850
870
|
previousEnabled: previousState.enabled,
|
|
851
871
|
previousRunning: previousState.running
|
|
@@ -869,7 +889,15 @@ var replaceDaemonService = async (options = {}) => {
|
|
|
869
889
|
}
|
|
870
890
|
let installed = applyDaemonService("install", definition, { run });
|
|
871
891
|
if (installed.kind === "ok") {
|
|
872
|
-
const
|
|
892
|
+
const sleep = options.sleep ?? wait;
|
|
893
|
+
const pollIntervalMs = Math.max(1, options.pollIntervalMs ?? 50);
|
|
894
|
+
const now = options.now ?? Date.now;
|
|
895
|
+
const deadline = now() + Math.max(0, options.waitTimeoutMs ?? 5e3);
|
|
896
|
+
let state = probeDaemonServiceState(definition, run);
|
|
897
|
+
while (state.kind === "ok" && (!state.registered || !state.enabled || !state.running) && now() < deadline) {
|
|
898
|
+
await sleep(pollIntervalMs);
|
|
899
|
+
state = probeDaemonServiceState(definition, run);
|
|
900
|
+
}
|
|
873
901
|
if (state.kind !== "ok") {
|
|
874
902
|
installed = { kind: "failed", detail: state.detail };
|
|
875
903
|
} else if (!state.registered || !state.enabled || !state.running) {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getBaseUrl
|
|
3
|
+
} from "./chunk-2UMNXADU.js";
|
|
4
|
+
|
|
5
|
+
// src/transcript-api.ts
|
|
6
|
+
var fetchTranscriptRecipients = async (apiKey, baseUrl = getBaseUrl()) => {
|
|
7
|
+
try {
|
|
8
|
+
const res = await fetch(`${baseUrl}/api/agent/encryption-account`, {
|
|
9
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
10
|
+
signal: AbortSignal.timeout(5e3)
|
|
11
|
+
});
|
|
12
|
+
if (!res.ok) return null;
|
|
13
|
+
const data = await res.json();
|
|
14
|
+
if (typeof data.publicKey !== "string" || data.publicKey.length === 0) return null;
|
|
15
|
+
if (data.providerRecovery === void 0 || data.providerRecovery === null) {
|
|
16
|
+
return { phonePublicKey: data.publicKey, provider: null };
|
|
17
|
+
}
|
|
18
|
+
if (typeof data.providerRecovery !== "object") return null;
|
|
19
|
+
const provider = data.providerRecovery;
|
|
20
|
+
if (typeof provider.required !== "boolean" || typeof provider.keyId !== "string" || provider.keyId.length === 0 || typeof provider.publicKey !== "string" || provider.publicKey.length === 0) return null;
|
|
21
|
+
return {
|
|
22
|
+
phonePublicKey: data.publicKey,
|
|
23
|
+
provider: { required: provider.required, keyId: provider.keyId, publicKey: provider.publicKey }
|
|
24
|
+
};
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var fetchAccountPublicKey = async (apiKey, baseUrl = getBaseUrl()) => (await fetchTranscriptRecipients(apiKey, baseUrl))?.phonePublicKey ?? null;
|
|
30
|
+
var uploadTranscriptRecords = async (apiKey, params, baseUrl = getBaseUrl()) => {
|
|
31
|
+
try {
|
|
32
|
+
const res = await fetch(`${baseUrl}/api/agent/transcript`, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
35
|
+
body: JSON.stringify(params),
|
|
36
|
+
signal: AbortSignal.timeout(15e3)
|
|
37
|
+
});
|
|
38
|
+
const body = await res.json().catch(() => null);
|
|
39
|
+
const keyRegistered = body?.keyRegistered === true;
|
|
40
|
+
const keyConflict = body?.keyConflict === true;
|
|
41
|
+
const ids = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
42
|
+
return {
|
|
43
|
+
ok: res.ok && !keyConflict && (params.dataEncryptionKey === void 0 || keyRegistered),
|
|
44
|
+
keyRegistered,
|
|
45
|
+
keyConflict,
|
|
46
|
+
settledIds: Array.isArray(body?.settledIds) ? ids(body.settledIds) : null,
|
|
47
|
+
rejectedIds: ids(body?.rejectedIds)
|
|
48
|
+
};
|
|
49
|
+
} catch {
|
|
50
|
+
return { ok: false, keyRegistered: false, keyConflict: false, settledIds: null, rejectedIds: [] };
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
fetchTranscriptRecipients,
|
|
56
|
+
fetchAccountPublicKey,
|
|
57
|
+
uploadTranscriptRecords
|
|
58
|
+
};
|
|
@@ -3,7 +3,7 @@ var COMMAND_LIST = [
|
|
|
3
3
|
{
|
|
4
4
|
name: "setup",
|
|
5
5
|
module: "pushary-setup",
|
|
6
|
-
summary: "Configure Claude Code, Codex, Gemini CLI, Hermes, Cursor,
|
|
6
|
+
summary: "Configure Claude Code, Codex, Gemini CLI, Hermes, Cursor, VS Code, or OpenCode with Pushary"
|
|
7
7
|
},
|
|
8
8
|
{
|
|
9
9
|
name: "claude",
|
|
@@ -133,6 +133,7 @@ var SETUP_OPTIONS = [
|
|
|
133
133
|
["--connect web", "Legacy browser subscribe page"],
|
|
134
134
|
["--connect none", "Skip the phone connect step, same as --skip-phone"],
|
|
135
135
|
["--control auto|on|off", "Manage phone-start automatically (default), require it, or disable its background service"],
|
|
136
|
+
["--transcripts on|off", "Enable or disable encrypted transcript sync on this machine"],
|
|
136
137
|
["--skip-phone", "Skip the phone connect step"],
|
|
137
138
|
["--verify roundtrip", "Send a real question and wait for you to answer it (default when a human is present)"],
|
|
138
139
|
["--verify push", "Only prove a notification was delivered. Automatic when nobody is at the terminal"],
|
|
@@ -140,7 +141,7 @@ var SETUP_OPTIONS = [
|
|
|
140
141
|
["--dry-run", "Print what would be written and change nothing"],
|
|
141
142
|
["--json", "Emit one machine-readable object instead of the human report"]
|
|
142
143
|
];
|
|
143
|
-
var AGENT_IDS = "claude_code, codex, gemini_cli, hermes, cursor, custom";
|
|
144
|
+
var AGENT_IDS = "claude_code, codex, gemini_cli, hermes, cursor, vscode, opencode, custom";
|
|
144
145
|
var USAGE_EXAMPLES = [
|
|
145
146
|
"npx @pushary/agent-hooks@latest setup",
|
|
146
147
|
"npx @pushary/agent-hooks@latest setup --agents auto --yes --skip-phone",
|
|
@@ -3,13 +3,13 @@ import {
|
|
|
3
3
|
} from "./chunk-BC3VCZ3E.js";
|
|
4
4
|
import {
|
|
5
5
|
resolveInstallSourceFor
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-XREW7RQ3.js";
|
|
7
7
|
import {
|
|
8
8
|
backupFile,
|
|
9
9
|
readJsonSafe,
|
|
10
10
|
writeFileAtomic,
|
|
11
11
|
writeJsonAtomic
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-4LL6LG5X.js";
|
|
13
13
|
import {
|
|
14
14
|
configFilePath
|
|
15
15
|
} from "./chunk-2UMNXADU.js";
|
|
@@ -125,7 +125,8 @@ var writeKey = (apiKey, options = {}) => {
|
|
|
125
125
|
...current,
|
|
126
126
|
apiKey,
|
|
127
127
|
installSource: resolveInstallSourceFor(current),
|
|
128
|
-
...options.controlMode ? { controlMode: options.controlMode } : {}
|
|
128
|
+
...options.controlMode ? { controlMode: options.controlMode } : {},
|
|
129
|
+
...options.transcriptsEnabled === void 0 ? {} : { transcriptsEnabled: options.transcriptsEnabled }
|
|
129
130
|
},
|
|
130
131
|
KEY_FILE_MODE
|
|
131
132
|
);
|
|
@@ -188,6 +189,11 @@ var readControlMode = () => {
|
|
|
188
189
|
const value = result.value.controlMode;
|
|
189
190
|
return value === "auto" || value === "on" || value === "off" ? value : null;
|
|
190
191
|
};
|
|
192
|
+
var readTranscriptPreference = () => {
|
|
193
|
+
const result = readJsonSafe(configFilePath());
|
|
194
|
+
if (result.kind !== "ok") return null;
|
|
195
|
+
return typeof result.value.transcriptsEnabled === "boolean" ? result.value.transcriptsEnabled : null;
|
|
196
|
+
};
|
|
191
197
|
var readKeySource = () => {
|
|
192
198
|
const configPath = configFilePath();
|
|
193
199
|
const envKey = process.env.PUSHARY_API_KEY?.trim() || null;
|
|
@@ -206,5 +212,6 @@ export {
|
|
|
206
212
|
clearKey,
|
|
207
213
|
readConfigFileKey,
|
|
208
214
|
readControlMode,
|
|
215
|
+
readTranscriptPreference,
|
|
209
216
|
readKeySource
|
|
210
217
|
};
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getBaseUrl
|
|
3
|
-
} from "./chunk-2UMNXADU.js";
|
|
4
1
|
import {
|
|
5
2
|
redactSecrets
|
|
6
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-3MCEES6Z.js";
|
|
7
4
|
|
|
8
5
|
// src/crypto.ts
|
|
9
6
|
import nacl from "tweetnacl";
|
|
@@ -79,46 +76,9 @@ var buildTranscriptRecords = (lines, sessionKey, seen = /* @__PURE__ */ new Set(
|
|
|
79
76
|
return { records, seen: nextSeen };
|
|
80
77
|
};
|
|
81
78
|
|
|
82
|
-
// src/transcript-api.ts
|
|
83
|
-
var fetchAccountPublicKey = async (apiKey, baseUrl = getBaseUrl()) => {
|
|
84
|
-
try {
|
|
85
|
-
const res = await fetch(`${baseUrl}/api/agent/encryption-account`, {
|
|
86
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
87
|
-
signal: AbortSignal.timeout(5e3)
|
|
88
|
-
});
|
|
89
|
-
if (!res.ok) return null;
|
|
90
|
-
const data = await res.json();
|
|
91
|
-
return typeof data.publicKey === "string" && data.publicKey.length > 0 ? data.publicKey : null;
|
|
92
|
-
} catch {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
var uploadTranscriptRecords = async (apiKey, params, baseUrl = getBaseUrl()) => {
|
|
97
|
-
try {
|
|
98
|
-
const res = await fetch(`${baseUrl}/api/agent/transcript`, {
|
|
99
|
-
method: "POST",
|
|
100
|
-
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
101
|
-
body: JSON.stringify(params),
|
|
102
|
-
signal: AbortSignal.timeout(15e3)
|
|
103
|
-
});
|
|
104
|
-
const body = await res.json().catch(() => null);
|
|
105
|
-
const keyRegistered = body?.keyRegistered === true;
|
|
106
|
-
const keyConflict = body?.keyConflict === true;
|
|
107
|
-
return {
|
|
108
|
-
ok: res.ok && !keyConflict && (params.dataEncryptionKey === void 0 || keyRegistered),
|
|
109
|
-
keyRegistered,
|
|
110
|
-
keyConflict
|
|
111
|
-
};
|
|
112
|
-
} catch {
|
|
113
|
-
return { ok: false, keyRegistered: false, keyConflict: false };
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
|
|
117
79
|
export {
|
|
118
80
|
decodePublicKey,
|
|
119
81
|
generateSessionKey,
|
|
120
82
|
wrapSessionKey,
|
|
121
|
-
buildTranscriptRecords
|
|
122
|
-
fetchAccountPublicKey,
|
|
123
|
-
uploadTranscriptRecords
|
|
83
|
+
buildTranscriptRecords
|
|
124
84
|
};
|