@threadbase-sh/streamer 1.44.4 → 1.46.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/cli.cjs +136 -16
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +134 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +134 -14
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -2173,14 +2173,19 @@ function scrapePermissionGate(lines) {
|
|
|
2173
2173
|
const options = [];
|
|
2174
2174
|
let cursor;
|
|
2175
2175
|
let firstOptionLine = -1;
|
|
2176
|
-
for (let i =
|
|
2177
|
-
const
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2176
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
2177
|
+
const stripped = stripGutter(lines[i]);
|
|
2178
|
+
const m = OPTION_RE.exec(stripped);
|
|
2179
|
+
if (m) {
|
|
2180
|
+
const index = Number.parseInt(m[2], 10);
|
|
2181
|
+
if (!Number.isFinite(index)) continue;
|
|
2182
|
+
firstOptionLine = i;
|
|
2183
|
+
if (m[1]) cursor = index;
|
|
2184
|
+
options.unshift({ index, label: m[3] });
|
|
2185
|
+
continue;
|
|
2186
|
+
}
|
|
2187
|
+
if (options.length === 0) continue;
|
|
2188
|
+
if (stripped.trim().length === 0 || BOX_ONLY_RE.test(lines[i].trim())) break;
|
|
2184
2189
|
}
|
|
2185
2190
|
if (options.length === 0) return null;
|
|
2186
2191
|
let prompt;
|
|
@@ -2220,6 +2225,17 @@ function scrapeDetail(lines, promptLine) {
|
|
|
2220
2225
|
}
|
|
2221
2226
|
return collected.length > 0 ? collected.reverse().join("\n") : void 0;
|
|
2222
2227
|
}
|
|
2228
|
+
var GATE_FOOTER_RE = /esc to cancel/i;
|
|
2229
|
+
var ASK_MENU_FOOTER_RE = /Enter to select/i;
|
|
2230
|
+
var YES_NO_LABEL_RE = /^(yes|no)\b/i;
|
|
2231
|
+
function detectGateScreen(lines) {
|
|
2232
|
+
if (lines.some((l) => ASK_MENU_FOOTER_RE.test(l))) return null;
|
|
2233
|
+
if (!lines.some((l) => GATE_FOOTER_RE.test(l))) return null;
|
|
2234
|
+
const gate = scrapePermissionGate(lines);
|
|
2235
|
+
if (!gate || gate.options.length < 2) return null;
|
|
2236
|
+
if (!gate.options.some((o) => YES_NO_LABEL_RE.test(o.label))) return null;
|
|
2237
|
+
return gate;
|
|
2238
|
+
}
|
|
2223
2239
|
|
|
2224
2240
|
// src/services/questions/detectQuestionFromScreen.ts
|
|
2225
2241
|
var ASK_FOOTER_RE = /Enter to select/i;
|
|
@@ -2367,6 +2383,8 @@ var PTY_ROWS2 = 40;
|
|
|
2367
2383
|
var SCREEN_SCROLLBACK2 = 1e3;
|
|
2368
2384
|
var CLAUDE_PROMPT_MARKERS = ["\u256D", "\u276F"];
|
|
2369
2385
|
var QUIET_DETECT_MS2 = 500;
|
|
2386
|
+
var OSC_TAIL_CHARS = 128;
|
|
2387
|
+
var SCRAPE_THROTTLE_MS = 300;
|
|
2370
2388
|
var CLAUDE_READY_FALLBACK_MS = 8e3;
|
|
2371
2389
|
function buildPasteBytes(input) {
|
|
2372
2390
|
return `\x1B[200~${input}\x1B[201~`;
|
|
@@ -2425,6 +2443,12 @@ var PTYManager = class {
|
|
|
2425
2443
|
// the next prompt-ready without a fresh 777 (gate closed). Prevents
|
|
2426
2444
|
// re-broadcasting open/close on every chunk.
|
|
2427
2445
|
permissionOpen = /* @__PURE__ */ new Set();
|
|
2446
|
+
// Last chunk's raw tail per session — prepended to the next chunk before
|
|
2447
|
+
// the OSC regex test so a split escape still matches. Consumed on match.
|
|
2448
|
+
oscTail = /* @__PURE__ */ new Map();
|
|
2449
|
+
// When the last full detection pass ran per session (any trigger) — the
|
|
2450
|
+
// clock the SCRAPE_THROTTLE_MS ceiling is measured against.
|
|
2451
|
+
lastDetectAt = /* @__PURE__ */ new Map();
|
|
2428
2452
|
// Content key of the last AskUserQuestion broadcast from the rendered screen,
|
|
2429
2453
|
// per session — de-dupes the same menu firing on consecutive repaints.
|
|
2430
2454
|
lastScreenQuestionKey = /* @__PURE__ */ new Map();
|
|
@@ -2433,6 +2457,13 @@ var PTYManager = class {
|
|
|
2433
2457
|
// on a prompt-ready/marker return and de-dupe consecutive repaints. Modelled
|
|
2434
2458
|
// on permissionOpen but keyed by content (a shell prompt has no OSC trigger).
|
|
2435
2459
|
shellPromptOpen = /* @__PURE__ */ new Map();
|
|
2460
|
+
// Content key (permissionContentKey, cursor excluded — see arm 3) of the
|
|
2461
|
+
// gate that was still painted on screen when arm 2 closed it. Suppresses
|
|
2462
|
+
// arm 3 re-claiming that same box from a later trigger-less/throttled pass
|
|
2463
|
+
// before Claude erases it — otherwise the paint-time claim reopens the
|
|
2464
|
+
// card it just closed. Cleared once detectGateScreen sees the box is gone,
|
|
2465
|
+
// so a genuinely new (even content-identical) gate is claimed normally.
|
|
2466
|
+
closedGateKey = /* @__PURE__ */ new Map();
|
|
2436
2467
|
// Tracks sessions (both fresh and resume) whose PTY has spawned but Claude
|
|
2437
2468
|
// hasn't yet reached an interactive prompt — i.e. onReady hasn't fired.
|
|
2438
2469
|
pendingReady = /* @__PURE__ */ new Set();
|
|
@@ -2786,8 +2817,11 @@ var PTYManager = class {
|
|
|
2786
2817
|
this.queuedInputs.delete(sessionId);
|
|
2787
2818
|
this.firstChunkAt.delete(sessionId);
|
|
2788
2819
|
this.permissionOpen.delete(sessionId);
|
|
2820
|
+
this.oscTail.delete(sessionId);
|
|
2821
|
+
this.lastDetectAt.delete(sessionId);
|
|
2789
2822
|
this.lastScreenQuestionKey.delete(sessionId);
|
|
2790
2823
|
this.shellPromptOpen.delete(sessionId);
|
|
2824
|
+
this.closedGateKey.delete(sessionId);
|
|
2791
2825
|
this.quietCheckers.get(sessionId)?.cancel();
|
|
2792
2826
|
this.quietCheckers.delete(sessionId);
|
|
2793
2827
|
this.clearReadyFallback(sessionId);
|
|
@@ -2882,8 +2916,11 @@ var PTYManager = class {
|
|
|
2882
2916
|
for (const timer of this.readyFallbackTimers.values()) clearTimeout(timer);
|
|
2883
2917
|
this.readyFallbackTimers.clear();
|
|
2884
2918
|
this.permissionOpen.clear();
|
|
2919
|
+
this.oscTail.clear();
|
|
2920
|
+
this.lastDetectAt.clear();
|
|
2885
2921
|
this.lastScreenQuestionKey.clear();
|
|
2886
2922
|
this.shellPromptOpen.clear();
|
|
2923
|
+
this.closedGateKey.clear();
|
|
2887
2924
|
}
|
|
2888
2925
|
handleOutput(sessionId, data) {
|
|
2889
2926
|
const session = this.sessions.get(sessionId);
|
|
@@ -2947,16 +2984,24 @@ var PTYManager = class {
|
|
|
2947
2984
|
async detectLivePrompts(sessionId, rawData, stripped) {
|
|
2948
2985
|
const session = this.sessions.get(sessionId);
|
|
2949
2986
|
if (!session) return;
|
|
2950
|
-
const
|
|
2951
|
-
const
|
|
2987
|
+
const oscWindow = (this.oscTail.get(sessionId) ?? "") + rawData;
|
|
2988
|
+
const oscPermission = hasPermissionOsc(oscWindow);
|
|
2989
|
+
const oscWaitingForInput = hasWaitingForInputOsc(oscWindow);
|
|
2990
|
+
if (rawData !== "") {
|
|
2991
|
+
if (oscPermission || oscWaitingForInput) this.oscTail.delete(sessionId);
|
|
2992
|
+
else this.oscTail.set(sessionId, oscWindow.slice(-OSC_TAIL_CHARS));
|
|
2993
|
+
}
|
|
2952
2994
|
const hasAskFooter = /Enter to select/i.test(stripped);
|
|
2953
2995
|
const hasPromptMarker = CLAUDE_PROMPT_MARKERS.some((m) => stripped.includes(m));
|
|
2954
2996
|
const hasShellPromptHint = /[[(]\s*y\s*\/\s*n\s*[\])]|press\s+(enter|return|any key)|\bcontinue\b\s*\?|^\s*(?:❯|>)?\s*\d+[.)]\s+\S/im.test(
|
|
2955
2997
|
stripped
|
|
2956
2998
|
);
|
|
2957
|
-
|
|
2999
|
+
const nowMs = Date.now();
|
|
3000
|
+
const scrapeDue = nowMs - (this.lastDetectAt.get(sessionId) ?? 0) >= SCRAPE_THROTTLE_MS;
|
|
3001
|
+
if (!oscPermission && !oscWaitingForInput && !hasAskFooter && !hasShellPromptHint && !scrapeDue && !this.permissionOpen.has(sessionId) && !this.shellPromptOpen.has(sessionId) && !this.lastScreenQuestionKey.has(sessionId)) {
|
|
2958
3002
|
return;
|
|
2959
3003
|
}
|
|
3004
|
+
this.lastDetectAt.set(sessionId, nowMs);
|
|
2960
3005
|
const lines = await this.getOutputLines(sessionId, 60);
|
|
2961
3006
|
const askFooterOnScreen = lines.some((l) => /Enter to select/i.test(l));
|
|
2962
3007
|
if (oscPermission || hasAskFooter || askFooterOnScreen) {
|
|
@@ -2976,13 +3021,30 @@ var PTYManager = class {
|
|
|
2976
3021
|
this.permissionOpen.add(sessionId);
|
|
2977
3022
|
this.onPermissionChange?.(sessionId, gate ?? { options: [] });
|
|
2978
3023
|
} else if (this.permissionOpen.has(sessionId) && !askFooterOnScreen) {
|
|
2979
|
-
const gate =
|
|
2980
|
-
|
|
3024
|
+
const gate = detectGateScreen(lines);
|
|
3025
|
+
const stillPainted = gate !== null || scrapePermissionGate(lines) !== null;
|
|
3026
|
+
if (oscWaitingForInput || !stillPainted && hasPromptMarker) {
|
|
2981
3027
|
this.permissionOpen.delete(sessionId);
|
|
2982
3028
|
this.onPermissionChange?.(sessionId, null);
|
|
3029
|
+
if (gate) {
|
|
3030
|
+
this.closedGateKey.set(sessionId, permissionContentKey({ ...gate, cursor: void 0 }));
|
|
3031
|
+
} else {
|
|
3032
|
+
this.closedGateKey.delete(sessionId);
|
|
3033
|
+
}
|
|
2983
3034
|
} else if (gate) {
|
|
2984
3035
|
this.onPermissionChange?.(sessionId, gate);
|
|
2985
3036
|
}
|
|
3037
|
+
} else if (!askFooterOnScreen && !oscWaitingForInput) {
|
|
3038
|
+
const gate = detectGateScreen(lines);
|
|
3039
|
+
if (gate) {
|
|
3040
|
+
const key = permissionContentKey({ ...gate, cursor: void 0 });
|
|
3041
|
+
if (this.closedGateKey.get(sessionId) !== key) {
|
|
3042
|
+
this.permissionOpen.add(sessionId);
|
|
3043
|
+
this.onPermissionChange?.(sessionId, gate);
|
|
3044
|
+
}
|
|
3045
|
+
} else {
|
|
3046
|
+
this.closedGateKey.delete(sessionId);
|
|
3047
|
+
}
|
|
2986
3048
|
}
|
|
2987
3049
|
if (askFooterOnScreen) {
|
|
2988
3050
|
const detected = detectQuestionFromScreen(lines);
|
|
@@ -3125,8 +3187,11 @@ var PTYManager = class {
|
|
|
3125
3187
|
this.queuedInputs.delete(sessionId);
|
|
3126
3188
|
this.firstChunkAt.delete(sessionId);
|
|
3127
3189
|
this.permissionOpen.delete(sessionId);
|
|
3190
|
+
this.oscTail.delete(sessionId);
|
|
3191
|
+
this.lastDetectAt.delete(sessionId);
|
|
3128
3192
|
this.lastScreenQuestionKey.delete(sessionId);
|
|
3129
3193
|
this.shellPromptOpen.delete(sessionId);
|
|
3194
|
+
this.closedGateKey.delete(sessionId);
|
|
3130
3195
|
this.quietCheckers.get(sessionId)?.cancel();
|
|
3131
3196
|
this.quietCheckers.delete(sessionId);
|
|
3132
3197
|
this.clearReadyFallback(sessionId);
|
|
@@ -5280,7 +5345,10 @@ var createMiscRoutes = (deps) => {
|
|
|
5280
5345
|
// Same contract: this server serves GET /api/config/feature-flags. Lives
|
|
5281
5346
|
// here rather than behind /api/config (admin-only) so a read-only client
|
|
5282
5347
|
// still learns the server supports flags even if it can't read values.
|
|
5283
|
-
featureFlags: true
|
|
5348
|
+
featureFlags: true,
|
|
5349
|
+
// Same contract: this server serves GET /api/projects/summary, which the
|
|
5350
|
+
// Hub's grouped views need before they can draw a tree.
|
|
5351
|
+
projectSummary: true
|
|
5284
5352
|
});
|
|
5285
5353
|
});
|
|
5286
5354
|
app.get("/api/profiles", (c) => c.json([]));
|
|
@@ -5427,6 +5495,11 @@ var createProjectRoutes = (deps) => {
|
|
|
5427
5495
|
deps.handleGetPopularProjects(url, c.env.outgoing);
|
|
5428
5496
|
return alreadyHandled4();
|
|
5429
5497
|
});
|
|
5498
|
+
app.get("/summary", (c) => {
|
|
5499
|
+
const url = new URL(c.req.url);
|
|
5500
|
+
deps.handleGetProjectSummaries(url, c.env.outgoing);
|
|
5501
|
+
return alreadyHandled4();
|
|
5502
|
+
});
|
|
5430
5503
|
return app;
|
|
5431
5504
|
};
|
|
5432
5505
|
|
|
@@ -6292,6 +6365,21 @@ var ConversationCache = class _ConversationCache {
|
|
|
6292
6365
|
ORDER BY cnt DESC
|
|
6293
6366
|
LIMIT ?`
|
|
6294
6367
|
),
|
|
6368
|
+
// Same table, same rows and same NULL filter /api/conversations lists
|
|
6369
|
+
// from, so a group's count/last-activity can never disagree with the
|
|
6370
|
+
// page it opens. Bare project_name is the one from the MAX(last_activity)
|
|
6371
|
+
// row (SQLite's documented min/max-aggregate bare-column rule).
|
|
6372
|
+
projectSummaries: db.prepare(
|
|
6373
|
+
`SELECT project_path, project_name, COUNT(*) as cnt, MAX(last_activity) as latest
|
|
6374
|
+
FROM conversation_meta
|
|
6375
|
+
WHERE project_path IS NOT NULL
|
|
6376
|
+
GROUP BY project_path
|
|
6377
|
+
ORDER BY latest DESC, project_path ASC
|
|
6378
|
+
LIMIT ? OFFSET ?`
|
|
6379
|
+
),
|
|
6380
|
+
projectSummaryCount: db.prepare(
|
|
6381
|
+
"SELECT COUNT(DISTINCT project_path) as n FROM conversation_meta WHERE project_path IS NOT NULL"
|
|
6382
|
+
),
|
|
6295
6383
|
getFileState: db.prepare("SELECT * FROM conversation_file_state WHERE path = ?"),
|
|
6296
6384
|
upsertFileState: db.prepare(
|
|
6297
6385
|
`INSERT INTO conversation_file_state
|
|
@@ -6667,6 +6755,23 @@ var ConversationCache = class _ConversationCache {
|
|
|
6667
6755
|
sessionCount: r.cnt
|
|
6668
6756
|
}));
|
|
6669
6757
|
}
|
|
6758
|
+
/** Every project with at least one cached conversation, most recently active
|
|
6759
|
+
* first. Paths are the raw `project_path` values, which is what
|
|
6760
|
+
* /api/conversations?project= matches on exactly — so a summary row is
|
|
6761
|
+
* always joinable against the page it describes. */
|
|
6762
|
+
listProjectSummaries(opts) {
|
|
6763
|
+
const total = this.stmts.projectSummaryCount.get().n;
|
|
6764
|
+
const rows = opts.limit === 0 ? [] : this.stmts.projectSummaries.all(opts.limit, opts.offset);
|
|
6765
|
+
return {
|
|
6766
|
+
total,
|
|
6767
|
+
projects: rows.map((r) => ({
|
|
6768
|
+
path: r.project_path,
|
|
6769
|
+
name: r.project_name ?? r.project_path.split(/[/\\]/).pop() ?? r.project_path,
|
|
6770
|
+
conversationCount: r.cnt,
|
|
6771
|
+
lastActivity: new Date(r.latest ?? 0).toISOString()
|
|
6772
|
+
}))
|
|
6773
|
+
};
|
|
6774
|
+
}
|
|
6670
6775
|
ensureFileIndex() {
|
|
6671
6776
|
if (this.fileIndexLoaded) return;
|
|
6672
6777
|
const rows = this.stmts.allFilePaths.all();
|
|
@@ -10808,6 +10913,7 @@ var StreamerServer = class {
|
|
|
10808
10913
|
handleSearchTarget: (id, req, res) => this.handleSearchTarget(id, req, res),
|
|
10809
10914
|
handleListProjects: (url, res) => handleListProjects(url, res),
|
|
10810
10915
|
handleGetPopularProjects: (url, res) => this.handleGetPopularProjects(url, res),
|
|
10916
|
+
handleGetProjectSummaries: (url, res) => this.handleGetProjectSummaries(url, res),
|
|
10811
10917
|
handlePairStart: (res) => this.handlePairStart(res),
|
|
10812
10918
|
handlePairExchange: (req, res) => this.handlePairExchange(req, res),
|
|
10813
10919
|
handleBrowse: (url, res) => this.handleBrowse(url, res),
|
|
@@ -12373,6 +12479,20 @@ var StreamerServer = class {
|
|
|
12373
12479
|
const projects = this.cache.getPopularProjects(limit);
|
|
12374
12480
|
json(res, 200, { projects, total: projects.length });
|
|
12375
12481
|
}
|
|
12482
|
+
handleGetProjectSummaries(url, res) {
|
|
12483
|
+
if (this.rejectIfWarmingUp(res)) return;
|
|
12484
|
+
const limit = intParam(url, "limit", 200);
|
|
12485
|
+
const offset = intParam(url, "offset", 0);
|
|
12486
|
+
if (!this.cache) {
|
|
12487
|
+
json(res, 503, {
|
|
12488
|
+
error: "Conversation cache unavailable",
|
|
12489
|
+
code: "CACHE_UNAVAILABLE"
|
|
12490
|
+
});
|
|
12491
|
+
return;
|
|
12492
|
+
}
|
|
12493
|
+
const { projects, total } = this.cache.listProjectSummaries({ limit, offset });
|
|
12494
|
+
json(res, 200, { projects, total, offset, hasMore: offset + projects.length < total });
|
|
12495
|
+
}
|
|
12376
12496
|
buildStatCache(previousScanner) {
|
|
12377
12497
|
if (!this.cache) return void 0;
|
|
12378
12498
|
if (!previousScanner) {
|