@threadbase-sh/streamer 1.67.4 → 1.68.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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.cjs
CHANGED
|
@@ -2622,6 +2622,9 @@ var import_path6 = require("path");
|
|
|
2622
2622
|
function permissionContentKey(gate) {
|
|
2623
2623
|
return `${gate.prompt ?? ""}::${gate.detail ?? ""}::${gate.options.map((o) => `${o.index}.${o.label}`).join(",")}::${gate.cursor ?? ""}`;
|
|
2624
2624
|
}
|
|
2625
|
+
function permissionGateKey(gate) {
|
|
2626
|
+
return permissionContentKey({ ...gate, cursor: void 0 });
|
|
2627
|
+
}
|
|
2625
2628
|
var OSC_777_PERMISSION_RE = /\x1b\]777;notify;Claude Code;[^\x07\x1b]*needs your permission/;
|
|
2626
2629
|
var OSC_777_WAITING_RE = /\x1b\]777;notify;Claude Code;[^\x07\x1b]*waiting for your input/;
|
|
2627
2630
|
function hasPermissionOsc(rawData) {
|
|
@@ -6388,6 +6391,10 @@ var createSessionRoutes = (deps) => {
|
|
|
6388
6391
|
await deps.handleSendAnswer(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
6389
6392
|
return alreadyHandled6();
|
|
6390
6393
|
});
|
|
6394
|
+
app.post("/:id/permission/answer", async (c) => {
|
|
6395
|
+
await deps.handlePermissionAnswer(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
6396
|
+
return alreadyHandled6();
|
|
6397
|
+
});
|
|
6391
6398
|
app.post("/:id/files", async (c) => {
|
|
6392
6399
|
await deps.handleUploadFile(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
6393
6400
|
return alreadyHandled6();
|
|
@@ -10546,9 +10553,98 @@ var SessionHandlers = class {
|
|
|
10546
10553
|
...gate.prompt ? { prompt: gate.prompt } : {},
|
|
10547
10554
|
...gate.detail ? { detail: gate.detail } : {},
|
|
10548
10555
|
options: gate.options,
|
|
10549
|
-
...gate.cursor !== void 0 ? { cursor: gate.cursor } : {}
|
|
10556
|
+
...gate.cursor !== void 0 ? { cursor: gate.cursor } : {},
|
|
10557
|
+
contentKey: permissionGateKey(gate)
|
|
10550
10558
|
});
|
|
10551
10559
|
}
|
|
10560
|
+
/**
|
|
10561
|
+
* Answer a permission gate — the validated counterpart of POST /:id/input.
|
|
10562
|
+
*
|
|
10563
|
+
* `/input` is a raw-bytes conduit (arrow-key nav uses it too) and stays that
|
|
10564
|
+
* way; this route is the semantic one, mirroring the /answer split. Two
|
|
10565
|
+
* things make it more than validation theatre:
|
|
10566
|
+
*
|
|
10567
|
+
* - The client sends `{ contentKey, optionIndex }` and NO keystrokes. The
|
|
10568
|
+
* keys are derived here from our own copy of the gate, so the client's
|
|
10569
|
+
* key-derivation can never drift from the server's.
|
|
10570
|
+
* - `contentKey` binds the answer to a specific gate. `isPermissionAnswer`
|
|
10571
|
+
* matches structurally, and approval gates repeat constantly ("2. Yes /
|
|
10572
|
+
* 3. No" for every tool call), so without this a delayed answer to gate A
|
|
10573
|
+
* could be written as gate B's answer — a user approving a bash command
|
|
10574
|
+
* they never saw, with a 200 and a normal permission_cancelled. Treat the
|
|
10575
|
+
* check as a security boundary.
|
|
10576
|
+
*
|
|
10577
|
+
* Every refusal happens BEFORE sendKeys. On success we deliberately broadcast
|
|
10578
|
+
* nothing: the PTY-side close (isPermissionAnswer in pty-manager) recognises
|
|
10579
|
+
* the bytes we just wrote and fires permission_cancelled itself.
|
|
10580
|
+
*/
|
|
10581
|
+
async handlePermissionAnswer(sessionId, req, res) {
|
|
10582
|
+
const body = await readBody2(req);
|
|
10583
|
+
const contentKey = body?.contentKey;
|
|
10584
|
+
const optionIndex = body?.optionIndex;
|
|
10585
|
+
if (typeof contentKey !== "string" || !Number.isInteger(optionIndex) || optionIndex < 0) {
|
|
10586
|
+
json(res, 400, { ok: false, reason: "Expected { contentKey: string, optionIndex: number }" });
|
|
10587
|
+
return;
|
|
10588
|
+
}
|
|
10589
|
+
const gateClosed = () => {
|
|
10590
|
+
this.pendingPermission.delete(sessionId);
|
|
10591
|
+
this.pendingPermissionKey.delete(sessionId);
|
|
10592
|
+
this.wsHub.broadcast({ type: "permission_cancelled", sessionId });
|
|
10593
|
+
json(res, 409, { ok: false, reason: "gate_closed" });
|
|
10594
|
+
};
|
|
10595
|
+
const gate = this.pendingPermission.get(sessionId);
|
|
10596
|
+
if (!gate) {
|
|
10597
|
+
gateClosed();
|
|
10598
|
+
return;
|
|
10599
|
+
}
|
|
10600
|
+
if (permissionGateKey(gate) !== contentKey) {
|
|
10601
|
+
json(res, 409, { ok: false, reason: "gate_mismatch" });
|
|
10602
|
+
return;
|
|
10603
|
+
}
|
|
10604
|
+
const option = gate.options[optionIndex];
|
|
10605
|
+
if (!option) {
|
|
10606
|
+
json(res, 409, { ok: false, reason: "unknown_option" });
|
|
10607
|
+
return;
|
|
10608
|
+
}
|
|
10609
|
+
if (!await this.permissionGateStillOpen(sessionId, contentKey)) {
|
|
10610
|
+
gateClosed();
|
|
10611
|
+
return;
|
|
10612
|
+
}
|
|
10613
|
+
try {
|
|
10614
|
+
this.ptyManager.sendKeys(sessionId, option.answerKeys ?? permissionAnswerKeys(option.index));
|
|
10615
|
+
} catch (err) {
|
|
10616
|
+
const message = err instanceof Error ? err.message : "Failed to send answer";
|
|
10617
|
+
json(res, 400, { ok: false, reason: message });
|
|
10618
|
+
return;
|
|
10619
|
+
}
|
|
10620
|
+
json(res, 200, { ok: true });
|
|
10621
|
+
}
|
|
10622
|
+
/**
|
|
10623
|
+
* Is THIS gate still the one on screen?
|
|
10624
|
+
*
|
|
10625
|
+
* Deliberately stricter than questionMenuStillOpen's "is a menu up": the
|
|
10626
|
+
* staleness window this exists to cover (the ~300ms scrape throttle plus the
|
|
10627
|
+
* wait for the next PTY chunk) is exactly where pendingPermission still says
|
|
10628
|
+
* gate A while the screen has moved to gate B — and since approval gates
|
|
10629
|
+
* repeat their shape, "some gate is open" would wave that through.
|
|
10630
|
+
*
|
|
10631
|
+
* Reads 60 lines because that is the window the detector that produced the
|
|
10632
|
+
* pending gate uses (pty-manager's scrape); `detail` walks up to 6 lines
|
|
10633
|
+
* above the prompt, so a shorter window can truncate it and manufacture a
|
|
10634
|
+
* mismatch on a healthy gate.
|
|
10635
|
+
*
|
|
10636
|
+
* Best-effort, like questionMenuStillOpen: a session we hold no PTY for, or
|
|
10637
|
+
* one that raced away mid-read, is not ours to veto.
|
|
10638
|
+
*/
|
|
10639
|
+
async permissionGateStillOpen(sessionId, contentKey) {
|
|
10640
|
+
if (!this.ptyManager.hasSession(sessionId)) return true;
|
|
10641
|
+
try {
|
|
10642
|
+
const onScreen = scrapePermissionGate(await this.ptyManager.getOutputLines(sessionId, 60));
|
|
10643
|
+
return onScreen !== null && permissionGateKey(onScreen) === contentKey;
|
|
10644
|
+
} catch {
|
|
10645
|
+
return true;
|
|
10646
|
+
}
|
|
10647
|
+
}
|
|
10552
10648
|
async handleSendAnswer(sessionId, req, res) {
|
|
10553
10649
|
const body = await readBody2(req);
|
|
10554
10650
|
const pending = this.pendingQuestions.get(sessionId);
|
|
@@ -12984,6 +13080,7 @@ function createApiDeps(deps) {
|
|
|
12984
13080
|
handleGetOutput: (id, res) => deps.sessionHandlers.handleGetOutput(id, res),
|
|
12985
13081
|
handleSendInput: (id, req, res) => deps.sessionHandlers.handleSendInput(id, req, res),
|
|
12986
13082
|
handleSendAnswer: (id, req, res) => deps.sessionHandlers.handleSendAnswer(id, req, res),
|
|
13083
|
+
handlePermissionAnswer: (id, req, res) => deps.sessionHandlers.handlePermissionAnswer(id, req, res),
|
|
12987
13084
|
handleCancel: (id, res) => deps.sessionHandlers.handleCancel(id, res),
|
|
12988
13085
|
handleStopSession: (id, res) => deps.sessionHandlers.handleStopSession(id, res),
|
|
12989
13086
|
handleSetSessionName: (id, req, res) => deps.sessionHandlers.handleSetSessionName(id, req, res),
|
|
@@ -13067,7 +13164,8 @@ function createApiDeps(deps) {
|
|
|
13067
13164
|
...pendingGate.prompt ? { prompt: pendingGate.prompt } : {},
|
|
13068
13165
|
...pendingGate.detail ? { detail: pendingGate.detail } : {},
|
|
13069
13166
|
options: pendingGate.options,
|
|
13070
|
-
...pendingGate.cursor !== void 0 ? { cursor: pendingGate.cursor } : {}
|
|
13167
|
+
...pendingGate.cursor !== void 0 ? { cursor: pendingGate.cursor } : {},
|
|
13168
|
+
contentKey: permissionGateKey(pendingGate)
|
|
13071
13169
|
})
|
|
13072
13170
|
);
|
|
13073
13171
|
}
|