@threadbase-sh/streamer 1.24.7 → 1.25.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 +419 -128
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +241 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +241 -68
- 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
|
@@ -1548,6 +1548,42 @@ function loadTailSize() {
|
|
|
1548
1548
|
}
|
|
1549
1549
|
return void 0;
|
|
1550
1550
|
}
|
|
1551
|
+
function loadDefaultPermissionMode() {
|
|
1552
|
+
try {
|
|
1553
|
+
const content = (0, import_fs.readFileSync)(configFile(), "utf-8");
|
|
1554
|
+
const match2 = content.match(/default_permission_mode:\s*(\S+)/);
|
|
1555
|
+
const value = match2?.[1]?.trim();
|
|
1556
|
+
if (value === "acceptEdits" || value === "manual") return value;
|
|
1557
|
+
} catch {
|
|
1558
|
+
}
|
|
1559
|
+
return void 0;
|
|
1560
|
+
}
|
|
1561
|
+
function setDefaultPermissionMode(mode) {
|
|
1562
|
+
const file2 = configFile();
|
|
1563
|
+
(0, import_fs.mkdirSync)(configDir(), { recursive: true });
|
|
1564
|
+
let content = "";
|
|
1565
|
+
try {
|
|
1566
|
+
content = (0, import_fs.readFileSync)(file2, "utf-8");
|
|
1567
|
+
} catch (err) {
|
|
1568
|
+
if (err.code !== "ENOENT") throw err;
|
|
1569
|
+
}
|
|
1570
|
+
const line = `default_permission_mode: ${mode}`;
|
|
1571
|
+
let updated;
|
|
1572
|
+
if (/^default_permission_mode:\s*.+$/m.test(content)) {
|
|
1573
|
+
updated = content.replace(/^default_permission_mode:\s*.+$/m, line);
|
|
1574
|
+
} else if (content.length === 0 || content.endsWith("\n")) {
|
|
1575
|
+
updated = `${content}${line}
|
|
1576
|
+
`;
|
|
1577
|
+
} else {
|
|
1578
|
+
updated = `${content}
|
|
1579
|
+
${line}
|
|
1580
|
+
`;
|
|
1581
|
+
}
|
|
1582
|
+
const tmpFile = `${file2}.tmp`;
|
|
1583
|
+
(0, import_fs.writeFileSync)(tmpFile, updated, { encoding: "utf-8", mode: 384 });
|
|
1584
|
+
(0, import_fs.chmodSync)(tmpFile, 384);
|
|
1585
|
+
(0, import_fs.renameSync)(tmpFile, file2);
|
|
1586
|
+
}
|
|
1551
1587
|
function validatePublicUrl(raw2) {
|
|
1552
1588
|
let parsed;
|
|
1553
1589
|
try {
|
|
@@ -123699,6 +123735,68 @@ var init_check_sqlite_abi = __esm({
|
|
|
123699
123735
|
}
|
|
123700
123736
|
});
|
|
123701
123737
|
|
|
123738
|
+
// src/lifecycle/prompt.ts
|
|
123739
|
+
var prompt_exports = {};
|
|
123740
|
+
__export(prompt_exports, {
|
|
123741
|
+
interactivePermissionModePrompt: () => interactivePermissionModePrompt,
|
|
123742
|
+
interactivePrompt: () => interactivePrompt
|
|
123743
|
+
});
|
|
123744
|
+
var import_node_process2, import_promises18, interactivePrompt, interactivePermissionModePrompt;
|
|
123745
|
+
var init_prompt = __esm({
|
|
123746
|
+
"src/lifecycle/prompt.ts"() {
|
|
123747
|
+
"use strict";
|
|
123748
|
+
import_node_process2 = require("process");
|
|
123749
|
+
import_promises18 = require("readline/promises");
|
|
123750
|
+
interactivePrompt = async ({ prodPort, suggestedAltPort, prodActive }) => {
|
|
123751
|
+
const rl = (0, import_promises18.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
|
|
123752
|
+
try {
|
|
123753
|
+
if (!prodActive) {
|
|
123754
|
+
import_node_process2.stdout.write(
|
|
123755
|
+
`
|
|
123756
|
+
Port ${prodPort} is already in use by another process.
|
|
123757
|
+
Running dev on port ${suggestedAltPort} instead.
|
|
123758
|
+
`
|
|
123759
|
+
);
|
|
123760
|
+
const rememberAns2 = (await rl.question("Remember this choice for this repo? [y/N]: ")).trim().toLowerCase();
|
|
123761
|
+
return {
|
|
123762
|
+
choice: "use-port",
|
|
123763
|
+
port: suggestedAltPort,
|
|
123764
|
+
remember: rememberAns2 === "y" || rememberAns2 === "yes"
|
|
123765
|
+
};
|
|
123766
|
+
}
|
|
123767
|
+
import_node_process2.stdout.write(
|
|
123768
|
+
`
|
|
123769
|
+
The supervised prod streamer is already holding port ${prodPort}.
|
|
123770
|
+
[r] Stop prod and take port ${prodPort}
|
|
123771
|
+
[p] Run dev on port ${suggestedAltPort} instead
|
|
123772
|
+
`
|
|
123773
|
+
);
|
|
123774
|
+
const choiceAns = (await rl.question("Choice [r/p]: ")).trim().toLowerCase();
|
|
123775
|
+
const rememberAns = (await rl.question("Remember this choice for this repo? [y/N]: ")).trim().toLowerCase();
|
|
123776
|
+
const remember = rememberAns === "y" || rememberAns === "yes";
|
|
123777
|
+
if (choiceAns === "r") {
|
|
123778
|
+
return { choice: "replace-prod", remember };
|
|
123779
|
+
}
|
|
123780
|
+
return { choice: "use-port", port: suggestedAltPort, remember };
|
|
123781
|
+
} finally {
|
|
123782
|
+
rl.close();
|
|
123783
|
+
}
|
|
123784
|
+
};
|
|
123785
|
+
interactivePermissionModePrompt = async () => {
|
|
123786
|
+
const rl = (0, import_promises18.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
|
|
123787
|
+
try {
|
|
123788
|
+
import_node_process2.stdout.write(
|
|
123789
|
+
"\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"
|
|
123790
|
+
);
|
|
123791
|
+
const ans = (await rl.question("Choice [a/m] (default a): ")).trim().toLowerCase();
|
|
123792
|
+
return ans === "m" ? "manual" : "acceptEdits";
|
|
123793
|
+
} finally {
|
|
123794
|
+
rl.close();
|
|
123795
|
+
}
|
|
123796
|
+
};
|
|
123797
|
+
}
|
|
123798
|
+
});
|
|
123799
|
+
|
|
123702
123800
|
// src/lifecycle/prefs.ts
|
|
123703
123801
|
function readPrefs() {
|
|
123704
123802
|
const path2 = prefsPath();
|
|
@@ -123781,7 +123879,7 @@ async function resolveDevPlan(opts) {
|
|
|
123781
123879
|
}
|
|
123782
123880
|
const prodActive = opts.isProdActive();
|
|
123783
123881
|
const portTaken = opts.portInUse(opts.requestedPort);
|
|
123784
|
-
if (!
|
|
123882
|
+
if (!portTaken) {
|
|
123785
123883
|
return { kind: "use-port", port: opts.requestedPort };
|
|
123786
123884
|
}
|
|
123787
123885
|
if (!opts.forget) {
|
|
@@ -123796,7 +123894,12 @@ async function resolveDevPlan(opts) {
|
|
|
123796
123894
|
}
|
|
123797
123895
|
}
|
|
123798
123896
|
const suggested = await opts.findFreePort(opts.requestedPort + 1);
|
|
123799
|
-
const answer = await opts.prompt({
|
|
123897
|
+
const answer = await opts.prompt({
|
|
123898
|
+
prodPort: opts.requestedPort,
|
|
123899
|
+
suggestedAltPort: suggested,
|
|
123900
|
+
prodActive,
|
|
123901
|
+
portTaken
|
|
123902
|
+
});
|
|
123800
123903
|
if (answer.remember && opts.repoToplevel) {
|
|
123801
123904
|
if (answer.choice === "replace-prod") {
|
|
123802
123905
|
writePrefForRepo(opts.repoToplevel, { choice: "replace-prod" });
|
|
@@ -123902,41 +124005,6 @@ var init_dev_takeover = __esm({
|
|
|
123902
124005
|
}
|
|
123903
124006
|
});
|
|
123904
124007
|
|
|
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
124008
|
// src/lifecycle/repo.ts
|
|
123941
124009
|
var repo_exports = {};
|
|
123942
124010
|
__export(repo_exports, {
|
|
@@ -124017,6 +124085,9 @@ var init_setKey = __esm({
|
|
|
124017
124085
|
);
|
|
124018
124086
|
})();
|
|
124019
124087
|
|
|
124088
|
+
// cli/index.ts
|
|
124089
|
+
var import_node_process3 = require("process");
|
|
124090
|
+
|
|
124020
124091
|
// node_modules/commander/lib/error.js
|
|
124021
124092
|
var CommanderError = class extends Error {
|
|
124022
124093
|
/**
|
|
@@ -136542,11 +136613,11 @@ var ConversationCache = class _ConversationCache {
|
|
|
136542
136613
|
INSERT INTO conversation_meta
|
|
136543
136614
|
(id, file_path, project_path, project_name, title, model, account, branch,
|
|
136544
136615
|
message_count, last_activity, first_message, last_message, preview, updated_at,
|
|
136545
|
-
mtime_ms, file_size, provider)
|
|
136616
|
+
mtime_ms, file_size, provider, scanner_meta_json)
|
|
136546
136617
|
VALUES
|
|
136547
136618
|
(@id, @file_path, @project_path, @project_name, @title, @model, @account, @branch,
|
|
136548
136619
|
@message_count, @last_activity, @first_message, @last_message, @preview, @updated_at,
|
|
136549
|
-
@mtime_ms, @file_size, @provider)
|
|
136620
|
+
@mtime_ms, @file_size, @provider, @scanner_meta_json)
|
|
136550
136621
|
ON CONFLICT(id) DO UPDATE SET
|
|
136551
136622
|
file_path = excluded.file_path,
|
|
136552
136623
|
project_path = excluded.project_path,
|
|
@@ -136566,7 +136637,8 @@ var ConversationCache = class _ConversationCache {
|
|
|
136566
136637
|
updated_at = excluded.updated_at,
|
|
136567
136638
|
mtime_ms = excluded.mtime_ms,
|
|
136568
136639
|
file_size = excluded.file_size,
|
|
136569
|
-
provider = excluded.provider
|
|
136640
|
+
provider = excluded.provider,
|
|
136641
|
+
scanner_meta_json = excluded.scanner_meta_json
|
|
136570
136642
|
WHERE conversation_meta.updated_at < excluded.updated_at
|
|
136571
136643
|
`),
|
|
136572
136644
|
getTail: db.prepare("SELECT * FROM conversation_tail WHERE conversation_id = ?"),
|
|
@@ -136603,6 +136675,27 @@ var ConversationCache = class _ConversationCache {
|
|
|
136603
136675
|
allFileStats: db.prepare(
|
|
136604
136676
|
"SELECT file_path, mtime_ms, file_size FROM conversation_meta WHERE mtime_ms IS NOT NULL AND file_size IS NOT NULL"
|
|
136605
136677
|
),
|
|
136678
|
+
allScannerStatCacheRows: db.prepare(
|
|
136679
|
+
"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"
|
|
136680
|
+
),
|
|
136681
|
+
updateScannerCache: db.prepare(
|
|
136682
|
+
"UPDATE conversation_meta SET mtime_ms = ?, file_size = ?, scanner_meta_json = ? WHERE id = ?"
|
|
136683
|
+
),
|
|
136684
|
+
getFileMetadata: db.prepare(
|
|
136685
|
+
"SELECT mtime_ms, file_size, is_agent, agent_entrypoints_key FROM conversation_file_metadata WHERE file_path = ?"
|
|
136686
|
+
),
|
|
136687
|
+
upsertFileMetadata: db.prepare(`
|
|
136688
|
+
INSERT INTO conversation_file_metadata
|
|
136689
|
+
(file_path, mtime_ms, file_size, is_agent, agent_entrypoints_key, updated_at)
|
|
136690
|
+
VALUES
|
|
136691
|
+
(@file_path, @mtime_ms, @file_size, @is_agent, @agent_entrypoints_key, @updated_at)
|
|
136692
|
+
ON CONFLICT(file_path) DO UPDATE SET
|
|
136693
|
+
mtime_ms = excluded.mtime_ms,
|
|
136694
|
+
file_size = excluded.file_size,
|
|
136695
|
+
is_agent = excluded.is_agent,
|
|
136696
|
+
agent_entrypoints_key = excluded.agent_entrypoints_key,
|
|
136697
|
+
updated_at = excluded.updated_at
|
|
136698
|
+
`),
|
|
136606
136699
|
upsertSessionName: db.prepare(`
|
|
136607
136700
|
INSERT INTO session_names (session_id, name, updated_at)
|
|
136608
136701
|
VALUES (?, ?, ?)
|
|
@@ -136643,8 +136736,35 @@ var ConversationCache = class _ConversationCache {
|
|
|
136643
136736
|
getDatabase() {
|
|
136644
136737
|
return this.db;
|
|
136645
136738
|
}
|
|
136646
|
-
|
|
136647
|
-
return this.agentEntrypoints;
|
|
136739
|
+
agentEntrypointsKey() {
|
|
136740
|
+
return [...this.agentEntrypoints].sort().join(",");
|
|
136741
|
+
}
|
|
136742
|
+
classifyAgentFile(filePath, mtimeMs, fileSize) {
|
|
136743
|
+
if (this.agentEntrypoints.size === 0) return false;
|
|
136744
|
+
const entrypointsKey = this.agentEntrypointsKey();
|
|
136745
|
+
const cached3 = this.stmts.getFileMetadata.get(filePath);
|
|
136746
|
+
if (cached3 && cached3.mtime_ms === mtimeMs && cached3.file_size === fileSize && cached3.agent_entrypoints_key === entrypointsKey) {
|
|
136747
|
+
return cached3.is_agent === 1;
|
|
136748
|
+
}
|
|
136749
|
+
const isAgent = isAgentFile(filePath, this.agentEntrypoints);
|
|
136750
|
+
this.stmts.upsertFileMetadata.run({
|
|
136751
|
+
file_path: filePath,
|
|
136752
|
+
mtime_ms: mtimeMs,
|
|
136753
|
+
file_size: fileSize,
|
|
136754
|
+
is_agent: isAgent ? 1 : 0,
|
|
136755
|
+
agent_entrypoints_key: entrypointsKey,
|
|
136756
|
+
updated_at: Date.now()
|
|
136757
|
+
});
|
|
136758
|
+
return isAgent;
|
|
136759
|
+
}
|
|
136760
|
+
isAgentFileCached(filePath) {
|
|
136761
|
+
let s3;
|
|
136762
|
+
try {
|
|
136763
|
+
s3 = (0, import_fs16.statSync)(filePath);
|
|
136764
|
+
} catch {
|
|
136765
|
+
return false;
|
|
136766
|
+
}
|
|
136767
|
+
return this.classifyAgentFile(filePath, s3.mtimeMs, s3.size);
|
|
136648
136768
|
}
|
|
136649
136769
|
static open(dbPath, tailSize = 10, migrationsDir, options) {
|
|
136650
136770
|
(0, import_fs16.mkdirSync)((0, import_path12.dirname)(dbPath), { recursive: true });
|
|
@@ -136820,11 +136940,9 @@ var ConversationCache = class _ConversationCache {
|
|
|
136820
136940
|
// in conversation-cache.test.ts).
|
|
136821
136941
|
upsertFromScannerMeta(metas) {
|
|
136822
136942
|
const filter = this.filterAgentConversations;
|
|
136823
|
-
const entrypoints = this.agentEntrypoints;
|
|
136824
136943
|
const upsertedIds = [];
|
|
136825
136944
|
const run2 = this.db.transaction((items) => {
|
|
136826
136945
|
for (const m2 of items) {
|
|
136827
|
-
if (filter && isAgentFile(m2.filePath, entrypoints)) continue;
|
|
136828
136946
|
const id = m2.sessionId || m2.id.split("/").pop()?.replace(/\.jsonl$/, "") || m2.id;
|
|
136829
136947
|
const lastActivityMs = m2.timestamp ? new Date(m2.timestamp).getTime() : null;
|
|
136830
136948
|
let mtimeMs = null;
|
|
@@ -136836,6 +136954,10 @@ var ConversationCache = class _ConversationCache {
|
|
|
136836
136954
|
} catch {
|
|
136837
136955
|
}
|
|
136838
136956
|
const seq = ++this.tailSeq;
|
|
136957
|
+
if (filter && mtimeMs !== null && fileSize !== null && this.classifyAgentFile(m2.filePath, mtimeMs, fileSize)) {
|
|
136958
|
+
continue;
|
|
136959
|
+
}
|
|
136960
|
+
const scannerMetaJson = JSON.stringify(m2);
|
|
136839
136961
|
this.stmts.upsertFull.run({
|
|
136840
136962
|
id,
|
|
136841
136963
|
file_path: m2.filePath,
|
|
@@ -136853,8 +136975,10 @@ var ConversationCache = class _ConversationCache {
|
|
|
136853
136975
|
updated_at: seq,
|
|
136854
136976
|
mtime_ms: mtimeMs,
|
|
136855
136977
|
file_size: fileSize,
|
|
136856
|
-
provider: m2.provider ?? CLAUDE_CODE_PROVIDER2
|
|
136978
|
+
provider: m2.provider ?? CLAUDE_CODE_PROVIDER2,
|
|
136979
|
+
scanner_meta_json: scannerMetaJson
|
|
136857
136980
|
});
|
|
136981
|
+
this.stmts.updateScannerCache.run(mtimeMs, fileSize, scannerMetaJson, id);
|
|
136858
136982
|
if (this.fileIndexLoaded) this.fileIndex.set(m2.filePath, id);
|
|
136859
136983
|
upsertedIds.push(id);
|
|
136860
136984
|
}
|
|
@@ -136978,6 +137102,21 @@ var ConversationCache = class _ConversationCache {
|
|
|
136978
137102
|
}
|
|
136979
137103
|
return map2;
|
|
136980
137104
|
}
|
|
137105
|
+
getScannerStatCache() {
|
|
137106
|
+
const rows = this.stmts.allScannerStatCacheRows.all();
|
|
137107
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
137108
|
+
for (const r of rows) {
|
|
137109
|
+
try {
|
|
137110
|
+
const meta3 = JSON.parse(r.scanner_meta_json);
|
|
137111
|
+
map2.set(r.file_path, {
|
|
137112
|
+
stat: { mtimeMs: r.mtime_ms, size: r.file_size },
|
|
137113
|
+
meta: meta3
|
|
137114
|
+
});
|
|
137115
|
+
} catch {
|
|
137116
|
+
}
|
|
137117
|
+
}
|
|
137118
|
+
return map2;
|
|
137119
|
+
}
|
|
136981
137120
|
getMetaById(id) {
|
|
136982
137121
|
const row = this.stmts.getFullById.get(id);
|
|
136983
137122
|
if (!row) return null;
|
|
@@ -138280,6 +138419,37 @@ function detectShellPrompt(lines) {
|
|
|
138280
138419
|
return null;
|
|
138281
138420
|
}
|
|
138282
138421
|
|
|
138422
|
+
// src/utils/debounce.ts
|
|
138423
|
+
function debounce(fn, waitMs) {
|
|
138424
|
+
let timer = null;
|
|
138425
|
+
let lastArgs = null;
|
|
138426
|
+
const run2 = () => {
|
|
138427
|
+
timer = null;
|
|
138428
|
+
if (lastArgs) {
|
|
138429
|
+
const args = lastArgs;
|
|
138430
|
+
lastArgs = null;
|
|
138431
|
+
fn(...args);
|
|
138432
|
+
}
|
|
138433
|
+
};
|
|
138434
|
+
const debounced = (...args) => {
|
|
138435
|
+
lastArgs = args;
|
|
138436
|
+
if (timer) clearTimeout(timer);
|
|
138437
|
+
timer = setTimeout(run2, waitMs);
|
|
138438
|
+
};
|
|
138439
|
+
debounced.cancel = () => {
|
|
138440
|
+
if (timer) clearTimeout(timer);
|
|
138441
|
+
timer = null;
|
|
138442
|
+
lastArgs = null;
|
|
138443
|
+
};
|
|
138444
|
+
debounced.flush = () => {
|
|
138445
|
+
if (timer) {
|
|
138446
|
+
clearTimeout(timer);
|
|
138447
|
+
run2();
|
|
138448
|
+
}
|
|
138449
|
+
};
|
|
138450
|
+
return debounced;
|
|
138451
|
+
}
|
|
138452
|
+
|
|
138283
138453
|
// src/pty-manager.ts
|
|
138284
138454
|
var OUTPUT_BUFFER_MAX2 = 65536;
|
|
138285
138455
|
var PTY_COLS2 = 120;
|
|
@@ -138287,11 +138457,13 @@ var PTY_ROWS2 = 40;
|
|
|
138287
138457
|
var SCREEN_SCROLLBACK2 = 1e3;
|
|
138288
138458
|
var CLAUDE_PROMPT_MARKERS = ["\u256D", "\u276F"];
|
|
138289
138459
|
var PROMPT_MARKER_FALLBACK_MS = 1e4;
|
|
138460
|
+
var QUIET_DETECT_MS = 500;
|
|
138290
138461
|
function buildPasteBytes(input) {
|
|
138291
138462
|
return `\x1B[200~${input}\x1B[201~`;
|
|
138292
138463
|
}
|
|
138293
138464
|
var SUBMIT_BYTES2 = "\r";
|
|
138294
138465
|
var SUBMIT_DELAY_MS = 16;
|
|
138466
|
+
var SUBMIT_MAX_WAIT_MS = 500;
|
|
138295
138467
|
function digestBytes2(s3) {
|
|
138296
138468
|
const escaped = s3.replace(new RegExp(String.fromCharCode(27), "g"), "\\x1b").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t");
|
|
138297
138469
|
if (escaped.length <= 200) return escaped;
|
|
@@ -138368,6 +138540,10 @@ var PTYManager = class {
|
|
|
138368
138540
|
// to a given input or fell silent. Reset on dispose().
|
|
138369
138541
|
chunkIndex = /* @__PURE__ */ new Map();
|
|
138370
138542
|
lastChunkAt = /* @__PURE__ */ new Map();
|
|
138543
|
+
// Per-session debounced "went quiet" checker, re-armed on every chunk. Fires
|
|
138544
|
+
// QUIET_DETECT_MS after the last chunk so ready/prompt detection doesn't
|
|
138545
|
+
// wait for another chunk that may never arrive (Claude blocked on input).
|
|
138546
|
+
quietCheckers = /* @__PURE__ */ new Map();
|
|
138371
138547
|
// In-flight start()/startFresh() calls keyed by sessionId. A second
|
|
138372
138548
|
// concurrent resume for the same session (double-tap, client retry) awaits
|
|
138373
138549
|
// the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).
|
|
@@ -138383,16 +138559,19 @@ var PTYManager = class {
|
|
|
138383
138559
|
}
|
|
138384
138560
|
// Resume an existing Claude conversation. sessionId is the JSONL UUID.
|
|
138385
138561
|
//
|
|
138386
|
-
//
|
|
138387
|
-
// Both suppress
|
|
138388
|
-
//
|
|
138389
|
-
//
|
|
138390
|
-
//
|
|
138391
|
-
//
|
|
138562
|
+
// options.permissionMode defaults to `acceptEdits` rather than
|
|
138563
|
+
// `--dangerously-skip-permissions`/`bypassPermissions`. Both suppress
|
|
138564
|
+
// file-edit prompts, but in an interactive (TUI) launch the skip-permissions
|
|
138565
|
+
// flag renders a blocking "Bypass Permissions mode" warning menu on every
|
|
138566
|
+
// boot that no known ~/.claude.json flag suppressed (as of Claude CLI
|
|
138567
|
+
// v2.1.x) — the session never reaches a usable prompt, so the mobile app
|
|
138568
|
+
// shows an empty/stuck screen. `acceptEdits` auto-approves file edits
|
|
138392
138569
|
// without that warning gate, while still prompting for shell commands.
|
|
138570
|
+
// `manual` (prompt for everything) is the only other mode callers may pass;
|
|
138571
|
+
// `bypassPermissions`/`plan`/`dontAsk`/`auto` are not supported here.
|
|
138393
138572
|
// (The other first-run gates — onboarding/theme, workspace trust,
|
|
138394
138573
|
// custom-API-key — are cleared by the seeded ~/.claude.json in
|
|
138395
|
-
// docker/entrypoint.sh.) startFresh() uses the same
|
|
138574
|
+
// docker/entrypoint.sh.) startFresh() uses the same default for the same reason.
|
|
138396
138575
|
async start(sessionId, options) {
|
|
138397
138576
|
const existing = this.sessions.get(sessionId);
|
|
138398
138577
|
if (existing) return toPublicSession2(existing);
|
|
@@ -138411,7 +138590,7 @@ var PTYManager = class {
|
|
|
138411
138590
|
resolveClaudeExe(),
|
|
138412
138591
|
[
|
|
138413
138592
|
"--permission-mode",
|
|
138414
|
-
"acceptEdits",
|
|
138593
|
+
options.permissionMode ?? "acceptEdits",
|
|
138415
138594
|
"--settings",
|
|
138416
138595
|
'{"spinnerTipsEnabled":false}',
|
|
138417
138596
|
"--resume",
|
|
@@ -138460,7 +138639,7 @@ var PTYManager = class {
|
|
|
138460
138639
|
const projectName = options.projectName ?? (0, import_path17.basename)(options.projectPath);
|
|
138461
138640
|
const args = [
|
|
138462
138641
|
"--permission-mode",
|
|
138463
|
-
"acceptEdits",
|
|
138642
|
+
options.permissionMode ?? "acceptEdits",
|
|
138464
138643
|
"--settings",
|
|
138465
138644
|
'{"spinnerTipsEnabled":false}',
|
|
138466
138645
|
"--session-id",
|
|
@@ -138554,9 +138733,20 @@ var PTYManager = class {
|
|
|
138554
138733
|
session.promptCount++;
|
|
138555
138734
|
return session.promptCount;
|
|
138556
138735
|
}
|
|
138557
|
-
// Two-step paste-then-submit. Writes the bracketed-paste body,
|
|
138558
|
-
//
|
|
138559
|
-
//
|
|
138736
|
+
// Two-step paste-then-submit. Writes the bracketed-paste body, then waits
|
|
138737
|
+
// for the PTY to go quiet before writing \r. See buildPasteBytes() for why
|
|
138738
|
+
// the split matters.
|
|
138739
|
+
//
|
|
138740
|
+
// The wait is quiescence-based, not a flat delay: a fixed SUBMIT_DELAY_MS
|
|
138741
|
+
// timer (the original fix) still races a TUI that's mid-redraw of its own
|
|
138742
|
+
// output (e.g. re-painting right after posting a question) when the paste
|
|
138743
|
+
// lands — the timer can elapse and fire \r while the TUI is still busy,
|
|
138744
|
+
// and that \r gets absorbed by the redraw instead of submitting (see
|
|
138745
|
+
// 2026-07 session 14dda340: a "Yes" reply was accepted into the input line
|
|
138746
|
+
// but never landed as a submitted JSONL turn). Polling in SUBMIT_DELAY_MS
|
|
138747
|
+
// steps and only submitting once lastChunkAt hasn't advanced for a full
|
|
138748
|
+
// step gives the TUI as many extra ticks as it needs, capped at
|
|
138749
|
+
// SUBMIT_MAX_WAIT_MS so a silent/wedged PTY still gets its \r eventually.
|
|
138560
138750
|
writeSubmit(sessionId, session, input, path2, promptCount) {
|
|
138561
138751
|
const pasteBytes = buildPasteBytes(input);
|
|
138562
138752
|
this.log.info(
|
|
@@ -138571,12 +138761,21 @@ var PTYManager = class {
|
|
|
138571
138761
|
phase: "paste"
|
|
138572
138762
|
}
|
|
138573
138763
|
);
|
|
138764
|
+
const pasteAt = Date.now();
|
|
138574
138765
|
session.process.write(pasteBytes);
|
|
138575
|
-
|
|
138766
|
+
const trySubmit = () => {
|
|
138576
138767
|
const current = this.sessions.get(sessionId);
|
|
138577
138768
|
if (!current || current !== session) return;
|
|
138769
|
+
const now = Date.now();
|
|
138770
|
+
const lastChunk = this.lastChunkAt.get(sessionId) ?? pasteAt;
|
|
138771
|
+
const quiet = now - lastChunk >= SUBMIT_DELAY_MS;
|
|
138772
|
+
const timedOut = now - pasteAt >= SUBMIT_MAX_WAIT_MS;
|
|
138773
|
+
if (!quiet && !timedOut) {
|
|
138774
|
+
setTimeout(trySubmit, SUBMIT_DELAY_MS);
|
|
138775
|
+
return;
|
|
138776
|
+
}
|
|
138578
138777
|
this.log.info(
|
|
138579
|
-
`[pty.input.submit] ${sessionId.slice(0, 8)} promptCount=${promptCount} digest=\\r`,
|
|
138778
|
+
`[pty.input.submit] ${sessionId.slice(0, 8)} promptCount=${promptCount} digest=\\r waitedMs=${now - pasteAt} timedOut=${timedOut}`,
|
|
138580
138779
|
{
|
|
138581
138780
|
event: "pty.input_write",
|
|
138582
138781
|
sessionId,
|
|
@@ -138584,11 +138783,14 @@ var PTYManager = class {
|
|
|
138584
138783
|
byteLen: SUBMIT_BYTES2.length,
|
|
138585
138784
|
digest: "\\r",
|
|
138586
138785
|
path: path2,
|
|
138587
|
-
phase: "submit"
|
|
138786
|
+
phase: "submit",
|
|
138787
|
+
waitedMs: now - pasteAt,
|
|
138788
|
+
timedOut
|
|
138588
138789
|
}
|
|
138589
138790
|
);
|
|
138590
138791
|
current.process.write(SUBMIT_BYTES2);
|
|
138591
|
-
}
|
|
138792
|
+
};
|
|
138793
|
+
setTimeout(trySubmit, SUBMIT_DELAY_MS);
|
|
138592
138794
|
}
|
|
138593
138795
|
// Drain any inputs that were sent while the session was still pendingReady,
|
|
138594
138796
|
// writing them in arrival order now that Claude is at its prompt.
|
|
@@ -138637,6 +138839,8 @@ var PTYManager = class {
|
|
|
138637
138839
|
this.permissionOpen.delete(sessionId);
|
|
138638
138840
|
this.lastScreenQuestionKey.delete(sessionId);
|
|
138639
138841
|
this.shellPromptOpen.delete(sessionId);
|
|
138842
|
+
this.quietCheckers.get(sessionId)?.cancel();
|
|
138843
|
+
this.quietCheckers.delete(sessionId);
|
|
138640
138844
|
try {
|
|
138641
138845
|
session.process.kill("SIGINT");
|
|
138642
138846
|
} catch {
|
|
@@ -138696,6 +138900,8 @@ var PTYManager = class {
|
|
|
138696
138900
|
this.firstChunkAt.clear();
|
|
138697
138901
|
this.chunkIndex.clear();
|
|
138698
138902
|
this.lastChunkAt.clear();
|
|
138903
|
+
for (const quiet of this.quietCheckers.values()) quiet.cancel();
|
|
138904
|
+
this.quietCheckers.clear();
|
|
138699
138905
|
this.permissionOpen.clear();
|
|
138700
138906
|
this.lastScreenQuestionKey.clear();
|
|
138701
138907
|
this.shellPromptOpen.clear();
|
|
@@ -138749,6 +138955,12 @@ var PTYManager = class {
|
|
|
138749
138955
|
err
|
|
138750
138956
|
});
|
|
138751
138957
|
});
|
|
138958
|
+
let quiet = this.quietCheckers.get(sessionId);
|
|
138959
|
+
if (!quiet) {
|
|
138960
|
+
quiet = debounce(() => this.handleQuiet(sessionId), QUIET_DETECT_MS);
|
|
138961
|
+
this.quietCheckers.set(sessionId, quiet);
|
|
138962
|
+
}
|
|
138963
|
+
quiet();
|
|
138752
138964
|
}
|
|
138753
138965
|
// Detect permission gates (OSC 777 + scraped options) and AskUserQuestion
|
|
138754
138966
|
// menus from the rendered screen, firing the additive callbacks. Async because
|
|
@@ -138824,6 +139036,24 @@ var PTYManager = class {
|
|
|
138824
139036
|
}
|
|
138825
139037
|
}
|
|
138826
139038
|
}
|
|
139039
|
+
// Fired QUIET_DETECT_MS after the last PTY chunk. Re-runs the same
|
|
139040
|
+
// ready/prompt detection handleOutput() runs per-chunk, using the last
|
|
139041
|
+
// rendered output — a session blocked on a prompt (or an unmarked boot
|
|
139042
|
+
// screen) may never produce another chunk to trigger detection otherwise.
|
|
139043
|
+
handleQuiet(sessionId) {
|
|
139044
|
+
const session = this.sessions.get(sessionId);
|
|
139045
|
+
if (session?.status !== "running") return;
|
|
139046
|
+
if (this.pendingReady.has(sessionId)) {
|
|
139047
|
+
this.markReady(sessionId, session, "quiet:timeout");
|
|
139048
|
+
}
|
|
139049
|
+
this.detectLivePrompts(sessionId, "", session.lastOutput).catch((err) => {
|
|
139050
|
+
this.log.warn("[pty.prompt_detect] failed", {
|
|
139051
|
+
event: "pty.prompt_detect_failed",
|
|
139052
|
+
sessionId,
|
|
139053
|
+
err
|
|
139054
|
+
});
|
|
139055
|
+
});
|
|
139056
|
+
}
|
|
138827
139057
|
// Transition a session from "running" to "waiting_input", clear pendingReady,
|
|
138828
139058
|
// and flush any queued input. Idempotent: callers can invoke at any chunk.
|
|
138829
139059
|
markReady(sessionId, session, reason) {
|
|
@@ -138864,6 +139094,8 @@ var PTYManager = class {
|
|
|
138864
139094
|
this.permissionOpen.delete(sessionId);
|
|
138865
139095
|
this.lastScreenQuestionKey.delete(sessionId);
|
|
138866
139096
|
this.shellPromptOpen.delete(sessionId);
|
|
139097
|
+
this.quietCheckers.get(sessionId)?.cancel();
|
|
139098
|
+
this.quietCheckers.delete(sessionId);
|
|
138867
139099
|
}
|
|
138868
139100
|
};
|
|
138869
139101
|
function toPublicSession2(s3) {
|
|
@@ -141091,7 +141323,7 @@ function pruneAgentConversations(cache) {
|
|
|
141091
141323
|
missing += 1;
|
|
141092
141324
|
continue;
|
|
141093
141325
|
}
|
|
141094
|
-
if (
|
|
141326
|
+
if (cache.isAgentFileCached(row.file_path)) {
|
|
141095
141327
|
cache.deleteByFilePath(row.file_path);
|
|
141096
141328
|
pruned += 1;
|
|
141097
141329
|
}
|
|
@@ -141511,37 +141743,6 @@ function computeConversationEtag({
|
|
|
141511
141743
|
return `"${digest}"`;
|
|
141512
141744
|
}
|
|
141513
141745
|
|
|
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
141746
|
// node_modules/date-fns/constants.js
|
|
141546
141747
|
var daysInYear = 365.2425;
|
|
141547
141748
|
var maxTime = Math.pow(10, 8) * 24 * 60 * 60 * 1e3;
|
|
@@ -141900,6 +142101,7 @@ var WSHub = class {
|
|
|
141900
142101
|
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
142102
|
var DEFAULT_SYSTEM_PROMPT = "When presenting options or choices to the user, limit the options to at most 3.";
|
|
141902
142103
|
var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
|
|
142104
|
+
var START_READY_TIMEOUT_MS = 15e3;
|
|
141903
142105
|
function parseIncludeAgentsEnv(raw2) {
|
|
141904
142106
|
if (raw2 === void 0) return false;
|
|
141905
142107
|
const v3 = raw2.trim().toLowerCase();
|
|
@@ -141968,6 +142170,7 @@ var StreamerServer = class {
|
|
|
141968
142170
|
sessionInputAttempts = /* @__PURE__ */ new Map();
|
|
141969
142171
|
ptyGracePeriodMs;
|
|
141970
142172
|
defaultSystemPrompt;
|
|
142173
|
+
defaultPermissionMode;
|
|
141971
142174
|
// Map of sessionId → grace timer; fires to kill PTY after WS disconnect
|
|
141972
142175
|
ptyGraceTimers = /* @__PURE__ */ new Map();
|
|
141973
142176
|
// Map of sessionId → set of subscribed WS clients
|
|
@@ -142016,6 +142219,7 @@ var StreamerServer = class {
|
|
|
142016
142219
|
this.codexRoots = config2.codexRoots ?? [(0, import_path21.join)((0, import_os10.homedir)(), ".codex", "sessions")];
|
|
142017
142220
|
this.ptyGracePeriodMs = config2.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
142018
142221
|
this.defaultSystemPrompt = config2.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
142222
|
+
this.defaultPermissionMode = config2.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
|
|
142019
142223
|
this.cacheDir = config2.cacheDir ?? loadCacheDir() ?? (0, import_path21.join)((0, import_os10.homedir)(), ".threadbase", "cache");
|
|
142020
142224
|
this.tailSize = config2.tailSize ?? loadTailSize() ?? 10;
|
|
142021
142225
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config2.directoryScanDebounceMs ?? 1e3;
|
|
@@ -142445,9 +142649,9 @@ var StreamerServer = class {
|
|
|
142445
142649
|
);
|
|
142446
142650
|
this.scannerPersistenceDisabled = true;
|
|
142447
142651
|
}
|
|
142448
|
-
const warmupScanner = this.newScanner();
|
|
142449
|
-
this.allScanners.add(warmupScanner);
|
|
142450
142652
|
const warmupStatCache = this.buildStatCache(null);
|
|
142653
|
+
const warmupScanner = this.newScanner(warmupStatCache ? { persistent: false } : void 0);
|
|
142654
|
+
this.allScanners.add(warmupScanner);
|
|
142451
142655
|
const shouldEmitProgress = createScanProgressThrottle();
|
|
142452
142656
|
const scanOpts = {
|
|
142453
142657
|
...this.scanProfiles ? { profiles: this.scanProfiles } : {},
|
|
@@ -142887,6 +143091,10 @@ var StreamerServer = class {
|
|
|
142887
143091
|
}
|
|
142888
143092
|
buildStatCache(previousScanner) {
|
|
142889
143093
|
if (!this.cache) return void 0;
|
|
143094
|
+
if (!previousScanner) {
|
|
143095
|
+
const persisted = this.cache.getScannerStatCache();
|
|
143096
|
+
return persisted.size > 0 ? persisted : void 0;
|
|
143097
|
+
}
|
|
142890
143098
|
const dbStats = this.cache.getFileStats();
|
|
142891
143099
|
if (dbStats.size === 0) return void 0;
|
|
142892
143100
|
const metaByPath = /* @__PURE__ */ new Map();
|
|
@@ -142916,9 +143124,9 @@ var StreamerServer = class {
|
|
|
142916
143124
|
// this — its per-file refreshFile (in findConversationByUuid) already
|
|
142917
143125
|
// reconciles the one conversation being requested, so paying a full-tree
|
|
142918
143126
|
// rescan just because some OTHER file changed is the stall this avoids.
|
|
142919
|
-
newScanner() {
|
|
143127
|
+
newScanner(options) {
|
|
142920
143128
|
return new ConversationScanner(
|
|
142921
|
-
this.scannerPersistenceDisabled ? { persistent: false } : void 0
|
|
143129
|
+
options ?? (this.scannerPersistenceDisabled ? { persistent: false } : void 0)
|
|
142922
143130
|
);
|
|
142923
143131
|
}
|
|
142924
143132
|
async getScanner(skipStaleRescan = false) {
|
|
@@ -142937,7 +143145,7 @@ var StreamerServer = class {
|
|
|
142937
143145
|
}
|
|
142938
143146
|
this.scannerStale = false;
|
|
142939
143147
|
const statCache = this.buildStatCache(this.scanner);
|
|
142940
|
-
this.scanner = this.newScanner();
|
|
143148
|
+
this.scanner = this.newScanner(statCache ? { persistent: false } : void 0);
|
|
142941
143149
|
this.allScanners.add(this.scanner);
|
|
142942
143150
|
this.scannerReady = this.scanner.scan({
|
|
142943
143151
|
...this.scanProfiles ? { profiles: this.scanProfiles } : {},
|
|
@@ -142960,9 +143168,8 @@ var StreamerServer = class {
|
|
|
142960
143168
|
this.scannerReady = null;
|
|
142961
143169
|
return this.getScanner();
|
|
142962
143170
|
}
|
|
142963
|
-
// refresh=1's scan: reuse the WARM
|
|
142964
|
-
//
|
|
142965
|
-
// with fullRescan:true — the escape hatch that bypasses the scanner's
|
|
143171
|
+
// refresh=1's scan: reuse the WARM scanner and re-run its scan with
|
|
143172
|
+
// fullRescan:true — the escape hatch that bypasses the scanner's
|
|
142966
143173
|
// dir-mtime discovery gate, since an explicit user pull-to-refresh is exactly
|
|
142967
143174
|
// the "don't trust the gate, check disk for real" signal. Unlike
|
|
142968
143175
|
// getFreshScanner() this does NOT discard the warm scanner. scannerReady is
|
|
@@ -143350,7 +143557,8 @@ var StreamerServer = class {
|
|
|
143350
143557
|
provider,
|
|
143351
143558
|
projectPath,
|
|
143352
143559
|
projectName: body.projectName,
|
|
143353
|
-
branch: body.branch
|
|
143560
|
+
branch: body.branch,
|
|
143561
|
+
permissionMode: this.defaultPermissionMode
|
|
143354
143562
|
});
|
|
143355
143563
|
this.sessionStore.addManaged(session);
|
|
143356
143564
|
void this.watchConversationFile(sessionId);
|
|
@@ -143674,7 +143882,8 @@ var StreamerServer = class {
|
|
|
143674
143882
|
const session = await this.ptyManager.start(convId, {
|
|
143675
143883
|
projectPath,
|
|
143676
143884
|
projectName,
|
|
143677
|
-
branch
|
|
143885
|
+
branch,
|
|
143886
|
+
permissionMode: this.defaultPermissionMode
|
|
143678
143887
|
});
|
|
143679
143888
|
this.sessionStore.addManaged(session);
|
|
143680
143889
|
void this.watchConversationFile(session.id);
|
|
@@ -143744,10 +143953,35 @@ var StreamerServer = class {
|
|
|
143744
143953
|
provider,
|
|
143745
143954
|
projectPath: resolvedPath,
|
|
143746
143955
|
projectName: body.projectName,
|
|
143747
|
-
systemPrompt: systemPromptParts.join("\n")
|
|
143956
|
+
systemPrompt: systemPromptParts.join("\n"),
|
|
143957
|
+
permissionMode: this.defaultPermissionMode
|
|
143748
143958
|
});
|
|
143749
143959
|
this.sessionStore.addManaged(session);
|
|
143750
|
-
|
|
143960
|
+
const readyOrFailed = new Promise((resolve6) => {
|
|
143961
|
+
const handler = (status) => {
|
|
143962
|
+
if (status === "waiting_input" || status === "idle") {
|
|
143963
|
+
this.sessionStatusBus.off(`status:${session.id}`, handler);
|
|
143964
|
+
resolve6(status === "waiting_input" ? "ready" : "failed");
|
|
143965
|
+
}
|
|
143966
|
+
};
|
|
143967
|
+
this.sessionStatusBus.on(`status:${session.id}`, handler);
|
|
143968
|
+
});
|
|
143969
|
+
const timeoutPromise = new Promise(
|
|
143970
|
+
(resolve6) => setTimeout(() => resolve6("timeout"), START_READY_TIMEOUT_MS)
|
|
143971
|
+
);
|
|
143972
|
+
const outcome = await Promise.race([readyOrFailed, timeoutPromise]);
|
|
143973
|
+
const current = this.sessionStore.get(session.id, this.ptyAttachedIds());
|
|
143974
|
+
if (outcome === "ready" && current) {
|
|
143975
|
+
json2(res, 200, { session: current });
|
|
143976
|
+
} else if (outcome === "failed" && current) {
|
|
143977
|
+
json2(res, 502, {
|
|
143978
|
+
id: session.id,
|
|
143979
|
+
status: "idle",
|
|
143980
|
+
error: current.failureReason ?? "Session exited before becoming ready"
|
|
143981
|
+
});
|
|
143982
|
+
} else {
|
|
143983
|
+
json2(res, 202, { id: session.id, status: "pending" });
|
|
143984
|
+
}
|
|
143751
143985
|
if (provider === CODEX_CLI_PROVIDER2) {
|
|
143752
143986
|
this.watchForCodexRollout(session.id, resolvedPath);
|
|
143753
143987
|
} else {
|
|
@@ -147937,7 +148171,10 @@ program2.name("threadbase-streamer").description("PTY session management, WebSoc
|
|
|
147937
148171
|
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
148172
|
"--public-url <url>",
|
|
147939
148173
|
"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(
|
|
148174
|
+
).option(
|
|
148175
|
+
"--default-permission-mode <mode>",
|
|
148176
|
+
"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)."
|
|
148177
|
+
).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
148178
|
"--prod",
|
|
147942
148179
|
"Run as if invoked by launchd: skip the dev-takeover prompt and signal handlers",
|
|
147943
148180
|
false
|
|
@@ -147956,11 +148193,25 @@ program2.command("serve").description("Start the streamer server").option("-p, -
|
|
|
147956
148193
|
if (opts.multiAgentFlow) {
|
|
147957
148194
|
process.env.MULTI_AGENT_FLOW = "true";
|
|
147958
148195
|
}
|
|
148196
|
+
if (opts.defaultPermissionMode !== void 0 && opts.defaultPermissionMode !== "acceptEdits" && opts.defaultPermissionMode !== "manual") {
|
|
148197
|
+
log7.error(
|
|
148198
|
+
`Invalid --default-permission-mode: ${opts.defaultPermissionMode}`,
|
|
148199
|
+
void 0,
|
|
148200
|
+
"console"
|
|
148201
|
+
);
|
|
148202
|
+
process.exit(1);
|
|
148203
|
+
}
|
|
147959
148204
|
const requestedPort = Number.parseInt(opts.port, 10);
|
|
147960
148205
|
const apiKey = opts.apiKey ?? loadOrCreateApiKey();
|
|
147961
148206
|
const publicUrl = opts.publicUrl ?? loadPublicUrl() ?? null;
|
|
147962
148207
|
const isProdInvocation = opts.prod === true || process.ppid === 1;
|
|
147963
148208
|
if (!isProdInvocation) appendDevSessionMarker();
|
|
148209
|
+
let resolvedDefaultPermissionMode = opts.defaultPermissionMode;
|
|
148210
|
+
if (resolvedDefaultPermissionMode === void 0 && !isProdInvocation && process.env.THREADBASE_SKIP_PERMISSION_MODE_PROMPT !== "true" && loadDefaultPermissionMode() === void 0 && import_node_process3.stdin.isTTY) {
|
|
148211
|
+
const { interactivePermissionModePrompt: interactivePermissionModePrompt2 } = await Promise.resolve().then(() => (init_prompt(), prompt_exports));
|
|
148212
|
+
resolvedDefaultPermissionMode = await interactivePermissionModePrompt2();
|
|
148213
|
+
setDefaultPermissionMode(resolvedDefaultPermissionMode);
|
|
148214
|
+
}
|
|
147964
148215
|
let resolvedPort = requestedPort;
|
|
147965
148216
|
if (!isProdInvocation) {
|
|
147966
148217
|
const { resolveDevPlan: resolveDevPlan2, detectProdActive: detectProdActive2, isPortInUse: isPortInUse2, findFreePort: findFreePort2, takeoverProd: takeoverProd2 } = await Promise.resolve().then(() => (init_dev_takeover(), dev_takeover_exports));
|
|
@@ -147992,7 +148243,8 @@ program2.command("serve").description("Start the streamer server").option("-p, -
|
|
|
147992
148243
|
verbose: opts.verbose,
|
|
147993
148244
|
logMenubarRequests: opts.logMenubarRequests,
|
|
147994
148245
|
browseRoot: opts.browseRoot,
|
|
147995
|
-
publicUrl: opts.publicUrl
|
|
148246
|
+
publicUrl: opts.publicUrl,
|
|
148247
|
+
defaultPermissionMode: resolvedDefaultPermissionMode
|
|
147996
148248
|
});
|
|
147997
148249
|
await server.listen(resolvedPort);
|
|
147998
148250
|
{
|
|
@@ -148006,13 +148258,21 @@ program2.command("serve").description("Start the streamer server").option("-p, -
|
|
|
148006
148258
|
wsUrl: `ws://localhost:${resolvedPort}/ws`
|
|
148007
148259
|
});
|
|
148008
148260
|
log7.info(`API key: ${apiKey}`, { apiKeyMasked: `${apiKey.slice(0, 6)}\u2026` });
|
|
148009
|
-
|
|
148010
|
-
|
|
148011
|
-
|
|
148012
|
-
|
|
148013
|
-
|
|
148014
|
-
|
|
148015
|
-
}
|
|
148261
|
+
try {
|
|
148262
|
+
await printServerBanner({
|
|
148263
|
+
port: resolvedPort,
|
|
148264
|
+
apiKey,
|
|
148265
|
+
publicUrl,
|
|
148266
|
+
includeQr: opts.pairQr !== false
|
|
148267
|
+
});
|
|
148268
|
+
} catch (err) {
|
|
148269
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
148270
|
+
log7.warn(`(skipped pairing QR: ${message})`, { reason: message });
|
|
148271
|
+
log7.info(
|
|
148272
|
+
printUrlBanner({ url: resolveServerUrl({ publicUrl, port: resolvedPort }) }),
|
|
148273
|
+
void 0,
|
|
148274
|
+
"console"
|
|
148275
|
+
);
|
|
148016
148276
|
}
|
|
148017
148277
|
const shutdown = async () => {
|
|
148018
148278
|
log7.info("Shutting down...");
|
|
@@ -148067,7 +148327,7 @@ program2.command("pair").description("Print a pairing QR code (server must alrea
|
|
|
148067
148327
|
const port = Number.parseInt(opts.port, 10);
|
|
148068
148328
|
const apiKey = loadOrCreateApiKey();
|
|
148069
148329
|
const publicUrl = loadPublicUrl() ?? null;
|
|
148070
|
-
await
|
|
148330
|
+
await printServerBanner({ port, apiKey, publicUrl, includeQr: true });
|
|
148071
148331
|
});
|
|
148072
148332
|
program2.command("set-key [key]").description("Set the streamer API key in ~/.threadbase/server.yaml").action(async (key) => {
|
|
148073
148333
|
const { runSetKey: runSetKey2 } = await Promise.resolve().then(() => (init_setKey(), setKey_exports));
|
|
@@ -148163,11 +148423,49 @@ program2.command("update").description("Check for streamer updates from GitHub R
|
|
|
148163
148423
|
});
|
|
148164
148424
|
registerProdCommands(program2);
|
|
148165
148425
|
program2.parse();
|
|
148166
|
-
|
|
148426
|
+
function generateQr(payload) {
|
|
148427
|
+
return new Promise((resolve6) => {
|
|
148428
|
+
import_qrcode_terminal.default.generate(payload, { small: true }, resolve6);
|
|
148429
|
+
});
|
|
148430
|
+
}
|
|
148431
|
+
function printUrlBanner({
|
|
148432
|
+
url: url2,
|
|
148433
|
+
qr: qr2,
|
|
148434
|
+
expiresAt
|
|
148435
|
+
}) {
|
|
148436
|
+
const contentLines = ["Threadbase Streamer \u2014 server address", "", url2];
|
|
148437
|
+
if (qr2) {
|
|
148438
|
+
contentLines.push("", ...qr2.split("\n").filter((l) => l.length > 0));
|
|
148439
|
+
if (expiresAt !== void 0) {
|
|
148440
|
+
contentLines.push(
|
|
148441
|
+
"",
|
|
148442
|
+
`Scan to pair a mobile client (expires ${new Date(expiresAt).toLocaleTimeString()})`
|
|
148443
|
+
);
|
|
148444
|
+
} else {
|
|
148445
|
+
contentLines.push("", "Scan to pair a mobile client");
|
|
148446
|
+
}
|
|
148447
|
+
}
|
|
148448
|
+
const width = Math.max(...contentLines.map((l) => l.length));
|
|
148449
|
+
const pad = (l) => `\u2551 ${l}${" ".repeat(width - l.length)} \u2551`;
|
|
148450
|
+
const top = `\u2554${"\u2550".repeat(width + 2)}\u2557`;
|
|
148451
|
+
const bottom = `\u255A${"\u2550".repeat(width + 2)}\u255D`;
|
|
148452
|
+
return `
|
|
148453
|
+
${top}
|
|
148454
|
+
${contentLines.map(pad).join("\n")}
|
|
148455
|
+
${bottom}
|
|
148456
|
+
`;
|
|
148457
|
+
}
|
|
148458
|
+
async function printServerBanner({
|
|
148167
148459
|
port,
|
|
148168
148460
|
apiKey,
|
|
148169
|
-
publicUrl
|
|
148461
|
+
publicUrl,
|
|
148462
|
+
includeQr
|
|
148170
148463
|
}) {
|
|
148464
|
+
const url2 = resolveServerUrl({ publicUrl, port });
|
|
148465
|
+
if (!includeQr) {
|
|
148466
|
+
log7.info(printUrlBanner({ url: url2 }), void 0, "console");
|
|
148467
|
+
return;
|
|
148468
|
+
}
|
|
148171
148469
|
const res = await fetch(`http://localhost:${port}/api/pair/start`, {
|
|
148172
148470
|
method: "POST",
|
|
148173
148471
|
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
@@ -148177,19 +148475,12 @@ async function printPairQR({
|
|
|
148177
148475
|
throw new Error(`/api/pair/start returned ${res.status}`);
|
|
148178
148476
|
}
|
|
148179
148477
|
const { token, expiresAt, expiresInSeconds } = await res.json();
|
|
148180
|
-
const url2 = resolveServerUrl({ publicUrl, port });
|
|
148181
148478
|
const expSeconds = Math.floor(expiresAt / 1e3);
|
|
148182
148479
|
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
|
-
);
|
|
148480
|
+
const qr2 = await generateQr(payload);
|
|
148481
|
+
log7.info(printUrlBanner({ url: url2, qr: qr2, expiresAt }), void 0, "console");
|
|
148482
|
+
log7.info(`Pair URL: ${payload}`, void 0, "console");
|
|
148483
|
+
log7.info(`Expires in ${expiresInSeconds}s`, void 0, "console");
|
|
148193
148484
|
}
|
|
148194
148485
|
/*! Bundled license information:
|
|
148195
148486
|
|