@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.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) {
|
|
@@ -2840,6 +2843,18 @@ function detectShellPrompt(lines) {
|
|
|
2840
2843
|
return null;
|
|
2841
2844
|
}
|
|
2842
2845
|
|
|
2846
|
+
// src/services/questions/permissionAnswerKeys.ts
|
|
2847
|
+
var ENTER2 = "\r";
|
|
2848
|
+
function permissionAnswerKeys(index) {
|
|
2849
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
2850
|
+
throw new Error(`Invalid permission option index: ${index}`);
|
|
2851
|
+
}
|
|
2852
|
+
return `${index}${ENTER2}`;
|
|
2853
|
+
}
|
|
2854
|
+
function isPermissionAnswer(gate, keys) {
|
|
2855
|
+
return gate.options.some((o) => keys === (o.answerKeys ?? permissionAnswerKeys(o.index)));
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2843
2858
|
// src/utils/deriveSessionName.ts
|
|
2844
2859
|
function deriveSessionName(firstMessageText) {
|
|
2845
2860
|
const firstLine = firstMessageText.split("\n", 1)[0]?.trim() ?? "";
|
|
@@ -2882,10 +2897,12 @@ var PTYManager = class {
|
|
|
2882
2897
|
onLiveQuestion;
|
|
2883
2898
|
onLiveQuestionGone;
|
|
2884
2899
|
onUserMessage;
|
|
2885
|
-
// Per-session permission-gate state
|
|
2886
|
-
//
|
|
2887
|
-
//
|
|
2888
|
-
|
|
2900
|
+
// Per-session permission-gate state: the gate currently open on a session's
|
|
2901
|
+
// screen. Set between an OSC 777 / paint-time claim (gate open) and the next
|
|
2902
|
+
// prompt-ready without a fresh 777 (gate closed). Prevents re-broadcasting
|
|
2903
|
+
// open/close on every chunk. Holds the gate itself rather than just the id so
|
|
2904
|
+
// sendKeys can tell whether the bytes it is writing are that gate's answer.
|
|
2905
|
+
permissionOpen = /* @__PURE__ */ new Map();
|
|
2889
2906
|
// Last chunk's raw tail per session — prepended to the next chunk before
|
|
2890
2907
|
// the OSC regex test so a split escape still matches. Consumed on match.
|
|
2891
2908
|
oscTail = /* @__PURE__ */ new Map();
|
|
@@ -3120,6 +3137,11 @@ var PTYManager = class {
|
|
|
3120
3137
|
);
|
|
3121
3138
|
session.process.write(keys);
|
|
3122
3139
|
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
3140
|
+
const openGate = this.permissionOpen.get(sessionId);
|
|
3141
|
+
if (openGate && isPermissionAnswer(openGate, keys)) {
|
|
3142
|
+
this.permissionOpen.delete(sessionId);
|
|
3143
|
+
this.onPermissionChange?.(sessionId, null);
|
|
3144
|
+
}
|
|
3123
3145
|
}
|
|
3124
3146
|
sendInput(sessionId, input) {
|
|
3125
3147
|
const session = this.sessions.get(sessionId);
|
|
@@ -3466,7 +3488,7 @@ var PTYManager = class {
|
|
|
3466
3488
|
}
|
|
3467
3489
|
if (oscPermission && !askFooterOnScreen) {
|
|
3468
3490
|
const gate = scrapePermissionGate(lines);
|
|
3469
|
-
this.permissionOpen.
|
|
3491
|
+
this.permissionOpen.set(sessionId, gate ?? { options: [] });
|
|
3470
3492
|
this.onPermissionChange?.(sessionId, gate ?? { options: [] });
|
|
3471
3493
|
} else if (this.permissionOpen.has(sessionId) && !askFooterOnScreen) {
|
|
3472
3494
|
const gate = detectGateScreen(lines);
|
|
@@ -3480,6 +3502,7 @@ var PTYManager = class {
|
|
|
3480
3502
|
this.closedGateKey.delete(sessionId);
|
|
3481
3503
|
}
|
|
3482
3504
|
} else if (gate) {
|
|
3505
|
+
this.permissionOpen.set(sessionId, gate);
|
|
3483
3506
|
this.onPermissionChange?.(sessionId, gate);
|
|
3484
3507
|
}
|
|
3485
3508
|
} else if (!askFooterOnScreen && !oscWaitingForInput) {
|
|
@@ -3487,7 +3510,7 @@ var PTYManager = class {
|
|
|
3487
3510
|
if (gate) {
|
|
3488
3511
|
const key = permissionContentKey({ ...gate, cursor: void 0 });
|
|
3489
3512
|
if (this.closedGateKey.get(sessionId) !== key) {
|
|
3490
|
-
this.permissionOpen.
|
|
3513
|
+
this.permissionOpen.set(sessionId, gate);
|
|
3491
3514
|
this.onPermissionChange?.(sessionId, gate);
|
|
3492
3515
|
}
|
|
3493
3516
|
} else {
|
|
@@ -6368,6 +6391,10 @@ var createSessionRoutes = (deps) => {
|
|
|
6368
6391
|
await deps.handleSendAnswer(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
6369
6392
|
return alreadyHandled6();
|
|
6370
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
|
+
});
|
|
6371
6398
|
app.post("/:id/files", async (c) => {
|
|
6372
6399
|
await deps.handleUploadFile(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
6373
6400
|
return alreadyHandled6();
|
|
@@ -9593,7 +9620,7 @@ function parseStatusLine(lines) {
|
|
|
9593
9620
|
|
|
9594
9621
|
// src/services/questions/answersToKeystrokes.ts
|
|
9595
9622
|
var DOWN = "\x1B[B";
|
|
9596
|
-
var
|
|
9623
|
+
var ENTER3 = "\r";
|
|
9597
9624
|
var UnknownOptionError = class extends Error {
|
|
9598
9625
|
constructor(question, value) {
|
|
9599
9626
|
super(`No option labelled "${value}" for question "${question}"`);
|
|
@@ -9614,7 +9641,7 @@ function answersToKeystrokes(questions, answers) {
|
|
|
9614
9641
|
const label = Array.isArray(raw) ? raw[0] : raw;
|
|
9615
9642
|
const target = q.options.findIndex((o) => o.label === label);
|
|
9616
9643
|
if (target < 0) throw new UnknownOptionError(q.question, label);
|
|
9617
|
-
out += DOWN.repeat(target) +
|
|
9644
|
+
out += DOWN.repeat(target) + ENTER3;
|
|
9618
9645
|
}
|
|
9619
9646
|
return out;
|
|
9620
9647
|
}
|
|
@@ -10526,9 +10553,98 @@ var SessionHandlers = class {
|
|
|
10526
10553
|
...gate.prompt ? { prompt: gate.prompt } : {},
|
|
10527
10554
|
...gate.detail ? { detail: gate.detail } : {},
|
|
10528
10555
|
options: gate.options,
|
|
10529
|
-
...gate.cursor !== void 0 ? { cursor: gate.cursor } : {}
|
|
10556
|
+
...gate.cursor !== void 0 ? { cursor: gate.cursor } : {},
|
|
10557
|
+
contentKey: permissionGateKey(gate)
|
|
10530
10558
|
});
|
|
10531
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
|
+
}
|
|
10532
10648
|
async handleSendAnswer(sessionId, req, res) {
|
|
10533
10649
|
const body = await readBody2(req);
|
|
10534
10650
|
const pending = this.pendingQuestions.get(sessionId);
|
|
@@ -12964,6 +13080,7 @@ function createApiDeps(deps) {
|
|
|
12964
13080
|
handleGetOutput: (id, res) => deps.sessionHandlers.handleGetOutput(id, res),
|
|
12965
13081
|
handleSendInput: (id, req, res) => deps.sessionHandlers.handleSendInput(id, req, res),
|
|
12966
13082
|
handleSendAnswer: (id, req, res) => deps.sessionHandlers.handleSendAnswer(id, req, res),
|
|
13083
|
+
handlePermissionAnswer: (id, req, res) => deps.sessionHandlers.handlePermissionAnswer(id, req, res),
|
|
12967
13084
|
handleCancel: (id, res) => deps.sessionHandlers.handleCancel(id, res),
|
|
12968
13085
|
handleStopSession: (id, res) => deps.sessionHandlers.handleStopSession(id, res),
|
|
12969
13086
|
handleSetSessionName: (id, req, res) => deps.sessionHandlers.handleSetSessionName(id, req, res),
|
|
@@ -13047,7 +13164,8 @@ function createApiDeps(deps) {
|
|
|
13047
13164
|
...pendingGate.prompt ? { prompt: pendingGate.prompt } : {},
|
|
13048
13165
|
...pendingGate.detail ? { detail: pendingGate.detail } : {},
|
|
13049
13166
|
options: pendingGate.options,
|
|
13050
|
-
...pendingGate.cursor !== void 0 ? { cursor: pendingGate.cursor } : {}
|
|
13167
|
+
...pendingGate.cursor !== void 0 ? { cursor: pendingGate.cursor } : {},
|
|
13168
|
+
contentKey: permissionGateKey(pendingGate)
|
|
13051
13169
|
})
|
|
13052
13170
|
);
|
|
13053
13171
|
}
|