@threadbase-sh/streamer 1.47.3 → 1.49.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 +540 -319
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +388 -171
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +385 -168
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.cjs
CHANGED
|
@@ -4637,11 +4637,11 @@ var require_tools = __commonJS({
|
|
|
4637
4637
|
}
|
|
4638
4638
|
}
|
|
4639
4639
|
}
|
|
4640
|
-
function buildFormatters(level, bindings,
|
|
4640
|
+
function buildFormatters(level, bindings, log15) {
|
|
4641
4641
|
return {
|
|
4642
4642
|
level,
|
|
4643
4643
|
bindings,
|
|
4644
|
-
log:
|
|
4644
|
+
log: log15
|
|
4645
4645
|
};
|
|
4646
4646
|
}
|
|
4647
4647
|
function normalizeDestFileDescriptor(destination) {
|
|
@@ -5023,11 +5023,11 @@ var require_proto = __commonJS({
|
|
|
5023
5023
|
}
|
|
5024
5024
|
} else instance[serializersSym] = serializers;
|
|
5025
5025
|
if (options.hasOwnProperty("formatters")) {
|
|
5026
|
-
const { level, bindings: chindings, log:
|
|
5026
|
+
const { level, bindings: chindings, log: log15 } = options.formatters;
|
|
5027
5027
|
instance[formattersSym] = buildFormatters(
|
|
5028
5028
|
level || formatters.level,
|
|
5029
5029
|
chindings || resetChildingsFormatter,
|
|
5030
|
-
|
|
5030
|
+
log15 || formatters.log
|
|
5031
5031
|
);
|
|
5032
5032
|
} else {
|
|
5033
5033
|
instance[formattersSym] = buildFormatters(
|
|
@@ -8206,7 +8206,7 @@ var require_merge = __commonJS({
|
|
|
8206
8206
|
var require_addPairToJSMap = __commonJS({
|
|
8207
8207
|
"node_modules/yaml/dist/nodes/addPairToJSMap.js"(exports2) {
|
|
8208
8208
|
"use strict";
|
|
8209
|
-
var
|
|
8209
|
+
var log15 = require_log();
|
|
8210
8210
|
var merge2 = require_merge();
|
|
8211
8211
|
var stringify = require_stringify();
|
|
8212
8212
|
var identity = require_identity();
|
|
@@ -8255,7 +8255,7 @@ var require_addPairToJSMap = __commonJS({
|
|
|
8255
8255
|
let jsonStr = JSON.stringify(strKey);
|
|
8256
8256
|
if (jsonStr.length > 40)
|
|
8257
8257
|
jsonStr = jsonStr.substring(0, 36) + '..."';
|
|
8258
|
-
|
|
8258
|
+
log15.warn(ctx.doc.options.logLevel, `Keys with collection values will be stringified due to JS Object restrictions: ${jsonStr}. Set mapAsMap: true to use object keys.`);
|
|
8259
8259
|
ctx.mapKeyWarned = true;
|
|
8260
8260
|
}
|
|
8261
8261
|
return strKey;
|
|
@@ -13671,7 +13671,7 @@ var require_public_api = __commonJS({
|
|
|
13671
13671
|
var composer = require_composer();
|
|
13672
13672
|
var Document = require_Document();
|
|
13673
13673
|
var errors = require_errors();
|
|
13674
|
-
var
|
|
13674
|
+
var log15 = require_log();
|
|
13675
13675
|
var identity = require_identity();
|
|
13676
13676
|
var lineCounter = require_line_counter();
|
|
13677
13677
|
var parser = require_parser();
|
|
@@ -13723,7 +13723,7 @@ var require_public_api = __commonJS({
|
|
|
13723
13723
|
const doc = parseDocument(src, options);
|
|
13724
13724
|
if (!doc)
|
|
13725
13725
|
return null;
|
|
13726
|
-
doc.warnings.forEach((warning) =>
|
|
13726
|
+
doc.warnings.forEach((warning) => log15.warn(doc.options.logLevel, warning));
|
|
13727
13727
|
if (doc.errors.length > 0) {
|
|
13728
13728
|
if (doc.options.logLevel !== "silent")
|
|
13729
13729
|
throw doc.errors[0];
|
|
@@ -37243,7 +37243,7 @@ var require_logging = __commonJS({
|
|
|
37243
37243
|
_logVerbosity = verbosity;
|
|
37244
37244
|
};
|
|
37245
37245
|
exports2.setLoggerVerbosity = setLoggerVerbosity;
|
|
37246
|
-
var
|
|
37246
|
+
var log15 = (severity, ...args) => {
|
|
37247
37247
|
let logFunction;
|
|
37248
37248
|
if (severity >= _logVerbosity) {
|
|
37249
37249
|
switch (severity) {
|
|
@@ -37265,7 +37265,7 @@ var require_logging = __commonJS({
|
|
|
37265
37265
|
}
|
|
37266
37266
|
}
|
|
37267
37267
|
};
|
|
37268
|
-
exports2.log =
|
|
37268
|
+
exports2.log = log15;
|
|
37269
37269
|
var tracersString = (_d = (_c = process.env.GRPC_NODE_TRACE) !== null && _c !== void 0 ? _c : process.env.GRPC_TRACE) !== null && _d !== void 0 ? _d : "";
|
|
37270
37270
|
var enabledTracers = /* @__PURE__ */ new Set();
|
|
37271
37271
|
var disabledTracers = /* @__PURE__ */ new Set();
|
|
@@ -127026,7 +127026,7 @@ function readMarker() {
|
|
|
127026
127026
|
const parsed = JSON.parse(raw2);
|
|
127027
127027
|
return MarkerSchema.parse(parsed);
|
|
127028
127028
|
} catch (err) {
|
|
127029
|
-
|
|
127029
|
+
log10.warn(`marker at ${markerPath()} is malformed; treating as absent`, {
|
|
127030
127030
|
error: err instanceof Error ? err.message : String(err)
|
|
127031
127031
|
});
|
|
127032
127032
|
return null;
|
|
@@ -127043,7 +127043,7 @@ function writeMarker(marker) {
|
|
|
127043
127043
|
function clearMarker() {
|
|
127044
127044
|
if ((0, import_node_fs22.existsSync)(markerPath())) (0, import_node_fs22.rmSync)(markerPath());
|
|
127045
127045
|
}
|
|
127046
|
-
var import_node_fs22, import_node_path25,
|
|
127046
|
+
var import_node_fs22, import_node_path25, log10;
|
|
127047
127047
|
var init_marker = __esm({
|
|
127048
127048
|
"src/lifecycle/marker.ts"() {
|
|
127049
127049
|
"use strict";
|
|
@@ -127052,7 +127052,7 @@ var init_marker = __esm({
|
|
|
127052
127052
|
init_logger();
|
|
127053
127053
|
init_constants();
|
|
127054
127054
|
init_marker_schema();
|
|
127055
|
-
|
|
127055
|
+
log10 = getLogger("lifecycle.marker");
|
|
127056
127056
|
}
|
|
127057
127057
|
});
|
|
127058
127058
|
|
|
@@ -127292,7 +127292,7 @@ function readPrefs() {
|
|
|
127292
127292
|
try {
|
|
127293
127293
|
return PrefsSchema.parse(JSON.parse((0, import_node_fs25.readFileSync)(path2, "utf8")));
|
|
127294
127294
|
} catch (err) {
|
|
127295
|
-
|
|
127295
|
+
log12.warn(`prefs at ${path2} are malformed; treating as empty`, {
|
|
127296
127296
|
error: err instanceof Error ? err.message : String(err)
|
|
127297
127297
|
});
|
|
127298
127298
|
return { repos: {} };
|
|
@@ -127327,7 +127327,7 @@ function forgetAll() {
|
|
|
127327
127327
|
const path2 = prefsPath();
|
|
127328
127328
|
if ((0, import_node_fs25.existsSync)(path2)) (0, import_node_fs25.rmSync)(path2);
|
|
127329
127329
|
}
|
|
127330
|
-
var import_node_fs25, import_node_path26,
|
|
127330
|
+
var import_node_fs25, import_node_path26, log12, PrefsSchema;
|
|
127331
127331
|
var init_prefs = __esm({
|
|
127332
127332
|
"src/lifecycle/prefs.ts"() {
|
|
127333
127333
|
"use strict";
|
|
@@ -127336,7 +127336,7 @@ var init_prefs = __esm({
|
|
|
127336
127336
|
init_zod();
|
|
127337
127337
|
init_logger();
|
|
127338
127338
|
init_constants();
|
|
127339
|
-
|
|
127339
|
+
log12 = getLogger("lifecycle.prefs");
|
|
127340
127340
|
PrefsSchema = external_exports.object({
|
|
127341
127341
|
repos: external_exports.record(
|
|
127342
127342
|
external_exports.string(),
|
|
@@ -127439,7 +127439,7 @@ function takeoverProd(opts) {
|
|
|
127439
127439
|
`prod is already suspended by dev pid ${existing.devPid} (since ${existing.suspendedAt}). Stop that dev session first, or run 'tb-streamer prod doctor'.`
|
|
127440
127440
|
);
|
|
127441
127441
|
}
|
|
127442
|
-
|
|
127442
|
+
log13.info(`stale marker found (pid ${existing.devPid} is gone) \u2014 clearing and proceeding`);
|
|
127443
127443
|
}
|
|
127444
127444
|
getSupervisor().bootoutAgent();
|
|
127445
127445
|
writeMarker({
|
|
@@ -127454,7 +127454,7 @@ function takeoverProd(opts) {
|
|
|
127454
127454
|
const m2 = readMarker();
|
|
127455
127455
|
if (m2 && m2.devPid === process.pid) {
|
|
127456
127456
|
writeMarker({ ...m2, userHeld: true });
|
|
127457
|
-
|
|
127457
|
+
log13.info(
|
|
127458
127458
|
`prod is suspended (userHeld). Run 'tb-streamer prod start' to restore the supervised instance.`
|
|
127459
127459
|
);
|
|
127460
127460
|
}
|
|
@@ -127473,14 +127473,14 @@ function takeoverProd(opts) {
|
|
|
127473
127473
|
});
|
|
127474
127474
|
process.on("uncaughtException", (err) => {
|
|
127475
127475
|
flipUserHeld();
|
|
127476
|
-
|
|
127476
|
+
log13.error(`uncaught: ${err.message}`);
|
|
127477
127477
|
process.exit(1);
|
|
127478
127478
|
});
|
|
127479
127479
|
process.on("exit", () => {
|
|
127480
127480
|
flipUserHeld();
|
|
127481
127481
|
});
|
|
127482
127482
|
}
|
|
127483
|
-
var import_node_net2,
|
|
127483
|
+
var import_node_net2, log13;
|
|
127484
127484
|
var init_dev_takeover = __esm({
|
|
127485
127485
|
"src/lifecycle/dev-takeover.ts"() {
|
|
127486
127486
|
"use strict";
|
|
@@ -127489,7 +127489,7 @@ var init_dev_takeover = __esm({
|
|
|
127489
127489
|
init_marker();
|
|
127490
127490
|
init_platform2();
|
|
127491
127491
|
init_prefs();
|
|
127492
|
-
|
|
127492
|
+
log13 = getLogger("lifecycle.dev-takeover");
|
|
127493
127493
|
}
|
|
127494
127494
|
});
|
|
127495
127495
|
|
|
@@ -135584,7 +135584,7 @@ var REF_PREFIX = "ref: refs/heads/";
|
|
|
135584
135584
|
var MAX_DEPTH = 6;
|
|
135585
135585
|
function readGitBranch(projectPath) {
|
|
135586
135586
|
if (!projectPath) return null;
|
|
135587
|
-
const
|
|
135587
|
+
const log15 = getLogger2();
|
|
135588
135588
|
let dir = projectPath;
|
|
135589
135589
|
let depth = 0;
|
|
135590
135590
|
while (depth < MAX_DEPTH) {
|
|
@@ -135593,11 +135593,11 @@ function readGitBranch(projectPath) {
|
|
|
135593
135593
|
const content = (0, import_fs2.readFileSync)(headPath, "utf-8").trim();
|
|
135594
135594
|
if (content.startsWith(REF_PREFIX)) {
|
|
135595
135595
|
const branch = content.slice(REF_PREFIX.length);
|
|
135596
|
-
|
|
135596
|
+
log15.trace({ projectPath, dir, branch }, "git: branch resolved");
|
|
135597
135597
|
return branch;
|
|
135598
135598
|
}
|
|
135599
135599
|
if (content.length >= 7) {
|
|
135600
|
-
|
|
135600
|
+
log15.trace({ projectPath, dir }, "git: detached HEAD");
|
|
135601
135601
|
return "(detached)";
|
|
135602
135602
|
}
|
|
135603
135603
|
return null;
|
|
@@ -135608,7 +135608,7 @@ function readGitBranch(projectPath) {
|
|
|
135608
135608
|
dir = parent;
|
|
135609
135609
|
depth++;
|
|
135610
135610
|
}
|
|
135611
|
-
|
|
135611
|
+
log15.trace({ projectPath }, "git: no .git found within depth");
|
|
135612
135612
|
return null;
|
|
135613
135613
|
}
|
|
135614
135614
|
function generateMatches(meta3, query) {
|
|
@@ -135901,8 +135901,8 @@ function cleanSystemTags(text) {
|
|
|
135901
135901
|
return text.replace(SYSTEM_TAG_RE, "").replace(/[^\S\n]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
|
|
135902
135902
|
}
|
|
135903
135903
|
async function parseMeta(filePath, account, tier) {
|
|
135904
|
-
const
|
|
135905
|
-
|
|
135904
|
+
const log15 = getLogger2();
|
|
135905
|
+
log15.trace({ filePath, account, tier: tier.name }, "parseMeta: start");
|
|
135906
135906
|
const state = initialReducerState();
|
|
135907
135907
|
const fileStream = (0, import_fs3.createReadStream)(filePath);
|
|
135908
135908
|
const rl = (0, import_readline.createInterface)({ input: fileStream, crlfDelay: Infinity });
|
|
@@ -135919,22 +135919,22 @@ async function parseMeta(filePath, account, tier) {
|
|
|
135919
135919
|
reduceLine(state, entry, tier);
|
|
135920
135920
|
}
|
|
135921
135921
|
} catch (err) {
|
|
135922
|
-
|
|
135922
|
+
log15.warn({ filePath, err }, "parseMeta: read failed");
|
|
135923
135923
|
return null;
|
|
135924
135924
|
}
|
|
135925
135925
|
if (state.badJsonLines > 0) {
|
|
135926
|
-
|
|
135926
|
+
log15.warn(
|
|
135927
135927
|
{ filePath, badJsonLines: state.badJsonLines },
|
|
135928
135928
|
"parseMeta: skipped malformed JSON lines"
|
|
135929
135929
|
);
|
|
135930
135930
|
}
|
|
135931
135931
|
const meta3 = finalizeMeta(state, filePath, account, tier);
|
|
135932
|
-
if (!meta3)
|
|
135932
|
+
if (!meta3) log15.trace({ filePath }, "parseMeta: no messages");
|
|
135933
135933
|
return meta3;
|
|
135934
135934
|
}
|
|
135935
135935
|
async function parseConversation(filePath, account) {
|
|
135936
|
-
const
|
|
135937
|
-
|
|
135936
|
+
const log15 = getLogger2();
|
|
135937
|
+
log15.trace({ filePath, account }, "parseConversation: start");
|
|
135938
135938
|
const messages = [];
|
|
135939
135939
|
let badJsonLines = 0;
|
|
135940
135940
|
const textParts = [];
|
|
@@ -135967,17 +135967,17 @@ async function parseConversation(filePath, account) {
|
|
|
135967
135967
|
}
|
|
135968
135968
|
}
|
|
135969
135969
|
} catch (err) {
|
|
135970
|
-
|
|
135970
|
+
log15.warn({ filePath, err }, "parseConversation: read failed");
|
|
135971
135971
|
return null;
|
|
135972
135972
|
}
|
|
135973
135973
|
if (badJsonLines > 0) {
|
|
135974
|
-
|
|
135974
|
+
log15.warn({ filePath, badJsonLines }, "parseConversation: skipped malformed JSON lines");
|
|
135975
135975
|
}
|
|
135976
135976
|
if (messages.length === 0) {
|
|
135977
|
-
|
|
135977
|
+
log15.trace({ filePath }, "parseConversation: no messages");
|
|
135978
135978
|
return null;
|
|
135979
135979
|
}
|
|
135980
|
-
|
|
135980
|
+
log15.debug({ filePath, messageCount: messages.length }, "parseConversation: complete");
|
|
135981
135981
|
applyTeamInfo(messages, state);
|
|
135982
135982
|
return {
|
|
135983
135983
|
id: filePath,
|
|
@@ -136242,15 +136242,15 @@ async function detectDefaultProfile() {
|
|
|
136242
136242
|
};
|
|
136243
136243
|
}
|
|
136244
136244
|
async function loadProfiles(configPath) {
|
|
136245
|
-
const
|
|
136245
|
+
const log15 = getLogger2();
|
|
136246
136246
|
try {
|
|
136247
136247
|
const resolved = resolveConfigDir(configPath);
|
|
136248
136248
|
const data = await (0, import_promises4.readFile)((0, import_path5.join)(resolved, PROFILES_FILE), "utf-8");
|
|
136249
136249
|
const profiles = JSON.parse(data);
|
|
136250
|
-
|
|
136250
|
+
log15.debug({ configPath, count: profiles.length }, "profiles: loaded");
|
|
136251
136251
|
return profiles;
|
|
136252
136252
|
} catch (err) {
|
|
136253
|
-
|
|
136253
|
+
log15.debug({ configPath, err }, "profiles: load failed, using default");
|
|
136254
136254
|
const defaultProfile = await detectDefaultProfile();
|
|
136255
136255
|
return [defaultProfile];
|
|
136256
136256
|
}
|
|
@@ -136261,7 +136261,7 @@ function canonicalPath(p2) {
|
|
|
136261
136261
|
var CodexCliProvider = class {
|
|
136262
136262
|
name = CODEX_CLI_PROVIDER;
|
|
136263
136263
|
async discover(roots) {
|
|
136264
|
-
const
|
|
136264
|
+
const log15 = getLogger2();
|
|
136265
136265
|
const results = [];
|
|
136266
136266
|
for (const root of roots) {
|
|
136267
136267
|
let paths;
|
|
@@ -136273,7 +136273,7 @@ var CodexCliProvider = class {
|
|
|
136273
136273
|
unique: true
|
|
136274
136274
|
});
|
|
136275
136275
|
} catch (err) {
|
|
136276
|
-
|
|
136276
|
+
log15.warn({ root, err }, "codex discovery: glob failed");
|
|
136277
136277
|
continue;
|
|
136278
136278
|
}
|
|
136279
136279
|
for (const filePath of paths) {
|
|
@@ -136281,7 +136281,7 @@ var CodexCliProvider = class {
|
|
|
136281
136281
|
const s3 = await (0, import_promises5.stat)(filePath);
|
|
136282
136282
|
if (s3.size > 0) results.push({ filePath: canonicalPath(filePath), account: "codex" });
|
|
136283
136283
|
} catch (err) {
|
|
136284
|
-
|
|
136284
|
+
log15.warn({ filePath, err }, "codex discovery: stat failed");
|
|
136285
136285
|
}
|
|
136286
136286
|
}
|
|
136287
136287
|
}
|
|
@@ -136425,7 +136425,7 @@ function getShortProjectName3(fullPath) {
|
|
|
136425
136425
|
return fullPath.split("/").filter(Boolean).slice(-3).join("/");
|
|
136426
136426
|
}
|
|
136427
136427
|
async function parseCodexConversation(filePath, account) {
|
|
136428
|
-
const
|
|
136428
|
+
const log15 = getLogger2();
|
|
136429
136429
|
const messages = [];
|
|
136430
136430
|
const textParts = [];
|
|
136431
136431
|
let sessionId = "";
|
|
@@ -136461,7 +136461,7 @@ async function parseCodexConversation(filePath, account) {
|
|
|
136461
136461
|
if (role === "user") lastUserText = text;
|
|
136462
136462
|
}
|
|
136463
136463
|
} catch (err) {
|
|
136464
|
-
|
|
136464
|
+
log15.warn({ filePath, err }, "parseCodexConversation: read failed");
|
|
136465
136465
|
return null;
|
|
136466
136466
|
}
|
|
136467
136467
|
if (messages.length === 0) return null;
|
|
@@ -136483,7 +136483,7 @@ async function parseCodexConversation(filePath, account) {
|
|
|
136483
136483
|
var EXCLUDED_SEGMENTS = ["/memory/", "/tool-results/"];
|
|
136484
136484
|
var STAT_CONCURRENCY = 32;
|
|
136485
136485
|
async function discoverJsonlFiles(dirs, onProgress) {
|
|
136486
|
-
const
|
|
136486
|
+
const log15 = getLogger2();
|
|
136487
136487
|
const results = [];
|
|
136488
136488
|
for (const { projectsDir, account } of dirs) {
|
|
136489
136489
|
let filePaths;
|
|
@@ -136494,7 +136494,7 @@ async function discoverJsonlFiles(dirs, onProgress) {
|
|
|
136494
136494
|
dot: false
|
|
136495
136495
|
});
|
|
136496
136496
|
} catch (err) {
|
|
136497
|
-
|
|
136497
|
+
log15.warn({ projectsDir, account, err }, "discovery: glob failed");
|
|
136498
136498
|
continue;
|
|
136499
136499
|
}
|
|
136500
136500
|
const filtered = filePaths.filter((fp) => !EXCLUDED_SEGMENTS.some((seg) => fp.includes(seg)));
|
|
@@ -136509,7 +136509,7 @@ async function discoverJsonlFiles(dirs, onProgress) {
|
|
|
136509
136509
|
const s3 = await (0, import_promises6.stat)(filePath);
|
|
136510
136510
|
return { filePath, size: s3.size };
|
|
136511
136511
|
} catch (err) {
|
|
136512
|
-
|
|
136512
|
+
log15.warn({ filePath, err }, "discovery: stat failed");
|
|
136513
136513
|
return { filePath, size: -1 };
|
|
136514
136514
|
}
|
|
136515
136515
|
})
|
|
@@ -136525,7 +136525,7 @@ async function discoverJsonlFiles(dirs, onProgress) {
|
|
|
136525
136525
|
}
|
|
136526
136526
|
}
|
|
136527
136527
|
}
|
|
136528
|
-
|
|
136528
|
+
log15.debug(
|
|
136529
136529
|
{
|
|
136530
136530
|
projectsDir,
|
|
136531
136531
|
account,
|
|
@@ -136539,7 +136539,7 @@ async function discoverJsonlFiles(dirs, onProgress) {
|
|
|
136539
136539
|
);
|
|
136540
136540
|
onProgress?.(results.length);
|
|
136541
136541
|
}
|
|
136542
|
-
|
|
136542
|
+
log15.debug({ totalFiles: results.length, dirs: dirs.length }, "discovery: complete");
|
|
136543
136543
|
return results;
|
|
136544
136544
|
}
|
|
136545
136545
|
var ThreadbaseProvider = class {
|
|
@@ -136849,7 +136849,7 @@ function classify(filePath, existing) {
|
|
|
136849
136849
|
return { change: "reindex", stat: stat42 };
|
|
136850
136850
|
}
|
|
136851
136851
|
async function parseMetaWithProvider(provider, filePath, account, tier) {
|
|
136852
|
-
const
|
|
136852
|
+
const log15 = getLogger2();
|
|
136853
136853
|
const acc = provider.createEmptyAccumulator();
|
|
136854
136854
|
const rl = (0, import_readline3.createInterface)({
|
|
136855
136855
|
input: (0, import_fs11.createReadStream)(filePath),
|
|
@@ -136867,11 +136867,11 @@ async function parseMetaWithProvider(provider, filePath, account, tier) {
|
|
|
136867
136867
|
try {
|
|
136868
136868
|
provider.reduceEntry(acc, entry, tier);
|
|
136869
136869
|
} catch (err) {
|
|
136870
|
-
|
|
136870
|
+
log15.warn({ filePath, provider: provider.name, err }, "provider reduce threw; line skipped");
|
|
136871
136871
|
}
|
|
136872
136872
|
}
|
|
136873
136873
|
} catch (err) {
|
|
136874
|
-
|
|
136874
|
+
log15.warn({ filePath, provider: provider.name, err }, "provider parse: read failed");
|
|
136875
136875
|
return null;
|
|
136876
136876
|
}
|
|
136877
136877
|
return provider.finalize(acc, filePath, account, tier);
|
|
@@ -137045,8 +137045,8 @@ CREATE INDEX IF NOT EXISTS idx_scanned_dirs_parent_root ON scanned_dirs(parent_r
|
|
|
137045
137045
|
function runMigrations(db) {
|
|
137046
137046
|
const current = db.pragma("user_version", { simple: true });
|
|
137047
137047
|
if (current >= SCHEMA_VERSION) return;
|
|
137048
|
-
const
|
|
137049
|
-
|
|
137048
|
+
const log15 = getLogger2();
|
|
137049
|
+
log15.info({ from: current, to: SCHEMA_VERSION }, "migrations: applying");
|
|
137050
137050
|
if (current >= 1 && current < 2 && tableExists(db, "conversations")) {
|
|
137051
137051
|
for (const [col, ddl] of [
|
|
137052
137052
|
[
|
|
@@ -137088,7 +137088,7 @@ function openDatabase(dbPath) {
|
|
|
137088
137088
|
}
|
|
137089
137089
|
var FULL_RECONCILE_EVERY_N_SCANS = 20;
|
|
137090
137090
|
async function discoverJsonlFilesGated(dirs, files, scannedDirs, options = {}) {
|
|
137091
|
-
const
|
|
137091
|
+
const log15 = getLogger2();
|
|
137092
137092
|
if (options.fullRescan) {
|
|
137093
137093
|
return discoverJsonlFiles(dirs);
|
|
137094
137094
|
}
|
|
@@ -137130,7 +137130,7 @@ async function discoverJsonlFilesGated(dirs, files, scannedDirs, options = {}) {
|
|
|
137130
137130
|
}
|
|
137131
137131
|
}
|
|
137132
137132
|
}
|
|
137133
|
-
|
|
137133
|
+
log15.debug(
|
|
137134
137134
|
{ totalFiles: results.length, dirs: dirs.length },
|
|
137135
137135
|
"dir-watermark: gated discovery complete"
|
|
137136
137136
|
);
|
|
@@ -137636,7 +137636,7 @@ var PersistentEngine = class {
|
|
|
137636
137636
|
// changed since the last index, and upsert their metadata. Returns the number
|
|
137637
137637
|
// of files seen on disk this pass (the scan "scanned" count).
|
|
137638
137638
|
async indexAll(activeProfiles, options) {
|
|
137639
|
-
const
|
|
137639
|
+
const log15 = getLogger2();
|
|
137640
137640
|
const tier = resolveTier(options.tier ?? "standard", options.tiers);
|
|
137641
137641
|
const enabled = options.providers ?? [CLAUDE_CODE_PROVIDER];
|
|
137642
137642
|
const discovered = [];
|
|
@@ -137700,7 +137700,7 @@ var PersistentEngine = class {
|
|
|
137700
137700
|
for (const path2 of this.files.activePathsByAccounts([...coveredAccounts])) {
|
|
137701
137701
|
if (!seen.has(path2)) this.markDeleted(path2);
|
|
137702
137702
|
}
|
|
137703
|
-
|
|
137703
|
+
log15.info({ scanned, indexed: this.conversations.count() }, "persistent: indexAll complete");
|
|
137704
137704
|
return { scanned };
|
|
137705
137705
|
}
|
|
137706
137706
|
// (Re)index a single file. Classifies the change vs. the persisted cursor:
|
|
@@ -137711,7 +137711,7 @@ var PersistentEngine = class {
|
|
|
137711
137711
|
// alongside the meta so callers (refreshFile) can keep, extend, or evict
|
|
137712
137712
|
// their own per-file caches without re-stat'ing the file (racy) themselves.
|
|
137713
137713
|
async indexFile(rawFilePath, account, tierName, customTiers, resolveGitBranch, force = false, provider) {
|
|
137714
|
-
const
|
|
137714
|
+
const log15 = getLogger2();
|
|
137715
137715
|
const tier = resolveTier(tierName, customTiers);
|
|
137716
137716
|
const filePath = canonicalPath(rawFilePath);
|
|
137717
137717
|
const existing = this.files.getByPath(filePath);
|
|
@@ -137742,7 +137742,7 @@ var PersistentEngine = class {
|
|
|
137742
137742
|
try {
|
|
137743
137743
|
result = await tailReduce(filePath, startOffset, startLine, state, tier);
|
|
137744
137744
|
} catch (err) {
|
|
137745
|
-
|
|
137745
|
+
log15.warn({ filePath, err }, "persistent: tail read failed");
|
|
137746
137746
|
return { meta: null, change };
|
|
137747
137747
|
}
|
|
137748
137748
|
const meta3 = finalizeMeta(state, filePath, account, tier);
|
|
@@ -137785,7 +137785,7 @@ var PersistentEngine = class {
|
|
|
137785
137785
|
)
|
|
137786
137786
|
);
|
|
137787
137787
|
}
|
|
137788
|
-
|
|
137788
|
+
log15.debug(
|
|
137789
137789
|
{ filePath, change, bytesRead: result.newOffset - startOffset, msgs: meta3.messageCount },
|
|
137790
137790
|
"persistent: indexed file"
|
|
137791
137791
|
);
|
|
@@ -137797,7 +137797,7 @@ var PersistentEngine = class {
|
|
|
137797
137797
|
// so the next pass classifies an unchanged file as "unchanged" and skips it;
|
|
137798
137798
|
// any change reparses from 0 again. No reducer_state is persisted.
|
|
137799
137799
|
async indexFileWithProvider(provider, filePath, account, tier, stat42, resolveGitBranch) {
|
|
137800
|
-
const
|
|
137800
|
+
const log15 = getLogger2();
|
|
137801
137801
|
const meta3 = await parseMetaWithProvider(provider, filePath, account, tier);
|
|
137802
137802
|
if (!meta3) {
|
|
137803
137803
|
this.markDeleted(filePath);
|
|
@@ -137825,7 +137825,7 @@ var PersistentEngine = class {
|
|
|
137825
137825
|
});
|
|
137826
137826
|
});
|
|
137827
137827
|
upsert();
|
|
137828
|
-
|
|
137828
|
+
log15.debug(
|
|
137829
137829
|
{ filePath, provider: provider.name, msgs: meta3.messageCount },
|
|
137830
137830
|
"persistent: indexed provider file"
|
|
137831
137831
|
);
|
|
@@ -137939,7 +137939,7 @@ var FileWatcher = class {
|
|
|
137939
137939
|
// Resolves once every underlying chokidar watcher has finished its initial
|
|
137940
137940
|
// scan, so the caller knows subsequent FS changes will be observed.
|
|
137941
137941
|
async start() {
|
|
137942
|
-
const
|
|
137942
|
+
const log15 = getLogger2();
|
|
137943
137943
|
const ready = [];
|
|
137944
137944
|
for (const profile of this.profiles) {
|
|
137945
137945
|
const dir = getProjectsDir(profile);
|
|
@@ -137951,7 +137951,7 @@ var FileWatcher = class {
|
|
|
137951
137951
|
watcher.on("add", (p2) => this.dispatch(p2, profile.id, "add")).on("change", (p2) => this.dispatch(p2, profile.id, "change")).on("unlink", (p2) => this.dispatch(p2, profile.id, "unlink"));
|
|
137952
137952
|
ready.push(new Promise((resolve4) => watcher.once("ready", () => resolve4())));
|
|
137953
137953
|
this.watchers.push(watcher);
|
|
137954
|
-
|
|
137954
|
+
log15.debug({ dir, account: profile.id }, "watcher: watching");
|
|
137955
137955
|
}
|
|
137956
137956
|
await Promise.all(ready);
|
|
137957
137957
|
}
|
|
@@ -137998,14 +137998,14 @@ var IndexQueue = class {
|
|
|
137998
137998
|
async drain() {
|
|
137999
137999
|
if (this.running) return;
|
|
138000
138000
|
this.running = true;
|
|
138001
|
-
const
|
|
138001
|
+
const log15 = getLogger2();
|
|
138002
138002
|
while (this.pending.size > 0) {
|
|
138003
138003
|
const [path2, job] = this.pending.entries().next().value;
|
|
138004
138004
|
this.pending.delete(path2);
|
|
138005
138005
|
try {
|
|
138006
138006
|
await this.process(job);
|
|
138007
138007
|
} catch (err) {
|
|
138008
|
-
|
|
138008
|
+
log15.warn({ filePath: job.filePath, err }, "index-queue: job failed");
|
|
138009
138009
|
}
|
|
138010
138010
|
}
|
|
138011
138011
|
this.running = false;
|
|
@@ -138114,13 +138114,13 @@ var ConversationScanner = class {
|
|
|
138114
138114
|
// active metas and run the identical filter/sort/view/paginate pipeline as
|
|
138115
138115
|
// the in-memory path — guaranteeing an identical ScanResult shape.
|
|
138116
138116
|
async scanPersistent(activeProfiles, options) {
|
|
138117
|
-
const
|
|
138117
|
+
const log15 = getLogger2();
|
|
138118
138118
|
const startedAt = Date.now();
|
|
138119
138119
|
const engine = this.engine();
|
|
138120
138120
|
const { scanned } = await engine.indexAll(activeProfiles, options);
|
|
138121
138121
|
const allMetas = engine.allActive();
|
|
138122
138122
|
const { conversations, total } = this.finalize(allMetas, options);
|
|
138123
|
-
|
|
138123
|
+
log15.info(
|
|
138124
138124
|
{ scanned, kept: allMetas.length, filteredTotal: total, elapsedMs: Date.now() - startedAt },
|
|
138125
138125
|
"scan: complete (persistent)"
|
|
138126
138126
|
);
|
|
@@ -138147,10 +138147,10 @@ var ConversationScanner = class {
|
|
|
138147
138147
|
return { conversations, total };
|
|
138148
138148
|
}
|
|
138149
138149
|
async scanInMemory(activeProfiles, options) {
|
|
138150
|
-
const
|
|
138150
|
+
const log15 = getLogger2();
|
|
138151
138151
|
const startedAt = Date.now();
|
|
138152
138152
|
const tier = this.lastTier;
|
|
138153
|
-
|
|
138153
|
+
log15.info(
|
|
138154
138154
|
{
|
|
138155
138155
|
activeProfiles: activeProfiles.length,
|
|
138156
138156
|
tier: tier.name,
|
|
@@ -138204,7 +138204,7 @@ var ConversationScanner = class {
|
|
|
138204
138204
|
return meta3;
|
|
138205
138205
|
} catch (err) {
|
|
138206
138206
|
parseFailures++;
|
|
138207
|
-
|
|
138207
|
+
log15.warn({ filePath, account, provider: provider.name, err }, "scan: parse threw");
|
|
138208
138208
|
return null;
|
|
138209
138209
|
}
|
|
138210
138210
|
})
|
|
@@ -138224,12 +138224,12 @@ var ConversationScanner = class {
|
|
|
138224
138224
|
options.onBatch?.(batchMetas);
|
|
138225
138225
|
}
|
|
138226
138226
|
scanned += batch.length;
|
|
138227
|
-
|
|
138227
|
+
log15.debug({ scanned, totalFiles, batchKept: batchMetas.length }, "scan: batch complete");
|
|
138228
138228
|
options.onProgress?.(scanned, totalFiles);
|
|
138229
138229
|
}
|
|
138230
138230
|
const { conversations, total } = this.finalize(allMetas, options);
|
|
138231
138231
|
const elapsedMs = Date.now() - startedAt;
|
|
138232
|
-
|
|
138232
|
+
log15.info(
|
|
138233
138233
|
{
|
|
138234
138234
|
totalFiles,
|
|
138235
138235
|
scanned,
|
|
@@ -138243,13 +138243,13 @@ var ConversationScanner = class {
|
|
|
138243
138243
|
return { conversations, total, scanned };
|
|
138244
138244
|
}
|
|
138245
138245
|
async search(query, options = {}) {
|
|
138246
|
-
const
|
|
138247
|
-
|
|
138246
|
+
const log15 = getLogger2();
|
|
138247
|
+
log15.debug({ query, indexSize: this.indexer.getDocumentCount() }, "search: start");
|
|
138248
138248
|
let results;
|
|
138249
138249
|
if (this.persistent) {
|
|
138250
138250
|
const engine = this.engine();
|
|
138251
138251
|
if (engine.conversations.count() === 0) {
|
|
138252
|
-
|
|
138252
|
+
log15.debug("search: persistent index empty, triggering scan");
|
|
138253
138253
|
const profiles = await this.resolveProfiles(options.profiles);
|
|
138254
138254
|
const activeProfiles = profiles.filter((p2) => p2.enabled && p2.scanHistory !== false);
|
|
138255
138255
|
await engine.indexAll(activeProfiles, { ...options, limit: void 0, offset: void 0 });
|
|
@@ -138262,7 +138262,7 @@ var ConversationScanner = class {
|
|
|
138262
138262
|
}));
|
|
138263
138263
|
} else {
|
|
138264
138264
|
if (this.indexer.getDocumentCount() === 0) {
|
|
138265
|
-
|
|
138265
|
+
log15.debug("search: index empty, triggering scan");
|
|
138266
138266
|
await this.scan({ ...options, limit: void 0, offset: void 0 });
|
|
138267
138267
|
}
|
|
138268
138268
|
results = this.indexer.search(query, {
|
|
@@ -138305,23 +138305,23 @@ var ConversationScanner = class {
|
|
|
138305
138305
|
const limit = options.limit ?? 50;
|
|
138306
138306
|
const offset = options.offset ?? 0;
|
|
138307
138307
|
const sliced = results.slice(offset, offset + limit);
|
|
138308
|
-
|
|
138308
|
+
log15.debug({ query, matched: results.length, returned: sliced.length }, "search: complete");
|
|
138309
138309
|
return sliced;
|
|
138310
138310
|
}
|
|
138311
138311
|
async getConversation(id, _options) {
|
|
138312
|
-
const
|
|
138312
|
+
const log15 = getLogger2();
|
|
138313
138313
|
const cid = canonicalPath(id);
|
|
138314
138314
|
const cached4 = this.conversationLRU.get(cid);
|
|
138315
138315
|
if (cached4) {
|
|
138316
|
-
|
|
138316
|
+
log15.debug({ id }, "getConversation: cache hit");
|
|
138317
138317
|
return cached4.conversation;
|
|
138318
138318
|
}
|
|
138319
138319
|
const meta3 = this.persistent ? this.engine().getByIdOrSession(cid) : this.metadataCache.get(cid) ?? this.resolveSessionId(cid);
|
|
138320
138320
|
if (!meta3) {
|
|
138321
|
-
|
|
138321
|
+
log15.debug({ id }, "getConversation: not found in metadata");
|
|
138322
138322
|
return null;
|
|
138323
138323
|
}
|
|
138324
|
-
|
|
138324
|
+
log15.debug({ id, filePath: meta3.filePath }, "getConversation: cache miss, parsing");
|
|
138325
138325
|
try {
|
|
138326
138326
|
if (this.persistent && meta3.provider !== CODEX_CLI_PROVIDER) {
|
|
138327
138327
|
const parsed = await parseConversationResumable(meta3.filePath, meta3.account);
|
|
@@ -138334,7 +138334,7 @@ var ConversationScanner = class {
|
|
|
138334
138334
|
}
|
|
138335
138335
|
return conversation;
|
|
138336
138336
|
} catch (err) {
|
|
138337
|
-
|
|
138337
|
+
log15.warn({ id, filePath: meta3.filePath, err }, "getConversation: parse failed");
|
|
138338
138338
|
return null;
|
|
138339
138339
|
}
|
|
138340
138340
|
}
|
|
@@ -138420,7 +138420,7 @@ var ConversationScanner = class {
|
|
|
138420
138420
|
return refresh;
|
|
138421
138421
|
}
|
|
138422
138422
|
async doRefreshFile(filePath, account) {
|
|
138423
|
-
const
|
|
138423
|
+
const log15 = getLogger2();
|
|
138424
138424
|
if (this.persistent) {
|
|
138425
138425
|
const engine = this.engine();
|
|
138426
138426
|
const previous2 = engine.getByIdOrSession(filePath);
|
|
@@ -138447,7 +138447,7 @@ var ConversationScanner = class {
|
|
|
138447
138447
|
} else if (change === "appended") {
|
|
138448
138448
|
await this.extendCachedConversations(cacheKeys, filePath, meta22.account);
|
|
138449
138449
|
}
|
|
138450
|
-
|
|
138450
|
+
log15.debug({ filePath, change, kept: !!meta22 }, "refreshFile: updated persistent index");
|
|
138451
138451
|
return meta22;
|
|
138452
138452
|
}
|
|
138453
138453
|
const previous = this.metadataCache.get(filePath);
|
|
@@ -138456,7 +138456,7 @@ var ConversationScanner = class {
|
|
|
138456
138456
|
try {
|
|
138457
138457
|
meta3 = await parseMeta(filePath, resolvedAccount, this.lastTier);
|
|
138458
138458
|
} catch (err) {
|
|
138459
|
-
|
|
138459
|
+
log15.warn({ filePath, err }, "refreshFile: parseMeta threw");
|
|
138460
138460
|
meta3 = null;
|
|
138461
138461
|
}
|
|
138462
138462
|
const evict = (m2) => {
|
|
@@ -138472,7 +138472,7 @@ var ConversationScanner = class {
|
|
|
138472
138472
|
this.removeFromSessionIndex(previous);
|
|
138473
138473
|
this.indexer.removeDocument(previous.id);
|
|
138474
138474
|
}
|
|
138475
|
-
|
|
138475
|
+
log15.debug({ filePath }, "refreshFile: dropped (no parseable messages)");
|
|
138476
138476
|
return null;
|
|
138477
138477
|
}
|
|
138478
138478
|
meta3.gitBranch = readGitBranch(meta3.projectPath);
|
|
@@ -138485,7 +138485,7 @@ var ConversationScanner = class {
|
|
|
138485
138485
|
} else {
|
|
138486
138486
|
this.indexer.addDocument(meta3);
|
|
138487
138487
|
}
|
|
138488
|
-
|
|
138488
|
+
log15.debug(
|
|
138489
138489
|
{ filePath, messageCount: meta3.messageCount },
|
|
138490
138490
|
"refreshFile: updated in-memory indexes"
|
|
138491
138491
|
);
|
|
@@ -142369,7 +142369,7 @@ function createLogsRoutes() {
|
|
|
142369
142369
|
|
|
142370
142370
|
// src/api/routes/misc.routes.ts
|
|
142371
142371
|
var import_node_child_process2 = require("child_process");
|
|
142372
|
-
var
|
|
142372
|
+
var import_node_crypto2 = require("crypto");
|
|
142373
142373
|
var import_os7 = require("os");
|
|
142374
142374
|
|
|
142375
142375
|
// src/db/repositories/push.repository.ts
|
|
@@ -142628,6 +142628,170 @@ var PushRepository = class {
|
|
|
142628
142628
|
|
|
142629
142629
|
// src/api/routes/misc.routes.ts
|
|
142630
142630
|
init_logger();
|
|
142631
|
+
|
|
142632
|
+
// src/services/push/apnsClient.ts
|
|
142633
|
+
var import_node_crypto = require("crypto");
|
|
142634
|
+
var import_node_http2 = require("http2");
|
|
142635
|
+
init_logger();
|
|
142636
|
+
var log3 = getLogger("apns");
|
|
142637
|
+
var APNS_HOST_SANDBOX = "api.sandbox.push.apple.com";
|
|
142638
|
+
var APNS_MAX_PAYLOAD_BYTES = 4096;
|
|
142639
|
+
var JWT_TTL_SECONDS = 3e3;
|
|
142640
|
+
var DEAD_TOKEN_REASONS = /* @__PURE__ */ new Set([
|
|
142641
|
+
"BadDeviceToken",
|
|
142642
|
+
"DeviceTokenNotForTopic",
|
|
142643
|
+
"Unregistered",
|
|
142644
|
+
"ExpiredToken"
|
|
142645
|
+
]);
|
|
142646
|
+
function base64url3(input) {
|
|
142647
|
+
return Buffer.from(input).toString("base64url");
|
|
142648
|
+
}
|
|
142649
|
+
function readApnsCredentialsFromEnv(env = process.env) {
|
|
142650
|
+
const key = env.APNS_KEY;
|
|
142651
|
+
if (!key || key.trim().length === 0) return null;
|
|
142652
|
+
const keyId = env.APNS_KEY_ID?.trim();
|
|
142653
|
+
const teamId = env.APNS_TEAM_ID?.trim();
|
|
142654
|
+
const bundleId = env.APNS_BUNDLE_ID?.trim();
|
|
142655
|
+
if (!keyId || !teamId || !bundleId) return null;
|
|
142656
|
+
const host = env.APNS_HOST ?? APNS_HOST_SANDBOX;
|
|
142657
|
+
return { key, keyId, teamId, bundleId, host };
|
|
142658
|
+
}
|
|
142659
|
+
function describeMissingApnsCredentials(env = process.env) {
|
|
142660
|
+
if (!env.APNS_KEY || env.APNS_KEY.trim().length === 0) {
|
|
142661
|
+
return "APNS_KEY is not set, so Live Activity push is disabled. Set it to the p8 key contents (not a path) to enable it.";
|
|
142662
|
+
}
|
|
142663
|
+
const missing = [
|
|
142664
|
+
["APNS_KEY_ID", env.APNS_KEY_ID],
|
|
142665
|
+
["APNS_TEAM_ID", env.APNS_TEAM_ID],
|
|
142666
|
+
["APNS_BUNDLE_ID", env.APNS_BUNDLE_ID]
|
|
142667
|
+
].filter(([, value]) => !value || value.trim().length === 0).map(([name]) => name);
|
|
142668
|
+
if (missing.length === 0) return null;
|
|
142669
|
+
return `APNS_KEY is set but ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} not, so Live Activity push is disabled. Under launchd, APNS_KEY_ID is derived from the AuthKey_<keyId>.p8 filename; the team and bundle ids must be set explicitly.`;
|
|
142670
|
+
}
|
|
142671
|
+
var ApnsClient = class {
|
|
142672
|
+
constructor(creds) {
|
|
142673
|
+
this.creds = creds;
|
|
142674
|
+
}
|
|
142675
|
+
creds;
|
|
142676
|
+
session = null;
|
|
142677
|
+
cachedJwt = null;
|
|
142678
|
+
/**
|
|
142679
|
+
* The `apns-topic` for Live Activity pushes.
|
|
142680
|
+
*
|
|
142681
|
+
* The `.push-type.liveactivity` suffix is mandatory and is why the signing key
|
|
142682
|
+
* must be Team Scoped (All Topics) — a key scoped to the bundle id alone
|
|
142683
|
+
* cannot sign this topic.
|
|
142684
|
+
*/
|
|
142685
|
+
get topic() {
|
|
142686
|
+
return `${this.creds.bundleId}.push-type.liveactivity`;
|
|
142687
|
+
}
|
|
142688
|
+
/**
|
|
142689
|
+
* Mint or reuse the provider JWT.
|
|
142690
|
+
*
|
|
142691
|
+
* ES256 over the p8 key. Cached until shortly before expiry: Apple rejects a
|
|
142692
|
+
* token older than an hour, but minting one per request is wasteful and can
|
|
142693
|
+
* trip APNs' provider-token-update throttle.
|
|
142694
|
+
*/
|
|
142695
|
+
getJwt(now = Date.now()) {
|
|
142696
|
+
const nowSeconds = Math.floor(now / 1e3);
|
|
142697
|
+
if (this.cachedJwt && this.cachedJwt.expiresAt > nowSeconds + 60) {
|
|
142698
|
+
return this.cachedJwt.token;
|
|
142699
|
+
}
|
|
142700
|
+
const header = base64url3(JSON.stringify({ alg: "ES256", kid: this.creds.keyId, typ: "JWT" }));
|
|
142701
|
+
const payload = base64url3(JSON.stringify({ iss: this.creds.teamId, iat: nowSeconds }));
|
|
142702
|
+
const signingInput = `${header}.${payload}`;
|
|
142703
|
+
const signature = (0, import_node_crypto.createSign)("SHA256").update(signingInput).sign({ key: this.creds.key, dsaEncoding: "ieee-p1363" });
|
|
142704
|
+
const token = `${signingInput}.${base64url3(signature)}`;
|
|
142705
|
+
this.cachedJwt = { token, expiresAt: nowSeconds + JWT_TTL_SECONDS };
|
|
142706
|
+
return token;
|
|
142707
|
+
}
|
|
142708
|
+
/**
|
|
142709
|
+
* Reuse one HTTP/2 session across sends.
|
|
142710
|
+
*
|
|
142711
|
+
* APNs expects a long-lived connection; a fresh TLS handshake per push is slow
|
|
142712
|
+
* and Apple treats connection churn as abuse.
|
|
142713
|
+
*/
|
|
142714
|
+
getSession() {
|
|
142715
|
+
if (this.session && !this.session.closed && !this.session.destroyed) {
|
|
142716
|
+
return this.session;
|
|
142717
|
+
}
|
|
142718
|
+
const session = (0, import_node_http2.connect)(`https://${this.creds.host}`);
|
|
142719
|
+
session.on("error", (err) => {
|
|
142720
|
+
log3.warn("apns.session_error", { event: "apns.session_error", err: String(err) });
|
|
142721
|
+
});
|
|
142722
|
+
this.session = session;
|
|
142723
|
+
return session;
|
|
142724
|
+
}
|
|
142725
|
+
/**
|
|
142726
|
+
* Send one push.
|
|
142727
|
+
*
|
|
142728
|
+
* Resolves with a result rather than rejecting on an APNs rejection: a
|
|
142729
|
+
* rejected push is an expected outcome the caller must act on (retire the
|
|
142730
|
+
* token), not an exception. Only a genuinely unexpected local failure throws,
|
|
142731
|
+
* and the caller logs it.
|
|
142732
|
+
*/
|
|
142733
|
+
async send(args) {
|
|
142734
|
+
const body = Buffer.from(JSON.stringify(args.payload), "utf-8");
|
|
142735
|
+
if (body.byteLength > APNS_MAX_PAYLOAD_BYTES) {
|
|
142736
|
+
throw new Error(
|
|
142737
|
+
`APNs payload is ${body.byteLength} bytes, over the ${APNS_MAX_PAYLOAD_BYTES} byte limit`
|
|
142738
|
+
);
|
|
142739
|
+
}
|
|
142740
|
+
const session = this.getSession();
|
|
142741
|
+
const headers = {
|
|
142742
|
+
[import_node_http2.constants.HTTP2_HEADER_METHOD]: "POST",
|
|
142743
|
+
[import_node_http2.constants.HTTP2_HEADER_PATH]: `/3/device/${args.deviceToken}`,
|
|
142744
|
+
[import_node_http2.constants.HTTP2_HEADER_AUTHORIZATION]: `bearer ${this.getJwt()}`,
|
|
142745
|
+
"apns-push-type": "liveactivity",
|
|
142746
|
+
"apns-topic": this.topic,
|
|
142747
|
+
"apns-priority": String(args.priority ?? 10),
|
|
142748
|
+
...args.expirationSeconds != null && {
|
|
142749
|
+
"apns-expiration": String(args.expirationSeconds)
|
|
142750
|
+
},
|
|
142751
|
+
[import_node_http2.constants.HTTP2_HEADER_CONTENT_TYPE]: "application/json",
|
|
142752
|
+
[import_node_http2.constants.HTTP2_HEADER_CONTENT_LENGTH]: String(body.byteLength)
|
|
142753
|
+
};
|
|
142754
|
+
return new Promise((resolve4, reject) => {
|
|
142755
|
+
const req = session.request(headers);
|
|
142756
|
+
req.setTimeout(args.timeoutMs ?? 1e4, () => {
|
|
142757
|
+
req.close(import_node_http2.constants.NGHTTP2_CANCEL);
|
|
142758
|
+
resolve4({ ok: false, status: 0, reason: "Timeout", tokenDead: false });
|
|
142759
|
+
});
|
|
142760
|
+
let status = 0;
|
|
142761
|
+
req.on("response", (resHeaders) => {
|
|
142762
|
+
status = Number(resHeaders[import_node_http2.constants.HTTP2_HEADER_STATUS] ?? 0);
|
|
142763
|
+
});
|
|
142764
|
+
const chunks = [];
|
|
142765
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
142766
|
+
req.on("error", reject);
|
|
142767
|
+
req.on("end", () => {
|
|
142768
|
+
const raw2 = Buffer.concat(chunks).toString("utf-8");
|
|
142769
|
+
let reason;
|
|
142770
|
+
if (raw2.length > 0) {
|
|
142771
|
+
try {
|
|
142772
|
+
reason = JSON.parse(raw2).reason;
|
|
142773
|
+
} catch {
|
|
142774
|
+
reason = raw2.slice(0, 200);
|
|
142775
|
+
}
|
|
142776
|
+
}
|
|
142777
|
+
resolve4({
|
|
142778
|
+
ok: status === 200,
|
|
142779
|
+
status,
|
|
142780
|
+
reason,
|
|
142781
|
+
tokenDead: reason != null && DEAD_TOKEN_REASONS.has(reason)
|
|
142782
|
+
});
|
|
142783
|
+
});
|
|
142784
|
+
req.end(body);
|
|
142785
|
+
});
|
|
142786
|
+
}
|
|
142787
|
+
/** Close the shared connection. Called on server shutdown. */
|
|
142788
|
+
close() {
|
|
142789
|
+
this.session?.close();
|
|
142790
|
+
this.session = null;
|
|
142791
|
+
}
|
|
142792
|
+
};
|
|
142793
|
+
|
|
142794
|
+
// src/api/routes/misc.routes.ts
|
|
142631
142795
|
function numberOrNull(value) {
|
|
142632
142796
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
142633
142797
|
}
|
|
@@ -142657,11 +142821,22 @@ function readRawBody3(req) {
|
|
|
142657
142821
|
function verifyWebhookSignature(body, header, secret) {
|
|
142658
142822
|
if (!header) return false;
|
|
142659
142823
|
const provided = header.startsWith("sha256=") ? header.slice(7) : header;
|
|
142660
|
-
const expected = (0,
|
|
142824
|
+
const expected = (0, import_node_crypto2.createHmac)("sha256", secret).update(body).digest("hex");
|
|
142661
142825
|
const a = Buffer.from(provided, "utf-8");
|
|
142662
142826
|
const b2 = Buffer.from(expected, "utf-8");
|
|
142663
142827
|
if (a.length !== b2.length) return false;
|
|
142664
|
-
return (0,
|
|
142828
|
+
return (0, import_node_crypto2.timingSafeEqual)(a, b2);
|
|
142829
|
+
}
|
|
142830
|
+
function describePushCapability(liveActivityEnabled, env = process.env) {
|
|
142831
|
+
if (liveActivityEnabled) return { liveActivity: true, notifications: false };
|
|
142832
|
+
return {
|
|
142833
|
+
liveActivity: false,
|
|
142834
|
+
notifications: false,
|
|
142835
|
+
// describeMissingApnsCredentials only explains a *credential* gap and
|
|
142836
|
+
// returns null once the credentials are complete — reachable here, because
|
|
142837
|
+
// an unavailable token store disables the feature with the key still set.
|
|
142838
|
+
liveActivityReason: describeMissingApnsCredentials(env) ?? "APNs credentials are set but the push token store is unavailable, so Live Activity push is disabled."
|
|
142839
|
+
};
|
|
142665
142840
|
}
|
|
142666
142841
|
var clientLog = getLogger("client");
|
|
142667
142842
|
var createMiscRoutes = (deps) => {
|
|
@@ -142684,7 +142859,12 @@ var createMiscRoutes = (deps) => {
|
|
|
142684
142859
|
featureFlags: true,
|
|
142685
142860
|
// Same contract: this server serves GET /api/projects/summary, which the
|
|
142686
142861
|
// Hub's grouped views need before they can draw a tree.
|
|
142687
|
-
projectSummary: true
|
|
142862
|
+
projectSummary: true,
|
|
142863
|
+
// Delivery capability, not endpoint support: whether this server can
|
|
142864
|
+
// actually send a push, so mobile can hide an affordance instead of
|
|
142865
|
+
// registering tokens nothing will ever send to. Absent on older servers,
|
|
142866
|
+
// which a client should read as "unknown", not "unavailable".
|
|
142867
|
+
push: describePushCapability(deps.liveActivityPushEnabled())
|
|
142688
142868
|
});
|
|
142689
142869
|
});
|
|
142690
142870
|
app.get("/api/profiles", (c) => c.json([]));
|
|
@@ -142745,9 +142925,10 @@ var createMiscRoutes = (deps) => {
|
|
|
142745
142925
|
return c.json({ ok: true });
|
|
142746
142926
|
});
|
|
142747
142927
|
app.get("/api/push/health", (c) => {
|
|
142928
|
+
const push = describePushCapability(deps.liveActivityPushEnabled());
|
|
142748
142929
|
const repo = deps.pushRepo();
|
|
142749
|
-
if (!repo) return c.json({ tokens: [], available: false });
|
|
142750
|
-
return c.json({ tokens: repo.listHealth(), available: true });
|
|
142930
|
+
if (!repo) return c.json({ tokens: [], available: false, push });
|
|
142931
|
+
return c.json({ tokens: repo.listHealth(), available: true, push });
|
|
142751
142932
|
});
|
|
142752
142933
|
app.post("/api/__update", async (c) => {
|
|
142753
142934
|
const cfg = loadUpdateConfig();
|
|
@@ -142815,7 +142996,7 @@ var createPairRoutes = (deps) => {
|
|
|
142815
142996
|
};
|
|
142816
142997
|
|
|
142817
142998
|
// src/api/routes/progress.routes.ts
|
|
142818
|
-
var
|
|
142999
|
+
var import_node_crypto3 = __toESM(require("crypto"), 1);
|
|
142819
143000
|
function readRawBody4(req) {
|
|
142820
143001
|
return new Promise((resolve4, reject) => {
|
|
142821
143002
|
const chunks = [];
|
|
@@ -142826,10 +143007,10 @@ function readRawBody4(req) {
|
|
|
142826
143007
|
}
|
|
142827
143008
|
function verifySignature(rawBody, signature, secret) {
|
|
142828
143009
|
if (!signature || signature.length === 0) return false;
|
|
142829
|
-
const expected =
|
|
143010
|
+
const expected = import_node_crypto3.default.createHmac("sha256", secret).update(rawBody).digest("hex");
|
|
142830
143011
|
if (expected.length !== signature.length) return false;
|
|
142831
143012
|
try {
|
|
142832
|
-
return
|
|
143013
|
+
return import_node_crypto3.default.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
|
|
142833
143014
|
} catch {
|
|
142834
143015
|
return false;
|
|
142835
143016
|
}
|
|
@@ -143382,7 +143563,7 @@ var import_promises13 = require("timers/promises");
|
|
|
143382
143563
|
|
|
143383
143564
|
// src/db/query-timing.ts
|
|
143384
143565
|
init_logger();
|
|
143385
|
-
var
|
|
143566
|
+
var log4 = getLogger("db");
|
|
143386
143567
|
var DEFAULT_SLOW_QUERY_MS = 35;
|
|
143387
143568
|
var LABEL = /* @__PURE__ */ Symbol("tbQueryLabel");
|
|
143388
143569
|
function deriveLabel(sql) {
|
|
@@ -143398,15 +143579,15 @@ function resolveSlowMs() {
|
|
|
143398
143579
|
}
|
|
143399
143580
|
function record2(label, ms2, rows, slowMs) {
|
|
143400
143581
|
if (slowMs > 0 && ms2 >= slowMs) {
|
|
143401
|
-
|
|
143582
|
+
log4.warn(
|
|
143402
143583
|
`[db] slow query ${label} ${ms2.toFixed(1)}ms rows=${rows}`,
|
|
143403
143584
|
{ event: "db.slow_query", stmt: label, ms: Math.round(ms2 * 100) / 100, rows },
|
|
143404
143585
|
"pino"
|
|
143405
143586
|
);
|
|
143406
143587
|
return;
|
|
143407
143588
|
}
|
|
143408
|
-
if (
|
|
143409
|
-
|
|
143589
|
+
if (log4.pino.isLevelEnabled("debug")) {
|
|
143590
|
+
log4.debug(
|
|
143410
143591
|
`[db] ${label} ${ms2.toFixed(2)}ms rows=${rows}`,
|
|
143411
143592
|
{ event: "db.query", stmt: label, ms: Math.round(ms2 * 100) / 100, rows },
|
|
143412
143593
|
"pino"
|
|
@@ -145950,10 +146131,10 @@ function fingerprintOf(ids) {
|
|
|
145950
146131
|
return `sha256:${(0, import_crypto11.createHash)("sha256").update(sorted.join("\n")).digest("hex")}`;
|
|
145951
146132
|
}
|
|
145952
146133
|
var CacheIntegrityMonitor = class {
|
|
145953
|
-
constructor(cache, wsHub,
|
|
146134
|
+
constructor(cache, wsHub, log15, cacheDir, rescan, runDuringReset) {
|
|
145954
146135
|
this.cache = cache;
|
|
145955
146136
|
this.wsHub = wsHub;
|
|
145956
|
-
this.log =
|
|
146137
|
+
this.log = log15;
|
|
145957
146138
|
this.cacheDir = cacheDir;
|
|
145958
146139
|
this.rescan = rescan;
|
|
145959
146140
|
this.runDuringReset = runDuringReset;
|
|
@@ -146810,166 +146991,100 @@ function deriveProjectChatTitle(input) {
|
|
|
146810
146991
|
return `Untitled \xB7 ${input.id.slice(0, 8)}`;
|
|
146811
146992
|
}
|
|
146812
146993
|
|
|
146813
|
-
// src/services/push/
|
|
146814
|
-
var import_node_crypto3 = require("crypto");
|
|
146815
|
-
var import_node_http2 = require("http2");
|
|
146994
|
+
// src/services/push/expoPushSender.ts
|
|
146816
146995
|
init_logger();
|
|
146817
|
-
var
|
|
146818
|
-
var
|
|
146819
|
-
var
|
|
146820
|
-
var
|
|
146821
|
-
var
|
|
146822
|
-
"BadDeviceToken",
|
|
146823
|
-
"DeviceTokenNotForTopic",
|
|
146824
|
-
"Unregistered",
|
|
146825
|
-
"ExpiredToken"
|
|
146826
|
-
]);
|
|
146827
|
-
function base64url3(input) {
|
|
146828
|
-
return Buffer.from(input).toString("base64url");
|
|
146829
|
-
}
|
|
146830
|
-
function readApnsCredentialsFromEnv(env = process.env) {
|
|
146831
|
-
const key = env.APNS_KEY;
|
|
146832
|
-
if (!key || key.trim().length === 0) return null;
|
|
146833
|
-
const keyId = env.APNS_KEY_ID?.trim();
|
|
146834
|
-
const teamId = env.APNS_TEAM_ID?.trim();
|
|
146835
|
-
const bundleId = env.APNS_BUNDLE_ID?.trim();
|
|
146836
|
-
if (!keyId || !teamId || !bundleId) return null;
|
|
146837
|
-
const host = env.APNS_HOST ?? APNS_HOST_SANDBOX;
|
|
146838
|
-
return { key, keyId, teamId, bundleId, host };
|
|
146839
|
-
}
|
|
146840
|
-
function describeMissingApnsCredentials(env = process.env) {
|
|
146841
|
-
if (!env.APNS_KEY || env.APNS_KEY.trim().length === 0) {
|
|
146842
|
-
return "APNS_KEY is not set, so Live Activity push is disabled. Set it to the p8 key contents (not a path) to enable it.";
|
|
146843
|
-
}
|
|
146844
|
-
const missing = [
|
|
146845
|
-
["APNS_KEY_ID", env.APNS_KEY_ID],
|
|
146846
|
-
["APNS_TEAM_ID", env.APNS_TEAM_ID],
|
|
146847
|
-
["APNS_BUNDLE_ID", env.APNS_BUNDLE_ID]
|
|
146848
|
-
].filter(([, value]) => !value || value.trim().length === 0).map(([name]) => name);
|
|
146849
|
-
if (missing.length === 0) return null;
|
|
146850
|
-
return `APNS_KEY is set but ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} not, so Live Activity push is disabled. Under launchd, APNS_KEY_ID is derived from the AuthKey_<keyId>.p8 filename; the team and bundle ids must be set explicitly.`;
|
|
146851
|
-
}
|
|
146852
|
-
var ApnsClient = class {
|
|
146853
|
-
constructor(creds) {
|
|
146854
|
-
this.creds = creds;
|
|
146855
|
-
}
|
|
146856
|
-
creds;
|
|
146857
|
-
session = null;
|
|
146858
|
-
cachedJwt = null;
|
|
146859
|
-
/**
|
|
146860
|
-
* The `apns-topic` for Live Activity pushes.
|
|
146861
|
-
*
|
|
146862
|
-
* The `.push-type.liveactivity` suffix is mandatory and is why the signing key
|
|
146863
|
-
* must be Team Scoped (All Topics) — a key scoped to the bundle id alone
|
|
146864
|
-
* cannot sign this topic.
|
|
146865
|
-
*/
|
|
146866
|
-
get topic() {
|
|
146867
|
-
return `${this.creds.bundleId}.push-type.liveactivity`;
|
|
146868
|
-
}
|
|
146996
|
+
var log5 = getLogger("expo-push");
|
|
146997
|
+
var EXPO_PUSH_ENDPOINT = "https://exp.host/--/api/v2/push/send";
|
|
146998
|
+
var EXPO_PUSH_BATCH_SIZE = 100;
|
|
146999
|
+
var DEAD_TOKEN_ERROR = "DeviceNotRegistered";
|
|
147000
|
+
var ExpoPushSender = class {
|
|
146869
147001
|
/**
|
|
146870
|
-
*
|
|
146871
|
-
*
|
|
146872
|
-
*
|
|
146873
|
-
*
|
|
146874
|
-
* trip APNs' provider-token-update throttle.
|
|
147002
|
+
* @param accessToken Expo access token, when the project has enhanced
|
|
147003
|
+
* security enabled. Optional on purpose: a self-hoster does not own the Expo
|
|
147004
|
+
* project and cannot obtain one, so requiring it would break the deployment
|
|
147005
|
+
* this transport exists to serve.
|
|
146875
147006
|
*/
|
|
146876
|
-
|
|
146877
|
-
|
|
146878
|
-
|
|
146879
|
-
return this.cachedJwt.token;
|
|
146880
|
-
}
|
|
146881
|
-
const header = base64url3(JSON.stringify({ alg: "ES256", kid: this.creds.keyId, typ: "JWT" }));
|
|
146882
|
-
const payload = base64url3(JSON.stringify({ iss: this.creds.teamId, iat: nowSeconds }));
|
|
146883
|
-
const signingInput = `${header}.${payload}`;
|
|
146884
|
-
const signature = (0, import_node_crypto3.createSign)("SHA256").update(signingInput).sign({ key: this.creds.key, dsaEncoding: "ieee-p1363" });
|
|
146885
|
-
const token = `${signingInput}.${base64url3(signature)}`;
|
|
146886
|
-
this.cachedJwt = { token, expiresAt: nowSeconds + JWT_TTL_SECONDS };
|
|
146887
|
-
return token;
|
|
147007
|
+
constructor(repo, accessToken) {
|
|
147008
|
+
this.repo = repo;
|
|
147009
|
+
this.accessToken = accessToken;
|
|
146888
147010
|
}
|
|
147011
|
+
repo;
|
|
147012
|
+
accessToken;
|
|
146889
147013
|
/**
|
|
146890
|
-
*
|
|
147014
|
+
* Send one message to every deliverable Expo token.
|
|
146891
147015
|
*
|
|
146892
|
-
*
|
|
146893
|
-
*
|
|
147016
|
+
* Sends are independent: Expo returns a ticket per token in one response, so
|
|
147017
|
+
* a dead device is recorded against its own row and never silences the other
|
|
147018
|
+
* devices in the batch.
|
|
146894
147019
|
*/
|
|
146895
|
-
|
|
146896
|
-
|
|
146897
|
-
|
|
147020
|
+
async send(message, now = Date.now()) {
|
|
147021
|
+
const rows = this.repo.listDeliverable();
|
|
147022
|
+
const outcome = { attempted: rows.length, succeeded: 0, retired: 0 };
|
|
147023
|
+
if (rows.length === 0) return outcome;
|
|
147024
|
+
for (let i = 0; i < rows.length; i += EXPO_PUSH_BATCH_SIZE) {
|
|
147025
|
+
const chunk = rows.slice(i, i + EXPO_PUSH_BATCH_SIZE);
|
|
147026
|
+
await this.sendChunk(chunk, message, now, outcome);
|
|
146898
147027
|
}
|
|
146899
|
-
|
|
146900
|
-
session.on("error", (err) => {
|
|
146901
|
-
log4.warn("apns.session_error", { event: "apns.session_error", err: String(err) });
|
|
146902
|
-
});
|
|
146903
|
-
this.session = session;
|
|
146904
|
-
return session;
|
|
147028
|
+
return outcome;
|
|
146905
147029
|
}
|
|
146906
|
-
|
|
146907
|
-
|
|
146908
|
-
|
|
146909
|
-
|
|
146910
|
-
|
|
146911
|
-
|
|
146912
|
-
|
|
146913
|
-
|
|
146914
|
-
|
|
146915
|
-
|
|
146916
|
-
|
|
146917
|
-
throw new Error(
|
|
146918
|
-
`APNs payload is ${body.byteLength} bytes, over the ${APNS_MAX_PAYLOAD_BYTES} byte limit`
|
|
146919
|
-
);
|
|
146920
|
-
}
|
|
146921
|
-
const session = this.getSession();
|
|
146922
|
-
const headers = {
|
|
146923
|
-
[import_node_http2.constants.HTTP2_HEADER_METHOD]: "POST",
|
|
146924
|
-
[import_node_http2.constants.HTTP2_HEADER_PATH]: `/3/device/${args.deviceToken}`,
|
|
146925
|
-
[import_node_http2.constants.HTTP2_HEADER_AUTHORIZATION]: `bearer ${this.getJwt()}`,
|
|
146926
|
-
"apns-push-type": "liveactivity",
|
|
146927
|
-
"apns-topic": this.topic,
|
|
146928
|
-
"apns-priority": String(args.priority ?? 10),
|
|
146929
|
-
...args.expirationSeconds != null && {
|
|
146930
|
-
"apns-expiration": String(args.expirationSeconds)
|
|
146931
|
-
},
|
|
146932
|
-
[import_node_http2.constants.HTTP2_HEADER_CONTENT_TYPE]: "application/json",
|
|
146933
|
-
[import_node_http2.constants.HTTP2_HEADER_CONTENT_LENGTH]: String(body.byteLength)
|
|
146934
|
-
};
|
|
146935
|
-
return new Promise((resolve4, reject) => {
|
|
146936
|
-
const req = session.request(headers);
|
|
146937
|
-
req.setTimeout(args.timeoutMs ?? 1e4, () => {
|
|
146938
|
-
req.close(import_node_http2.constants.NGHTTP2_CANCEL);
|
|
146939
|
-
resolve4({ ok: false, status: 0, reason: "Timeout", tokenDead: false });
|
|
146940
|
-
});
|
|
146941
|
-
let status = 0;
|
|
146942
|
-
req.on("response", (resHeaders) => {
|
|
146943
|
-
status = Number(resHeaders[import_node_http2.constants.HTTP2_HEADER_STATUS] ?? 0);
|
|
147030
|
+
async sendChunk(rows, message, now, outcome) {
|
|
147031
|
+
let payload;
|
|
147032
|
+
try {
|
|
147033
|
+
const res = await fetch(EXPO_PUSH_ENDPOINT, {
|
|
147034
|
+
method: "POST",
|
|
147035
|
+
headers: {
|
|
147036
|
+
"content-type": "application/json",
|
|
147037
|
+
accept: "application/json",
|
|
147038
|
+
...this.accessToken && { authorization: `Bearer ${this.accessToken}` }
|
|
147039
|
+
},
|
|
147040
|
+
body: JSON.stringify(rows.map((row) => ({ to: row.token, ...message })))
|
|
146944
147041
|
});
|
|
146945
|
-
|
|
146946
|
-
|
|
146947
|
-
|
|
146948
|
-
|
|
146949
|
-
|
|
146950
|
-
|
|
146951
|
-
|
|
146952
|
-
try {
|
|
146953
|
-
reason = JSON.parse(raw2).reason;
|
|
146954
|
-
} catch {
|
|
146955
|
-
reason = raw2.slice(0, 200);
|
|
146956
|
-
}
|
|
146957
|
-
}
|
|
146958
|
-
resolve4({
|
|
146959
|
-
ok: status === 200,
|
|
146960
|
-
status,
|
|
146961
|
-
reason,
|
|
146962
|
-
tokenDead: reason != null && DEAD_TOKEN_REASONS.has(reason)
|
|
147042
|
+
if (!res.ok) {
|
|
147043
|
+
const code = `HTTP_${res.status}`;
|
|
147044
|
+
for (const row of rows) this.repo.recordFailure(row.token, code, now);
|
|
147045
|
+
log5.warn("expo_push.request_rejected", {
|
|
147046
|
+
event: "expo_push.request_rejected",
|
|
147047
|
+
status: res.status,
|
|
147048
|
+
tokens: rows.length
|
|
146963
147049
|
});
|
|
147050
|
+
return;
|
|
147051
|
+
}
|
|
147052
|
+
payload = await res.json();
|
|
147053
|
+
} catch (err) {
|
|
147054
|
+
for (const row of rows) this.repo.recordFailure(row.token, "SendError", now);
|
|
147055
|
+
log5.error("expo_push.send_failed", {
|
|
147056
|
+
event: "expo_push.send_failed",
|
|
147057
|
+
tokens: rows.length,
|
|
147058
|
+
err: String(err)
|
|
147059
|
+
});
|
|
147060
|
+
return;
|
|
147061
|
+
}
|
|
147062
|
+
const tickets = payload?.data;
|
|
147063
|
+
rows.forEach((row, index) => {
|
|
147064
|
+
const ticket = Array.isArray(tickets) ? tickets[index] : void 0;
|
|
147065
|
+
if (!ticket) {
|
|
147066
|
+
this.repo.recordFailure(row.token, "NoTicket", now);
|
|
147067
|
+
return;
|
|
147068
|
+
}
|
|
147069
|
+
if (ticket.status === "ok") {
|
|
147070
|
+
this.repo.recordSuccess(row.token, now);
|
|
147071
|
+
outcome.succeeded += 1;
|
|
147072
|
+
return;
|
|
147073
|
+
}
|
|
147074
|
+
const code = ticket.details?.error ?? "PushError";
|
|
147075
|
+
this.repo.recordFailure(row.token, code, now);
|
|
147076
|
+
if (code === DEAD_TOKEN_ERROR) {
|
|
147077
|
+
this.repo.revoke(row.token, now);
|
|
147078
|
+
outcome.retired += 1;
|
|
147079
|
+
}
|
|
147080
|
+
log5.warn("expo_push.send_rejected", {
|
|
147081
|
+
event: "expo_push.send_rejected",
|
|
147082
|
+
code,
|
|
147083
|
+
message: ticket.message,
|
|
147084
|
+
retired: code === DEAD_TOKEN_ERROR
|
|
146964
147085
|
});
|
|
146965
|
-
req.end(body);
|
|
146966
147086
|
});
|
|
146967
147087
|
}
|
|
146968
|
-
/** Close the shared connection. Called on server shutdown. */
|
|
146969
|
-
close() {
|
|
146970
|
-
this.session?.close();
|
|
146971
|
-
this.session = null;
|
|
146972
|
-
}
|
|
146973
147088
|
};
|
|
146974
147089
|
|
|
146975
147090
|
// src/services/push/liveActivityNotifier.ts
|
|
@@ -146986,7 +147101,7 @@ function truncateLastOutput(raw2) {
|
|
|
146986
147101
|
}
|
|
146987
147102
|
|
|
146988
147103
|
// src/services/push/liveActivityNotifier.ts
|
|
146989
|
-
var
|
|
147104
|
+
var log6 = getLogger("live-activity");
|
|
146990
147105
|
function contentStateForSession(args) {
|
|
146991
147106
|
const status = toLiveActivityStatus(args.session.status);
|
|
146992
147107
|
if (!status) return null;
|
|
@@ -147046,7 +147161,7 @@ var LiveActivityNotifier = class {
|
|
|
147046
147161
|
}
|
|
147047
147162
|
await this.maybeSendName(session);
|
|
147048
147163
|
} catch (err) {
|
|
147049
|
-
|
|
147164
|
+
log6.error("live_activity.notify_failed", {
|
|
147050
147165
|
event: "live_activity.notify_failed",
|
|
147051
147166
|
sessionId: session.id,
|
|
147052
147167
|
status: session.status,
|
|
@@ -147068,7 +147183,7 @@ var LiveActivityNotifier = class {
|
|
|
147068
147183
|
});
|
|
147069
147184
|
this.openActivity.set(session.id, { sessionNameSent: session.sessionName != null });
|
|
147070
147185
|
if (outcome.attempted > 0) {
|
|
147071
|
-
|
|
147186
|
+
log6.info("live_activity.updated", {
|
|
147072
147187
|
event: "live_activity.updated",
|
|
147073
147188
|
sessionId: session.id,
|
|
147074
147189
|
status: contentState.status,
|
|
@@ -147092,7 +147207,7 @@ var LiveActivityNotifier = class {
|
|
|
147092
147207
|
});
|
|
147093
147208
|
open3.sessionNameSent = true;
|
|
147094
147209
|
if (outcome.attempted > 0) {
|
|
147095
|
-
|
|
147210
|
+
log6.info("live_activity.updated", {
|
|
147096
147211
|
event: "live_activity.updated",
|
|
147097
147212
|
sessionId: session.id,
|
|
147098
147213
|
status: contentState.status,
|
|
@@ -147111,7 +147226,7 @@ var LiveActivityNotifier = class {
|
|
|
147111
147226
|
if (!contentState) return;
|
|
147112
147227
|
const outcome = await this.sender.end({ sessionId: session.id, contentState });
|
|
147113
147228
|
if (outcome.attempted > 0) {
|
|
147114
|
-
|
|
147229
|
+
log6.info("live_activity.ended", {
|
|
147115
147230
|
event: "live_activity.ended",
|
|
147116
147231
|
sessionId: session.id,
|
|
147117
147232
|
...outcome
|
|
@@ -147129,7 +147244,7 @@ init_logger();
|
|
|
147129
147244
|
|
|
147130
147245
|
// src/services/push/liveActivitySender.ts
|
|
147131
147246
|
init_logger();
|
|
147132
|
-
var
|
|
147247
|
+
var log7 = getLogger("live-activity");
|
|
147133
147248
|
var ACTIVITY_MAX_LIFETIME_MS = 8 * 60 * 60 * 1e3;
|
|
147134
147249
|
function buildActivityKitPayload(args) {
|
|
147135
147250
|
return {
|
|
@@ -147193,7 +147308,7 @@ var LiveActivitySender = class {
|
|
|
147193
147308
|
);
|
|
147194
147309
|
for (const { row, result, error: error51 } of results) {
|
|
147195
147310
|
if (error51) {
|
|
147196
|
-
|
|
147311
|
+
log7.error("live_activity.send_failed", {
|
|
147197
147312
|
event: "live_activity.send_failed",
|
|
147198
147313
|
sessionId: args.sessionId,
|
|
147199
147314
|
activityId: row.activity_id,
|
|
@@ -147214,7 +147329,7 @@ var LiveActivitySender = class {
|
|
|
147214
147329
|
this.repo.expire(row.token, now);
|
|
147215
147330
|
outcome.retired += 1;
|
|
147216
147331
|
}
|
|
147217
|
-
|
|
147332
|
+
log7.warn("live_activity.send_rejected", {
|
|
147218
147333
|
event: "live_activity.send_rejected",
|
|
147219
147334
|
sessionId: args.sessionId,
|
|
147220
147335
|
activityId: row.activity_id,
|
|
@@ -147259,7 +147374,7 @@ var LiveActivitySender = class {
|
|
|
147259
147374
|
};
|
|
147260
147375
|
|
|
147261
147376
|
// src/services/push/liveActivityRenewal.ts
|
|
147262
|
-
var
|
|
147377
|
+
var log8 = getLogger("live-activity");
|
|
147263
147378
|
var RENEWAL_LEAD_MS = 30 * 60 * 1e3;
|
|
147264
147379
|
var MAX_TIMER_MS = 60 * 60 * 1e3;
|
|
147265
147380
|
function renewalDueAt(row) {
|
|
@@ -147307,7 +147422,7 @@ var LiveActivityRenewalScheduler = class {
|
|
|
147307
147422
|
await this.renew(row, now);
|
|
147308
147423
|
}
|
|
147309
147424
|
} catch (err) {
|
|
147310
|
-
|
|
147425
|
+
log8.error("live_activity.renewal_sweep_failed", {
|
|
147311
147426
|
event: "live_activity.renewal_sweep_failed",
|
|
147312
147427
|
err: String(err)
|
|
147313
147428
|
});
|
|
@@ -147336,7 +147451,7 @@ var LiveActivityRenewalScheduler = class {
|
|
|
147336
147451
|
if (!session || !status) {
|
|
147337
147452
|
this.deps.repo.claimRenewal(row.token, now);
|
|
147338
147453
|
this.deps.repo.expire(row.token, now);
|
|
147339
|
-
|
|
147454
|
+
log8.info("live_activity.renewal_skipped", {
|
|
147340
147455
|
event: "live_activity.renewal_skipped",
|
|
147341
147456
|
sessionId: row.session_id,
|
|
147342
147457
|
activityId: row.activity_id,
|
|
@@ -147371,7 +147486,7 @@ var LiveActivityRenewalScheduler = class {
|
|
|
147371
147486
|
startedAt,
|
|
147372
147487
|
now
|
|
147373
147488
|
});
|
|
147374
|
-
|
|
147489
|
+
log8.info("live_activity.renewed", {
|
|
147375
147490
|
event: "live_activity.renewed",
|
|
147376
147491
|
sessionId: session.id,
|
|
147377
147492
|
activityId: row.activity_id,
|
|
@@ -147381,7 +147496,7 @@ var LiveActivityRenewalScheduler = class {
|
|
|
147381
147496
|
replacementRequested: started
|
|
147382
147497
|
});
|
|
147383
147498
|
} catch (err) {
|
|
147384
|
-
|
|
147499
|
+
log8.error("live_activity.renewal_failed", {
|
|
147385
147500
|
event: "live_activity.renewal_failed",
|
|
147386
147501
|
sessionId: session.id,
|
|
147387
147502
|
activityId: row.activity_id,
|
|
@@ -147426,6 +147541,78 @@ var LiveActivityRenewalScheduler = class {
|
|
|
147426
147541
|
}
|
|
147427
147542
|
};
|
|
147428
147543
|
|
|
147544
|
+
// src/services/push/waitingInputNotifier.ts
|
|
147545
|
+
init_logger();
|
|
147546
|
+
var log9 = getLogger("expo-push");
|
|
147547
|
+
function waitingInputMessage(session, serverId) {
|
|
147548
|
+
return {
|
|
147549
|
+
title: session.projectName || "Threadbase",
|
|
147550
|
+
body: "Waiting for your input",
|
|
147551
|
+
data: { sessionId: session.id, serverId }
|
|
147552
|
+
};
|
|
147553
|
+
}
|
|
147554
|
+
var WaitingInputNotifier = class {
|
|
147555
|
+
/**
|
|
147556
|
+
* @param isWatched Whether a client is currently subscribed to this session
|
|
147557
|
+
* over WebSocket. Mobile subscribes while the session screen is open and the
|
|
147558
|
+
* socket dies when the app is backgrounded, so this is the available signal
|
|
147559
|
+
* for "the user is already looking" — and a push to someone already reading
|
|
147560
|
+
* the output is pure noise.
|
|
147561
|
+
*/
|
|
147562
|
+
constructor(sender, serverId, isWatched) {
|
|
147563
|
+
this.sender = sender;
|
|
147564
|
+
this.serverId = serverId;
|
|
147565
|
+
this.isWatched = isWatched;
|
|
147566
|
+
}
|
|
147567
|
+
sender;
|
|
147568
|
+
serverId;
|
|
147569
|
+
isWatched;
|
|
147570
|
+
/** Sessions with a turn the user started that has not yet been answered. */
|
|
147571
|
+
openTurn = /* @__PURE__ */ new Set();
|
|
147572
|
+
/**
|
|
147573
|
+
* React to a session status change.
|
|
147574
|
+
*
|
|
147575
|
+
* Fire-and-forget by design: a push must never delay or fail a session
|
|
147576
|
+
* transition, so this returns a promise the caller may ignore and every error
|
|
147577
|
+
* is logged rather than propagated.
|
|
147578
|
+
*/
|
|
147579
|
+
async onStatusChange(session, previousStatus) {
|
|
147580
|
+
try {
|
|
147581
|
+
if (session.status === "running") {
|
|
147582
|
+
if (previousStatus === "waiting_input") this.openTurn.add(session.id);
|
|
147583
|
+
return;
|
|
147584
|
+
}
|
|
147585
|
+
if (session.status !== "waiting_input") {
|
|
147586
|
+
this.openTurn.delete(session.id);
|
|
147587
|
+
return;
|
|
147588
|
+
}
|
|
147589
|
+
if (!this.openTurn.delete(session.id)) return;
|
|
147590
|
+
if (this.isWatched(session.id)) {
|
|
147591
|
+
log9.debug("expo_push.suppressed_watched", {
|
|
147592
|
+
event: "expo_push.suppressed_watched",
|
|
147593
|
+
sessionId: session.id
|
|
147594
|
+
});
|
|
147595
|
+
return;
|
|
147596
|
+
}
|
|
147597
|
+
const outcome = await this.sender.send(waitingInputMessage(session, this.serverId));
|
|
147598
|
+
if (outcome.attempted > 0) {
|
|
147599
|
+
log9.info("expo_push.waiting_input", {
|
|
147600
|
+
event: "expo_push.waiting_input",
|
|
147601
|
+
sessionId: session.id,
|
|
147602
|
+
...outcome
|
|
147603
|
+
});
|
|
147604
|
+
}
|
|
147605
|
+
} catch (err) {
|
|
147606
|
+
log9.error("expo_push.notify_failed", {
|
|
147607
|
+
event: "expo_push.notify_failed",
|
|
147608
|
+
sessionId: session.id,
|
|
147609
|
+
status: session.status,
|
|
147610
|
+
err: String(err)
|
|
147611
|
+
});
|
|
147612
|
+
}
|
|
147613
|
+
}
|
|
147614
|
+
};
|
|
147615
|
+
|
|
147429
147616
|
// src/server.ts
|
|
147430
147617
|
init_detectPermissionGate();
|
|
147431
147618
|
init_detectQuestionFromScreen();
|
|
@@ -148773,6 +148960,10 @@ var StreamerServer = class {
|
|
|
148773
148960
|
apnsClient = null;
|
|
148774
148961
|
liveActivityNotifier = null;
|
|
148775
148962
|
liveActivityRenewal = null;
|
|
148963
|
+
// "Your turn" notifications over Expo's relay (#528). Needs no credential of
|
|
148964
|
+
// its own, so unlike the Live Activity path it is on wherever the cache DB
|
|
148965
|
+
// opened — with no registered device it simply sends nothing.
|
|
148966
|
+
waitingInputNotifier = null;
|
|
148776
148967
|
discoveryCache = null;
|
|
148777
148968
|
// Single-flight for process discovery. Mobile polls GET /api/sessions and
|
|
148778
148969
|
// retries on timeout; without this, every concurrent request starts its own
|
|
@@ -149072,6 +149263,7 @@ var StreamerServer = class {
|
|
|
149072
149263
|
this.wsHub.broadcast({ type: "session_update", session: resp });
|
|
149073
149264
|
}
|
|
149074
149265
|
void this.liveActivityNotifier?.onStatusChange(session, previousStatus);
|
|
149266
|
+
void this.waitingInputNotifier?.onStatusChange(session, previousStatus);
|
|
149075
149267
|
this.sessionStatusBus.emit(`status:${session.id}`, session.status, session);
|
|
149076
149268
|
}
|
|
149077
149269
|
});
|
|
@@ -149117,6 +149309,7 @@ var StreamerServer = class {
|
|
|
149117
149309
|
cache: () => this.cache,
|
|
149118
149310
|
cacheMonitor: () => this.cacheMonitor,
|
|
149119
149311
|
pushRepo: () => this.pushRepo,
|
|
149312
|
+
liveActivityPushEnabled: () => this.liveActivityNotifier !== null,
|
|
149120
149313
|
devicesRepo: () => this.devicesRepo,
|
|
149121
149314
|
projectsRepo: () => this.projectsRepo,
|
|
149122
149315
|
conversationsRepo: () => this.conversationsRepo,
|
|
@@ -149384,6 +149577,33 @@ var StreamerServer = class {
|
|
|
149384
149577
|
topic: `${creds.bundleId}.push-type.liveactivity`
|
|
149385
149578
|
});
|
|
149386
149579
|
}
|
|
149580
|
+
/**
|
|
149581
|
+
* Bring up "your turn" notifications over Expo's relay (#528).
|
|
149582
|
+
*
|
|
149583
|
+
* Unconditional, unlike Live Activity push: Expo holds the app's APNs and FCM
|
|
149584
|
+
* credentials, so a self-hosted streamer needs no credential of its own. The
|
|
149585
|
+
* access token is optional and only relevant if the Expo project has enhanced
|
|
149586
|
+
* security enabled — requiring one would lock out every self-hoster, since
|
|
149587
|
+
* they do not own the project. It is never logged.
|
|
149588
|
+
*/
|
|
149589
|
+
initWaitingInputPush(pushRepo) {
|
|
149590
|
+
const sender = new ExpoPushSender(pushRepo, process.env.THREADBASE_EXPO_ACCESS_TOKEN);
|
|
149591
|
+
const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0, import_os13.hostname)();
|
|
149592
|
+
this.waitingInputNotifier = new WaitingInputNotifier(
|
|
149593
|
+
sender,
|
|
149594
|
+
serverId,
|
|
149595
|
+
(id) => this.hasSessionSubscriber(id)
|
|
149596
|
+
);
|
|
149597
|
+
}
|
|
149598
|
+
/** Whether any live socket is subscribed to this session — "someone is looking". */
|
|
149599
|
+
hasSessionSubscriber(sessionId) {
|
|
149600
|
+
const subs = this.sessionSubscribers.get(sessionId);
|
|
149601
|
+
if (!subs) return false;
|
|
149602
|
+
for (const ws2 of subs) {
|
|
149603
|
+
if (ws2.readyState === ws2.OPEN) return true;
|
|
149604
|
+
}
|
|
149605
|
+
return false;
|
|
149606
|
+
}
|
|
149387
149607
|
/**
|
|
149388
149608
|
* Classify sessions left behind by previous streamer runs (C1 Phase 3a).
|
|
149389
149609
|
*
|
|
@@ -149986,6 +150206,7 @@ var StreamerServer = class {
|
|
|
149986
150206
|
this.pushRepo = new PushRepository(db);
|
|
149987
150207
|
this.devicesRepo = new DevicesRepository(db);
|
|
149988
150208
|
this.initLiveActivityPush(this.pushRepo);
|
|
150209
|
+
this.initWaitingInputPush(this.pushRepo);
|
|
149989
150210
|
this.cacheMonitor = new CacheIntegrityMonitor(
|
|
149990
150211
|
this.cache,
|
|
149991
150212
|
this.wsHub,
|
|
@@ -156833,7 +157054,7 @@ init_platform2();
|
|
|
156833
157054
|
init_logger();
|
|
156834
157055
|
init_protocol();
|
|
156835
157056
|
init_socket();
|
|
156836
|
-
var
|
|
157057
|
+
var log11 = getLogger("prod");
|
|
156837
157058
|
function clearSupervisorLogs() {
|
|
156838
157059
|
let paths;
|
|
156839
157060
|
try {
|
|
@@ -157017,12 +157238,12 @@ function registerProdCommands(program3) {
|
|
|
157017
157238
|
prod.command("start").description("Restore prod after a user-held suspension").option("--clear-logs", "Clear existing stdout + stderr logs", false).action(async (opts) => {
|
|
157018
157239
|
if (opts.clearLogs === true) clearSupervisorLogs();
|
|
157019
157240
|
const r = await runProdStart();
|
|
157020
|
-
|
|
157241
|
+
log11.info(r.message, void 0, "console");
|
|
157021
157242
|
if (!r.ok) process.exitCode = 1;
|
|
157022
157243
|
});
|
|
157023
157244
|
prod.command("stop").description("Unload the launchd agent (prod will not auto-restart)").action(async () => {
|
|
157024
157245
|
const r = await runProdStop();
|
|
157025
|
-
|
|
157246
|
+
log11.info(r.message, void 0, "console");
|
|
157026
157247
|
});
|
|
157027
157248
|
prod.command("status").description("Report whether prod is supervised, suspended, or down").action(async () => {
|
|
157028
157249
|
const s3 = await runProdStatus();
|
|
@@ -157031,7 +157252,7 @@ function registerProdCommands(program3) {
|
|
|
157031
157252
|
`pid: ${s3.agentPid ?? "(none)"}`,
|
|
157032
157253
|
s3.marker ? `marker: ${s3.marker.userHeld ? "userHeld (intentional stop)" : "dev-suspended"}, devPid=${s3.marker.devPid}, port=${s3.marker.port}, repo=${s3.marker.repoToplevel}` : "marker: none"
|
|
157033
157254
|
];
|
|
157034
|
-
|
|
157255
|
+
log11.info(parts.join("\n "), void 0, "console");
|
|
157035
157256
|
});
|
|
157036
157257
|
prod.command("restart").description("Stop + restart the supervised streamer (re-reads service definition)").option("--clear-logs", "Clear existing stdout + stderr logs", false).action(async (opts) => {
|
|
157037
157258
|
const sup = getSupervisor();
|
|
@@ -157040,24 +157261,24 @@ function registerProdCommands(program3) {
|
|
|
157040
157261
|
sup.bootoutAgent();
|
|
157041
157262
|
sup.bootstrapAgent(specPath, { afterBootout: true });
|
|
157042
157263
|
const what = process.platform === "darwin" ? `agent restarted from ${specPath}` : `task '${TASK_NAME}' restarted`;
|
|
157043
|
-
|
|
157264
|
+
log11.info(what, void 0, "console");
|
|
157044
157265
|
});
|
|
157045
157266
|
prod.command("doctor").description("Detect stale markers, missing agents, and pty-host health").option("--fix", "Apply repairs (default is dry-run)", false).action(async (opts) => {
|
|
157046
157267
|
const r = await runProdDoctor({ fix: opts.fix === true });
|
|
157047
157268
|
if (r.ptyHost) {
|
|
157048
|
-
|
|
157269
|
+
log11.info(
|
|
157049
157270
|
r.ptyHost.reachable ? `pty-host: reachable, protocol=${r.ptyHost.protocolVersion}, sessions=${r.ptyHost.sessionCount}` : `pty-host: unreachable (${r.ptyHost.error})`,
|
|
157050
157271
|
void 0,
|
|
157051
157272
|
"console"
|
|
157052
157273
|
);
|
|
157053
157274
|
}
|
|
157054
|
-
|
|
157055
|
-
for (const f2 of r.findings)
|
|
157275
|
+
log11.info(`findings: ${r.findings.length === 0 ? "(none)" : ""}`, void 0, "console");
|
|
157276
|
+
for (const f2 of r.findings) log11.info(` - ${f2}`, void 0, "console");
|
|
157056
157277
|
if (r.repairs.length) {
|
|
157057
|
-
|
|
157058
|
-
for (const fix of r.repairs)
|
|
157278
|
+
log11.info(`repairs:`, void 0, "console");
|
|
157279
|
+
for (const fix of r.repairs) log11.info(` - ${fix}`, void 0, "console");
|
|
157059
157280
|
} else if (!opts.fix && r.findings.length > 0) {
|
|
157060
|
-
|
|
157281
|
+
log11.info(`(re-run with --fix to apply repairs)`, void 0, "console");
|
|
157061
157282
|
}
|
|
157062
157283
|
});
|
|
157063
157284
|
prod.command("logs").description("Tail the supervised streamer's stdout + stderr log files").option("-n, --lines <count>", "Seed with the last N lines (default 50)", "50").option("--no-follow", "Print last N lines and exit (do not follow)").option("--errors-only", "Tail only stderr", false).option("--clear", "Truncate stdout + stderr logs in place, then exit", false).action(async (opts) => {
|
|
@@ -157068,14 +157289,14 @@ function registerProdCommands(program3) {
|
|
|
157068
157289
|
errorsOnly: opts.errorsOnly === true,
|
|
157069
157290
|
clear: opts.clear === true
|
|
157070
157291
|
});
|
|
157071
|
-
if (r.message)
|
|
157292
|
+
if (r.message) log11.info(r.message, void 0, "console");
|
|
157072
157293
|
if (!r.ok) process.exitCode = 1;
|
|
157073
157294
|
});
|
|
157074
157295
|
program3.addCommand(prod);
|
|
157075
157296
|
}
|
|
157076
157297
|
|
|
157077
157298
|
// cli/index.ts
|
|
157078
|
-
var
|
|
157299
|
+
var log14 = getLogger("cli");
|
|
157079
157300
|
var program2 = new Command();
|
|
157080
157301
|
program2.name("threadbase-streamer").description("PTY session management, WebSocket streaming, and REST API server for Claude Code").version(getVersion());
|
|
157081
157302
|
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(
|
|
@@ -157117,14 +157338,14 @@ program2.command("serve").description("Start the streamer server").option("-p, -
|
|
|
157117
157338
|
const { checkSqliteAbi: checkSqliteAbi2 } = await Promise.resolve().then(() => (init_check_sqlite_abi(), check_sqlite_abi_exports));
|
|
157118
157339
|
checkSqliteAbi2();
|
|
157119
157340
|
} catch (err) {
|
|
157120
|
-
|
|
157341
|
+
log14.error(err instanceof Error ? err.message : String(err), void 0, "console");
|
|
157121
157342
|
process.exit(1);
|
|
157122
157343
|
}
|
|
157123
157344
|
if (opts.multiAgentFlow) {
|
|
157124
157345
|
process.env.MULTI_AGENT_FLOW = "true";
|
|
157125
157346
|
}
|
|
157126
157347
|
if (opts.defaultPermissionMode !== void 0 && !isPermissionMode(opts.defaultPermissionMode)) {
|
|
157127
|
-
|
|
157348
|
+
log14.error(
|
|
157128
157349
|
`Invalid --default-permission-mode: ${opts.defaultPermissionMode} (expected one of ${PERMISSION_MODES.join(", ")})`,
|
|
157129
157350
|
void 0,
|
|
157130
157351
|
"console"
|
|
@@ -157140,7 +157361,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
|
|
|
157140
157361
|
const value = eq === -1 ? true : entry.slice(eq + 1);
|
|
157141
157362
|
const def = findFlag(id);
|
|
157142
157363
|
if (!def) {
|
|
157143
|
-
|
|
157364
|
+
log14.error(
|
|
157144
157365
|
`Invalid --claude-flag id: ${id} (expected one of ${CLAUDE_FLAGS.map((f2) => f2.id).join(", ")})`,
|
|
157145
157366
|
void 0,
|
|
157146
157367
|
"console"
|
|
@@ -157157,7 +157378,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
|
|
|
157157
157378
|
claudeFlags = validateFlagValues(raw2);
|
|
157158
157379
|
for (const id of Object.keys(raw2)) {
|
|
157159
157380
|
if (!(id in claudeFlags)) {
|
|
157160
|
-
|
|
157381
|
+
log14.error(`Invalid value for --claude-flag ${id}`, void 0, "console");
|
|
157161
157382
|
process.exit(1);
|
|
157162
157383
|
}
|
|
157163
157384
|
}
|
|
@@ -157166,13 +157387,13 @@ program2.command("serve").description("Start the streamer server").option("-p, -
|
|
|
157166
157387
|
if (Array.isArray(opts.feature) && opts.feature.length > 0) {
|
|
157167
157388
|
const parsed = parseFeatureFlagArgs(opts.feature);
|
|
157168
157389
|
if (parsed.errors.length > 0) {
|
|
157169
|
-
for (const error51 of parsed.errors)
|
|
157390
|
+
for (const error51 of parsed.errors) log14.error(`--feature: ${error51}`, void 0, "console");
|
|
157170
157391
|
process.exit(1);
|
|
157171
157392
|
}
|
|
157172
157393
|
featureFlags = parsed.values;
|
|
157173
157394
|
}
|
|
157174
157395
|
if (opts.defaultEffort !== void 0 && !isEffortLevel(opts.defaultEffort)) {
|
|
157175
|
-
|
|
157396
|
+
log14.error(
|
|
157176
157397
|
`Invalid --default-effort: ${opts.defaultEffort} (expected one of ${EFFORT_LEVELS.join(", ")})`,
|
|
157177
157398
|
void 0,
|
|
157178
157399
|
"console"
|
|
@@ -157183,7 +157404,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
|
|
|
157183
157404
|
if (opts.ptyGracePeriodMs !== void 0) {
|
|
157184
157405
|
const parsed = Number(opts.ptyGracePeriodMs);
|
|
157185
157406
|
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
157186
|
-
|
|
157407
|
+
log14.error(
|
|
157187
157408
|
`Invalid --pty-grace-period-ms: ${opts.ptyGracePeriodMs} (expected a non-negative integer; 0 disables auto-hold)`,
|
|
157188
157409
|
void 0,
|
|
157189
157410
|
"console"
|
|
@@ -157202,14 +157423,14 @@ program2.command("serve").description("Start the streamer server").option("-p, -
|
|
|
157202
157423
|
if (isProdInvocation) {
|
|
157203
157424
|
const { truncateOversizedLogs: truncateOversizedLogs2 } = await Promise.resolve().then(() => (init_log_cap(), log_cap_exports));
|
|
157204
157425
|
for (const f2 of truncateOversizedLogs2()) {
|
|
157205
|
-
|
|
157426
|
+
log14.info(`truncated oversized log at boot: ${f2}`, { path: f2, event: "log.truncated" });
|
|
157206
157427
|
}
|
|
157207
157428
|
}
|
|
157208
157429
|
if (process.platform === "darwin") {
|
|
157209
157430
|
const { detectConflictingAgents: detectConflictingAgents2, formatConflictMessage: formatConflictMessage2 } = await Promise.resolve().then(() => (init_conflict_check(), conflict_check_exports));
|
|
157210
157431
|
const conflicts = detectConflictingAgents2();
|
|
157211
157432
|
if (conflicts.length > 0) {
|
|
157212
|
-
|
|
157433
|
+
log14.warn(formatConflictMessage2(conflicts), void 0, "console");
|
|
157213
157434
|
}
|
|
157214
157435
|
}
|
|
157215
157436
|
let resolvedDefaultPermissionMode = opts.defaultPermissionMode;
|
|
@@ -157224,7 +157445,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
|
|
|
157224
157445
|
resolvedAutoResumeOnBoot = await interactiveAutoResumePrompt2();
|
|
157225
157446
|
setAutoResumeOnBoot(resolvedAutoResumeOnBoot);
|
|
157226
157447
|
} else if (resolvedAutoResumeOnBoot === void 0) {
|
|
157227
|
-
|
|
157448
|
+
log14.info(
|
|
157228
157449
|
"auto_resume_on_boot is not set; interrupted sessions will wait for you to resume them.\nSet `auto_resume_on_boot: true` in ~/.threadbase/server.yaml to resume them automatically.",
|
|
157229
157450
|
void 0,
|
|
157230
157451
|
"console"
|
|
@@ -157274,15 +157495,15 @@ program2.command("serve").description("Start the streamer server").option("-p, -
|
|
|
157274
157495
|
await server.listen(resolvedPort);
|
|
157275
157496
|
{
|
|
157276
157497
|
const v2 = getVersion();
|
|
157277
|
-
|
|
157498
|
+
log14.info(`Threadbase Streamer v${v2}`, { version: v2, port: resolvedPort });
|
|
157278
157499
|
}
|
|
157279
|
-
|
|
157500
|
+
log14.info(`Listening on http://localhost:${resolvedPort}`, {
|
|
157280
157501
|
url: `http://localhost:${resolvedPort}`
|
|
157281
157502
|
});
|
|
157282
|
-
|
|
157503
|
+
log14.info(`WebSocket at ws://localhost:${resolvedPort}/ws`, {
|
|
157283
157504
|
wsUrl: `ws://localhost:${resolvedPort}/ws`
|
|
157284
157505
|
});
|
|
157285
|
-
|
|
157506
|
+
log14.info(`API key: ${apiKey}`, { apiKeyMasked: `${apiKey.slice(0, 6)}\u2026` });
|
|
157286
157507
|
try {
|
|
157287
157508
|
await printServerBanner({
|
|
157288
157509
|
port: resolvedPort,
|
|
@@ -157292,15 +157513,15 @@ program2.command("serve").description("Start the streamer server").option("-p, -
|
|
|
157292
157513
|
});
|
|
157293
157514
|
} catch (err) {
|
|
157294
157515
|
const message = err instanceof Error ? err.message : String(err);
|
|
157295
|
-
|
|
157296
|
-
|
|
157516
|
+
log14.warn(`(skipped pairing QR: ${message})`, { reason: message });
|
|
157517
|
+
log14.info(
|
|
157297
157518
|
printUrlBanner({ url: resolveServerUrl({ publicUrl, port: resolvedPort }) }),
|
|
157298
157519
|
void 0,
|
|
157299
157520
|
"console"
|
|
157300
157521
|
);
|
|
157301
157522
|
}
|
|
157302
157523
|
const shutdown = async () => {
|
|
157303
|
-
|
|
157524
|
+
log14.info("Shutting down...");
|
|
157304
157525
|
await server.close();
|
|
157305
157526
|
process.exit(0);
|
|
157306
157527
|
};
|
|
@@ -157308,7 +157529,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
|
|
|
157308
157529
|
process.on("SIGINT", shutdown);
|
|
157309
157530
|
process.on("SIGTERM", shutdown);
|
|
157310
157531
|
process.on("uncaughtException", (err) => {
|
|
157311
|
-
|
|
157532
|
+
log14.error(`uncaught: ${err.message}`, {
|
|
157312
157533
|
error: err.message,
|
|
157313
157534
|
stack: err.stack,
|
|
157314
157535
|
event: "process.uncaught"
|
|
@@ -157317,7 +157538,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
|
|
|
157317
157538
|
});
|
|
157318
157539
|
process.on("unhandledRejection", (reason) => {
|
|
157319
157540
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
157320
|
-
|
|
157541
|
+
log14.error(`unhandled rejection: ${msg}`, {
|
|
157321
157542
|
error: msg,
|
|
157322
157543
|
event: "process.unhandled_rejection"
|
|
157323
157544
|
});
|
|
@@ -157342,10 +157563,10 @@ program2.command("cache").description("Manage the local SQLite conversation cach
|
|
|
157342
157563
|
const f2 = dbPath + suffix;
|
|
157343
157564
|
if (existsSync16(f2)) {
|
|
157344
157565
|
rmSync6(f2);
|
|
157345
|
-
|
|
157566
|
+
log14.info(`Deleted ${f2}`, { path: f2 }, "console");
|
|
157346
157567
|
}
|
|
157347
157568
|
}
|
|
157348
|
-
|
|
157569
|
+
log14.info("Cache cleared. Restart the server to rebuild.", void 0, "console");
|
|
157349
157570
|
})
|
|
157350
157571
|
);
|
|
157351
157572
|
program2.command("pair").description("Print a pairing QR code (server must already be running)").option("-p, --port <number>", "Port the server is listening on", "8766").action(async (opts) => {
|
|
@@ -157370,7 +157591,7 @@ program2.command("set-key [key]").description("Set the streamer API key in ~/.th
|
|
|
157370
157591
|
program2.command("update").description("Check for streamer updates from GitHub Releases and install them").option("--check", "Check only; do not install", false).option("--version <version>", "Pin to a specific release tag").option("--allow-major", "Allow a major-version bump", false).option("--force", "Skip the active-session defer check", false).option("--dry-run", "Print what would be installed without writing to disk", false).option("-p, --port <number>", "Port of the running streamer for active-session check", "8766").action(async (opts) => {
|
|
157371
157592
|
const cfg = loadUpdateConfig();
|
|
157372
157593
|
if (!cfg) {
|
|
157373
|
-
|
|
157594
|
+
log14.warn(
|
|
157374
157595
|
`No update config found at ${DEFAULT_CONFIG_PATH}. Create one with at least 'github_repo: owner/name' to enable updates.`,
|
|
157375
157596
|
void 0,
|
|
157376
157597
|
"console"
|
|
@@ -157389,11 +157610,11 @@ program2.command("update").description("Check for streamer updates from GitHub R
|
|
|
157389
157610
|
appendUpdateLog(
|
|
157390
157611
|
`[check] current=${result2.current} latest=${result2.latest ?? "none"} status=${result2.reason}`
|
|
157391
157612
|
);
|
|
157392
|
-
|
|
157393
|
-
|
|
157394
|
-
|
|
157395
|
-
|
|
157396
|
-
|
|
157613
|
+
log14.info(`Current : ${result2.current}`, void 0, "console");
|
|
157614
|
+
log14.info(`Latest : ${result2.latest ?? "(none)"}`, void 0, "console");
|
|
157615
|
+
log14.info(`Channel : ${cfg.channel}`, void 0, "console");
|
|
157616
|
+
log14.info(`Diff : ${result2.diff ?? "(none)"}`, void 0, "console");
|
|
157617
|
+
log14.info(`Status : ${result2.reason}`, void 0, "console");
|
|
157397
157618
|
return;
|
|
157398
157619
|
}
|
|
157399
157620
|
const port = Number.parseInt(opts.port, 10);
|
|
@@ -157409,20 +157630,20 @@ program2.command("update").description("Check for streamer updates from GitHub R
|
|
|
157409
157630
|
});
|
|
157410
157631
|
switch (result.kind) {
|
|
157411
157632
|
case "no-op":
|
|
157412
|
-
|
|
157413
|
-
|
|
157414
|
-
|
|
157633
|
+
log14.info(`Current : ${result.current}`, void 0, "console");
|
|
157634
|
+
log14.info(`Latest : ${result.latest ?? "(none)"}`, void 0, "console");
|
|
157635
|
+
log14.info(`Status : ${result.reason}`, void 0, "console");
|
|
157415
157636
|
break;
|
|
157416
157637
|
case "unsupported-install":
|
|
157417
|
-
|
|
157638
|
+
log14.warn(result.reason, void 0, "console");
|
|
157418
157639
|
process.exitCode = 2;
|
|
157419
157640
|
break;
|
|
157420
157641
|
case "deferred":
|
|
157421
|
-
|
|
157642
|
+
log14.warn(`Deferred: ${result.reason}`, void 0, "console");
|
|
157422
157643
|
process.exitCode = 2;
|
|
157423
157644
|
break;
|
|
157424
157645
|
case "dry-run":
|
|
157425
|
-
|
|
157646
|
+
log14.info(
|
|
157426
157647
|
`Would install ${result.latest} from ${result.tarballUrl}`,
|
|
157427
157648
|
void 0,
|
|
157428
157649
|
"console"
|
|
@@ -157430,28 +157651,28 @@ program2.command("update").description("Check for streamer updates from GitHub R
|
|
|
157430
157651
|
break;
|
|
157431
157652
|
case "installed":
|
|
157432
157653
|
if (result.restart.method.startsWith("failed:")) {
|
|
157433
|
-
|
|
157654
|
+
log14.error(
|
|
157434
157655
|
`Installed ${result.installed} on disk, but the running service was not updated. Restart: ${result.restart.method}.`,
|
|
157435
157656
|
void 0,
|
|
157436
157657
|
"console"
|
|
157437
157658
|
);
|
|
157438
157659
|
process.exitCode = 1;
|
|
157439
157660
|
} else {
|
|
157440
|
-
|
|
157661
|
+
log14.info(
|
|
157441
157662
|
`Installed ${result.installed} (was ${result.previous}). Restart: ${result.restart.method}.`,
|
|
157442
157663
|
void 0,
|
|
157443
157664
|
"console"
|
|
157444
157665
|
);
|
|
157445
157666
|
}
|
|
157446
157667
|
if (result.pruned.length > 0) {
|
|
157447
|
-
|
|
157668
|
+
log14.info(`Pruned old releases: ${result.pruned.join(", ")}`, void 0, "console");
|
|
157448
157669
|
}
|
|
157449
157670
|
break;
|
|
157450
157671
|
}
|
|
157451
157672
|
} catch (err) {
|
|
157452
157673
|
const message = err instanceof Error ? err.message : String(err);
|
|
157453
157674
|
appendUpdateLog(`[error] ${message}`);
|
|
157454
|
-
|
|
157675
|
+
log14.error(`Update failed: ${message}`, { error: message }, "console");
|
|
157455
157676
|
process.exitCode = 1;
|
|
157456
157677
|
}
|
|
157457
157678
|
});
|
|
@@ -157481,14 +157702,14 @@ program2.command("pty-host").description(
|
|
|
157481
157702
|
onConnection: (transport) => host.accept(transport)
|
|
157482
157703
|
});
|
|
157483
157704
|
} catch (err) {
|
|
157484
|
-
|
|
157705
|
+
log14.error(
|
|
157485
157706
|
`pty-host could not listen on ${opts.socket}: ${err instanceof Error ? err.message : String(err)}`,
|
|
157486
157707
|
void 0,
|
|
157487
157708
|
"console"
|
|
157488
157709
|
);
|
|
157489
157710
|
process.exit(1);
|
|
157490
157711
|
}
|
|
157491
|
-
|
|
157712
|
+
log14.info(`pty-host listening on ${opts.socket}`, {
|
|
157492
157713
|
event: "pty_host.listening",
|
|
157493
157714
|
socket: opts.socket
|
|
157494
157715
|
});
|
|
@@ -157542,7 +157763,7 @@ async function printServerBanner({
|
|
|
157542
157763
|
}) {
|
|
157543
157764
|
const url2 = resolveServerUrl({ publicUrl, port });
|
|
157544
157765
|
if (!includeQr) {
|
|
157545
|
-
|
|
157766
|
+
log14.info(printUrlBanner({ url: url2 }), void 0, "console");
|
|
157546
157767
|
return;
|
|
157547
157768
|
}
|
|
157548
157769
|
const res = await fetch(`http://localhost:${port}/api/pair/start`, {
|
|
@@ -157557,9 +157778,9 @@ async function printServerBanner({
|
|
|
157557
157778
|
const expSeconds = Math.floor(expiresAt / 1e3);
|
|
157558
157779
|
const payload = `threadbase://pair?url=${encodeURIComponent(url2)}&token=${token}&exp=${expSeconds}`;
|
|
157559
157780
|
const qr2 = await generateQr(payload);
|
|
157560
|
-
|
|
157561
|
-
|
|
157562
|
-
|
|
157781
|
+
log14.info(printUrlBanner({ url: url2, qr: qr2, expiresAt }), void 0, "console");
|
|
157782
|
+
log14.info(`Pair URL: ${payload}`, void 0, "console");
|
|
157783
|
+
log14.info(`Expires in ${expiresInSeconds}s`, void 0, "console");
|
|
157563
157784
|
}
|
|
157564
157785
|
/*! Bundled license information:
|
|
157565
157786
|
|