@promus/cli 0.24.17
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/README.md +18 -0
- package/bin/promus +33 -0
- package/package.json +51 -0
- package/src/commands/_agents.ts +14 -0
- package/src/commands/_inft-ref.ts +43 -0
- package/src/commands/_unlock.ts +74 -0
- package/src/commands/admin-autotopup-tick.ts +73 -0
- package/src/commands/admin.test.ts +34 -0
- package/src/commands/admin.ts +32 -0
- package/src/commands/balance.test.ts +10 -0
- package/src/commands/balance.ts +112 -0
- package/src/commands/chat-sandbox.tsx +520 -0
- package/src/commands/chat-telegram.ts +398 -0
- package/src/commands/chat.tsx +1916 -0
- package/src/commands/deploy.ts +204 -0
- package/src/commands/drain.ts +90 -0
- package/src/commands/gateway-logs.ts +47 -0
- package/src/commands/gateway-run.ts +54 -0
- package/src/commands/gateway-start.ts +218 -0
- package/src/commands/gateway-status.ts +88 -0
- package/src/commands/gateway-stop.ts +133 -0
- package/src/commands/gateway.ts +101 -0
- package/src/commands/init/cost.test.ts +169 -0
- package/src/commands/init/cost.ts +154 -0
- package/src/commands/init/funding-gate.ts +67 -0
- package/src/commands/init/model-picker.ts +81 -0
- package/src/commands/init/operator-picker.ts +263 -0
- package/src/commands/init/resume.ts +136 -0
- package/src/commands/init/sandbox-provision.test.ts +497 -0
- package/src/commands/init/sandbox-provision.ts +1177 -0
- package/src/commands/init/telegram-step.ts +229 -0
- package/src/commands/init/wizard-state.ts +95 -0
- package/src/commands/init.ts +612 -0
- package/src/commands/inspect.ts +529 -0
- package/src/commands/ledger.ts +176 -0
- package/src/commands/logs.ts +86 -0
- package/src/commands/migrate-keystore.ts +155 -0
- package/src/commands/model.ts +48 -0
- package/src/commands/pairing-approve.ts +114 -0
- package/src/commands/pairing-clear.ts +42 -0
- package/src/commands/pairing-list.ts +58 -0
- package/src/commands/pairing-revoke.ts +52 -0
- package/src/commands/pairing.test.ts +88 -0
- package/src/commands/pairing.ts +81 -0
- package/src/commands/pause.ts +99 -0
- package/src/commands/profile.ts +184 -0
- package/src/commands/restore.ts +221 -0
- package/src/commands/resume.ts +181 -0
- package/src/commands/status.ts +119 -0
- package/src/commands/sync.ts +147 -0
- package/src/commands/telegram-remove.ts +65 -0
- package/src/commands/telegram-setup.ts +74 -0
- package/src/commands/telegram-status.ts +89 -0
- package/src/commands/telegram.test.ts +50 -0
- package/src/commands/telegram.ts +44 -0
- package/src/commands/topup.ts +303 -0
- package/src/commands/transfer.test.ts +111 -0
- package/src/commands/transfer.ts +520 -0
- package/src/commands/upgrade.test.ts +137 -0
- package/src/commands/upgrade.ts +690 -0
- package/src/config/load.ts +35 -0
- package/src/config/render.test.ts +96 -0
- package/src/config/render.ts +110 -0
- package/src/index.ts +378 -0
- package/src/sandbox/client.test.ts +251 -0
- package/src/sandbox/client.ts +424 -0
- package/src/ui/app.tsx +677 -0
- package/src/ui/approval-summary.test.ts +154 -0
- package/src/ui/approval-summary.ts +34 -0
- package/src/ui/markdown-parse.ts +219 -0
- package/src/ui/markdown.test.ts +146 -0
- package/src/ui/markdown.tsx +37 -0
- package/src/ui/state.test.ts +74 -0
- package/src/ui/state.ts +198 -0
- package/src/util/bootstrap-mode.test.ts +40 -0
- package/src/util/bootstrap-mode.ts +25 -0
- package/src/util/bootstrap-progress-box.test.ts +190 -0
- package/src/util/bootstrap-progress-box.ts +378 -0
- package/src/util/brain-secrets.ts +96 -0
- package/src/util/cli-version.ts +28 -0
- package/src/util/format.test.ts +16 -0
- package/src/util/format.ts +11 -0
- package/src/util/gateway-spawn.test.ts +86 -0
- package/src/util/gateway-spawn.ts +128 -0
- package/src/util/gateway-version.test.ts +113 -0
- package/src/util/gateway-version.ts +154 -0
- package/src/util/github-releases.test.ts +116 -0
- package/src/util/github-releases.ts +79 -0
- package/src/util/profile-key.test.ts +60 -0
- package/src/util/profile-key.ts +25 -0
- package/src/util/ref-resolver.test.ts +77 -0
- package/src/util/ref-resolver.ts +55 -0
- package/src/util/silence-console.test.ts +53 -0
- package/src/util/silence-console.ts +40 -0
- package/src/util/telegram-secrets.test.ts +227 -0
- package/src/util/telegram-secrets.ts +223 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-mode telegram dispatch wiring for chat.tsx.
|
|
3
|
+
*
|
|
4
|
+
* Two pieces:
|
|
5
|
+
*
|
|
6
|
+
* 1. `buildTelegramRuntimeContext`: composes the side-band context the plugin
|
|
7
|
+
* consumes via `(ctx as any).telegram`. The context's `dispatchUserMessage`
|
|
8
|
+
* points at a *deferred* callback ref; chat.tsx populates the ref AFTER
|
|
9
|
+
* brain init but BEFORE any inbound TG message can race.
|
|
10
|
+
*
|
|
11
|
+
* 2. `buildTelegramDispatch`: factory for the deferred callback itself.
|
|
12
|
+
* Returns a handle with `{ dispatch, drainQueue, getQueueSize }`. chat.tsx
|
|
13
|
+
* wires the dispatch into the slot AND subscribes to status idle so it
|
|
14
|
+
* can call drainQueue to wake any messages that arrived during a stdin
|
|
15
|
+
* turn (closes G4 starvation).
|
|
16
|
+
*
|
|
17
|
+
* Bypass commands (parseBypassCommand) skip the queue + busy gate. `/stop`
|
|
18
|
+
* aborts the active brain turn; `/status` reports thinking/idle; the rest
|
|
19
|
+
* are placeholders for future B5 inline-keyboard approvals.
|
|
20
|
+
*/
|
|
21
|
+
import type {
|
|
22
|
+
ActivityLog,
|
|
23
|
+
Brain,
|
|
24
|
+
FrozenPrefix,
|
|
25
|
+
MemorySyncManager,
|
|
26
|
+
PermissionDecision,
|
|
27
|
+
PermissionPrompter,
|
|
28
|
+
PermissionRequest,
|
|
29
|
+
PermissionService,
|
|
30
|
+
} from '@promus/core'
|
|
31
|
+
import { applyPerms, applyYolo, newEventId } from '@promus/core'
|
|
32
|
+
import {
|
|
33
|
+
ActiveSessionTracker,
|
|
34
|
+
type ApprovalChoice,
|
|
35
|
+
type BypassCommand,
|
|
36
|
+
type TelegramApprovalBridge,
|
|
37
|
+
type TelegramDispatchInput,
|
|
38
|
+
type TelegramDispatchResult,
|
|
39
|
+
type TelegramRuntimeContext,
|
|
40
|
+
makeApprovalIdFactory,
|
|
41
|
+
parseBypassCommand,
|
|
42
|
+
} from '@promus/plugin-telegram'
|
|
43
|
+
import { summarizeApprovalSubject } from '../ui/approval-summary'
|
|
44
|
+
|
|
45
|
+
export type DispatchUserMessage = (input: TelegramDispatchInput) => Promise<TelegramDispatchResult>
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Mutable callback ref. chat.tsx holds it across boot; we hand the ref into
|
|
49
|
+
* the plugin's runtime context via a closure that defers to the ref's current
|
|
50
|
+
* value at call-time.
|
|
51
|
+
*/
|
|
52
|
+
export interface TelegramDispatchSlot {
|
|
53
|
+
current: DispatchUserMessage | null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface RowSinkRef {
|
|
57
|
+
current: ((text: string) => void) | null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildTelegramRuntimeContext(opts: {
|
|
61
|
+
botToken: string
|
|
62
|
+
allowedUserIds: number[]
|
|
63
|
+
agentName: string
|
|
64
|
+
slot: TelegramDispatchSlot
|
|
65
|
+
systemRowSink: RowSinkRef
|
|
66
|
+
}): TelegramRuntimeContext {
|
|
67
|
+
return {
|
|
68
|
+
botToken: opts.botToken,
|
|
69
|
+
allowedUserIds: opts.allowedUserIds,
|
|
70
|
+
agentName: opts.agentName,
|
|
71
|
+
dispatchUserMessage: async input => {
|
|
72
|
+
const cb = opts.slot.current
|
|
73
|
+
if (!cb) {
|
|
74
|
+
return {
|
|
75
|
+
response: 'agent is still booting; try again in a moment.',
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return cb(input)
|
|
79
|
+
},
|
|
80
|
+
onProcessingStart: async (chatId, _msgId) => {
|
|
81
|
+
opts.systemRowSink.current?.(`tg replying to chat ${chatId}`)
|
|
82
|
+
},
|
|
83
|
+
onProcessingEnd: async (chatId, _msgId, ok) => {
|
|
84
|
+
opts.systemRowSink.current?.(
|
|
85
|
+
ok ? `tg reply sent to chat ${chatId}` : `tg reply FAILED to chat ${chatId}`,
|
|
86
|
+
)
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface BuildDispatchDeps {
|
|
92
|
+
activity: ActivityLog
|
|
93
|
+
sync: MemorySyncManager
|
|
94
|
+
permission: PermissionService
|
|
95
|
+
pushAssistantRow: (text: string) => void
|
|
96
|
+
pushInboundRow: (preview: string) => void
|
|
97
|
+
/** Returns true if the brain is currently busy on another turn. */
|
|
98
|
+
isBusy: () => boolean
|
|
99
|
+
buildPrefix: () => Promise<FrozenPrefix>
|
|
100
|
+
brain: Brain & { refreshUserContext: (prefix: FrozenPrefix) => void }
|
|
101
|
+
/** Mark the brain as "thinking" / idle in the TUI state. */
|
|
102
|
+
setThinking: (on: boolean) => void
|
|
103
|
+
setActiveAbort: (ctrl: AbortController | null) => void
|
|
104
|
+
refreshBalances: () => void
|
|
105
|
+
formatInboundPreview: (input: TelegramDispatchInput) => string
|
|
106
|
+
/**
|
|
107
|
+
* Optional approval bridge from the listener. When present, dispatch swaps
|
|
108
|
+
* permission.setPrompter to a TG-aware prompter for the turn duration so
|
|
109
|
+
* the operator can approve tool calls from their phone via inline keyboard.
|
|
110
|
+
*/
|
|
111
|
+
approvalBridge?: TelegramApprovalBridge
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface TelegramDispatchHandle {
|
|
115
|
+
dispatch: DispatchUserMessage
|
|
116
|
+
/** Re-run the queue. Called by chat.tsx when stdin turn ends (closes G4). */
|
|
117
|
+
drainQueue: () => void
|
|
118
|
+
getQueueSize: () => number
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Build the deferred dispatch callback. Caller assigns `handle.dispatch` into
|
|
123
|
+
* `slot.current` once brain.init resolves, and wires `handle.drainQueue` into
|
|
124
|
+
* a status-change effect.
|
|
125
|
+
*/
|
|
126
|
+
export function buildTelegramDispatch(deps: BuildDispatchDeps): TelegramDispatchHandle {
|
|
127
|
+
const queue: { input: TelegramDispatchInput; resolve: (r: TelegramDispatchResult) => void }[] = []
|
|
128
|
+
let draining = false
|
|
129
|
+
const tracker = new ActiveSessionTracker()
|
|
130
|
+
const pendingApprovals = new Map<string, (choice: ApprovalChoice) => void>()
|
|
131
|
+
const approvalIdFactory = makeApprovalIdFactory()
|
|
132
|
+
let callbackInstalled = false
|
|
133
|
+
const ensureCallbackInstalled = (): void => {
|
|
134
|
+
if (callbackInstalled) return
|
|
135
|
+
const install = deps.approvalBridge?.installCallbackHandler.current
|
|
136
|
+
if (!install) return
|
|
137
|
+
install((approvalId, choice, _fromUserId) => {
|
|
138
|
+
const r = pendingApprovals.get(approvalId)
|
|
139
|
+
if (r) {
|
|
140
|
+
pendingApprovals.delete(approvalId)
|
|
141
|
+
r(choice)
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
callbackInstalled = true
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const drain = async (): Promise<void> => {
|
|
148
|
+
if (draining) return
|
|
149
|
+
draining = true
|
|
150
|
+
try {
|
|
151
|
+
while (queue.length > 0) {
|
|
152
|
+
if (deps.isBusy()) return
|
|
153
|
+
const item = queue.shift()!
|
|
154
|
+
ensureCallbackInstalled()
|
|
155
|
+
try {
|
|
156
|
+
const r = await runOne(item.input, deps, tracker, {
|
|
157
|
+
pendingApprovals,
|
|
158
|
+
approvalIdFactory,
|
|
159
|
+
})
|
|
160
|
+
item.resolve(r)
|
|
161
|
+
} catch (err) {
|
|
162
|
+
item.resolve({
|
|
163
|
+
response: `error processing your message: ${(err as Error).message.slice(0, 200)}`,
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} finally {
|
|
168
|
+
draining = false
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
dispatch: (input: TelegramDispatchInput) =>
|
|
174
|
+
new Promise<TelegramDispatchResult>(resolve => {
|
|
175
|
+
deps.pushInboundRow(deps.formatInboundPreview(input))
|
|
176
|
+
|
|
177
|
+
// Bypass commands skip the queue + busy gate entirely.
|
|
178
|
+
const bypass = parseBypassCommand(input.text)
|
|
179
|
+
if (bypass) {
|
|
180
|
+
void Promise.resolve(handleBypass(bypass, input, deps, tracker)).then(resolve)
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
queue.push({ input, resolve })
|
|
185
|
+
void drain()
|
|
186
|
+
}),
|
|
187
|
+
drainQueue: () => {
|
|
188
|
+
void drain()
|
|
189
|
+
},
|
|
190
|
+
getQueueSize: () => queue.length,
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function handleBypass(
|
|
195
|
+
bypass: { command: BypassCommand; args: string[] },
|
|
196
|
+
input: TelegramDispatchInput,
|
|
197
|
+
deps: BuildDispatchDeps,
|
|
198
|
+
tracker: ActiveSessionTracker,
|
|
199
|
+
): Promise<TelegramDispatchResult> {
|
|
200
|
+
const { command: cmd, args } = bypass
|
|
201
|
+
switch (cmd) {
|
|
202
|
+
case '/stop': {
|
|
203
|
+
const aborted = tracker.abortActive(input.sessionKey)
|
|
204
|
+
if (!aborted && deps.isBusy()) {
|
|
205
|
+
return { response: 'no active turn to stop here, but the agent is busy on stdin.' }
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
response: aborted ? 'stopped the current turn.' : 'no active turn to stop.',
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
case '/new':
|
|
212
|
+
case '/reset': {
|
|
213
|
+
// v0.20.0: real reset clears this channel's history. Falls back to a
|
|
214
|
+
// friendly note when the brain doesn't expose channel ops (StubBrain).
|
|
215
|
+
if (typeof deps.brain.clearChannel === 'function') {
|
|
216
|
+
await deps.brain.clearChannel(input.sessionKey)
|
|
217
|
+
return { response: "conversation reset (this chat's history cleared)." }
|
|
218
|
+
}
|
|
219
|
+
return { response: 'this brain does not support reset.' }
|
|
220
|
+
}
|
|
221
|
+
case '/status': {
|
|
222
|
+
const busy = deps.isBusy()
|
|
223
|
+
const qs = '' // queue size could be read via closure; keep terse here
|
|
224
|
+
return {
|
|
225
|
+
response: busy ? `currently thinking on another turn${qs}.` : `idle${qs}.`,
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
case '/approve':
|
|
229
|
+
case '/deny': {
|
|
230
|
+
return {
|
|
231
|
+
response: 'inline-keyboard approval is not yet wired in this build (B5 ships in v0.18.1).',
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
case '/yolo': {
|
|
235
|
+
const r = applyYolo(deps.permission)
|
|
236
|
+
return { response: r.message }
|
|
237
|
+
}
|
|
238
|
+
case '/perms': {
|
|
239
|
+
const r = applyPerms(deps.permission, args[0])
|
|
240
|
+
return { response: r.message }
|
|
241
|
+
}
|
|
242
|
+
case '/background':
|
|
243
|
+
case '/restart': {
|
|
244
|
+
return { response: `${cmd} is reserved for a future bundle.` }
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
interface RunOneOpts {
|
|
250
|
+
pendingApprovals: Map<string, (c: ApprovalChoice) => void>
|
|
251
|
+
approvalIdFactory: () => string
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function runOne(
|
|
255
|
+
input: TelegramDispatchInput,
|
|
256
|
+
deps: BuildDispatchDeps,
|
|
257
|
+
tracker: ActiveSessionTracker,
|
|
258
|
+
opts: RunOneOpts,
|
|
259
|
+
): Promise<TelegramDispatchResult> {
|
|
260
|
+
// If the listener filled the approval bridge, swap the permission prompter
|
|
261
|
+
// to the TG-aware one for the turn duration. The brain will issue an
|
|
262
|
+
// inline-keyboard approval message; the operator clicks from their phone;
|
|
263
|
+
// the callback resolves the prompter's Promise. Permission resolves go
|
|
264
|
+
// through the normal PermissionService.resolve path: 'off' bypass, 'strict'
|
|
265
|
+
// deny, 'prompt' consults the prompter. We use 'prompt' for TG turns so
|
|
266
|
+
// the bridge is exercised; chat-telegram previously forced 'off' to bypass
|
|
267
|
+
// the TUI modal entirely.
|
|
268
|
+
const previousPrompter = (deps.permission as unknown as { prompter: PermissionPrompter }).prompter
|
|
269
|
+
const bridgeReady =
|
|
270
|
+
!!deps.approvalBridge?.sendApproval.current &&
|
|
271
|
+
!!deps.approvalBridge?.installCallbackHandler.current
|
|
272
|
+
const previousMode = deps.permission.getMode()
|
|
273
|
+
if (bridgeReady) {
|
|
274
|
+
const tgPrompter = buildTelegramPrompter({
|
|
275
|
+
chatId: input.chatId,
|
|
276
|
+
bridge: deps.approvalBridge!,
|
|
277
|
+
pendingApprovals: opts.pendingApprovals,
|
|
278
|
+
approvalIdFactory: opts.approvalIdFactory,
|
|
279
|
+
})
|
|
280
|
+
deps.permission.setPrompter(tgPrompter)
|
|
281
|
+
// Use 'prompt' so dangerous patterns + value-moving txs route through the
|
|
282
|
+
// TG prompter. Tools without prompts (e.g. fs.read) still pass.
|
|
283
|
+
deps.permission.setMode('prompt')
|
|
284
|
+
} else {
|
|
285
|
+
// No bridge: fall back to YOLO so brain doesn't deadlock on a TUI modal
|
|
286
|
+
// the phone-side operator can't reach.
|
|
287
|
+
deps.permission.setMode('off')
|
|
288
|
+
}
|
|
289
|
+
deps.setThinking(true)
|
|
290
|
+
const abortCtrl = new AbortController()
|
|
291
|
+
deps.setActiveAbort(abortCtrl)
|
|
292
|
+
// Synchronous mark-active BEFORE any await closes the race window per
|
|
293
|
+
// hermes base.py:1471. Two messages in the same tick now see the lock.
|
|
294
|
+
tracker.markActive(input.sessionKey, abortCtrl)
|
|
295
|
+
try {
|
|
296
|
+
const refreshed = await deps.buildPrefix()
|
|
297
|
+
deps.brain.refreshUserContext(refreshed)
|
|
298
|
+
await deps.activity.append({
|
|
299
|
+
ts: Date.now(),
|
|
300
|
+
kind: 'wake',
|
|
301
|
+
data: { source: 'telegram', chatId: input.chatId, userId: input.userId },
|
|
302
|
+
})
|
|
303
|
+
const turn = await deps.brain.infer({
|
|
304
|
+
event: {
|
|
305
|
+
id: newEventId(),
|
|
306
|
+
source: 'telegram',
|
|
307
|
+
payload: { label: 'telegram-message', data: input.text },
|
|
308
|
+
ts: Date.now(),
|
|
309
|
+
},
|
|
310
|
+
channelKey: input.sessionKey,
|
|
311
|
+
signal: abortCtrl.signal,
|
|
312
|
+
// Forward per-turn tool-call observer to the brain. The listener
|
|
313
|
+
// attaches a ProgressTracker on every dispatch; dropping it here
|
|
314
|
+
// would silently disable TG's live progress message.
|
|
315
|
+
onToolEvent: input.onToolEvent
|
|
316
|
+
? ev => {
|
|
317
|
+
input.onToolEvent?.({
|
|
318
|
+
kind: ev.kind,
|
|
319
|
+
tool: ev.tool,
|
|
320
|
+
callId: ev.callId,
|
|
321
|
+
argsPreview: ev.argsPreview,
|
|
322
|
+
ok: ev.ok,
|
|
323
|
+
})
|
|
324
|
+
}
|
|
325
|
+
: undefined,
|
|
326
|
+
})
|
|
327
|
+
await deps.activity.append({
|
|
328
|
+
ts: Date.now(),
|
|
329
|
+
kind: 'brain-response',
|
|
330
|
+
data: {
|
|
331
|
+
content: turn.content,
|
|
332
|
+
toolCalls: turn.toolCalls.length,
|
|
333
|
+
finishReason: turn.finishReason,
|
|
334
|
+
usage: turn.usage,
|
|
335
|
+
source: 'telegram',
|
|
336
|
+
},
|
|
337
|
+
})
|
|
338
|
+
const response = (turn.content ?? '').trim()
|
|
339
|
+
if (response.length > 0) deps.pushAssistantRow(response)
|
|
340
|
+
deps.refreshBalances()
|
|
341
|
+
let syncTx: string | undefined
|
|
342
|
+
try {
|
|
343
|
+
const res = await deps.sync.flushTurn()
|
|
344
|
+
if (res.txHash) syncTx = res.txHash
|
|
345
|
+
} catch {
|
|
346
|
+
// sync errors stay in the activity log; not surfaced to TG.
|
|
347
|
+
}
|
|
348
|
+
return { response: response.length === 0 ? '(no reply)' : response, syncTx }
|
|
349
|
+
} finally {
|
|
350
|
+
deps.setThinking(false)
|
|
351
|
+
deps.setActiveAbort(null)
|
|
352
|
+
tracker.markIdle(input.sessionKey)
|
|
353
|
+
deps.permission.setMode(previousMode)
|
|
354
|
+
if (bridgeReady && previousPrompter) {
|
|
355
|
+
deps.permission.setPrompter(previousPrompter)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const APPROVAL_TIMEOUT_MS = 5 * 60_000
|
|
361
|
+
|
|
362
|
+
function buildTelegramPrompter(opts: {
|
|
363
|
+
chatId: number
|
|
364
|
+
bridge: TelegramApprovalBridge
|
|
365
|
+
pendingApprovals: Map<string, (c: ApprovalChoice) => void>
|
|
366
|
+
approvalIdFactory: () => string
|
|
367
|
+
}): PermissionPrompter {
|
|
368
|
+
return async (req: PermissionRequest) => {
|
|
369
|
+
const send = opts.bridge.sendApproval.current
|
|
370
|
+
if (!send) return 'deny'
|
|
371
|
+
const approvalId = opts.approvalIdFactory()
|
|
372
|
+
const body = formatApprovalBody(req)
|
|
373
|
+
return new Promise<PermissionDecision>(resolve => {
|
|
374
|
+
const timer = setTimeout(() => {
|
|
375
|
+
if (opts.pendingApprovals.delete(approvalId)) resolve('deny')
|
|
376
|
+
}, APPROVAL_TIMEOUT_MS)
|
|
377
|
+
opts.pendingApprovals.set(approvalId, choice => {
|
|
378
|
+
clearTimeout(timer)
|
|
379
|
+
resolve(mapChoiceToDecision(choice))
|
|
380
|
+
})
|
|
381
|
+
void send(opts.chatId, body, approvalId).catch(() => {
|
|
382
|
+
clearTimeout(timer)
|
|
383
|
+
if (opts.pendingApprovals.delete(approvalId)) resolve('deny')
|
|
384
|
+
})
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function mapChoiceToDecision(choice: ApprovalChoice): PermissionDecision {
|
|
390
|
+
if (choice === 'once') return 'allow-once'
|
|
391
|
+
if (choice === 'session' || choice === 'always') return 'allow-session'
|
|
392
|
+
return 'deny'
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function formatApprovalBody(req: PermissionRequest): string {
|
|
396
|
+
const subject = summarizeApprovalSubject(req)
|
|
397
|
+
return `🔐 Approval needed for ${req.kind}\n\n${subject}\n\nReason: ${req.reason}`
|
|
398
|
+
}
|