@threadbase-sh/streamer 1.67.4 → 1.68.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/handlers/sessions.handlers.d.ts +40 -0
- package/dist/api/handlers/sessions.handlers.d.ts.map +1 -1
- package/dist/api/routes/sessions.routes.d.ts.map +1 -1
- package/dist/api/types/api-deps.d.ts +1 -0
- package/dist/api/types/api-deps.d.ts.map +1 -1
- package/dist/cli.cjs +130 -28
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +100 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +100 -2
- package/dist/index.js.map +1 -1
- package/dist/server-wiring.d.ts.map +1 -1
- package/dist/services/questions/detectPermissionGate.d.ts +16 -0
- package/dist/services/questions/detectPermissionGate.d.ts.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2569,6 +2569,9 @@ import { basename as basename2 } from "path";
|
|
|
2569
2569
|
function permissionContentKey(gate) {
|
|
2570
2570
|
return `${gate.prompt ?? ""}::${gate.detail ?? ""}::${gate.options.map((o) => `${o.index}.${o.label}`).join(",")}::${gate.cursor ?? ""}`;
|
|
2571
2571
|
}
|
|
2572
|
+
function permissionGateKey(gate) {
|
|
2573
|
+
return permissionContentKey({ ...gate, cursor: void 0 });
|
|
2574
|
+
}
|
|
2572
2575
|
var OSC_777_PERMISSION_RE = /\x1b\]777;notify;Claude Code;[^\x07\x1b]*needs your permission/;
|
|
2573
2576
|
var OSC_777_WAITING_RE = /\x1b\]777;notify;Claude Code;[^\x07\x1b]*waiting for your input/;
|
|
2574
2577
|
function hasPermissionOsc(rawData) {
|
|
@@ -6340,6 +6343,10 @@ var createSessionRoutes = (deps) => {
|
|
|
6340
6343
|
await deps.handleSendAnswer(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
6341
6344
|
return alreadyHandled6();
|
|
6342
6345
|
});
|
|
6346
|
+
app.post("/:id/permission/answer", async (c) => {
|
|
6347
|
+
await deps.handlePermissionAnswer(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
6348
|
+
return alreadyHandled6();
|
|
6349
|
+
});
|
|
6343
6350
|
app.post("/:id/files", async (c) => {
|
|
6344
6351
|
await deps.handleUploadFile(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
6345
6352
|
return alreadyHandled6();
|
|
@@ -10506,9 +10513,98 @@ var SessionHandlers = class {
|
|
|
10506
10513
|
...gate.prompt ? { prompt: gate.prompt } : {},
|
|
10507
10514
|
...gate.detail ? { detail: gate.detail } : {},
|
|
10508
10515
|
options: gate.options,
|
|
10509
|
-
...gate.cursor !== void 0 ? { cursor: gate.cursor } : {}
|
|
10516
|
+
...gate.cursor !== void 0 ? { cursor: gate.cursor } : {},
|
|
10517
|
+
contentKey: permissionGateKey(gate)
|
|
10510
10518
|
});
|
|
10511
10519
|
}
|
|
10520
|
+
/**
|
|
10521
|
+
* Answer a permission gate — the validated counterpart of POST /:id/input.
|
|
10522
|
+
*
|
|
10523
|
+
* `/input` is a raw-bytes conduit (arrow-key nav uses it too) and stays that
|
|
10524
|
+
* way; this route is the semantic one, mirroring the /answer split. Two
|
|
10525
|
+
* things make it more than validation theatre:
|
|
10526
|
+
*
|
|
10527
|
+
* - The client sends `{ contentKey, optionIndex }` and NO keystrokes. The
|
|
10528
|
+
* keys are derived here from our own copy of the gate, so the client's
|
|
10529
|
+
* key-derivation can never drift from the server's.
|
|
10530
|
+
* - `contentKey` binds the answer to a specific gate. `isPermissionAnswer`
|
|
10531
|
+
* matches structurally, and approval gates repeat constantly ("2. Yes /
|
|
10532
|
+
* 3. No" for every tool call), so without this a delayed answer to gate A
|
|
10533
|
+
* could be written as gate B's answer — a user approving a bash command
|
|
10534
|
+
* they never saw, with a 200 and a normal permission_cancelled. Treat the
|
|
10535
|
+
* check as a security boundary.
|
|
10536
|
+
*
|
|
10537
|
+
* Every refusal happens BEFORE sendKeys. On success we deliberately broadcast
|
|
10538
|
+
* nothing: the PTY-side close (isPermissionAnswer in pty-manager) recognises
|
|
10539
|
+
* the bytes we just wrote and fires permission_cancelled itself.
|
|
10540
|
+
*/
|
|
10541
|
+
async handlePermissionAnswer(sessionId, req, res) {
|
|
10542
|
+
const body = await readBody2(req);
|
|
10543
|
+
const contentKey = body?.contentKey;
|
|
10544
|
+
const optionIndex = body?.optionIndex;
|
|
10545
|
+
if (typeof contentKey !== "string" || !Number.isInteger(optionIndex) || optionIndex < 0) {
|
|
10546
|
+
json(res, 400, { ok: false, reason: "Expected { contentKey: string, optionIndex: number }" });
|
|
10547
|
+
return;
|
|
10548
|
+
}
|
|
10549
|
+
const gateClosed = () => {
|
|
10550
|
+
this.pendingPermission.delete(sessionId);
|
|
10551
|
+
this.pendingPermissionKey.delete(sessionId);
|
|
10552
|
+
this.wsHub.broadcast({ type: "permission_cancelled", sessionId });
|
|
10553
|
+
json(res, 409, { ok: false, reason: "gate_closed" });
|
|
10554
|
+
};
|
|
10555
|
+
const gate = this.pendingPermission.get(sessionId);
|
|
10556
|
+
if (!gate) {
|
|
10557
|
+
gateClosed();
|
|
10558
|
+
return;
|
|
10559
|
+
}
|
|
10560
|
+
if (permissionGateKey(gate) !== contentKey) {
|
|
10561
|
+
json(res, 409, { ok: false, reason: "gate_mismatch" });
|
|
10562
|
+
return;
|
|
10563
|
+
}
|
|
10564
|
+
const option = gate.options[optionIndex];
|
|
10565
|
+
if (!option) {
|
|
10566
|
+
json(res, 409, { ok: false, reason: "unknown_option" });
|
|
10567
|
+
return;
|
|
10568
|
+
}
|
|
10569
|
+
if (!await this.permissionGateStillOpen(sessionId, contentKey)) {
|
|
10570
|
+
gateClosed();
|
|
10571
|
+
return;
|
|
10572
|
+
}
|
|
10573
|
+
try {
|
|
10574
|
+
this.ptyManager.sendKeys(sessionId, option.answerKeys ?? permissionAnswerKeys(option.index));
|
|
10575
|
+
} catch (err) {
|
|
10576
|
+
const message = err instanceof Error ? err.message : "Failed to send answer";
|
|
10577
|
+
json(res, 400, { ok: false, reason: message });
|
|
10578
|
+
return;
|
|
10579
|
+
}
|
|
10580
|
+
json(res, 200, { ok: true });
|
|
10581
|
+
}
|
|
10582
|
+
/**
|
|
10583
|
+
* Is THIS gate still the one on screen?
|
|
10584
|
+
*
|
|
10585
|
+
* Deliberately stricter than questionMenuStillOpen's "is a menu up": the
|
|
10586
|
+
* staleness window this exists to cover (the ~300ms scrape throttle plus the
|
|
10587
|
+
* wait for the next PTY chunk) is exactly where pendingPermission still says
|
|
10588
|
+
* gate A while the screen has moved to gate B — and since approval gates
|
|
10589
|
+
* repeat their shape, "some gate is open" would wave that through.
|
|
10590
|
+
*
|
|
10591
|
+
* Reads 60 lines because that is the window the detector that produced the
|
|
10592
|
+
* pending gate uses (pty-manager's scrape); `detail` walks up to 6 lines
|
|
10593
|
+
* above the prompt, so a shorter window can truncate it and manufacture a
|
|
10594
|
+
* mismatch on a healthy gate.
|
|
10595
|
+
*
|
|
10596
|
+
* Best-effort, like questionMenuStillOpen: a session we hold no PTY for, or
|
|
10597
|
+
* one that raced away mid-read, is not ours to veto.
|
|
10598
|
+
*/
|
|
10599
|
+
async permissionGateStillOpen(sessionId, contentKey) {
|
|
10600
|
+
if (!this.ptyManager.hasSession(sessionId)) return true;
|
|
10601
|
+
try {
|
|
10602
|
+
const onScreen = scrapePermissionGate(await this.ptyManager.getOutputLines(sessionId, 60));
|
|
10603
|
+
return onScreen !== null && permissionGateKey(onScreen) === contentKey;
|
|
10604
|
+
} catch {
|
|
10605
|
+
return true;
|
|
10606
|
+
}
|
|
10607
|
+
}
|
|
10512
10608
|
async handleSendAnswer(sessionId, req, res) {
|
|
10513
10609
|
const body = await readBody2(req);
|
|
10514
10610
|
const pending = this.pendingQuestions.get(sessionId);
|
|
@@ -12955,6 +13051,7 @@ function createApiDeps(deps) {
|
|
|
12955
13051
|
handleGetOutput: (id, res) => deps.sessionHandlers.handleGetOutput(id, res),
|
|
12956
13052
|
handleSendInput: (id, req, res) => deps.sessionHandlers.handleSendInput(id, req, res),
|
|
12957
13053
|
handleSendAnswer: (id, req, res) => deps.sessionHandlers.handleSendAnswer(id, req, res),
|
|
13054
|
+
handlePermissionAnswer: (id, req, res) => deps.sessionHandlers.handlePermissionAnswer(id, req, res),
|
|
12958
13055
|
handleCancel: (id, res) => deps.sessionHandlers.handleCancel(id, res),
|
|
12959
13056
|
handleStopSession: (id, res) => deps.sessionHandlers.handleStopSession(id, res),
|
|
12960
13057
|
handleSetSessionName: (id, req, res) => deps.sessionHandlers.handleSetSessionName(id, req, res),
|
|
@@ -13038,7 +13135,8 @@ function createApiDeps(deps) {
|
|
|
13038
13135
|
...pendingGate.prompt ? { prompt: pendingGate.prompt } : {},
|
|
13039
13136
|
...pendingGate.detail ? { detail: pendingGate.detail } : {},
|
|
13040
13137
|
options: pendingGate.options,
|
|
13041
|
-
...pendingGate.cursor !== void 0 ? { cursor: pendingGate.cursor } : {}
|
|
13138
|
+
...pendingGate.cursor !== void 0 ? { cursor: pendingGate.cursor } : {},
|
|
13139
|
+
contentKey: permissionGateKey(pendingGate)
|
|
13042
13140
|
})
|
|
13043
13141
|
);
|
|
13044
13142
|
}
|