@threadbase-sh/streamer 1.67.3 → 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 +145 -16
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +128 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +128 -10
- package/dist/index.js.map +1 -1
- package/dist/pty-manager.d.ts.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/services/questions/permissionAnswerKeys.d.ts +8 -0
- package/dist/services/questions/permissionAnswerKeys.d.ts.map +1 -0
- 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) {
|
|
@@ -2787,6 +2790,18 @@ function detectShellPrompt(lines) {
|
|
|
2787
2790
|
return null;
|
|
2788
2791
|
}
|
|
2789
2792
|
|
|
2793
|
+
// src/services/questions/permissionAnswerKeys.ts
|
|
2794
|
+
var ENTER2 = "\r";
|
|
2795
|
+
function permissionAnswerKeys(index) {
|
|
2796
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
2797
|
+
throw new Error(`Invalid permission option index: ${index}`);
|
|
2798
|
+
}
|
|
2799
|
+
return `${index}${ENTER2}`;
|
|
2800
|
+
}
|
|
2801
|
+
function isPermissionAnswer(gate, keys) {
|
|
2802
|
+
return gate.options.some((o) => keys === (o.answerKeys ?? permissionAnswerKeys(o.index)));
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2790
2805
|
// src/utils/deriveSessionName.ts
|
|
2791
2806
|
function deriveSessionName(firstMessageText) {
|
|
2792
2807
|
const firstLine = firstMessageText.split("\n", 1)[0]?.trim() ?? "";
|
|
@@ -2829,10 +2844,12 @@ var PTYManager = class {
|
|
|
2829
2844
|
onLiveQuestion;
|
|
2830
2845
|
onLiveQuestionGone;
|
|
2831
2846
|
onUserMessage;
|
|
2832
|
-
// Per-session permission-gate state
|
|
2833
|
-
//
|
|
2834
|
-
//
|
|
2835
|
-
|
|
2847
|
+
// Per-session permission-gate state: the gate currently open on a session's
|
|
2848
|
+
// screen. Set between an OSC 777 / paint-time claim (gate open) and the next
|
|
2849
|
+
// prompt-ready without a fresh 777 (gate closed). Prevents re-broadcasting
|
|
2850
|
+
// open/close on every chunk. Holds the gate itself rather than just the id so
|
|
2851
|
+
// sendKeys can tell whether the bytes it is writing are that gate's answer.
|
|
2852
|
+
permissionOpen = /* @__PURE__ */ new Map();
|
|
2836
2853
|
// Last chunk's raw tail per session — prepended to the next chunk before
|
|
2837
2854
|
// the OSC regex test so a split escape still matches. Consumed on match.
|
|
2838
2855
|
oscTail = /* @__PURE__ */ new Map();
|
|
@@ -3067,6 +3084,11 @@ var PTYManager = class {
|
|
|
3067
3084
|
);
|
|
3068
3085
|
session.process.write(keys);
|
|
3069
3086
|
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
3087
|
+
const openGate = this.permissionOpen.get(sessionId);
|
|
3088
|
+
if (openGate && isPermissionAnswer(openGate, keys)) {
|
|
3089
|
+
this.permissionOpen.delete(sessionId);
|
|
3090
|
+
this.onPermissionChange?.(sessionId, null);
|
|
3091
|
+
}
|
|
3070
3092
|
}
|
|
3071
3093
|
sendInput(sessionId, input) {
|
|
3072
3094
|
const session = this.sessions.get(sessionId);
|
|
@@ -3413,7 +3435,7 @@ var PTYManager = class {
|
|
|
3413
3435
|
}
|
|
3414
3436
|
if (oscPermission && !askFooterOnScreen) {
|
|
3415
3437
|
const gate = scrapePermissionGate(lines);
|
|
3416
|
-
this.permissionOpen.
|
|
3438
|
+
this.permissionOpen.set(sessionId, gate ?? { options: [] });
|
|
3417
3439
|
this.onPermissionChange?.(sessionId, gate ?? { options: [] });
|
|
3418
3440
|
} else if (this.permissionOpen.has(sessionId) && !askFooterOnScreen) {
|
|
3419
3441
|
const gate = detectGateScreen(lines);
|
|
@@ -3427,6 +3449,7 @@ var PTYManager = class {
|
|
|
3427
3449
|
this.closedGateKey.delete(sessionId);
|
|
3428
3450
|
}
|
|
3429
3451
|
} else if (gate) {
|
|
3452
|
+
this.permissionOpen.set(sessionId, gate);
|
|
3430
3453
|
this.onPermissionChange?.(sessionId, gate);
|
|
3431
3454
|
}
|
|
3432
3455
|
} else if (!askFooterOnScreen && !oscWaitingForInput) {
|
|
@@ -3434,7 +3457,7 @@ var PTYManager = class {
|
|
|
3434
3457
|
if (gate) {
|
|
3435
3458
|
const key = permissionContentKey({ ...gate, cursor: void 0 });
|
|
3436
3459
|
if (this.closedGateKey.get(sessionId) !== key) {
|
|
3437
|
-
this.permissionOpen.
|
|
3460
|
+
this.permissionOpen.set(sessionId, gate);
|
|
3438
3461
|
this.onPermissionChange?.(sessionId, gate);
|
|
3439
3462
|
}
|
|
3440
3463
|
} else {
|
|
@@ -6320,6 +6343,10 @@ var createSessionRoutes = (deps) => {
|
|
|
6320
6343
|
await deps.handleSendAnswer(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
6321
6344
|
return alreadyHandled6();
|
|
6322
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
|
+
});
|
|
6323
6350
|
app.post("/:id/files", async (c) => {
|
|
6324
6351
|
await deps.handleUploadFile(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
6325
6352
|
return alreadyHandled6();
|
|
@@ -9553,7 +9580,7 @@ function parseStatusLine(lines) {
|
|
|
9553
9580
|
|
|
9554
9581
|
// src/services/questions/answersToKeystrokes.ts
|
|
9555
9582
|
var DOWN = "\x1B[B";
|
|
9556
|
-
var
|
|
9583
|
+
var ENTER3 = "\r";
|
|
9557
9584
|
var UnknownOptionError = class extends Error {
|
|
9558
9585
|
constructor(question, value) {
|
|
9559
9586
|
super(`No option labelled "${value}" for question "${question}"`);
|
|
@@ -9574,7 +9601,7 @@ function answersToKeystrokes(questions, answers) {
|
|
|
9574
9601
|
const label = Array.isArray(raw) ? raw[0] : raw;
|
|
9575
9602
|
const target = q.options.findIndex((o) => o.label === label);
|
|
9576
9603
|
if (target < 0) throw new UnknownOptionError(q.question, label);
|
|
9577
|
-
out += DOWN.repeat(target) +
|
|
9604
|
+
out += DOWN.repeat(target) + ENTER3;
|
|
9578
9605
|
}
|
|
9579
9606
|
return out;
|
|
9580
9607
|
}
|
|
@@ -10486,9 +10513,98 @@ var SessionHandlers = class {
|
|
|
10486
10513
|
...gate.prompt ? { prompt: gate.prompt } : {},
|
|
10487
10514
|
...gate.detail ? { detail: gate.detail } : {},
|
|
10488
10515
|
options: gate.options,
|
|
10489
|
-
...gate.cursor !== void 0 ? { cursor: gate.cursor } : {}
|
|
10516
|
+
...gate.cursor !== void 0 ? { cursor: gate.cursor } : {},
|
|
10517
|
+
contentKey: permissionGateKey(gate)
|
|
10490
10518
|
});
|
|
10491
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
|
+
}
|
|
10492
10608
|
async handleSendAnswer(sessionId, req, res) {
|
|
10493
10609
|
const body = await readBody2(req);
|
|
10494
10610
|
const pending = this.pendingQuestions.get(sessionId);
|
|
@@ -12935,6 +13051,7 @@ function createApiDeps(deps) {
|
|
|
12935
13051
|
handleGetOutput: (id, res) => deps.sessionHandlers.handleGetOutput(id, res),
|
|
12936
13052
|
handleSendInput: (id, req, res) => deps.sessionHandlers.handleSendInput(id, req, res),
|
|
12937
13053
|
handleSendAnswer: (id, req, res) => deps.sessionHandlers.handleSendAnswer(id, req, res),
|
|
13054
|
+
handlePermissionAnswer: (id, req, res) => deps.sessionHandlers.handlePermissionAnswer(id, req, res),
|
|
12938
13055
|
handleCancel: (id, res) => deps.sessionHandlers.handleCancel(id, res),
|
|
12939
13056
|
handleStopSession: (id, res) => deps.sessionHandlers.handleStopSession(id, res),
|
|
12940
13057
|
handleSetSessionName: (id, req, res) => deps.sessionHandlers.handleSetSessionName(id, req, res),
|
|
@@ -13018,7 +13135,8 @@ function createApiDeps(deps) {
|
|
|
13018
13135
|
...pendingGate.prompt ? { prompt: pendingGate.prompt } : {},
|
|
13019
13136
|
...pendingGate.detail ? { detail: pendingGate.detail } : {},
|
|
13020
13137
|
options: pendingGate.options,
|
|
13021
|
-
...pendingGate.cursor !== void 0 ? { cursor: pendingGate.cursor } : {}
|
|
13138
|
+
...pendingGate.cursor !== void 0 ? { cursor: pendingGate.cursor } : {},
|
|
13139
|
+
contentKey: permissionGateKey(pendingGate)
|
|
13022
13140
|
})
|
|
13023
13141
|
);
|
|
13024
13142
|
}
|