@threadbase-sh/streamer 1.24.7 → 1.26.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 +478 -145
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +300 -85
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +300 -85
- package/dist/index.js.map +1 -1
- package/dist/migrations/007_add_scanner_warmup_cache.sql +15 -0
- package/dist/seed-claude-config.cjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -1530,6 +1530,15 @@ function loadPublicUrl() {
|
|
|
1530
1530
|
}
|
|
1531
1531
|
return void 0;
|
|
1532
1532
|
}
|
|
1533
|
+
function loadBrowserCors() {
|
|
1534
|
+
try {
|
|
1535
|
+
const content = (0, import_fs.readFileSync)(configFile(), "utf-8");
|
|
1536
|
+
const match2 = content.match(/browser_cors:\s*(.+)/);
|
|
1537
|
+
if (match2?.[1]) return match2[1].trim();
|
|
1538
|
+
} catch {
|
|
1539
|
+
}
|
|
1540
|
+
return void 0;
|
|
1541
|
+
}
|
|
1533
1542
|
function loadCacheDir() {
|
|
1534
1543
|
try {
|
|
1535
1544
|
const content = (0, import_fs.readFileSync)(configFile(), "utf-8");
|
|
@@ -1548,6 +1557,42 @@ function loadTailSize() {
|
|
|
1548
1557
|
}
|
|
1549
1558
|
return void 0;
|
|
1550
1559
|
}
|
|
1560
|
+
function loadDefaultPermissionMode() {
|
|
1561
|
+
try {
|
|
1562
|
+
const content = (0, import_fs.readFileSync)(configFile(), "utf-8");
|
|
1563
|
+
const match2 = content.match(/default_permission_mode:\s*(\S+)/);
|
|
1564
|
+
const value = match2?.[1]?.trim();
|
|
1565
|
+
if (value === "acceptEdits" || value === "manual") return value;
|
|
1566
|
+
} catch {
|
|
1567
|
+
}
|
|
1568
|
+
return void 0;
|
|
1569
|
+
}
|
|
1570
|
+
function setDefaultPermissionMode(mode) {
|
|
1571
|
+
const file2 = configFile();
|
|
1572
|
+
(0, import_fs.mkdirSync)(configDir(), { recursive: true });
|
|
1573
|
+
let content = "";
|
|
1574
|
+
try {
|
|
1575
|
+
content = (0, import_fs.readFileSync)(file2, "utf-8");
|
|
1576
|
+
} catch (err) {
|
|
1577
|
+
if (err.code !== "ENOENT") throw err;
|
|
1578
|
+
}
|
|
1579
|
+
const line = `default_permission_mode: ${mode}`;
|
|
1580
|
+
let updated;
|
|
1581
|
+
if (/^default_permission_mode:\s*.+$/m.test(content)) {
|
|
1582
|
+
updated = content.replace(/^default_permission_mode:\s*.+$/m, line);
|
|
1583
|
+
} else if (content.length === 0 || content.endsWith("\n")) {
|
|
1584
|
+
updated = `${content}${line}
|
|
1585
|
+
`;
|
|
1586
|
+
} else {
|
|
1587
|
+
updated = `${content}
|
|
1588
|
+
${line}
|
|
1589
|
+
`;
|
|
1590
|
+
}
|
|
1591
|
+
const tmpFile = `${file2}.tmp`;
|
|
1592
|
+
(0, import_fs.writeFileSync)(tmpFile, updated, { encoding: "utf-8", mode: 384 });
|
|
1593
|
+
(0, import_fs.chmodSync)(tmpFile, 384);
|
|
1594
|
+
(0, import_fs.renameSync)(tmpFile, file2);
|
|
1595
|
+
}
|
|
1551
1596
|
function validatePublicUrl(raw2) {
|
|
1552
1597
|
let parsed;
|
|
1553
1598
|
try {
|
|
@@ -123699,6 +123744,68 @@ var init_check_sqlite_abi = __esm({
|
|
|
123699
123744
|
}
|
|
123700
123745
|
});
|
|
123701
123746
|
|
|
123747
|
+
// src/lifecycle/prompt.ts
|
|
123748
|
+
var prompt_exports = {};
|
|
123749
|
+
__export(prompt_exports, {
|
|
123750
|
+
interactivePermissionModePrompt: () => interactivePermissionModePrompt,
|
|
123751
|
+
interactivePrompt: () => interactivePrompt
|
|
123752
|
+
});
|
|
123753
|
+
var import_node_process2, import_promises18, interactivePrompt, interactivePermissionModePrompt;
|
|
123754
|
+
var init_prompt = __esm({
|
|
123755
|
+
"src/lifecycle/prompt.ts"() {
|
|
123756
|
+
"use strict";
|
|
123757
|
+
import_node_process2 = require("process");
|
|
123758
|
+
import_promises18 = require("readline/promises");
|
|
123759
|
+
interactivePrompt = async ({ prodPort, suggestedAltPort, prodActive }) => {
|
|
123760
|
+
const rl = (0, import_promises18.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
|
|
123761
|
+
try {
|
|
123762
|
+
if (!prodActive) {
|
|
123763
|
+
import_node_process2.stdout.write(
|
|
123764
|
+
`
|
|
123765
|
+
Port ${prodPort} is already in use by another process.
|
|
123766
|
+
Running dev on port ${suggestedAltPort} instead.
|
|
123767
|
+
`
|
|
123768
|
+
);
|
|
123769
|
+
const rememberAns2 = (await rl.question("Remember this choice for this repo? [y/N]: ")).trim().toLowerCase();
|
|
123770
|
+
return {
|
|
123771
|
+
choice: "use-port",
|
|
123772
|
+
port: suggestedAltPort,
|
|
123773
|
+
remember: rememberAns2 === "y" || rememberAns2 === "yes"
|
|
123774
|
+
};
|
|
123775
|
+
}
|
|
123776
|
+
import_node_process2.stdout.write(
|
|
123777
|
+
`
|
|
123778
|
+
The supervised prod streamer is already holding port ${prodPort}.
|
|
123779
|
+
[r] Stop prod and take port ${prodPort}
|
|
123780
|
+
[p] Run dev on port ${suggestedAltPort} instead
|
|
123781
|
+
`
|
|
123782
|
+
);
|
|
123783
|
+
const choiceAns = (await rl.question("Choice [r/p]: ")).trim().toLowerCase();
|
|
123784
|
+
const rememberAns = (await rl.question("Remember this choice for this repo? [y/N]: ")).trim().toLowerCase();
|
|
123785
|
+
const remember = rememberAns === "y" || rememberAns === "yes";
|
|
123786
|
+
if (choiceAns === "r") {
|
|
123787
|
+
return { choice: "replace-prod", remember };
|
|
123788
|
+
}
|
|
123789
|
+
return { choice: "use-port", port: suggestedAltPort, remember };
|
|
123790
|
+
} finally {
|
|
123791
|
+
rl.close();
|
|
123792
|
+
}
|
|
123793
|
+
};
|
|
123794
|
+
interactivePermissionModePrompt = async () => {
|
|
123795
|
+
const rl = (0, import_promises18.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
|
|
123796
|
+
try {
|
|
123797
|
+
import_node_process2.stdout.write(
|
|
123798
|
+
"\nWhich permission mode should spawned Claude Code sessions use?\n [a] acceptEdits \u2014 auto-approve file edits, still prompt for shell commands (default)\n [m] manual \u2014 prompt for every edit and command\n"
|
|
123799
|
+
);
|
|
123800
|
+
const ans = (await rl.question("Choice [a/m] (default a): ")).trim().toLowerCase();
|
|
123801
|
+
return ans === "m" ? "manual" : "acceptEdits";
|
|
123802
|
+
} finally {
|
|
123803
|
+
rl.close();
|
|
123804
|
+
}
|
|
123805
|
+
};
|
|
123806
|
+
}
|
|
123807
|
+
});
|
|
123808
|
+
|
|
123702
123809
|
// src/lifecycle/prefs.ts
|
|
123703
123810
|
function readPrefs() {
|
|
123704
123811
|
const path2 = prefsPath();
|
|
@@ -123781,7 +123888,7 @@ async function resolveDevPlan(opts) {
|
|
|
123781
123888
|
}
|
|
123782
123889
|
const prodActive = opts.isProdActive();
|
|
123783
123890
|
const portTaken = opts.portInUse(opts.requestedPort);
|
|
123784
|
-
if (!
|
|
123891
|
+
if (!portTaken) {
|
|
123785
123892
|
return { kind: "use-port", port: opts.requestedPort };
|
|
123786
123893
|
}
|
|
123787
123894
|
if (!opts.forget) {
|
|
@@ -123796,7 +123903,12 @@ async function resolveDevPlan(opts) {
|
|
|
123796
123903
|
}
|
|
123797
123904
|
}
|
|
123798
123905
|
const suggested = await opts.findFreePort(opts.requestedPort + 1);
|
|
123799
|
-
const answer = await opts.prompt({
|
|
123906
|
+
const answer = await opts.prompt({
|
|
123907
|
+
prodPort: opts.requestedPort,
|
|
123908
|
+
suggestedAltPort: suggested,
|
|
123909
|
+
prodActive,
|
|
123910
|
+
portTaken
|
|
123911
|
+
});
|
|
123800
123912
|
if (answer.remember && opts.repoToplevel) {
|
|
123801
123913
|
if (answer.choice === "replace-prod") {
|
|
123802
123914
|
writePrefForRepo(opts.repoToplevel, { choice: "replace-prod" });
|
|
@@ -123902,41 +124014,6 @@ var init_dev_takeover = __esm({
|
|
|
123902
124014
|
}
|
|
123903
124015
|
});
|
|
123904
124016
|
|
|
123905
|
-
// src/lifecycle/prompt.ts
|
|
123906
|
-
var prompt_exports = {};
|
|
123907
|
-
__export(prompt_exports, {
|
|
123908
|
-
interactivePrompt: () => interactivePrompt
|
|
123909
|
-
});
|
|
123910
|
-
var import_node_process2, import_promises18, interactivePrompt;
|
|
123911
|
-
var init_prompt = __esm({
|
|
123912
|
-
"src/lifecycle/prompt.ts"() {
|
|
123913
|
-
"use strict";
|
|
123914
|
-
import_node_process2 = require("process");
|
|
123915
|
-
import_promises18 = require("readline/promises");
|
|
123916
|
-
interactivePrompt = async ({ prodPort, suggestedAltPort }) => {
|
|
123917
|
-
const rl = (0, import_promises18.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
|
|
123918
|
-
try {
|
|
123919
|
-
import_node_process2.stdout.write(
|
|
123920
|
-
`
|
|
123921
|
-
Prod streamer is running on port ${prodPort}.
|
|
123922
|
-
[r] Stop prod and take port ${prodPort}
|
|
123923
|
-
[p] Run dev on port ${suggestedAltPort} instead
|
|
123924
|
-
`
|
|
123925
|
-
);
|
|
123926
|
-
const choiceAns = (await rl.question("Choice [r/p]: ")).trim().toLowerCase();
|
|
123927
|
-
const rememberAns = (await rl.question("Remember this choice for this repo? [y/N]: ")).trim().toLowerCase();
|
|
123928
|
-
const remember = rememberAns === "y" || rememberAns === "yes";
|
|
123929
|
-
if (choiceAns === "r") {
|
|
123930
|
-
return { choice: "replace-prod", remember };
|
|
123931
|
-
}
|
|
123932
|
-
return { choice: "use-port", port: suggestedAltPort, remember };
|
|
123933
|
-
} finally {
|
|
123934
|
-
rl.close();
|
|
123935
|
-
}
|
|
123936
|
-
};
|
|
123937
|
-
}
|
|
123938
|
-
});
|
|
123939
|
-
|
|
123940
124017
|
// src/lifecycle/repo.ts
|
|
123941
124018
|
var repo_exports = {};
|
|
123942
124019
|
__export(repo_exports, {
|
|
@@ -124017,6 +124094,9 @@ var init_setKey = __esm({
|
|
|
124017
124094
|
);
|
|
124018
124095
|
})();
|
|
124019
124096
|
|
|
124097
|
+
// cli/index.ts
|
|
124098
|
+
var import_node_process3 = require("process");
|
|
124099
|
+
|
|
124020
124100
|
// node_modules/commander/lib/error.js
|
|
124021
124101
|
var CommanderError = class extends Error {
|
|
124022
124102
|
/**
|
|
@@ -135731,25 +135811,55 @@ var authMiddleware = (deps) => async (c, next) => {
|
|
|
135731
135811
|
};
|
|
135732
135812
|
|
|
135733
135813
|
// src/api/middleware/cors.middleware.ts
|
|
135734
|
-
var
|
|
135814
|
+
var DEFAULT_DEV_ORIGINS = [
|
|
135735
135815
|
"http://localhost:8081",
|
|
135736
135816
|
"http://localhost:19006",
|
|
135737
135817
|
"http://localhost:3000"
|
|
135738
|
-
]
|
|
135739
|
-
|
|
135740
|
-
|
|
135741
|
-
const
|
|
135742
|
-
|
|
135743
|
-
|
|
135744
|
-
|
|
135745
|
-
|
|
135746
|
-
|
|
135747
|
-
|
|
135748
|
-
|
|
135749
|
-
|
|
135750
|
-
|
|
135751
|
-
|
|
135752
|
-
|
|
135818
|
+
];
|
|
135819
|
+
function resolveAllowedOrigins(raw2) {
|
|
135820
|
+
if (!raw2) return null;
|
|
135821
|
+
const trimmed = raw2.trim();
|
|
135822
|
+
const lower = trimmed.toLowerCase();
|
|
135823
|
+
if (lower === "0" || lower === "false" || lower === "no" || lower === "off" || trimmed === "") {
|
|
135824
|
+
return null;
|
|
135825
|
+
}
|
|
135826
|
+
const origins = new Set(DEFAULT_DEV_ORIGINS);
|
|
135827
|
+
if (!["1", "true", "yes", "on"].includes(lower)) {
|
|
135828
|
+
for (const o of trimmed.split(",")) {
|
|
135829
|
+
const origin = o.trim();
|
|
135830
|
+
if (origin) origins.add(origin);
|
|
135831
|
+
}
|
|
135832
|
+
}
|
|
135833
|
+
return origins;
|
|
135834
|
+
}
|
|
135835
|
+
var corsMiddleware = (configValue) => {
|
|
135836
|
+
const allowedOrigins = resolveAllowedOrigins(
|
|
135837
|
+
process.env.THREADBASE_ALLOW_BROWSER_CORS ?? configValue
|
|
135838
|
+
);
|
|
135839
|
+
return async (c, next) => {
|
|
135840
|
+
const origin = c.req.header("origin");
|
|
135841
|
+
const allowedOrigin = allowedOrigins && origin && allowedOrigins.has(origin) ? origin : null;
|
|
135842
|
+
if (allowedOrigin) {
|
|
135843
|
+
const raw2 = c.env.outgoing;
|
|
135844
|
+
raw2.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
135845
|
+
raw2.setHeader("Vary", "Origin");
|
|
135846
|
+
raw2.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, OPTIONS");
|
|
135847
|
+
raw2.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, If-None-Match");
|
|
135848
|
+
raw2.setHeader("Access-Control-Expose-Headers", "ETag");
|
|
135849
|
+
c.res.headers.set("Access-Control-Allow-Origin", allowedOrigin);
|
|
135850
|
+
c.res.headers.set("Vary", "Origin");
|
|
135851
|
+
c.res.headers.set("Access-Control-Allow-Methods", "GET, POST, PATCH, OPTIONS");
|
|
135852
|
+
c.res.headers.set(
|
|
135853
|
+
"Access-Control-Allow-Headers",
|
|
135854
|
+
"Authorization, Content-Type, If-None-Match"
|
|
135855
|
+
);
|
|
135856
|
+
c.res.headers.set("Access-Control-Expose-Headers", "ETag");
|
|
135857
|
+
}
|
|
135858
|
+
if (c.req.method === "OPTIONS") {
|
|
135859
|
+
return c.newResponse(null, allowedOrigin ? 204 : 403);
|
|
135860
|
+
}
|
|
135861
|
+
await next();
|
|
135862
|
+
};
|
|
135753
135863
|
};
|
|
135754
135864
|
|
|
135755
135865
|
// src/api/middleware/error.middleware.ts
|
|
@@ -136246,7 +136356,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
|
|
|
136246
136356
|
event: "http.request"
|
|
136247
136357
|
});
|
|
136248
136358
|
});
|
|
136249
|
-
app.use("*", corsMiddleware());
|
|
136359
|
+
app.use("*", corsMiddleware(deps.browserCors));
|
|
136250
136360
|
app.use("*", authMiddleware(deps));
|
|
136251
136361
|
app.onError(errorMiddleware);
|
|
136252
136362
|
app.route("/healthz", createHealthRoutes());
|
|
@@ -136542,11 +136652,11 @@ var ConversationCache = class _ConversationCache {
|
|
|
136542
136652
|
INSERT INTO conversation_meta
|
|
136543
136653
|
(id, file_path, project_path, project_name, title, model, account, branch,
|
|
136544
136654
|
message_count, last_activity, first_message, last_message, preview, updated_at,
|
|
136545
|
-
mtime_ms, file_size, provider)
|
|
136655
|
+
mtime_ms, file_size, provider, scanner_meta_json)
|
|
136546
136656
|
VALUES
|
|
136547
136657
|
(@id, @file_path, @project_path, @project_name, @title, @model, @account, @branch,
|
|
136548
136658
|
@message_count, @last_activity, @first_message, @last_message, @preview, @updated_at,
|
|
136549
|
-
@mtime_ms, @file_size, @provider)
|
|
136659
|
+
@mtime_ms, @file_size, @provider, @scanner_meta_json)
|
|
136550
136660
|
ON CONFLICT(id) DO UPDATE SET
|
|
136551
136661
|
file_path = excluded.file_path,
|
|
136552
136662
|
project_path = excluded.project_path,
|
|
@@ -136566,7 +136676,8 @@ var ConversationCache = class _ConversationCache {
|
|
|
136566
136676
|
updated_at = excluded.updated_at,
|
|
136567
136677
|
mtime_ms = excluded.mtime_ms,
|
|
136568
136678
|
file_size = excluded.file_size,
|
|
136569
|
-
provider = excluded.provider
|
|
136679
|
+
provider = excluded.provider,
|
|
136680
|
+
scanner_meta_json = excluded.scanner_meta_json
|
|
136570
136681
|
WHERE conversation_meta.updated_at < excluded.updated_at
|
|
136571
136682
|
`),
|
|
136572
136683
|
getTail: db.prepare("SELECT * FROM conversation_tail WHERE conversation_id = ?"),
|
|
@@ -136603,6 +136714,27 @@ var ConversationCache = class _ConversationCache {
|
|
|
136603
136714
|
allFileStats: db.prepare(
|
|
136604
136715
|
"SELECT file_path, mtime_ms, file_size FROM conversation_meta WHERE mtime_ms IS NOT NULL AND file_size IS NOT NULL"
|
|
136605
136716
|
),
|
|
136717
|
+
allScannerStatCacheRows: db.prepare(
|
|
136718
|
+
"SELECT file_path, mtime_ms, file_size, scanner_meta_json FROM conversation_meta WHERE mtime_ms IS NOT NULL AND file_size IS NOT NULL AND scanner_meta_json IS NOT NULL"
|
|
136719
|
+
),
|
|
136720
|
+
updateScannerCache: db.prepare(
|
|
136721
|
+
"UPDATE conversation_meta SET mtime_ms = ?, file_size = ?, scanner_meta_json = ? WHERE id = ?"
|
|
136722
|
+
),
|
|
136723
|
+
getFileMetadata: db.prepare(
|
|
136724
|
+
"SELECT mtime_ms, file_size, is_agent, agent_entrypoints_key FROM conversation_file_metadata WHERE file_path = ?"
|
|
136725
|
+
),
|
|
136726
|
+
upsertFileMetadata: db.prepare(`
|
|
136727
|
+
INSERT INTO conversation_file_metadata
|
|
136728
|
+
(file_path, mtime_ms, file_size, is_agent, agent_entrypoints_key, updated_at)
|
|
136729
|
+
VALUES
|
|
136730
|
+
(@file_path, @mtime_ms, @file_size, @is_agent, @agent_entrypoints_key, @updated_at)
|
|
136731
|
+
ON CONFLICT(file_path) DO UPDATE SET
|
|
136732
|
+
mtime_ms = excluded.mtime_ms,
|
|
136733
|
+
file_size = excluded.file_size,
|
|
136734
|
+
is_agent = excluded.is_agent,
|
|
136735
|
+
agent_entrypoints_key = excluded.agent_entrypoints_key,
|
|
136736
|
+
updated_at = excluded.updated_at
|
|
136737
|
+
`),
|
|
136606
136738
|
upsertSessionName: db.prepare(`
|
|
136607
136739
|
INSERT INTO session_names (session_id, name, updated_at)
|
|
136608
136740
|
VALUES (?, ?, ?)
|
|
@@ -136643,8 +136775,35 @@ var ConversationCache = class _ConversationCache {
|
|
|
136643
136775
|
getDatabase() {
|
|
136644
136776
|
return this.db;
|
|
136645
136777
|
}
|
|
136646
|
-
|
|
136647
|
-
return this.agentEntrypoints;
|
|
136778
|
+
agentEntrypointsKey() {
|
|
136779
|
+
return [...this.agentEntrypoints].sort().join(",");
|
|
136780
|
+
}
|
|
136781
|
+
classifyAgentFile(filePath, mtimeMs, fileSize) {
|
|
136782
|
+
if (this.agentEntrypoints.size === 0) return false;
|
|
136783
|
+
const entrypointsKey = this.agentEntrypointsKey();
|
|
136784
|
+
const cached3 = this.stmts.getFileMetadata.get(filePath);
|
|
136785
|
+
if (cached3 && cached3.mtime_ms === mtimeMs && cached3.file_size === fileSize && cached3.agent_entrypoints_key === entrypointsKey) {
|
|
136786
|
+
return cached3.is_agent === 1;
|
|
136787
|
+
}
|
|
136788
|
+
const isAgent = isAgentFile(filePath, this.agentEntrypoints);
|
|
136789
|
+
this.stmts.upsertFileMetadata.run({
|
|
136790
|
+
file_path: filePath,
|
|
136791
|
+
mtime_ms: mtimeMs,
|
|
136792
|
+
file_size: fileSize,
|
|
136793
|
+
is_agent: isAgent ? 1 : 0,
|
|
136794
|
+
agent_entrypoints_key: entrypointsKey,
|
|
136795
|
+
updated_at: Date.now()
|
|
136796
|
+
});
|
|
136797
|
+
return isAgent;
|
|
136798
|
+
}
|
|
136799
|
+
isAgentFileCached(filePath) {
|
|
136800
|
+
let s3;
|
|
136801
|
+
try {
|
|
136802
|
+
s3 = (0, import_fs16.statSync)(filePath);
|
|
136803
|
+
} catch {
|
|
136804
|
+
return false;
|
|
136805
|
+
}
|
|
136806
|
+
return this.classifyAgentFile(filePath, s3.mtimeMs, s3.size);
|
|
136648
136807
|
}
|
|
136649
136808
|
static open(dbPath, tailSize = 10, migrationsDir, options) {
|
|
136650
136809
|
(0, import_fs16.mkdirSync)((0, import_path12.dirname)(dbPath), { recursive: true });
|
|
@@ -136820,11 +136979,9 @@ var ConversationCache = class _ConversationCache {
|
|
|
136820
136979
|
// in conversation-cache.test.ts).
|
|
136821
136980
|
upsertFromScannerMeta(metas) {
|
|
136822
136981
|
const filter = this.filterAgentConversations;
|
|
136823
|
-
const entrypoints = this.agentEntrypoints;
|
|
136824
136982
|
const upsertedIds = [];
|
|
136825
136983
|
const run2 = this.db.transaction((items) => {
|
|
136826
136984
|
for (const m2 of items) {
|
|
136827
|
-
if (filter && isAgentFile(m2.filePath, entrypoints)) continue;
|
|
136828
136985
|
const id = m2.sessionId || m2.id.split("/").pop()?.replace(/\.jsonl$/, "") || m2.id;
|
|
136829
136986
|
const lastActivityMs = m2.timestamp ? new Date(m2.timestamp).getTime() : null;
|
|
136830
136987
|
let mtimeMs = null;
|
|
@@ -136836,6 +136993,10 @@ var ConversationCache = class _ConversationCache {
|
|
|
136836
136993
|
} catch {
|
|
136837
136994
|
}
|
|
136838
136995
|
const seq = ++this.tailSeq;
|
|
136996
|
+
if (filter && mtimeMs !== null && fileSize !== null && this.classifyAgentFile(m2.filePath, mtimeMs, fileSize)) {
|
|
136997
|
+
continue;
|
|
136998
|
+
}
|
|
136999
|
+
const scannerMetaJson = JSON.stringify(m2);
|
|
136839
137000
|
this.stmts.upsertFull.run({
|
|
136840
137001
|
id,
|
|
136841
137002
|
file_path: m2.filePath,
|
|
@@ -136853,8 +137014,10 @@ var ConversationCache = class _ConversationCache {
|
|
|
136853
137014
|
updated_at: seq,
|
|
136854
137015
|
mtime_ms: mtimeMs,
|
|
136855
137016
|
file_size: fileSize,
|
|
136856
|
-
provider: m2.provider ?? CLAUDE_CODE_PROVIDER2
|
|
137017
|
+
provider: m2.provider ?? CLAUDE_CODE_PROVIDER2,
|
|
137018
|
+
scanner_meta_json: scannerMetaJson
|
|
136857
137019
|
});
|
|
137020
|
+
this.stmts.updateScannerCache.run(mtimeMs, fileSize, scannerMetaJson, id);
|
|
136858
137021
|
if (this.fileIndexLoaded) this.fileIndex.set(m2.filePath, id);
|
|
136859
137022
|
upsertedIds.push(id);
|
|
136860
137023
|
}
|
|
@@ -136978,6 +137141,21 @@ var ConversationCache = class _ConversationCache {
|
|
|
136978
137141
|
}
|
|
136979
137142
|
return map2;
|
|
136980
137143
|
}
|
|
137144
|
+
getScannerStatCache() {
|
|
137145
|
+
const rows = this.stmts.allScannerStatCacheRows.all();
|
|
137146
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
137147
|
+
for (const r of rows) {
|
|
137148
|
+
try {
|
|
137149
|
+
const meta3 = JSON.parse(r.scanner_meta_json);
|
|
137150
|
+
map2.set(r.file_path, {
|
|
137151
|
+
stat: { mtimeMs: r.mtime_ms, size: r.file_size },
|
|
137152
|
+
meta: meta3
|
|
137153
|
+
});
|
|
137154
|
+
} catch {
|
|
137155
|
+
}
|
|
137156
|
+
}
|
|
137157
|
+
return map2;
|
|
137158
|
+
}
|
|
136981
137159
|
getMetaById(id) {
|
|
136982
137160
|
const row = this.stmts.getFullById.get(id);
|
|
136983
137161
|
if (!row) return null;
|
|
@@ -138280,6 +138458,37 @@ function detectShellPrompt(lines) {
|
|
|
138280
138458
|
return null;
|
|
138281
138459
|
}
|
|
138282
138460
|
|
|
138461
|
+
// src/utils/debounce.ts
|
|
138462
|
+
function debounce(fn, waitMs) {
|
|
138463
|
+
let timer = null;
|
|
138464
|
+
let lastArgs = null;
|
|
138465
|
+
const run2 = () => {
|
|
138466
|
+
timer = null;
|
|
138467
|
+
if (lastArgs) {
|
|
138468
|
+
const args = lastArgs;
|
|
138469
|
+
lastArgs = null;
|
|
138470
|
+
fn(...args);
|
|
138471
|
+
}
|
|
138472
|
+
};
|
|
138473
|
+
const debounced = (...args) => {
|
|
138474
|
+
lastArgs = args;
|
|
138475
|
+
if (timer) clearTimeout(timer);
|
|
138476
|
+
timer = setTimeout(run2, waitMs);
|
|
138477
|
+
};
|
|
138478
|
+
debounced.cancel = () => {
|
|
138479
|
+
if (timer) clearTimeout(timer);
|
|
138480
|
+
timer = null;
|
|
138481
|
+
lastArgs = null;
|
|
138482
|
+
};
|
|
138483
|
+
debounced.flush = () => {
|
|
138484
|
+
if (timer) {
|
|
138485
|
+
clearTimeout(timer);
|
|
138486
|
+
run2();
|
|
138487
|
+
}
|
|
138488
|
+
};
|
|
138489
|
+
return debounced;
|
|
138490
|
+
}
|
|
138491
|
+
|
|
138283
138492
|
// src/pty-manager.ts
|
|
138284
138493
|
var OUTPUT_BUFFER_MAX2 = 65536;
|
|
138285
138494
|
var PTY_COLS2 = 120;
|
|
@@ -138287,11 +138496,13 @@ var PTY_ROWS2 = 40;
|
|
|
138287
138496
|
var SCREEN_SCROLLBACK2 = 1e3;
|
|
138288
138497
|
var CLAUDE_PROMPT_MARKERS = ["\u256D", "\u276F"];
|
|
138289
138498
|
var PROMPT_MARKER_FALLBACK_MS = 1e4;
|
|
138499
|
+
var QUIET_DETECT_MS = 500;
|
|
138290
138500
|
function buildPasteBytes(input) {
|
|
138291
138501
|
return `\x1B[200~${input}\x1B[201~`;
|
|
138292
138502
|
}
|
|
138293
138503
|
var SUBMIT_BYTES2 = "\r";
|
|
138294
138504
|
var SUBMIT_DELAY_MS = 16;
|
|
138505
|
+
var SUBMIT_MAX_WAIT_MS = 500;
|
|
138295
138506
|
function digestBytes2(s3) {
|
|
138296
138507
|
const escaped = s3.replace(new RegExp(String.fromCharCode(27), "g"), "\\x1b").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t");
|
|
138297
138508
|
if (escaped.length <= 200) return escaped;
|
|
@@ -138368,6 +138579,10 @@ var PTYManager = class {
|
|
|
138368
138579
|
// to a given input or fell silent. Reset on dispose().
|
|
138369
138580
|
chunkIndex = /* @__PURE__ */ new Map();
|
|
138370
138581
|
lastChunkAt = /* @__PURE__ */ new Map();
|
|
138582
|
+
// Per-session debounced "went quiet" checker, re-armed on every chunk. Fires
|
|
138583
|
+
// QUIET_DETECT_MS after the last chunk so ready/prompt detection doesn't
|
|
138584
|
+
// wait for another chunk that may never arrive (Claude blocked on input).
|
|
138585
|
+
quietCheckers = /* @__PURE__ */ new Map();
|
|
138371
138586
|
// In-flight start()/startFresh() calls keyed by sessionId. A second
|
|
138372
138587
|
// concurrent resume for the same session (double-tap, client retry) awaits
|
|
138373
138588
|
// the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).
|
|
@@ -138383,16 +138598,19 @@ var PTYManager = class {
|
|
|
138383
138598
|
}
|
|
138384
138599
|
// Resume an existing Claude conversation. sessionId is the JSONL UUID.
|
|
138385
138600
|
//
|
|
138386
|
-
//
|
|
138387
|
-
// Both suppress
|
|
138388
|
-
//
|
|
138389
|
-
//
|
|
138390
|
-
//
|
|
138391
|
-
//
|
|
138601
|
+
// options.permissionMode defaults to `acceptEdits` rather than
|
|
138602
|
+
// `--dangerously-skip-permissions`/`bypassPermissions`. Both suppress
|
|
138603
|
+
// file-edit prompts, but in an interactive (TUI) launch the skip-permissions
|
|
138604
|
+
// flag renders a blocking "Bypass Permissions mode" warning menu on every
|
|
138605
|
+
// boot that no known ~/.claude.json flag suppressed (as of Claude CLI
|
|
138606
|
+
// v2.1.x) — the session never reaches a usable prompt, so the mobile app
|
|
138607
|
+
// shows an empty/stuck screen. `acceptEdits` auto-approves file edits
|
|
138392
138608
|
// without that warning gate, while still prompting for shell commands.
|
|
138609
|
+
// `manual` (prompt for everything) is the only other mode callers may pass;
|
|
138610
|
+
// `bypassPermissions`/`plan`/`dontAsk`/`auto` are not supported here.
|
|
138393
138611
|
// (The other first-run gates — onboarding/theme, workspace trust,
|
|
138394
138612
|
// custom-API-key — are cleared by the seeded ~/.claude.json in
|
|
138395
|
-
// docker/entrypoint.sh.) startFresh() uses the same
|
|
138613
|
+
// docker/entrypoint.sh.) startFresh() uses the same default for the same reason.
|
|
138396
138614
|
async start(sessionId, options) {
|
|
138397
138615
|
const existing = this.sessions.get(sessionId);
|
|
138398
138616
|
if (existing) return toPublicSession2(existing);
|
|
@@ -138411,7 +138629,7 @@ var PTYManager = class {
|
|
|
138411
138629
|
resolveClaudeExe(),
|
|
138412
138630
|
[
|
|
138413
138631
|
"--permission-mode",
|
|
138414
|
-
"acceptEdits",
|
|
138632
|
+
options.permissionMode ?? "acceptEdits",
|
|
138415
138633
|
"--settings",
|
|
138416
138634
|
'{"spinnerTipsEnabled":false}',
|
|
138417
138635
|
"--resume",
|
|
@@ -138460,7 +138678,7 @@ var PTYManager = class {
|
|
|
138460
138678
|
const projectName = options.projectName ?? (0, import_path17.basename)(options.projectPath);
|
|
138461
138679
|
const args = [
|
|
138462
138680
|
"--permission-mode",
|
|
138463
|
-
"acceptEdits",
|
|
138681
|
+
options.permissionMode ?? "acceptEdits",
|
|
138464
138682
|
"--settings",
|
|
138465
138683
|
'{"spinnerTipsEnabled":false}',
|
|
138466
138684
|
"--session-id",
|
|
@@ -138554,9 +138772,20 @@ var PTYManager = class {
|
|
|
138554
138772
|
session.promptCount++;
|
|
138555
138773
|
return session.promptCount;
|
|
138556
138774
|
}
|
|
138557
|
-
// Two-step paste-then-submit. Writes the bracketed-paste body,
|
|
138558
|
-
//
|
|
138559
|
-
//
|
|
138775
|
+
// Two-step paste-then-submit. Writes the bracketed-paste body, then waits
|
|
138776
|
+
// for the PTY to go quiet before writing \r. See buildPasteBytes() for why
|
|
138777
|
+
// the split matters.
|
|
138778
|
+
//
|
|
138779
|
+
// The wait is quiescence-based, not a flat delay: a fixed SUBMIT_DELAY_MS
|
|
138780
|
+
// timer (the original fix) still races a TUI that's mid-redraw of its own
|
|
138781
|
+
// output (e.g. re-painting right after posting a question) when the paste
|
|
138782
|
+
// lands — the timer can elapse and fire \r while the TUI is still busy,
|
|
138783
|
+
// and that \r gets absorbed by the redraw instead of submitting (see
|
|
138784
|
+
// 2026-07 session 14dda340: a "Yes" reply was accepted into the input line
|
|
138785
|
+
// but never landed as a submitted JSONL turn). Polling in SUBMIT_DELAY_MS
|
|
138786
|
+
// steps and only submitting once lastChunkAt hasn't advanced for a full
|
|
138787
|
+
// step gives the TUI as many extra ticks as it needs, capped at
|
|
138788
|
+
// SUBMIT_MAX_WAIT_MS so a silent/wedged PTY still gets its \r eventually.
|
|
138560
138789
|
writeSubmit(sessionId, session, input, path2, promptCount) {
|
|
138561
138790
|
const pasteBytes = buildPasteBytes(input);
|
|
138562
138791
|
this.log.info(
|
|
@@ -138571,12 +138800,21 @@ var PTYManager = class {
|
|
|
138571
138800
|
phase: "paste"
|
|
138572
138801
|
}
|
|
138573
138802
|
);
|
|
138803
|
+
const pasteAt = Date.now();
|
|
138574
138804
|
session.process.write(pasteBytes);
|
|
138575
|
-
|
|
138805
|
+
const trySubmit = () => {
|
|
138576
138806
|
const current = this.sessions.get(sessionId);
|
|
138577
138807
|
if (!current || current !== session) return;
|
|
138808
|
+
const now = Date.now();
|
|
138809
|
+
const lastChunk = this.lastChunkAt.get(sessionId) ?? pasteAt;
|
|
138810
|
+
const quiet = now - lastChunk >= SUBMIT_DELAY_MS;
|
|
138811
|
+
const timedOut = now - pasteAt >= SUBMIT_MAX_WAIT_MS;
|
|
138812
|
+
if (!quiet && !timedOut) {
|
|
138813
|
+
setTimeout(trySubmit, SUBMIT_DELAY_MS);
|
|
138814
|
+
return;
|
|
138815
|
+
}
|
|
138578
138816
|
this.log.info(
|
|
138579
|
-
`[pty.input.submit] ${sessionId.slice(0, 8)} promptCount=${promptCount} digest=\\r`,
|
|
138817
|
+
`[pty.input.submit] ${sessionId.slice(0, 8)} promptCount=${promptCount} digest=\\r waitedMs=${now - pasteAt} timedOut=${timedOut}`,
|
|
138580
138818
|
{
|
|
138581
138819
|
event: "pty.input_write",
|
|
138582
138820
|
sessionId,
|
|
@@ -138584,11 +138822,14 @@ var PTYManager = class {
|
|
|
138584
138822
|
byteLen: SUBMIT_BYTES2.length,
|
|
138585
138823
|
digest: "\\r",
|
|
138586
138824
|
path: path2,
|
|
138587
|
-
phase: "submit"
|
|
138825
|
+
phase: "submit",
|
|
138826
|
+
waitedMs: now - pasteAt,
|
|
138827
|
+
timedOut
|
|
138588
138828
|
}
|
|
138589
138829
|
);
|
|
138590
138830
|
current.process.write(SUBMIT_BYTES2);
|
|
138591
|
-
}
|
|
138831
|
+
};
|
|
138832
|
+
setTimeout(trySubmit, SUBMIT_DELAY_MS);
|
|
138592
138833
|
}
|
|
138593
138834
|
// Drain any inputs that were sent while the session was still pendingReady,
|
|
138594
138835
|
// writing them in arrival order now that Claude is at its prompt.
|
|
@@ -138637,6 +138878,8 @@ var PTYManager = class {
|
|
|
138637
138878
|
this.permissionOpen.delete(sessionId);
|
|
138638
138879
|
this.lastScreenQuestionKey.delete(sessionId);
|
|
138639
138880
|
this.shellPromptOpen.delete(sessionId);
|
|
138881
|
+
this.quietCheckers.get(sessionId)?.cancel();
|
|
138882
|
+
this.quietCheckers.delete(sessionId);
|
|
138640
138883
|
try {
|
|
138641
138884
|
session.process.kill("SIGINT");
|
|
138642
138885
|
} catch {
|
|
@@ -138696,6 +138939,8 @@ var PTYManager = class {
|
|
|
138696
138939
|
this.firstChunkAt.clear();
|
|
138697
138940
|
this.chunkIndex.clear();
|
|
138698
138941
|
this.lastChunkAt.clear();
|
|
138942
|
+
for (const quiet of this.quietCheckers.values()) quiet.cancel();
|
|
138943
|
+
this.quietCheckers.clear();
|
|
138699
138944
|
this.permissionOpen.clear();
|
|
138700
138945
|
this.lastScreenQuestionKey.clear();
|
|
138701
138946
|
this.shellPromptOpen.clear();
|
|
@@ -138749,6 +138994,12 @@ var PTYManager = class {
|
|
|
138749
138994
|
err
|
|
138750
138995
|
});
|
|
138751
138996
|
});
|
|
138997
|
+
let quiet = this.quietCheckers.get(sessionId);
|
|
138998
|
+
if (!quiet) {
|
|
138999
|
+
quiet = debounce(() => this.handleQuiet(sessionId), QUIET_DETECT_MS);
|
|
139000
|
+
this.quietCheckers.set(sessionId, quiet);
|
|
139001
|
+
}
|
|
139002
|
+
quiet();
|
|
138752
139003
|
}
|
|
138753
139004
|
// Detect permission gates (OSC 777 + scraped options) and AskUserQuestion
|
|
138754
139005
|
// menus from the rendered screen, firing the additive callbacks. Async because
|
|
@@ -138824,6 +139075,24 @@ var PTYManager = class {
|
|
|
138824
139075
|
}
|
|
138825
139076
|
}
|
|
138826
139077
|
}
|
|
139078
|
+
// Fired QUIET_DETECT_MS after the last PTY chunk. Re-runs the same
|
|
139079
|
+
// ready/prompt detection handleOutput() runs per-chunk, using the last
|
|
139080
|
+
// rendered output — a session blocked on a prompt (or an unmarked boot
|
|
139081
|
+
// screen) may never produce another chunk to trigger detection otherwise.
|
|
139082
|
+
handleQuiet(sessionId) {
|
|
139083
|
+
const session = this.sessions.get(sessionId);
|
|
139084
|
+
if (session?.status !== "running") return;
|
|
139085
|
+
if (this.pendingReady.has(sessionId)) {
|
|
139086
|
+
this.markReady(sessionId, session, "quiet:timeout");
|
|
139087
|
+
}
|
|
139088
|
+
this.detectLivePrompts(sessionId, "", session.lastOutput).catch((err) => {
|
|
139089
|
+
this.log.warn("[pty.prompt_detect] failed", {
|
|
139090
|
+
event: "pty.prompt_detect_failed",
|
|
139091
|
+
sessionId,
|
|
139092
|
+
err
|
|
139093
|
+
});
|
|
139094
|
+
});
|
|
139095
|
+
}
|
|
138827
139096
|
// Transition a session from "running" to "waiting_input", clear pendingReady,
|
|
138828
139097
|
// and flush any queued input. Idempotent: callers can invoke at any chunk.
|
|
138829
139098
|
markReady(sessionId, session, reason) {
|
|
@@ -138864,6 +139133,8 @@ var PTYManager = class {
|
|
|
138864
139133
|
this.permissionOpen.delete(sessionId);
|
|
138865
139134
|
this.lastScreenQuestionKey.delete(sessionId);
|
|
138866
139135
|
this.shellPromptOpen.delete(sessionId);
|
|
139136
|
+
this.quietCheckers.get(sessionId)?.cancel();
|
|
139137
|
+
this.quietCheckers.delete(sessionId);
|
|
138867
139138
|
}
|
|
138868
139139
|
};
|
|
138869
139140
|
function toPublicSession2(s3) {
|
|
@@ -141091,7 +141362,7 @@ function pruneAgentConversations(cache) {
|
|
|
141091
141362
|
missing += 1;
|
|
141092
141363
|
continue;
|
|
141093
141364
|
}
|
|
141094
|
-
if (
|
|
141365
|
+
if (cache.isAgentFileCached(row.file_path)) {
|
|
141095
141366
|
cache.deleteByFilePath(row.file_path);
|
|
141096
141367
|
pruned += 1;
|
|
141097
141368
|
}
|
|
@@ -141511,37 +141782,6 @@ function computeConversationEtag({
|
|
|
141511
141782
|
return `"${digest}"`;
|
|
141512
141783
|
}
|
|
141513
141784
|
|
|
141514
|
-
// src/utils/debounce.ts
|
|
141515
|
-
function debounce(fn, waitMs) {
|
|
141516
|
-
let timer = null;
|
|
141517
|
-
let lastArgs = null;
|
|
141518
|
-
const run2 = () => {
|
|
141519
|
-
timer = null;
|
|
141520
|
-
if (lastArgs) {
|
|
141521
|
-
const args = lastArgs;
|
|
141522
|
-
lastArgs = null;
|
|
141523
|
-
fn(...args);
|
|
141524
|
-
}
|
|
141525
|
-
};
|
|
141526
|
-
const debounced = (...args) => {
|
|
141527
|
-
lastArgs = args;
|
|
141528
|
-
if (timer) clearTimeout(timer);
|
|
141529
|
-
timer = setTimeout(run2, waitMs);
|
|
141530
|
-
};
|
|
141531
|
-
debounced.cancel = () => {
|
|
141532
|
-
if (timer) clearTimeout(timer);
|
|
141533
|
-
timer = null;
|
|
141534
|
-
lastArgs = null;
|
|
141535
|
-
};
|
|
141536
|
-
debounced.flush = () => {
|
|
141537
|
-
if (timer) {
|
|
141538
|
-
clearTimeout(timer);
|
|
141539
|
-
run2();
|
|
141540
|
-
}
|
|
141541
|
-
};
|
|
141542
|
-
return debounced;
|
|
141543
|
-
}
|
|
141544
|
-
|
|
141545
141785
|
// node_modules/date-fns/constants.js
|
|
141546
141786
|
var daysInYear = 365.2425;
|
|
141547
141787
|
var maxTime = Math.pow(10, 8) * 24 * 60 * 60 * 1e3;
|
|
@@ -141900,6 +142140,7 @@ var WSHub = class {
|
|
|
141900
142140
|
var BROWSE_SYSTEM_PROMPT = (browseRoot) => `You are working within the project boundary: ${browseRoot}. Do not read, write, or execute commands that access files or directories outside this boundary.`;
|
|
141901
142141
|
var DEFAULT_SYSTEM_PROMPT = "When presenting options or choices to the user, limit the options to at most 3.";
|
|
141902
142142
|
var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
|
|
142143
|
+
var START_READY_TIMEOUT_MS = 15e3;
|
|
141903
142144
|
function parseIncludeAgentsEnv(raw2) {
|
|
141904
142145
|
if (raw2 === void 0) return false;
|
|
141905
142146
|
const v3 = raw2.trim().toLowerCase();
|
|
@@ -141962,12 +142203,14 @@ var StreamerServer = class {
|
|
|
141962
142203
|
disableDb = false;
|
|
141963
142204
|
browseRoot = null;
|
|
141964
142205
|
publicUrl = null;
|
|
142206
|
+
browserCors;
|
|
141965
142207
|
pairTokens = new PairTokenStore();
|
|
141966
142208
|
exchangeAttempts = /* @__PURE__ */ new Map();
|
|
141967
142209
|
sessionStartAttempts = /* @__PURE__ */ new Map();
|
|
141968
142210
|
sessionInputAttempts = /* @__PURE__ */ new Map();
|
|
141969
142211
|
ptyGracePeriodMs;
|
|
141970
142212
|
defaultSystemPrompt;
|
|
142213
|
+
defaultPermissionMode;
|
|
141971
142214
|
// Map of sessionId → grace timer; fires to kill PTY after WS disconnect
|
|
141972
142215
|
ptyGraceTimers = /* @__PURE__ */ new Map();
|
|
141973
142216
|
// Map of sessionId → set of subscribed WS clients
|
|
@@ -142016,6 +142259,7 @@ var StreamerServer = class {
|
|
|
142016
142259
|
this.codexRoots = config2.codexRoots ?? [(0, import_path21.join)((0, import_os10.homedir)(), ".codex", "sessions")];
|
|
142017
142260
|
this.ptyGracePeriodMs = config2.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
142018
142261
|
this.defaultSystemPrompt = config2.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
142262
|
+
this.defaultPermissionMode = config2.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
|
|
142019
142263
|
this.cacheDir = config2.cacheDir ?? loadCacheDir() ?? (0, import_path21.join)((0, import_os10.homedir)(), ".threadbase", "cache");
|
|
142020
142264
|
this.tailSize = config2.tailSize ?? loadTailSize() ?? 10;
|
|
142021
142265
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config2.directoryScanDebounceMs ?? 1e3;
|
|
@@ -142045,6 +142289,7 @@ var StreamerServer = class {
|
|
|
142045
142289
|
this.log.warn(`Warning: ${result.error}`, { error: result.error });
|
|
142046
142290
|
}
|
|
142047
142291
|
}
|
|
142292
|
+
this.browserCors = config2.browserCors ?? loadBrowserCors();
|
|
142048
142293
|
this.sessionStore = new SessionStore();
|
|
142049
142294
|
this.wsHub = new WSHub();
|
|
142050
142295
|
this.fileWatcher = new ConversationWatcher({
|
|
@@ -142199,6 +142444,7 @@ var StreamerServer = class {
|
|
|
142199
142444
|
rotateApiKey: () => this.rotateApiKey(),
|
|
142200
142445
|
publicUrl: this.publicUrl,
|
|
142201
142446
|
browseRoot: this.browseRoot,
|
|
142447
|
+
browserCors: this.browserCors,
|
|
142202
142448
|
ptyManager: this.ptyManager,
|
|
142203
142449
|
sessionStore: this.sessionStore,
|
|
142204
142450
|
wsHub: this.wsHub,
|
|
@@ -142445,9 +142691,9 @@ var StreamerServer = class {
|
|
|
142445
142691
|
);
|
|
142446
142692
|
this.scannerPersistenceDisabled = true;
|
|
142447
142693
|
}
|
|
142448
|
-
const warmupScanner = this.newScanner();
|
|
142449
|
-
this.allScanners.add(warmupScanner);
|
|
142450
142694
|
const warmupStatCache = this.buildStatCache(null);
|
|
142695
|
+
const warmupScanner = this.newScanner(warmupStatCache ? { persistent: false } : void 0);
|
|
142696
|
+
this.allScanners.add(warmupScanner);
|
|
142451
142697
|
const shouldEmitProgress = createScanProgressThrottle();
|
|
142452
142698
|
const scanOpts = {
|
|
142453
142699
|
...this.scanProfiles ? { profiles: this.scanProfiles } : {},
|
|
@@ -142887,6 +143133,10 @@ var StreamerServer = class {
|
|
|
142887
143133
|
}
|
|
142888
143134
|
buildStatCache(previousScanner) {
|
|
142889
143135
|
if (!this.cache) return void 0;
|
|
143136
|
+
if (!previousScanner) {
|
|
143137
|
+
const persisted = this.cache.getScannerStatCache();
|
|
143138
|
+
return persisted.size > 0 ? persisted : void 0;
|
|
143139
|
+
}
|
|
142890
143140
|
const dbStats = this.cache.getFileStats();
|
|
142891
143141
|
if (dbStats.size === 0) return void 0;
|
|
142892
143142
|
const metaByPath = /* @__PURE__ */ new Map();
|
|
@@ -142916,9 +143166,9 @@ var StreamerServer = class {
|
|
|
142916
143166
|
// this — its per-file refreshFile (in findConversationByUuid) already
|
|
142917
143167
|
// reconciles the one conversation being requested, so paying a full-tree
|
|
142918
143168
|
// rescan just because some OTHER file changed is the stall this avoids.
|
|
142919
|
-
newScanner() {
|
|
143169
|
+
newScanner(options) {
|
|
142920
143170
|
return new ConversationScanner(
|
|
142921
|
-
this.scannerPersistenceDisabled ? { persistent: false } : void 0
|
|
143171
|
+
options ?? (this.scannerPersistenceDisabled ? { persistent: false } : void 0)
|
|
142922
143172
|
);
|
|
142923
143173
|
}
|
|
142924
143174
|
async getScanner(skipStaleRescan = false) {
|
|
@@ -142937,7 +143187,7 @@ var StreamerServer = class {
|
|
|
142937
143187
|
}
|
|
142938
143188
|
this.scannerStale = false;
|
|
142939
143189
|
const statCache = this.buildStatCache(this.scanner);
|
|
142940
|
-
this.scanner = this.newScanner();
|
|
143190
|
+
this.scanner = this.newScanner(statCache ? { persistent: false } : void 0);
|
|
142941
143191
|
this.allScanners.add(this.scanner);
|
|
142942
143192
|
this.scannerReady = this.scanner.scan({
|
|
142943
143193
|
...this.scanProfiles ? { profiles: this.scanProfiles } : {},
|
|
@@ -142960,9 +143210,8 @@ var StreamerServer = class {
|
|
|
142960
143210
|
this.scannerReady = null;
|
|
142961
143211
|
return this.getScanner();
|
|
142962
143212
|
}
|
|
142963
|
-
// refresh=1's scan: reuse the WARM
|
|
142964
|
-
//
|
|
142965
|
-
// with fullRescan:true — the escape hatch that bypasses the scanner's
|
|
143213
|
+
// refresh=1's scan: reuse the WARM scanner and re-run its scan with
|
|
143214
|
+
// fullRescan:true — the escape hatch that bypasses the scanner's
|
|
142966
143215
|
// dir-mtime discovery gate, since an explicit user pull-to-refresh is exactly
|
|
142967
143216
|
// the "don't trust the gate, check disk for real" signal. Unlike
|
|
142968
143217
|
// getFreshScanner() this does NOT discard the warm scanner. scannerReady is
|
|
@@ -143350,7 +143599,8 @@ var StreamerServer = class {
|
|
|
143350
143599
|
provider,
|
|
143351
143600
|
projectPath,
|
|
143352
143601
|
projectName: body.projectName,
|
|
143353
|
-
branch: body.branch
|
|
143602
|
+
branch: body.branch,
|
|
143603
|
+
permissionMode: this.defaultPermissionMode
|
|
143354
143604
|
});
|
|
143355
143605
|
this.sessionStore.addManaged(session);
|
|
143356
143606
|
void this.watchConversationFile(sessionId);
|
|
@@ -143674,7 +143924,8 @@ var StreamerServer = class {
|
|
|
143674
143924
|
const session = await this.ptyManager.start(convId, {
|
|
143675
143925
|
projectPath,
|
|
143676
143926
|
projectName,
|
|
143677
|
-
branch
|
|
143927
|
+
branch,
|
|
143928
|
+
permissionMode: this.defaultPermissionMode
|
|
143678
143929
|
});
|
|
143679
143930
|
this.sessionStore.addManaged(session);
|
|
143680
143931
|
void this.watchConversationFile(session.id);
|
|
@@ -143744,10 +143995,35 @@ var StreamerServer = class {
|
|
|
143744
143995
|
provider,
|
|
143745
143996
|
projectPath: resolvedPath,
|
|
143746
143997
|
projectName: body.projectName,
|
|
143747
|
-
systemPrompt: systemPromptParts.join("\n")
|
|
143998
|
+
systemPrompt: systemPromptParts.join("\n"),
|
|
143999
|
+
permissionMode: this.defaultPermissionMode
|
|
143748
144000
|
});
|
|
143749
144001
|
this.sessionStore.addManaged(session);
|
|
143750
|
-
|
|
144002
|
+
const readyOrFailed = new Promise((resolve6) => {
|
|
144003
|
+
const handler = (status) => {
|
|
144004
|
+
if (status === "waiting_input" || status === "idle") {
|
|
144005
|
+
this.sessionStatusBus.off(`status:${session.id}`, handler);
|
|
144006
|
+
resolve6(status === "waiting_input" ? "ready" : "failed");
|
|
144007
|
+
}
|
|
144008
|
+
};
|
|
144009
|
+
this.sessionStatusBus.on(`status:${session.id}`, handler);
|
|
144010
|
+
});
|
|
144011
|
+
const timeoutPromise = new Promise(
|
|
144012
|
+
(resolve6) => setTimeout(() => resolve6("timeout"), START_READY_TIMEOUT_MS)
|
|
144013
|
+
);
|
|
144014
|
+
const outcome = await Promise.race([readyOrFailed, timeoutPromise]);
|
|
144015
|
+
const current = this.sessionStore.get(session.id, this.ptyAttachedIds());
|
|
144016
|
+
if (outcome === "ready" && current) {
|
|
144017
|
+
json2(res, 200, { session: current });
|
|
144018
|
+
} else if (outcome === "failed" && current) {
|
|
144019
|
+
json2(res, 502, {
|
|
144020
|
+
id: session.id,
|
|
144021
|
+
status: "idle",
|
|
144022
|
+
error: current.failureReason ?? "Session exited before becoming ready"
|
|
144023
|
+
});
|
|
144024
|
+
} else {
|
|
144025
|
+
json2(res, 202, { id: session.id, status: "pending" });
|
|
144026
|
+
}
|
|
143751
144027
|
if (provider === CODEX_CLI_PROVIDER2) {
|
|
143752
144028
|
this.watchForCodexRollout(session.id, resolvedPath);
|
|
143753
144029
|
} else {
|
|
@@ -147937,7 +148213,10 @@ program2.name("threadbase-streamer").description("PTY session management, WebSoc
|
|
|
147937
148213
|
program2.command("serve").description("Start the streamer server").option("-p, --port <number>", "Port to listen on", "8766").option("--api-key <key>", "API key for authentication").option("--local-no-auth", "Skip auth for localhost requests", false).option("-v, --verbose", "Verbose output", false).option("--log-menubar-requests", "Log /healthz requests from the menubar app", false).option("--browse-root <path>", "Root directory for file browsing").option(
|
|
147938
148214
|
"--public-url <url>",
|
|
147939
148215
|
"Public URL clients should use to reach this server (https:// required, except localhost). Falls back to THREADBASE_PUBLIC_URL env or public_url: in ~/.threadbase/server.yaml."
|
|
147940
|
-
).option(
|
|
148216
|
+
).option(
|
|
148217
|
+
"--default-permission-mode <mode>",
|
|
148218
|
+
"Claude Code permission mode for spawned sessions: acceptEdits (auto-approve file edits, default) or manual (prompt for everything). Falls back to default_permission_mode: in ~/.threadbase/server.yaml, or a first-run interactive prompt on a human TTY invocation (skip with THREADBASE_SKIP_PERMISSION_MODE_PROMPT=true)."
|
|
148219
|
+
).option("--no-pair-qr", "Skip the pairing QR on startup").option("--replace-prod", "Stop the launchd-supervised prod streamer and bind its port", false).option("--forget", "Clear this repo's remembered dev-vs-prod choice and re-prompt", false).option("--forget-all", "Clear every repo's remembered dev-vs-prod choice", false).option(
|
|
147941
148220
|
"--prod",
|
|
147942
148221
|
"Run as if invoked by launchd: skip the dev-takeover prompt and signal handlers",
|
|
147943
148222
|
false
|
|
@@ -147956,11 +148235,25 @@ program2.command("serve").description("Start the streamer server").option("-p, -
|
|
|
147956
148235
|
if (opts.multiAgentFlow) {
|
|
147957
148236
|
process.env.MULTI_AGENT_FLOW = "true";
|
|
147958
148237
|
}
|
|
148238
|
+
if (opts.defaultPermissionMode !== void 0 && opts.defaultPermissionMode !== "acceptEdits" && opts.defaultPermissionMode !== "manual") {
|
|
148239
|
+
log7.error(
|
|
148240
|
+
`Invalid --default-permission-mode: ${opts.defaultPermissionMode}`,
|
|
148241
|
+
void 0,
|
|
148242
|
+
"console"
|
|
148243
|
+
);
|
|
148244
|
+
process.exit(1);
|
|
148245
|
+
}
|
|
147959
148246
|
const requestedPort = Number.parseInt(opts.port, 10);
|
|
147960
148247
|
const apiKey = opts.apiKey ?? loadOrCreateApiKey();
|
|
147961
148248
|
const publicUrl = opts.publicUrl ?? loadPublicUrl() ?? null;
|
|
147962
148249
|
const isProdInvocation = opts.prod === true || process.ppid === 1;
|
|
147963
148250
|
if (!isProdInvocation) appendDevSessionMarker();
|
|
148251
|
+
let resolvedDefaultPermissionMode = opts.defaultPermissionMode;
|
|
148252
|
+
if (resolvedDefaultPermissionMode === void 0 && !isProdInvocation && process.env.THREADBASE_SKIP_PERMISSION_MODE_PROMPT !== "true" && loadDefaultPermissionMode() === void 0 && import_node_process3.stdin.isTTY) {
|
|
148253
|
+
const { interactivePermissionModePrompt: interactivePermissionModePrompt2 } = await Promise.resolve().then(() => (init_prompt(), prompt_exports));
|
|
148254
|
+
resolvedDefaultPermissionMode = await interactivePermissionModePrompt2();
|
|
148255
|
+
setDefaultPermissionMode(resolvedDefaultPermissionMode);
|
|
148256
|
+
}
|
|
147964
148257
|
let resolvedPort = requestedPort;
|
|
147965
148258
|
if (!isProdInvocation) {
|
|
147966
148259
|
const { resolveDevPlan: resolveDevPlan2, detectProdActive: detectProdActive2, isPortInUse: isPortInUse2, findFreePort: findFreePort2, takeoverProd: takeoverProd2 } = await Promise.resolve().then(() => (init_dev_takeover(), dev_takeover_exports));
|
|
@@ -147992,7 +148285,8 @@ program2.command("serve").description("Start the streamer server").option("-p, -
|
|
|
147992
148285
|
verbose: opts.verbose,
|
|
147993
148286
|
logMenubarRequests: opts.logMenubarRequests,
|
|
147994
148287
|
browseRoot: opts.browseRoot,
|
|
147995
|
-
publicUrl: opts.publicUrl
|
|
148288
|
+
publicUrl: opts.publicUrl,
|
|
148289
|
+
defaultPermissionMode: resolvedDefaultPermissionMode
|
|
147996
148290
|
});
|
|
147997
148291
|
await server.listen(resolvedPort);
|
|
147998
148292
|
{
|
|
@@ -148006,13 +148300,21 @@ program2.command("serve").description("Start the streamer server").option("-p, -
|
|
|
148006
148300
|
wsUrl: `ws://localhost:${resolvedPort}/ws`
|
|
148007
148301
|
});
|
|
148008
148302
|
log7.info(`API key: ${apiKey}`, { apiKeyMasked: `${apiKey.slice(0, 6)}\u2026` });
|
|
148009
|
-
|
|
148010
|
-
|
|
148011
|
-
|
|
148012
|
-
|
|
148013
|
-
|
|
148014
|
-
|
|
148015
|
-
}
|
|
148303
|
+
try {
|
|
148304
|
+
await printServerBanner({
|
|
148305
|
+
port: resolvedPort,
|
|
148306
|
+
apiKey,
|
|
148307
|
+
publicUrl,
|
|
148308
|
+
includeQr: opts.pairQr !== false
|
|
148309
|
+
});
|
|
148310
|
+
} catch (err) {
|
|
148311
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
148312
|
+
log7.warn(`(skipped pairing QR: ${message})`, { reason: message });
|
|
148313
|
+
log7.info(
|
|
148314
|
+
printUrlBanner({ url: resolveServerUrl({ publicUrl, port: resolvedPort }) }),
|
|
148315
|
+
void 0,
|
|
148316
|
+
"console"
|
|
148317
|
+
);
|
|
148016
148318
|
}
|
|
148017
148319
|
const shutdown = async () => {
|
|
148018
148320
|
log7.info("Shutting down...");
|
|
@@ -148067,7 +148369,7 @@ program2.command("pair").description("Print a pairing QR code (server must alrea
|
|
|
148067
148369
|
const port = Number.parseInt(opts.port, 10);
|
|
148068
148370
|
const apiKey = loadOrCreateApiKey();
|
|
148069
148371
|
const publicUrl = loadPublicUrl() ?? null;
|
|
148070
|
-
await
|
|
148372
|
+
await printServerBanner({ port, apiKey, publicUrl, includeQr: true });
|
|
148071
148373
|
});
|
|
148072
148374
|
program2.command("set-key [key]").description("Set the streamer API key in ~/.threadbase/server.yaml").action(async (key) => {
|
|
148073
148375
|
const { runSetKey: runSetKey2 } = await Promise.resolve().then(() => (init_setKey(), setKey_exports));
|
|
@@ -148163,11 +148465,49 @@ program2.command("update").description("Check for streamer updates from GitHub R
|
|
|
148163
148465
|
});
|
|
148164
148466
|
registerProdCommands(program2);
|
|
148165
148467
|
program2.parse();
|
|
148166
|
-
|
|
148468
|
+
function generateQr(payload) {
|
|
148469
|
+
return new Promise((resolve6) => {
|
|
148470
|
+
import_qrcode_terminal.default.generate(payload, { small: true }, resolve6);
|
|
148471
|
+
});
|
|
148472
|
+
}
|
|
148473
|
+
function printUrlBanner({
|
|
148474
|
+
url: url2,
|
|
148475
|
+
qr: qr2,
|
|
148476
|
+
expiresAt
|
|
148477
|
+
}) {
|
|
148478
|
+
const contentLines = ["Threadbase Streamer \u2014 server address", "", url2];
|
|
148479
|
+
if (qr2) {
|
|
148480
|
+
contentLines.push("", ...qr2.split("\n").filter((l) => l.length > 0));
|
|
148481
|
+
if (expiresAt !== void 0) {
|
|
148482
|
+
contentLines.push(
|
|
148483
|
+
"",
|
|
148484
|
+
`Scan to pair a mobile client (expires ${new Date(expiresAt).toLocaleTimeString()})`
|
|
148485
|
+
);
|
|
148486
|
+
} else {
|
|
148487
|
+
contentLines.push("", "Scan to pair a mobile client");
|
|
148488
|
+
}
|
|
148489
|
+
}
|
|
148490
|
+
const width = Math.max(...contentLines.map((l) => l.length));
|
|
148491
|
+
const pad = (l) => `\u2551 ${l}${" ".repeat(width - l.length)} \u2551`;
|
|
148492
|
+
const top = `\u2554${"\u2550".repeat(width + 2)}\u2557`;
|
|
148493
|
+
const bottom = `\u255A${"\u2550".repeat(width + 2)}\u255D`;
|
|
148494
|
+
return `
|
|
148495
|
+
${top}
|
|
148496
|
+
${contentLines.map(pad).join("\n")}
|
|
148497
|
+
${bottom}
|
|
148498
|
+
`;
|
|
148499
|
+
}
|
|
148500
|
+
async function printServerBanner({
|
|
148167
148501
|
port,
|
|
148168
148502
|
apiKey,
|
|
148169
|
-
publicUrl
|
|
148503
|
+
publicUrl,
|
|
148504
|
+
includeQr
|
|
148170
148505
|
}) {
|
|
148506
|
+
const url2 = resolveServerUrl({ publicUrl, port });
|
|
148507
|
+
if (!includeQr) {
|
|
148508
|
+
log7.info(printUrlBanner({ url: url2 }), void 0, "console");
|
|
148509
|
+
return;
|
|
148510
|
+
}
|
|
148171
148511
|
const res = await fetch(`http://localhost:${port}/api/pair/start`, {
|
|
148172
148512
|
method: "POST",
|
|
148173
148513
|
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
@@ -148177,19 +148517,12 @@ async function printPairQR({
|
|
|
148177
148517
|
throw new Error(`/api/pair/start returned ${res.status}`);
|
|
148178
148518
|
}
|
|
148179
148519
|
const { token, expiresAt, expiresInSeconds } = await res.json();
|
|
148180
|
-
const url2 = resolveServerUrl({ publicUrl, port });
|
|
148181
148520
|
const expSeconds = Math.floor(expiresAt / 1e3);
|
|
148182
148521
|
const payload = `threadbase://pair?url=${encodeURIComponent(url2)}&token=${token}&exp=${expSeconds}`;
|
|
148183
|
-
|
|
148184
|
-
|
|
148185
|
-
log7.info(`
|
|
148186
|
-
log7.info(`
|
|
148187
|
-
log7.info(
|
|
148188
|
-
`Expires : ${new Date(expiresAt).toLocaleTimeString()} (${expiresInSeconds}s)
|
|
148189
|
-
`,
|
|
148190
|
-
void 0,
|
|
148191
|
-
"console"
|
|
148192
|
-
);
|
|
148522
|
+
const qr2 = await generateQr(payload);
|
|
148523
|
+
log7.info(printUrlBanner({ url: url2, qr: qr2, expiresAt }), void 0, "console");
|
|
148524
|
+
log7.info(`Pair URL: ${payload}`, void 0, "console");
|
|
148525
|
+
log7.info(`Expires in ${expiresInSeconds}s`, void 0, "console");
|
|
148193
148526
|
}
|
|
148194
148527
|
/*! Bundled license information:
|
|
148195
148528
|
|