@indigoai-us/hq-cli 5.345.175 → 5.345.177
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/CHANGELOG.md +25 -0
- package/dist/command-catalog.generated.d.ts +65 -0
- package/dist/command-catalog.generated.js +80 -0
- package/dist/commands/bot.d.ts +28 -2
- package/dist/commands/bot.js +29 -6
- package/dist/commands/daemon.js +16 -0
- package/dist/commands/lanes-loop.d.ts +4 -1
- package/dist/commands/lanes-loop.js +23 -4
- package/dist/commands/lanes-shared.d.ts +5 -0
- package/dist/commands/lanes.d.ts +5 -7
- package/dist/commands/lanes.js +65 -5
- package/dist/commands/sync-core.d.ts +17 -0
- package/dist/commands/sync-core.js +46 -5
- package/dist/commands/sync-manifest.d.ts +6 -0
- package/dist/commands/sync-manifest.js +13 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +448 -226
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/bot/cli-update.d.ts +36 -0
- package/dist/lib/bot/cli-update.js +160 -42
- package/dist/lib/bot/run.d.ts +21 -0
- package/dist/lib/bot/run.js +127 -10
- package/dist/lib/bot/runtime/claude.d.ts +7 -1
- package/dist/lib/bot/runtime/claude.js +29 -0
- package/dist/lib/bot/runtime/codex.d.ts +5 -0
- package/dist/lib/bot/runtime/codex.js +13 -0
- package/dist/lib/bot/runtime/types.d.ts +10 -0
- package/dist/lib/bot/runtime-sign-in-check.d.ts +32 -0
- package/dist/lib/bot/runtime-sign-in-check.js +53 -0
- package/dist/lib/bot/runtime-sign-in.d.ts +32 -3
- package/dist/lib/bot/runtime-sign-in.js +17 -2
- package/dist/lib/bot/status.d.ts +25 -1
- package/dist/lib/bot/status.js +28 -0
- package/dist/lib/daemon/catalog.d.ts +4 -0
- package/dist/lib/daemon/catalog.js +47 -19
- package/dist/lib/daemon/run.js +1 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/lanes/loop-disposition.d.ts +44 -0
- package/dist/lib/lanes/loop-disposition.js +302 -0
- package/dist/lib/lanes/loop.d.ts +16 -3
- package/dist/lib/lanes/loop.js +167 -60
- package/dist/lib/lanes/reconcile.d.ts +5 -0
- package/dist/lib/lanes/reconcile.js +79 -4
- package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +3 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.js +41 -12
- package/dist/lib/sync-core/engine-home.d.ts +11 -4
- package/dist/lib/sync-core/engine-home.js +22 -21
- package/dist/lib/sync-core/manifest-scopes.d.ts +15 -0
- package/dist/lib/sync-core/manifest-scopes.js +26 -0
- package/dist/lib/sync-core/reconcile.d.ts +2 -2
- package/dist/lib/sync-core/reconcile.js +70 -46
- package/dist/lib/sync-core/release.d.ts +2 -2
- package/dist/lib/sync-core/release.js +31 -6
- package/dist/lib/sync-core/rollout-state.d.ts +6 -3
- package/dist/lib/sync-core/rollout-state.js +26 -22
- package/package.json +8 -8
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.
|
|
1
|
+
{"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.104","bundleSha256":"40c72e049db21ee4551fa737676d8022cd090149ede9e3c9e53a9fae3f28dadc","formatVersion":1}
|
|
@@ -23,6 +23,15 @@
|
|
|
23
23
|
* that target for CLI_UPDATE_NOT_CONVERGED_COOLDOWN_MS instead of restarting
|
|
24
24
|
* again. Restarts are also capped per window, so an install that keeps
|
|
25
25
|
* flipping cannot turn into a restart loop.
|
|
26
|
+
*
|
|
27
|
+
* The same watcher covers the macOS HQ.app launcher. A bot's LaunchAgent runs
|
|
28
|
+
* `HQ.app/Contents/MacOS/hq-sync-menubar --hq-launchd-task node hq bot run`, and
|
|
29
|
+
* that launcher is what macOS privacy (TCC) attributes the bot's file access
|
|
30
|
+
* to. When an HQ.app update replaces the launcher binary under a running bot,
|
|
31
|
+
* every coding-tool turn after that fails within milliseconds ("An unknown
|
|
32
|
+
* error occurred (Unexpected)") because ~/Documents access no longer resolves,
|
|
33
|
+
* until the bot is restarted under the new launcher. So a changed launcher
|
|
34
|
+
* binary is handled like an updated CLI: confirm, drain, exit for launchd.
|
|
26
35
|
*/
|
|
27
36
|
import type { BotLogger } from "./log.js";
|
|
28
37
|
/**
|
|
@@ -62,7 +71,18 @@ export interface CliUpdate {
|
|
|
62
71
|
from: string;
|
|
63
72
|
to: string;
|
|
64
73
|
packageRoot: string;
|
|
74
|
+
/** "launcher": the HQ.app launcher binary was replaced; the CLI version is unchanged. */
|
|
75
|
+
reason?: "launcher";
|
|
76
|
+
}
|
|
77
|
+
/** One line for the log and the exit reason. */
|
|
78
|
+
export declare function describeCliUpdate(update: CliUpdate): string;
|
|
79
|
+
/** Identity of a file on disk: an app update replaces the inode or changes size/mtime. */
|
|
80
|
+
export interface LauncherStamp {
|
|
81
|
+
ino: number;
|
|
82
|
+
size: number;
|
|
83
|
+
mtimeMs: number;
|
|
65
84
|
}
|
|
85
|
+
export declare function statLauncher(p: string): LauncherStamp | null;
|
|
66
86
|
interface CliUpdateLedger {
|
|
67
87
|
v: 1;
|
|
68
88
|
/** Written just before an update restart; settled by the next process. */
|
|
@@ -70,6 +90,7 @@ interface CliUpdateLedger {
|
|
|
70
90
|
from: string;
|
|
71
91
|
to: string;
|
|
72
92
|
at: string;
|
|
93
|
+
reason?: "launcher";
|
|
73
94
|
};
|
|
74
95
|
/** ISO times of recent update restarts (for the per-window cap). */
|
|
75
96
|
restarts: string[];
|
|
@@ -95,6 +116,9 @@ export interface CliUpdateWatcherOptions {
|
|
|
95
116
|
dir: string;
|
|
96
117
|
/** The script this process was launched with (process.argv[1]); null = do not watch. */
|
|
97
118
|
entry: string | null;
|
|
119
|
+
/** The HQ.app launcher binary that started this process (macOS LaunchAgent); null = do not watch. */
|
|
120
|
+
launcher?: string | null;
|
|
121
|
+
statLauncher?: (p: string) => LauncherStamp | null;
|
|
98
122
|
runningVersion?: string;
|
|
99
123
|
now?: () => number;
|
|
100
124
|
read?: (entry: string) => InstalledCliRead;
|
|
@@ -107,6 +131,18 @@ export interface CliUpdateWatcherOptions {
|
|
|
107
131
|
cooldownMs?: number;
|
|
108
132
|
}
|
|
109
133
|
export declare function createCliUpdateWatcher(opts: CliUpdateWatcherOptions): CliUpdateWatch;
|
|
134
|
+
/**
|
|
135
|
+
* The HQ.app launcher binary this process runs under, or null. A LaunchAgent
|
|
136
|
+
* written for HQ.app runs `<HQ.app>/Contents/MacOS/hq-sync-menubar
|
|
137
|
+
* --hq-launchd-task node hq ...`; the launcher spawns node and waits, so it is
|
|
138
|
+
* the direct parent. Only a parent whose command line carries the launcher
|
|
139
|
+
* flag counts.
|
|
140
|
+
*/
|
|
141
|
+
export declare function detectHqAppLauncher(opts?: {
|
|
142
|
+
platform?: NodeJS.Platform;
|
|
143
|
+
ppid?: number;
|
|
144
|
+
parentCommand?: (pid: number) => string | null;
|
|
145
|
+
}): string | null;
|
|
110
146
|
/**
|
|
111
147
|
* Who starts this bot again after it exits.
|
|
112
148
|
*
|
|
@@ -23,7 +23,17 @@
|
|
|
23
23
|
* that target for CLI_UPDATE_NOT_CONVERGED_COOLDOWN_MS instead of restarting
|
|
24
24
|
* again. Restarts are also capped per window, so an install that keeps
|
|
25
25
|
* flipping cannot turn into a restart loop.
|
|
26
|
+
*
|
|
27
|
+
* The same watcher covers the macOS HQ.app launcher. A bot's LaunchAgent runs
|
|
28
|
+
* `HQ.app/Contents/MacOS/hq-sync-menubar --hq-launchd-task node hq bot run`, and
|
|
29
|
+
* that launcher is what macOS privacy (TCC) attributes the bot's file access
|
|
30
|
+
* to. When an HQ.app update replaces the launcher binary under a running bot,
|
|
31
|
+
* every coding-tool turn after that fails within milliseconds ("An unknown
|
|
32
|
+
* error occurred (Unexpected)") because ~/Documents access no longer resolves,
|
|
33
|
+
* until the bot is restarted under the new launcher. So a changed launcher
|
|
34
|
+
* binary is handled like an updated CLI: confirm, drain, exit for launchd.
|
|
26
35
|
*/
|
|
36
|
+
import { spawnSync } from "node:child_process";
|
|
27
37
|
import * as fs from "node:fs";
|
|
28
38
|
import * as path from "node:path";
|
|
29
39
|
import semver from "semver";
|
|
@@ -98,6 +108,22 @@ export function readInstalledCli(entry, io = fs) {
|
|
|
98
108
|
}
|
|
99
109
|
return { ok: false, reason: `no ${CLI_NAME} package.json above ${real}` };
|
|
100
110
|
}
|
|
111
|
+
/** One line for the log and the exit reason. */
|
|
112
|
+
export function describeCliUpdate(update) {
|
|
113
|
+
return update.reason === "launcher"
|
|
114
|
+
? `HQ.app was updated (${update.packageRoot}); restarting under the new launcher`
|
|
115
|
+
: `hq-cli updated ${update.from} -> ${update.to}; restarting to load it`;
|
|
116
|
+
}
|
|
117
|
+
export function statLauncher(p) {
|
|
118
|
+
try {
|
|
119
|
+
const st = fs.statSync(p);
|
|
120
|
+
return { ino: st.ino, size: st.size, mtimeMs: st.mtimeMs };
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const sameStamp = (a, b) => a.ino === b.ino && a.size === b.size && a.mtimeMs === b.mtimeMs;
|
|
101
127
|
function ledgerPath(dir) {
|
|
102
128
|
return path.join(dir, CLI_UPDATE_LEDGER_FILE);
|
|
103
129
|
}
|
|
@@ -133,6 +159,19 @@ export function createCliUpdateWatcher(opts) {
|
|
|
133
159
|
const restartWindowMs = opts.restartWindowMs ?? CLI_UPDATE_RESTART_WINDOW_MS;
|
|
134
160
|
const cooldownMs = opts.cooldownMs ?? CLI_UPDATE_NOT_CONVERGED_COOLDOWN_MS;
|
|
135
161
|
const { dir, entry } = opts;
|
|
162
|
+
const launcher = opts.launcher ?? null;
|
|
163
|
+
const stat = opts.statLauncher ?? statLauncher;
|
|
164
|
+
/** The launcher as it was when this process started; null = not watched. */
|
|
165
|
+
const launcherBaseline = launcher ? stat(launcher) : null;
|
|
166
|
+
let launcherCandidate = null;
|
|
167
|
+
let launcherSeen = 0;
|
|
168
|
+
let lastLauncherNote = null;
|
|
169
|
+
const launcherNote = (log, level, message) => {
|
|
170
|
+
if (message === lastLauncherNote)
|
|
171
|
+
return;
|
|
172
|
+
lastLauncherNote = message;
|
|
173
|
+
log(level, message);
|
|
174
|
+
};
|
|
136
175
|
let nextCheckAt = now() + checkIntervalMs;
|
|
137
176
|
let candidate = null;
|
|
138
177
|
let seen = 0;
|
|
@@ -151,6 +190,10 @@ export function createCliUpdateWatcher(opts) {
|
|
|
151
190
|
return {
|
|
152
191
|
runningVersion,
|
|
153
192
|
startup(log) {
|
|
193
|
+
if (launcher && launcherBaseline)
|
|
194
|
+
log("info", `watching ${launcher} for HQ.app updates`);
|
|
195
|
+
else if (launcher)
|
|
196
|
+
log("warn", `not watching for HQ.app updates: ${launcher} is not readable`);
|
|
154
197
|
if (!entry) {
|
|
155
198
|
log("info", "not watching for hq-cli updates (no entry script)");
|
|
156
199
|
return;
|
|
@@ -159,7 +202,10 @@ export function createCliUpdateWatcher(opts) {
|
|
|
159
202
|
const pending = ledger.pending;
|
|
160
203
|
if (pending) {
|
|
161
204
|
delete ledger.pending;
|
|
162
|
-
if (
|
|
205
|
+
if (pending.reason === "launcher") {
|
|
206
|
+
log("info", `restarted after an HQ.app update; now running hq-cli ${runningVersion}`);
|
|
207
|
+
}
|
|
208
|
+
else if (runningVersion === pending.to) {
|
|
163
209
|
log("info", `hq-cli update converged: now running ${runningVersion} (was ${pending.from})`);
|
|
164
210
|
if (ledger.blocked?.version === runningVersion)
|
|
165
211
|
delete ledger.blocked;
|
|
@@ -183,60 +229,132 @@ export function createCliUpdateWatcher(opts) {
|
|
|
183
229
|
log("info", `watching ${entry} for hq-cli updates (running ${runningVersion})`);
|
|
184
230
|
},
|
|
185
231
|
poll(log) {
|
|
186
|
-
if (!entry)
|
|
232
|
+
if (!entry && !launcherBaseline)
|
|
187
233
|
return null;
|
|
188
234
|
const at = now();
|
|
189
235
|
if (at < nextCheckAt)
|
|
190
236
|
return null;
|
|
191
237
|
nextCheckAt = at + checkIntervalMs;
|
|
192
|
-
|
|
193
|
-
if (!installed.ok) {
|
|
194
|
-
candidate = null;
|
|
195
|
-
seen = 0;
|
|
196
|
-
note(log, "warn", `hq-cli install not readable, not restarting: ${installed.reason}`);
|
|
197
|
-
return null;
|
|
198
|
-
}
|
|
199
|
-
if (installed.version === runningVersion) {
|
|
200
|
-
candidate = null;
|
|
201
|
-
seen = 0;
|
|
202
|
-
lastNote = null;
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
if (rewriteInProgress(installed.packageRoot)) {
|
|
206
|
-
candidate = null;
|
|
207
|
-
seen = 0;
|
|
208
|
-
note(log, "info", `hq-cli ${installed.version} is still being installed; waiting for it to finish`);
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
if (candidate === installed.version)
|
|
212
|
-
seen += 1;
|
|
213
|
-
else {
|
|
214
|
-
candidate = installed.version;
|
|
215
|
-
seen = 1;
|
|
216
|
-
}
|
|
217
|
-
if (seen < confirmations) {
|
|
218
|
-
note(log, "info", `hq-cli ${installed.version} is installed (running ${runningVersion}); confirming before restarting`);
|
|
219
|
-
return null;
|
|
220
|
-
}
|
|
221
|
-
const ledger = readCliUpdateLedger(dir);
|
|
222
|
-
if (ledger.blocked?.version === installed.version && at < Date.parse(ledger.blocked.until)) {
|
|
223
|
-
note(log, "warn", `not restarting for hq-cli ${installed.version}: an earlier restart for it did not load it (retry after ${ledger.blocked.until})`);
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
if (recentRestarts(ledger, at).length >= restartLimit) {
|
|
227
|
-
note(log, "warn", `not restarting for hq-cli ${installed.version}: already restarted ${restartLimit} times for updates in the last ${Math.round(restartWindowMs / 60_000)} minutes`);
|
|
228
|
-
return null;
|
|
229
|
-
}
|
|
230
|
-
return { from: runningVersion, to: installed.version, packageRoot: installed.packageRoot };
|
|
238
|
+
return pollCli(log, at) ?? pollLauncher(log, at);
|
|
231
239
|
},
|
|
232
240
|
recordRestart(update) {
|
|
233
241
|
const at = now();
|
|
234
242
|
const ledger = readCliUpdateLedger(dir);
|
|
235
243
|
ledger.restarts = [...recentRestarts(ledger, at), new Date(at).toISOString()];
|
|
236
|
-
ledger.pending = {
|
|
244
|
+
ledger.pending = {
|
|
245
|
+
from: update.from,
|
|
246
|
+
to: update.to,
|
|
247
|
+
at: new Date(at).toISOString(),
|
|
248
|
+
...(update.reason ? { reason: update.reason } : {}),
|
|
249
|
+
};
|
|
237
250
|
writeCliUpdateLedger(dir, ledger);
|
|
238
251
|
},
|
|
239
252
|
};
|
|
253
|
+
function underRestartCap(log, at, what, say = note) {
|
|
254
|
+
if (recentRestarts(readCliUpdateLedger(dir), at).length < restartLimit)
|
|
255
|
+
return true;
|
|
256
|
+
say(log, "warn", `not restarting for ${what}: already restarted ${restartLimit} times for updates in the last ${Math.round(restartWindowMs / 60_000)} minutes`);
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
function pollLauncher(log, at) {
|
|
260
|
+
if (!launcher || !launcherBaseline)
|
|
261
|
+
return null;
|
|
262
|
+
const current = stat(launcher);
|
|
263
|
+
if (!current) {
|
|
264
|
+
// Mid-replace: the old bundle is gone and the new one is not there yet.
|
|
265
|
+
launcherCandidate = null;
|
|
266
|
+
launcherSeen = 0;
|
|
267
|
+
launcherNote(log, "info", `${launcher} is missing (HQ.app being replaced?); waiting`);
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
if (sameStamp(current, launcherBaseline)) {
|
|
271
|
+
launcherCandidate = null;
|
|
272
|
+
launcherSeen = 0;
|
|
273
|
+
lastLauncherNote = null;
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
if (launcherCandidate && sameStamp(current, launcherCandidate))
|
|
277
|
+
launcherSeen += 1;
|
|
278
|
+
else {
|
|
279
|
+
launcherCandidate = current;
|
|
280
|
+
launcherSeen = 1;
|
|
281
|
+
}
|
|
282
|
+
if (launcherSeen < confirmations) {
|
|
283
|
+
launcherNote(log, "info", `${launcher} changed (HQ.app update); confirming before restarting`);
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
if (!underRestartCap(log, at, "the HQ.app update", launcherNote))
|
|
287
|
+
return null;
|
|
288
|
+
return { from: runningVersion, to: runningVersion, packageRoot: launcher, reason: "launcher" };
|
|
289
|
+
}
|
|
290
|
+
function pollCli(log, at) {
|
|
291
|
+
if (!entry)
|
|
292
|
+
return null;
|
|
293
|
+
const installed = read(entry);
|
|
294
|
+
if (!installed.ok) {
|
|
295
|
+
candidate = null;
|
|
296
|
+
seen = 0;
|
|
297
|
+
note(log, "warn", `hq-cli install not readable, not restarting: ${installed.reason}`);
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
if (installed.version === runningVersion) {
|
|
301
|
+
candidate = null;
|
|
302
|
+
seen = 0;
|
|
303
|
+
lastNote = null;
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
if (rewriteInProgress(installed.packageRoot)) {
|
|
307
|
+
candidate = null;
|
|
308
|
+
seen = 0;
|
|
309
|
+
note(log, "info", `hq-cli ${installed.version} is still being installed; waiting for it to finish`);
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
if (candidate === installed.version)
|
|
313
|
+
seen += 1;
|
|
314
|
+
else {
|
|
315
|
+
candidate = installed.version;
|
|
316
|
+
seen = 1;
|
|
317
|
+
}
|
|
318
|
+
if (seen < confirmations) {
|
|
319
|
+
note(log, "info", `hq-cli ${installed.version} is installed (running ${runningVersion}); confirming before restarting`);
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
const ledger = readCliUpdateLedger(dir);
|
|
323
|
+
if (ledger.blocked?.version === installed.version && at < Date.parse(ledger.blocked.until)) {
|
|
324
|
+
note(log, "warn", `not restarting for hq-cli ${installed.version}: an earlier restart for it did not load it (retry after ${ledger.blocked.until})`);
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
if (!underRestartCap(log, at, `hq-cli ${installed.version}`))
|
|
328
|
+
return null;
|
|
329
|
+
return { from: runningVersion, to: installed.version, packageRoot: installed.packageRoot };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* The HQ.app launcher binary this process runs under, or null. A LaunchAgent
|
|
334
|
+
* written for HQ.app runs `<HQ.app>/Contents/MacOS/hq-sync-menubar
|
|
335
|
+
* --hq-launchd-task node hq ...`; the launcher spawns node and waits, so it is
|
|
336
|
+
* the direct parent. Only a parent whose command line carries the launcher
|
|
337
|
+
* flag counts.
|
|
338
|
+
*/
|
|
339
|
+
export function detectHqAppLauncher(opts = {}) {
|
|
340
|
+
if ((opts.platform ?? process.platform) !== "darwin")
|
|
341
|
+
return null;
|
|
342
|
+
const ppid = opts.ppid ?? process.ppid;
|
|
343
|
+
if (!ppid || ppid <= 1)
|
|
344
|
+
return null;
|
|
345
|
+
const parentCommand = opts.parentCommand ??
|
|
346
|
+
((pid) => {
|
|
347
|
+
const r = spawnSync("ps", ["-o", "command=", "-p", String(pid)], { encoding: "utf8", timeout: 5_000 });
|
|
348
|
+
return r.status === 0 ? r.stdout.trim() : null;
|
|
349
|
+
});
|
|
350
|
+
const command = parentCommand(ppid);
|
|
351
|
+
if (!command)
|
|
352
|
+
return null;
|
|
353
|
+
const flag = command.indexOf(" --hq-launchd-task ");
|
|
354
|
+
if (flag <= 0)
|
|
355
|
+
return null;
|
|
356
|
+
const binary = command.slice(0, flag);
|
|
357
|
+
return binary.endsWith("/Contents/MacOS/hq-sync-menubar") ? binary : null;
|
|
240
358
|
}
|
|
241
359
|
export function detectBotSupervisor(name, opts = {}) {
|
|
242
360
|
const env = opts.env ?? process.env;
|
package/dist/lib/bot/run.d.ts
CHANGED
|
@@ -56,6 +56,7 @@ import { type BotRuntime, type RuntimeTurnInput, type RuntimeTurnResult } from "
|
|
|
56
56
|
import { type BotSessionScope } from "./paths.js";
|
|
57
57
|
import { type DecisionDmClient } from "./decisions.js";
|
|
58
58
|
import type { VaultObjectDownloader } from "../agent-kit/attachment-download.js";
|
|
59
|
+
import { type RuntimeSignInCheck } from "./runtime-sign-in.js";
|
|
59
60
|
import { type HeartbeatAgentUidSource, type HeartbeatCredsSource, type HeartbeatIdentityMismatchReporter } from "../heartbeat-identity.js";
|
|
60
61
|
import { type BotState, type BotStatusFile } from "./status.js";
|
|
61
62
|
import { type CliUpdate, type CliUpdateWatch } from "./cli-update.js";
|
|
@@ -197,6 +198,15 @@ export interface BotRunDeps {
|
|
|
197
198
|
* thread are recorded through it. Unset leaves every owner DM on the model path.
|
|
198
199
|
*/
|
|
199
200
|
decisions?: DecisionDmClient;
|
|
201
|
+
/**
|
|
202
|
+
* The bot's own sign-in check (runtime-sign-in-check.ts), run with the
|
|
203
|
+
* turn's environment at start, before each turn, and while signed out.
|
|
204
|
+
* Injected in tests; unset runs the runtime's status command. A runtime
|
|
205
|
+
* with no status command answers `signedIn: null` and turns run as before.
|
|
206
|
+
*/
|
|
207
|
+
checkSignIn?: (env: NodeJS.ProcessEnv) => Promise<RuntimeSignInCheck>;
|
|
208
|
+
/** How often to ask the tool again while it is signed out (default SIGN_IN_CHECK_MS). */
|
|
209
|
+
signInCheckMs?: number;
|
|
200
210
|
}
|
|
201
211
|
export declare const KICKOFF_MESSAGE_ID = "kickoff";
|
|
202
212
|
/**
|
|
@@ -219,6 +229,17 @@ export declare const KICKOFF_MESSAGE_ID = "kickoff";
|
|
|
219
229
|
* Pure so the rule is unit-testable.
|
|
220
230
|
*/
|
|
221
231
|
export declare function turnEnv(base: NodeJS.ProcessEnv, kind: BotKind, agentUid?: string, companySlug?: string): NodeJS.ProcessEnv;
|
|
232
|
+
/** The account name this process runs as, or null when the OS cannot say. */
|
|
233
|
+
export declare function osUserName(): string | null;
|
|
234
|
+
/**
|
|
235
|
+
* Claude Code keys its macOS Keychain login on `$USER` (falling back to the
|
|
236
|
+
* literal "unknown"), so a tool spawned from an environment without USER
|
|
237
|
+
* reads a different Keychain item than the owner's terminal and is signed
|
|
238
|
+
* out. launchd and the HQ app's launcher may leave it unset; fill it (and
|
|
239
|
+
* LOGNAME) from the account this process runs as. Never overrides a USER
|
|
240
|
+
* already set. Pure.
|
|
241
|
+
*/
|
|
242
|
+
export declare function withOsUser(env: NodeJS.ProcessEnv, name?: string | null): NodeJS.ProcessEnv;
|
|
222
243
|
/** The core worker HQ's setup bot runs. */
|
|
223
244
|
export declare const SETUP_WORKER_ID = "setup";
|
|
224
245
|
/**
|
package/dist/lib/bot/run.js
CHANGED
|
@@ -50,6 +50,7 @@ import { ownerContextBlock } from "./owner-context.js";
|
|
|
50
50
|
import { randomUUID } from "node:crypto";
|
|
51
51
|
import { hasPromotionHold } from "./promotion-hold.js";
|
|
52
52
|
import * as fs from "node:fs";
|
|
53
|
+
import * as os from "node:os";
|
|
53
54
|
import * as path from "node:path";
|
|
54
55
|
import { fullJitterDelayMs } from "../mesh/live/backoff.js";
|
|
55
56
|
import { acquirePidLock, defaultPidLockDeps, releasePidLock } from "../mesh/live/daemon/pid-lock.js";
|
|
@@ -82,14 +83,15 @@ import { resolveRoomMentionsForBot, roomPostHasMentionTokens, } from "./room-men
|
|
|
82
83
|
import { buildJobTurnPrompt, isCatchUpRun, isJobItem, jobReportHeader, jobRunOutput, machineTimeZone, planJobRuns, stripJobHeader, SUPERSEDED_DISPOSITION } from "./job-run.js";
|
|
83
84
|
import { botJobQuietOptionEnabled } from "./job-quiet-flag.js";
|
|
84
85
|
import { clearPendingJobCompletion, isCompletionDue, isCompletionExpired, isCompletionForOtherAgent, readPendingJobCompletions, recordPendingJobCompletion } from "./job-completions.js";
|
|
85
|
-
import { SIGN_IN_RETRY_MS, signInNeededText } from "./runtime-sign-in.js";
|
|
86
|
+
import { SIGN_IN_CHECK_FRESH_MS, SIGN_IN_CHECK_MS, SIGN_IN_RETRY_MS, signInNeededText, signedOutExcerpt, } from "./runtime-sign-in.js";
|
|
87
|
+
import { checkRuntimeSignIn } from "./runtime-sign-in-check.js";
|
|
86
88
|
import { classifyRuntimeError, classifyRuntimeFailure, runtimeDidNotFinishText, runtimeFailureReason, runtimeFailureText, runtimeRepairNotice, withRetrySuggestion, } from "./runtime-failure.js";
|
|
87
89
|
import { HQ_BOT_AGENT_UID_ENV } from "../../commands/onboard-bot-membership.js";
|
|
88
90
|
import { captureHeartbeatIdentityMismatch, identityMismatchMessage, isHeartbeatIdentityMismatch, } from "../heartbeat-identity.js";
|
|
89
91
|
import { clearBotSession, ensureBotCompanyScope, readBotSession, writeBotSession } from "./session.js";
|
|
90
92
|
import { defaultBotStatus, patchBotStatus, readBotStatus } from "./status.js";
|
|
91
93
|
import { CLI_VERSION } from "../../cli-version.js";
|
|
92
|
-
import { CLI_UPDATE_RESTART_EXIT_CODE } from "./cli-update.js";
|
|
94
|
+
import { CLI_UPDATE_RESTART_EXIT_CODE, describeCliUpdate } from "./cli-update.js";
|
|
93
95
|
import { createInboxMqttWakeFlagReader, createInboxPollBackoffFlagReader, safeErrorClass } from "./inbox-poll-backoff-flag.js";
|
|
94
96
|
import { createInboxWakeWatcher } from "./inbox-wake.js";
|
|
95
97
|
import { workMeshRoot } from "../mesh/live/paths.js";
|
|
@@ -195,6 +197,29 @@ export function turnEnv(base, kind, agentUid, companySlug) {
|
|
|
195
197
|
}
|
|
196
198
|
return env;
|
|
197
199
|
}
|
|
200
|
+
/** The account name this process runs as, or null when the OS cannot say. */
|
|
201
|
+
export function osUserName() {
|
|
202
|
+
try {
|
|
203
|
+
const name = os.userInfo().username?.trim();
|
|
204
|
+
return name || null;
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Claude Code keys its macOS Keychain login on `$USER` (falling back to the
|
|
212
|
+
* literal "unknown"), so a tool spawned from an environment without USER
|
|
213
|
+
* reads a different Keychain item than the owner's terminal and is signed
|
|
214
|
+
* out. launchd and the HQ app's launcher may leave it unset; fill it (and
|
|
215
|
+
* LOGNAME) from the account this process runs as. Never overrides a USER
|
|
216
|
+
* already set. Pure.
|
|
217
|
+
*/
|
|
218
|
+
export function withOsUser(env, name = osUserName()) {
|
|
219
|
+
if (env.USER?.trim() || !name)
|
|
220
|
+
return env;
|
|
221
|
+
return { ...env, USER: name, ...(env.LOGNAME?.trim() ? {} : { LOGNAME: name }) };
|
|
222
|
+
}
|
|
198
223
|
/** The core worker HQ's setup bot runs. */
|
|
199
224
|
export const SETUP_WORKER_ID = SETUP_BOT_WORKER_ID;
|
|
200
225
|
/**
|
|
@@ -495,7 +520,7 @@ export async function runBot(deps) {
|
|
|
495
520
|
cliUpdatePending = update;
|
|
496
521
|
stopping = true;
|
|
497
522
|
const busy = running.size;
|
|
498
|
-
log("info",
|
|
523
|
+
log("info", `${describeCliUpdate(update)}${busy ? ` after ${busy} running turn${busy === 1 ? "" : "s"} finish${busy === 1 ? "es" : ""}` : ""}`);
|
|
499
524
|
return true;
|
|
500
525
|
};
|
|
501
526
|
// ── Preflight: creds, pid lock, owner verification ─────────────────────────
|
|
@@ -714,14 +739,17 @@ export async function runBot(deps) {
|
|
|
714
739
|
/** Conversations already told this time that the bot needs a sign-in. */
|
|
715
740
|
const toldAboutSignIn = new Set();
|
|
716
741
|
const isSignInFailure = (excerpt) => classifyRuntimeFailure(excerpt) === "signed-out";
|
|
717
|
-
const markSignInBroken = (excerpt) => {
|
|
718
|
-
|
|
742
|
+
const markSignInBroken = (excerpt, state = "expired", bump = true) => {
|
|
743
|
+
if (bump)
|
|
744
|
+
nextSignInRetryAt = now().getTime() + signInRetryMs;
|
|
719
745
|
modelHealth = "degraded";
|
|
720
746
|
if (signInIssue)
|
|
721
747
|
return;
|
|
722
|
-
signInIssue = { state
|
|
748
|
+
signInIssue = { state, runtime: config.runtime, since: now().toISOString() };
|
|
723
749
|
setStatus({ runtimeSignIn: signInIssue, lastError: excerpt.slice(0, 400) });
|
|
724
|
-
log("warn",
|
|
750
|
+
log("warn", state === "signed-out"
|
|
751
|
+
? `${runtime.id} is not signed in on this computer (its own check); holding turns and checking every ${Math.round(signInCheckMs / 1000)}s`
|
|
752
|
+
: `${runtime.id} needs to sign in again on this computer; holding turns and retrying every ${Math.round(signInRetryMs / 1000)}s`);
|
|
725
753
|
};
|
|
726
754
|
const markSignInWorks = () => {
|
|
727
755
|
if (!signInIssue)
|
|
@@ -731,6 +759,70 @@ export async function runBot(deps) {
|
|
|
731
759
|
setStatus({ runtimeSignIn: null });
|
|
732
760
|
log("info", `${runtime.id} sign-in works again; resuming`);
|
|
733
761
|
};
|
|
762
|
+
// ── The bot's own sign-in check ────────────────────────────────────────────
|
|
763
|
+
// The tool is asked in the turn's environment (not the owner's shell): at
|
|
764
|
+
// start, before a turn, and on a timer while it says signed out, so a
|
|
765
|
+
// message is never spent on a turn that cannot start and the app can read
|
|
766
|
+
// the answer before it sends a job.
|
|
767
|
+
const signInCheckMs = Math.max(0, deps.signInCheckMs ?? SIGN_IN_CHECK_MS);
|
|
768
|
+
const checkSignIn = deps.checkSignIn ?? ((env) => checkRuntimeSignIn({ runtime, env, cwd: turnCwd, now }));
|
|
769
|
+
let lastSignInCheck = null;
|
|
770
|
+
let nextSignInCheckAt = 0;
|
|
771
|
+
/** Ask the tool; record the answer in status.json; return it. */
|
|
772
|
+
const runSignInCheck = async (reason) => {
|
|
773
|
+
const env = withOsUser(turnEnv(process.env, kind, config.agentUid, companySlug));
|
|
774
|
+
let check;
|
|
775
|
+
try {
|
|
776
|
+
check = await checkSignIn(env);
|
|
777
|
+
}
|
|
778
|
+
catch (err) {
|
|
779
|
+
check = { signedIn: null, checkedAt: now().toISOString(), detail: err instanceof Error ? err.message : String(err) };
|
|
780
|
+
}
|
|
781
|
+
lastSignInCheck = check;
|
|
782
|
+
nextSignInCheckAt = now().getTime() + signInCheckMs;
|
|
783
|
+
setStatus({ runtimeSignInCheck: check });
|
|
784
|
+
if (check.signedIn === false) {
|
|
785
|
+
// A repeat "no" while already held does not push the retry out again:
|
|
786
|
+
// the hold's own timer decides when a real turn gets to disagree.
|
|
787
|
+
markSignInBroken(signedOutExcerpt(runtime.id), "signed-out", signInIssue == null);
|
|
788
|
+
}
|
|
789
|
+
else if (check.signedIn === true) {
|
|
790
|
+
if (signInIssue) {
|
|
791
|
+
// The tool answers for itself now: the held messages run at once.
|
|
792
|
+
nextSignInRetryAt = 0;
|
|
793
|
+
markSignInWorks();
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
else if (check.detail) {
|
|
797
|
+
log("debug", `sign-in check (${reason}) could not tell: ${check.detail}`);
|
|
798
|
+
}
|
|
799
|
+
return check;
|
|
800
|
+
};
|
|
801
|
+
/** When a real turn last ran despite the tool saying "signed out". */
|
|
802
|
+
let lastTurnDespiteSignedOutAt = now().getTime();
|
|
803
|
+
/**
|
|
804
|
+
* Before a turn: a fresh "signed in" stands; anything else is asked again.
|
|
805
|
+
* The tool's status command only knows about its own account sign-in, so a
|
|
806
|
+
* tool that works some other way (an API key, Bedrock, Vertex) says "no"
|
|
807
|
+
* and would be held forever. Once per retry window a real turn runs anyway
|
|
808
|
+
* and its outcome decides: success clears the hold, a sign-in failure
|
|
809
|
+
* keeps it.
|
|
810
|
+
*/
|
|
811
|
+
const signedOutBeforeTurn = async () => {
|
|
812
|
+
const fresh = lastSignInCheck?.signedIn === true && now().getTime() - new Date(lastSignInCheck.checkedAt).getTime() < SIGN_IN_CHECK_FRESH_MS;
|
|
813
|
+
if (fresh)
|
|
814
|
+
return false;
|
|
815
|
+
const check = await runSignInCheck("turn");
|
|
816
|
+
if (check.signedIn !== false)
|
|
817
|
+
return false;
|
|
818
|
+
const t = now().getTime();
|
|
819
|
+
if (signInRetryMs > 0 && t - lastTurnDespiteSignedOutAt >= signInRetryMs) {
|
|
820
|
+
lastTurnDespiteSignedOutAt = t;
|
|
821
|
+
log("info", `${runtime.id} still says it is not signed in; letting one turn try so a tool signed in another way is not held forever`);
|
|
822
|
+
return false;
|
|
823
|
+
}
|
|
824
|
+
return true;
|
|
825
|
+
};
|
|
734
826
|
/** Say once per conversation that the bot cannot work until its tool signs in. */
|
|
735
827
|
const tellSignInNeeded = async (conversation, post, kickoff = false, retryOf) => {
|
|
736
828
|
if (toldAboutSignIn.has(conversation))
|
|
@@ -741,7 +833,7 @@ export async function runBot(deps) {
|
|
|
741
833
|
runtime: config.runtime,
|
|
742
834
|
botName: config.name,
|
|
743
835
|
context: kickoff ? "kickoff" : "message",
|
|
744
|
-
text: signInNeededText(config.runtime, kickoff),
|
|
836
|
+
text: signInNeededText(config.runtime, kickoff, signInIssue?.state ?? "expired"),
|
|
745
837
|
...(retryOf ? { retryOf } : {}),
|
|
746
838
|
}));
|
|
747
839
|
toldAboutSignIn.add(conversation);
|
|
@@ -955,7 +1047,7 @@ export async function runBot(deps) {
|
|
|
955
1047
|
// resets). Claude gets the full hook set via its payload session id;
|
|
956
1048
|
// Codex/Grok only see HQ_SESSION_ID.
|
|
957
1049
|
const sid = session?.sessionId ?? randomUUID();
|
|
958
|
-
const env = turnEnv(process.env, kind, config.agentUid, companySlug);
|
|
1050
|
+
const env = withOsUser(turnEnv(process.env, kind, config.agentUid, companySlug));
|
|
959
1051
|
let bind = { env };
|
|
960
1052
|
if (companySlug) {
|
|
961
1053
|
try {
|
|
@@ -966,6 +1058,12 @@ export async function runBot(deps) {
|
|
|
966
1058
|
}
|
|
967
1059
|
bind = { newSessionId: sid, env: { ...env, HQ_SESSION_ID: sid } };
|
|
968
1060
|
}
|
|
1061
|
+
// The tool's own word first: a turn on a signed-out tool cannot start,
|
|
1062
|
+
// and its failure text varies by version (on 2026-10-28 it was a bare
|
|
1063
|
+
// "unknown error" that read as a crash). Held like any sign-in failure.
|
|
1064
|
+
if (await signedOutBeforeTurn()) {
|
|
1065
|
+
throw new RuntimeError(`${runtime.id} is not signed in on this computer`, signedOutExcerpt(runtime.id), 1, { started: false, durationMs: 0 });
|
|
1066
|
+
}
|
|
969
1067
|
const tTurnStart = now().getTime();
|
|
970
1068
|
// Model and thinking level are read fresh each turn, so a change from the
|
|
971
1069
|
// bot's profile (`hq bot set`) applies to the next message without a restart.
|
|
@@ -1020,6 +1118,14 @@ export async function runBot(deps) {
|
|
|
1020
1118
|
setStatus({ runtimeModelFallback: null });
|
|
1021
1119
|
}
|
|
1022
1120
|
log("info", `model turn done in ${Math.round((now().getTime() - tTurnStart) / 1000)}s (${Math.round((tTurnStart - tReceived) / 1000)}s before it)`);
|
|
1121
|
+
// Clears a reported runtime crash (runtimeCrashSinceStart): the tool works again.
|
|
1122
|
+
// Stamped with when the turn STARTED, so a long turn begun before a crash
|
|
1123
|
+
// that finishes after it does not clear the crash for turns that follow.
|
|
1124
|
+
// Never moves backwards: two turns run side by side, and an older one
|
|
1125
|
+
// finishing last must not bring back a crash a newer turn already cleared.
|
|
1126
|
+
const prevOk = Date.parse(readStatus()?.lastTurnOkAt ?? "");
|
|
1127
|
+
if (!Number.isFinite(prevOk) || tTurnStart > prevOk)
|
|
1128
|
+
setStatus({ lastTurnOkAt: new Date(tTurnStart).toISOString() });
|
|
1023
1129
|
// A brief job's session is one run long; nothing resumes it.
|
|
1024
1130
|
if (turn.sessionId && !isBriefJobSessionScope(sessionScope)) {
|
|
1025
1131
|
writeBotSession(dir, {
|
|
@@ -2431,6 +2537,10 @@ export async function runBot(deps) {
|
|
|
2431
2537
|
attempts.delete(id);
|
|
2432
2538
|
log("error", `turn on ${id} ${outcome.aborted ? "stopped by the owner" : "failed"} after it started; told the owner, not re-run`);
|
|
2433
2539
|
};
|
|
2540
|
+
// The bot's first look at its tool's sign-in, so `hq bot status` and the
|
|
2541
|
+
// app know before any message arrives. A tool that cannot be asked changes
|
|
2542
|
+
// nothing; the first turn then reports for itself.
|
|
2543
|
+
await runSignInCheck("start");
|
|
2434
2544
|
// The kickoff runs alongside the heartbeat, before the first inbox poll, so
|
|
2435
2545
|
// an owner message sent meanwhile is answered after it in the same session.
|
|
2436
2546
|
const kickoff = kickoffAfterIntro
|
|
@@ -2532,6 +2642,13 @@ export async function runBot(deps) {
|
|
|
2532
2642
|
}
|
|
2533
2643
|
if (modelHoldUntil)
|
|
2534
2644
|
liftModelHold();
|
|
2645
|
+
// While the tool says it is signed out, ask it again on a timer (cheap,
|
|
2646
|
+
// no turn): the moment it is signed in, the held messages run.
|
|
2647
|
+
if (signInIssue && busyLanes.size === 0 && now().getTime() >= nextSignInCheckAt) {
|
|
2648
|
+
await runSignInCheck("watch");
|
|
2649
|
+
if (!signInIssue)
|
|
2650
|
+
pump();
|
|
2651
|
+
}
|
|
2535
2652
|
// A kickoff held for a sign-in retries on the same timer as messages.
|
|
2536
2653
|
if (busyLanes.size === 0 &&
|
|
2537
2654
|
(!signInIssue || now().getTime() >= nextSignInRetryAt) &&
|
|
@@ -2662,7 +2779,7 @@ export async function runBot(deps) {
|
|
|
2662
2779
|
catch (err) {
|
|
2663
2780
|
log("warn", `could not record the update restart: ${err instanceof Error ? err.message : String(err)}`);
|
|
2664
2781
|
}
|
|
2665
|
-
void finish("stopped", CLI_UPDATE_RESTART_EXIT_CODE,
|
|
2782
|
+
void finish("stopped", CLI_UPDATE_RESTART_EXIT_CODE, describeCliUpdate(update), () => deps.onCliUpdateRestart?.(update, log));
|
|
2666
2783
|
});
|
|
2667
2784
|
return { stop, done, status: readStatus, idle: () => pending.size === 0 };
|
|
2668
2785
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* carries the final text and session_id.
|
|
16
16
|
* Verified against Claude Code 2.1.258 on 2026-09-12 (fixtures/claude-stream.ndjson).
|
|
17
17
|
*/
|
|
18
|
-
import type { BotRuntime } from "./types.js";
|
|
18
|
+
import type { BotRuntime, SpawnResult } from "./types.js";
|
|
19
19
|
export declare const CLAUDE_PERMISSION_MODE_ENV = "HQ_BOT_CLAUDE_PERMISSION_MODE";
|
|
20
20
|
export declare const DEFAULT_CLAUDE_PERMISSION_MODE = "bypassPermissions";
|
|
21
21
|
export declare const GATED_CLAUDE_PERMISSION_MODE = "acceptEdits";
|
|
@@ -84,4 +84,10 @@ export interface ClaudeRuntimeOptions {
|
|
|
84
84
|
systemBinDirs?: readonly string[];
|
|
85
85
|
}
|
|
86
86
|
export declare function createClaudeRuntime(env?: NodeJS.ProcessEnv, opts?: ClaudeRuntimeOptions): BotRuntime;
|
|
87
|
+
/**
|
|
88
|
+
* `claude auth status --json` prints `{"loggedIn": true|false, ...}` and
|
|
89
|
+
* exits 0 either way. Null when the output is not that (an older CLI, or a
|
|
90
|
+
* tool that could not run at all).
|
|
91
|
+
*/
|
|
92
|
+
export declare function parseClaudeAuthStatus(result: SpawnResult): boolean | null;
|
|
87
93
|
//# sourceMappingURL=claude.d.ts.map
|
|
@@ -230,6 +230,35 @@ export function createClaudeRuntime(env = process.env, opts = {}) {
|
|
|
230
230
|
failureReason(stdout) {
|
|
231
231
|
return messagesStreamFailure(stdout);
|
|
232
232
|
},
|
|
233
|
+
signInStatus: { args: ["auth", "status", "--json"], parse: parseClaudeAuthStatus },
|
|
233
234
|
};
|
|
234
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* `claude auth status --json` prints `{"loggedIn": true|false, ...}` and
|
|
238
|
+
* exits 0 either way. Null when the output is not that (an older CLI, or a
|
|
239
|
+
* tool that could not run at all).
|
|
240
|
+
*/
|
|
241
|
+
export function parseClaudeAuthStatus(result) {
|
|
242
|
+
const text = result.stdout ?? "";
|
|
243
|
+
const start = text.indexOf("{");
|
|
244
|
+
if (start >= 0) {
|
|
245
|
+
try {
|
|
246
|
+
const parsed = JSON.parse(text.slice(start));
|
|
247
|
+
if (typeof parsed.loggedIn === "boolean")
|
|
248
|
+
return parsed.loggedIn;
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
/* fall through to the loose match */
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const m = /"loggedIn"\s*:\s*(true|false)/.exec(text);
|
|
255
|
+
if (m)
|
|
256
|
+
return m[1] === "true";
|
|
257
|
+
const plain = `${text}\n${result.stderr ?? ""}`;
|
|
258
|
+
if (/\bnot logged in\b/i.test(plain))
|
|
259
|
+
return false;
|
|
260
|
+
if (/\blogged in\b/i.test(plain))
|
|
261
|
+
return true;
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
235
264
|
//# sourceMappingURL=claude.js.map
|