@indigoai-us/hq-cli 5.345.198 → 5.345.199

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.
Files changed (102) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/command-catalog.generated.d.ts +106 -6
  3. package/dist/command-catalog.generated.js +130 -6
  4. package/dist/commands/bot-events.d.ts +19 -0
  5. package/dist/commands/bot-events.js +46 -0
  6. package/dist/commands/bot-jobs.d.ts +2 -0
  7. package/dist/commands/bot-jobs.js +43 -2
  8. package/dist/commands/bot.d.ts +5 -0
  9. package/dist/commands/bot.js +33 -1
  10. package/dist/commands/lanes-report.d.ts +2 -1
  11. package/dist/commands/lanes-report.js +44 -1
  12. package/dist/commands/lanes.d.ts +9 -0
  13. package/dist/commands/lanes.js +95 -9
  14. package/dist/commands/meetings.js +1 -21
  15. package/dist/commands/signals-run.d.ts +2 -0
  16. package/dist/commands/signals-run.js +6 -1
  17. package/dist/lanes-drain-hook.js +9 -0
  18. package/dist/lib/bot/api.d.ts +5 -0
  19. package/dist/lib/bot/claude-login.d.ts +31 -0
  20. package/dist/lib/bot/claude-login.js +160 -0
  21. package/dist/lib/bot/events.d.ts +18 -0
  22. package/dist/lib/bot/events.js +160 -0
  23. package/dist/lib/bot/executive-settings.d.ts +10 -0
  24. package/dist/lib/bot/executive-settings.js +43 -0
  25. package/dist/lib/bot/fleet-cron.js +4 -0
  26. package/dist/lib/bot/fleet-local.js +10 -10
  27. package/dist/lib/bot/job-run.js +1 -1
  28. package/dist/lib/bot/jobs/hot-signal-sync.d.ts +8 -0
  29. package/dist/lib/bot/jobs/hot-signal-sync.js +2 -0
  30. package/dist/lib/bot/jobs/meeting-detect.d.ts +28 -0
  31. package/dist/lib/bot/jobs/meeting-detect.js +176 -0
  32. package/dist/lib/bot/jobs/meeting-rider.d.ts +53 -0
  33. package/dist/lib/bot/jobs/meeting-rider.js +284 -0
  34. package/dist/lib/bot/jobs/signal-to-lane.d.ts +32 -0
  35. package/dist/lib/bot/jobs/signal-to-lane.js +169 -0
  36. package/dist/lib/bot/local-jobs.d.ts +15 -2
  37. package/dist/lib/bot/local-jobs.js +24 -7
  38. package/dist/lib/bot/run.d.ts +7 -49
  39. package/dist/lib/bot/run.js +100 -4
  40. package/dist/lib/bot/status.d.ts +21 -0
  41. package/dist/lib/doctor/checks/bots.d.ts +6 -0
  42. package/dist/lib/doctor/checks/bots.js +47 -1
  43. package/dist/lib/hot-channels/adapters/meetings.d.ts +1 -0
  44. package/dist/lib/hot-channels/adapters/meetings.js +3 -1
  45. package/dist/lib/hot-channels/meeting-source-ref.d.ts +3 -0
  46. package/dist/lib/hot-channels/meeting-source-ref.js +7 -0
  47. package/dist/lib/hot-channels/schedule.js +2 -2
  48. package/dist/lib/hot-channels/signals-pass.d.ts +9 -32
  49. package/dist/lib/hot-channels/signals-pass.js +51 -17
  50. package/dist/lib/lanes/admission.d.ts +2 -0
  51. package/dist/lib/lanes/admission.js +4 -0
  52. package/dist/lib/lanes/approval-gate.js +4 -1
  53. package/dist/lib/lanes/links.d.ts +8 -1
  54. package/dist/lib/lanes/links.js +84 -3
  55. package/dist/lib/lanes/outcome.d.ts +154 -0
  56. package/dist/lib/lanes/outcome.js +151 -0
  57. package/dist/lib/lanes/reconcile.js +1 -0
  58. package/dist/lib/lanes/signal-brief-template.d.ts +7 -0
  59. package/dist/lib/lanes/signal-brief-template.js +40 -0
  60. package/dist/lib/lanes/signal-lane.d.ts +24 -0
  61. package/dist/lib/lanes/signal-lane.js +99 -0
  62. package/dist/lib/lanes/source-report.d.ts +37 -0
  63. package/dist/lib/lanes/source-report.js +66 -0
  64. package/dist/lib/lanes/spawn.d.ts +6 -0
  65. package/dist/lib/lanes/spawn.js +21 -0
  66. package/dist/lib/lanes/store.d.ts +3 -0
  67. package/dist/lib/lanes/store.js +64 -0
  68. package/dist/lib/lanes/story-ledger.d.ts +3 -0
  69. package/dist/lib/lanes/story-ledger.js +1 -0
  70. package/dist/lib/lanes/types.d.ts +137 -0
  71. package/dist/lib/lanes/types.js +17 -2
  72. package/dist/lib/lanes/watcher.js +4 -0
  73. package/dist/lib/live-meetings.d.ts +19 -0
  74. package/dist/lib/live-meetings.js +23 -0
  75. package/dist/lib/mesh/live/daemon/doctor.js +6 -7
  76. package/dist/lib/mesh/live/spool.d.ts +8 -0
  77. package/dist/lib/mesh/live/spool.js +59 -40
  78. package/dist/lib/plan-limit-nag.d.ts +30 -4
  79. package/dist/lib/plan-limit-nag.js +68 -17
  80. package/dist/lib/signals/engine.d.ts +4 -0
  81. package/dist/lib/signals/engine.js +25 -5
  82. package/dist/lib/signals/incremental.d.ts +42 -0
  83. package/dist/lib/signals/incremental.js +191 -0
  84. package/dist/lib/signals/prior-signals.d.ts +11 -0
  85. package/dist/lib/signals/prior-signals.js +44 -0
  86. package/dist/lib/signals/reply.js +6 -1
  87. package/dist/lib/signals/settings.d.ts +5 -0
  88. package/dist/lib/signals/settings.js +17 -0
  89. package/dist/lib/signals/steps/extract.d.ts +23 -2
  90. package/dist/lib/signals/steps/extract.js +142 -9
  91. package/dist/lib/signals/steps/types.d.ts +1 -0
  92. package/dist/lib/signals/write-guard.js +7 -0
  93. package/dist/utils/cognito-callback-port.d.ts +15 -0
  94. package/dist/utils/cognito-callback-port.js +51 -0
  95. package/dist/utils/cognito-config.js +2 -3
  96. package/dist/utils/cognito-session.d.ts +7 -6
  97. package/dist/utils/cognito-session.js +14 -14
  98. package/dist/utils/interactive-caller.d.ts +15 -0
  99. package/dist/utils/interactive-caller.js +70 -0
  100. package/dist/utils/login-browser.js +3 -1
  101. package/dist/utils/resident-package-preload.js +4 -0
  102. package/package.json +7 -7
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Can a Claude bot on the HQ Fleet Runtime read a Claude Code login here?
3
+ *
4
+ * The runtime never runs the `claude` binary; it calls Anthropic with the
5
+ * owner's Claude Code OAuth token, read in this order (its
6
+ * `resolve_anthropic_token`): ANTHROPIC_TOKEN, CLAUDE_CODE_OAUTH_TOKEN and
7
+ * ANTHROPIC_API_KEY from the environment, the HQ Fleet login file,
8
+ * ~/.claude/.credentials.json, then the macOS Keychain item
9
+ * "Claude Code-credentials". This mirrors that order so `hq doctor` and
10
+ * `hq bot create` can say which sources were checked and why each failed,
11
+ * without printing a token.
12
+ */
13
+ import { execFileSync } from "node:child_process";
14
+ import * as fs from "node:fs";
15
+ import * as os from "node:os";
16
+ import * as path from "node:path";
17
+ import { fleetHome } from "./fleet-local.js";
18
+ export const CLAUDE_KEYCHAIN_SERVICE = "Claude Code-credentials";
19
+ export const CLAUDE_LOGIN_ENV_VARS = ["ANTHROPIC_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY"];
20
+ function readKeychainWithSecurity() {
21
+ try {
22
+ const stdout = execFileSync("security", ["find-generic-password", "-s", CLAUDE_KEYCHAIN_SERVICE, "-w"], {
23
+ encoding: "utf8",
24
+ stdio: ["ignore", "pipe", "pipe"],
25
+ timeout: 5000,
26
+ });
27
+ return { code: 0, stdout, stderr: "" };
28
+ }
29
+ catch (error) {
30
+ const failure = error;
31
+ const stderr = failure.stderr ? String(failure.stderr) : (failure.message ?? "");
32
+ return { code: typeof failure.status === "number" ? failure.status : -1, stdout: "", stderr };
33
+ }
34
+ }
35
+ function oauthRecord(value) {
36
+ if (!value || typeof value !== "object")
37
+ return null;
38
+ const record = value;
39
+ const accessToken = typeof record.accessToken === "string" ? record.accessToken.trim() : "";
40
+ if (!accessToken)
41
+ return null;
42
+ return {
43
+ accessToken,
44
+ refreshToken: typeof record.refreshToken === "string" ? record.refreshToken : "",
45
+ expiresAt: typeof record.expiresAt === "number" ? record.expiresAt : 0,
46
+ };
47
+ }
48
+ function describeRecord(record, now) {
49
+ if (!record.expiresAt || now < record.expiresAt - 60_000) {
50
+ return { status: "ok", reason: "access token present and not expired", usable: true };
51
+ }
52
+ if (record.refreshToken) {
53
+ return { status: "expired", reason: "access token expired; the bot refreshes it with the refresh token", usable: true };
54
+ }
55
+ return { status: "expired", reason: "access token expired and no refresh token", usable: false };
56
+ }
57
+ function readJsonFile(file) {
58
+ if (!fs.existsSync(file))
59
+ return { parsed: null, problem: `${file} does not exist` };
60
+ try {
61
+ return { parsed: JSON.parse(fs.readFileSync(file, "utf8")), problem: null };
62
+ }
63
+ catch (error) {
64
+ return { parsed: null, problem: `${file} could not be read (${error.message})` };
65
+ }
66
+ }
67
+ export function checkClaudeCodeLogin(deps = {}) {
68
+ const env = deps.env ?? process.env;
69
+ const home = deps.home ?? os.homedir();
70
+ const platform = deps.platform ?? process.platform;
71
+ const now = (deps.now ?? Date.now)();
72
+ const sources = [];
73
+ let via = null;
74
+ const found = (source) => {
75
+ if (via === null)
76
+ via = source;
77
+ };
78
+ for (const name of CLAUDE_LOGIN_ENV_VARS) {
79
+ const set = !!env[name]?.trim();
80
+ sources.push({ source: `env:${name}`, status: set ? "ok" : "missing", reason: set ? "set" : "not set in this environment" });
81
+ if (set)
82
+ found(`env:${name}`);
83
+ }
84
+ const fleetLogin = path.join(fleetHome(home, env), ".anthropic_oauth.json");
85
+ const fleetFile = readJsonFile(fleetLogin);
86
+ const fleetRecord = fleetFile.parsed ? oauthRecord(fleetFile.parsed) : null;
87
+ if (fleetRecord) {
88
+ const described = describeRecord(fleetRecord, now);
89
+ sources.push({ source: "hq_fleet_oauth_file", status: described.status, reason: described.reason });
90
+ if (described.usable)
91
+ found("hq_fleet_oauth_file");
92
+ }
93
+ else {
94
+ sources.push({ source: "hq_fleet_oauth_file", status: "missing", reason: fleetFile.problem ?? `${fleetLogin} holds no access token` });
95
+ }
96
+ const credentialsFile = path.join(home, ".claude", ".credentials.json");
97
+ const claudeFile = readJsonFile(credentialsFile);
98
+ const claudeRecord = claudeFile.parsed ? oauthRecord(claudeFile.parsed.claudeAiOauth) : null;
99
+ if (claudeRecord) {
100
+ const described = describeRecord(claudeRecord, now);
101
+ sources.push({ source: "claude_code_credentials_file", status: described.status, reason: described.reason });
102
+ if (described.usable)
103
+ found("claude_code_credentials_file");
104
+ }
105
+ else {
106
+ sources.push({
107
+ source: "claude_code_credentials_file",
108
+ status: "missing",
109
+ reason: claudeFile.problem ?? `${credentialsFile} holds no claudeAiOauth access token`,
110
+ });
111
+ }
112
+ if (platform !== "darwin") {
113
+ sources.push({ source: "macos_keychain", status: "skipped", reason: "not macOS" });
114
+ }
115
+ else {
116
+ const read = (deps.readKeychain ?? readKeychainWithSecurity)();
117
+ if (read.code !== 0) {
118
+ const detail = read.stderr.replace(/\s+/g, " ").trim().slice(0, 200) || `exit ${read.code}`;
119
+ sources.push({ source: "macos_keychain", status: "missing", reason: `no readable '${CLAUDE_KEYCHAIN_SERVICE}' item (${detail})` });
120
+ }
121
+ else {
122
+ let payload;
123
+ try {
124
+ payload = JSON.parse(read.stdout.trim());
125
+ }
126
+ catch {
127
+ payload = null;
128
+ }
129
+ const record = payload && typeof payload === "object" ? oauthRecord(payload.claudeAiOauth) : null;
130
+ if (record) {
131
+ const described = describeRecord(record, now);
132
+ sources.push({ source: "macos_keychain", status: described.status, reason: described.reason });
133
+ if (described.usable)
134
+ found("macos_keychain");
135
+ }
136
+ else {
137
+ sources.push({ source: "macos_keychain", status: "missing", reason: "keychain item has no claudeAiOauth access token" });
138
+ }
139
+ }
140
+ }
141
+ return { signedIn: via !== null, via, sources };
142
+ }
143
+ const SOURCE_LABELS = {
144
+ hq_fleet_oauth_file: "the HQ Fleet login file",
145
+ claude_code_credentials_file: "~/.claude/.credentials.json",
146
+ macos_keychain: "the macOS Keychain",
147
+ };
148
+ /** One sentence listing every source that did not yield a login. */
149
+ export function describeClaudeLoginSources(check) {
150
+ const parts = check.sources
151
+ .filter((entry) => entry.status !== "ok" && entry.status !== "skipped")
152
+ .map((entry) => `${SOURCE_LABELS[entry.source] ?? entry.source.replace("env:", "")}: ${entry.reason}`);
153
+ return parts.length ? `Checked ${parts.join("; ")}.` : "";
154
+ }
155
+ export function claudeLoginFix(botName) {
156
+ return botName
157
+ ? `Run \`claude /login\` in a terminal on this computer, then \`hq bot restart ${botName}\`.`
158
+ : "Run `claude /login` in a terminal on this computer, then retry.";
159
+ }
160
+ //# sourceMappingURL=claude-login.js.map
@@ -0,0 +1,18 @@
1
+ import type { InboxItem } from "./api.js";
2
+ import { type LocalJob } from "./local-jobs.js";
3
+ /** Rotate journals at five MiB without discarding unread records. */
4
+ export declare const EVENT_ROTATE_BYTES: number;
5
+ export declare const EVENT_PAYLOAD_BYTES: number;
6
+ export interface BotEvent {
7
+ event: string;
8
+ at: string;
9
+ payload: Record<string, unknown>;
10
+ }
11
+ export declare function validateEventPayload(payload: unknown): asserts payload is Record<string, unknown>;
12
+ export declare function emitBotEvent(dir: string, event: string, payload: unknown, now?: Date, alreadyEmitted?: (record: BotEvent) => boolean): BotEvent;
13
+ export declare function eventMatches(job: LocalJob, record: BotEvent): boolean;
14
+ /** Pending items survive a crash between queueing and the next poll. */
15
+ export declare function drainBotEvents(dir: string): InboxItem[];
16
+ export declare function ackBotEvent(dir: string, messageId: string): void;
17
+ export declare function tailBotEvents(dir: string, predicate?: (event: BotEvent) => boolean): BotEvent[];
18
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1,160 @@
1
+ /** Local event journal and durable inbox. Cursor and queued deliveries commit together. */
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { randomUUID } from "node:crypto";
5
+ import { readLocalJobsStrict, withJobsLock } from "./local-jobs.js";
6
+ /** Rotate journals at five MiB without discarding unread records. */
7
+ export const EVENT_ROTATE_BYTES = 5 * 1024 * 1024;
8
+ export const EVENT_PAYLOAD_BYTES = 16 * 1024;
9
+ const journal = "events.jsonl";
10
+ const stateFile = "events-state.json";
11
+ export function validateEventPayload(payload) {
12
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
13
+ throw new Error("Event payload must be a JSON object.");
14
+ if (Buffer.byteLength(JSON.stringify(payload), "utf8") >= EVENT_PAYLOAD_BYTES)
15
+ throw new Error("Event payload must be under 16 KB.");
16
+ }
17
+ function readState(dir) {
18
+ try {
19
+ const state = JSON.parse(fs.readFileSync(path.join(dir, stateFile), "utf8"));
20
+ if (state.v !== 1 || !state.cursors || !Array.isArray(state.pending))
21
+ throw new Error("Invalid event state");
22
+ return state;
23
+ }
24
+ catch (err) {
25
+ if (err.code === "ENOENT")
26
+ return { v: 1, cursors: {}, pending: [] };
27
+ throw err; // Never reset a corrupt cursor and replay the journal.
28
+ }
29
+ }
30
+ function writeState(dir, state) {
31
+ const file = path.join(dir, stateFile);
32
+ const tmp = `${file}.${process.pid}.tmp`;
33
+ fs.writeFileSync(tmp, JSON.stringify(state), { mode: 0o600 });
34
+ fs.renameSync(tmp, file);
35
+ }
36
+ function files(dir) {
37
+ return fs.readdirSync(dir).filter((f) => /^events\.\d+-[a-f0-9-]+\.jsonl$/.test(f)).sort().concat(journal);
38
+ }
39
+ /** A persisted intent makes both sides of the journal rename recoverable. */
40
+ function recoverRotation(dir, state) {
41
+ if (!state.rotating)
42
+ return;
43
+ const archived = state.rotating;
44
+ if (!fs.existsSync(path.join(dir, archived))) {
45
+ fs.renameSync(path.join(dir, journal), path.join(dir, archived));
46
+ }
47
+ state.cursors[archived] = state.cursors[journal] ?? 0;
48
+ delete state.cursors[journal];
49
+ delete state.rotating;
50
+ writeState(dir, state);
51
+ }
52
+ export function emitBotEvent(dir, event, payload, now = new Date(), alreadyEmitted) {
53
+ if (!event.trim() || event.length > 256 || /[\r\n]/.test(event))
54
+ throw new Error("Event name must be 1–256 characters without newlines.");
55
+ validateEventPayload(payload);
56
+ const record = { event, at: now.toISOString(), payload };
57
+ return withJobsLock(dir, () => {
58
+ const state = readState(dir);
59
+ recoverRotation(dir, state);
60
+ // Optional producer idempotency: scan and append under the same lock as
61
+ // rotation, including archived journals and only fully written lines.
62
+ if (alreadyEmitted) {
63
+ for (const name of files(dir)) {
64
+ const candidate = path.join(dir, name);
65
+ if (!fs.existsSync(candidate))
66
+ continue;
67
+ const lines = fs.readFileSync(candidate, "utf8").split("\n").slice(0, -1);
68
+ for (const line of lines.filter(Boolean)) {
69
+ const prior = JSON.parse(line);
70
+ if (alreadyEmitted(prior))
71
+ return prior;
72
+ }
73
+ }
74
+ }
75
+ const file = path.join(dir, journal);
76
+ const bytes = fs.existsSync(file) ? fs.readFileSync(file) : Buffer.alloc(0);
77
+ // A process killed during append may leave an incomplete final line.
78
+ // It was never accepted as an event: discard it before the next append,
79
+ // otherwise that next valid record would join it and poison the drain.
80
+ const size = bytes.length && bytes.at(-1) !== 10 ? bytes.lastIndexOf(10) + 1 : bytes.length;
81
+ if (size !== bytes.length)
82
+ fs.truncateSync(file, size);
83
+ if (size >= EVENT_ROTATE_BYTES) {
84
+ state.rotating = `events.${Date.now()}-${randomUUID()}.jsonl`;
85
+ writeState(dir, state);
86
+ recoverRotation(dir, state);
87
+ }
88
+ fs.appendFileSync(file, `${JSON.stringify(record)}\n`, { mode: 0o600 });
89
+ return record;
90
+ });
91
+ }
92
+ export function eventMatches(job, record) {
93
+ return job.trigger?.kind === "event" && job.trigger.event === record.event &&
94
+ Object.entries(job.trigger.filter ?? {}).every(([key, value]) => {
95
+ if (!Object.hasOwn(record.payload, key))
96
+ return false;
97
+ // JSON objects can shadow toString/valueOf with non-callable fields.
98
+ // Such a value cannot match a string filter and must not poison the drain.
99
+ try {
100
+ return String(record.payload[key]) === value;
101
+ }
102
+ catch {
103
+ return false;
104
+ }
105
+ });
106
+ }
107
+ /** Pending items survive a crash between queueing and the next poll. */
108
+ export function drainBotEvents(dir) {
109
+ return withJobsLock(dir, () => {
110
+ const state = readState(dir);
111
+ recoverRotation(dir, state);
112
+ const jobs = readLocalJobsStrict(dir);
113
+ for (const name of files(dir)) {
114
+ const file = path.join(dir, name);
115
+ if (!fs.existsSync(file))
116
+ continue;
117
+ let offset = state.cursors[name] ?? 0;
118
+ // Consumed archives need only a stat, not a full read on every poll.
119
+ if (offset === fs.statSync(file).size)
120
+ continue;
121
+ const bytes = fs.readFileSync(file);
122
+ if (offset > bytes.length)
123
+ throw new Error(`Event journal was truncated: ${name}`);
124
+ while (offset < bytes.length) {
125
+ const end = bytes.indexOf(10, offset);
126
+ if (end < 0)
127
+ break; // An interrupted append is never treated as a full event.
128
+ const record = JSON.parse(bytes.subarray(offset, end).toString("utf8"));
129
+ validateEventPayload(record.payload);
130
+ for (const job of jobs.filter((j) => eventMatches(j, record))) {
131
+ state.pending.push({
132
+ messageId: `localjob-event-${randomUUID()}`, channel: "job", jobId: job.jobId,
133
+ prompt: job.prompt, firedAt: record.at, receivedAt: record.at,
134
+ context: { ...job.context, payload: record.payload, event: record },
135
+ });
136
+ }
137
+ offset = end + 1;
138
+ }
139
+ state.cursors[name] = offset;
140
+ }
141
+ writeState(dir, state);
142
+ return state.pending;
143
+ });
144
+ }
145
+ export function ackBotEvent(dir, messageId) {
146
+ withJobsLock(dir, () => {
147
+ const state = readState(dir);
148
+ state.pending = state.pending.filter((item) => item.messageId !== messageId);
149
+ writeState(dir, state);
150
+ });
151
+ }
152
+ export function tailBotEvents(dir, predicate = () => true) {
153
+ return withJobsLock(dir, () => files(dir).flatMap((name) => {
154
+ const file = path.join(dir, name);
155
+ if (!fs.existsSync(file))
156
+ return [];
157
+ return fs.readFileSync(file, "utf8").split("\n").slice(0, -1).filter(Boolean).map((line) => JSON.parse(line));
158
+ }).filter(predicate).slice(-50));
159
+ }
160
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1,10 @@
1
+ import { type BotConfig } from "./config.js";
2
+ export declare function executiveRiderEnabled(home?: string): boolean;
3
+ export declare function isExecutiveBot(config: Pick<BotConfig, "name" | "kind" | "workerId">, home?: string): boolean;
4
+ export declare function executiveStatus(config: Pick<BotConfig, "name" | "kind" | "workerId">, dir: string, home?: string): {
5
+ is_executive: boolean;
6
+ rider_enabled: boolean;
7
+ detect_source_last: string | null;
8
+ last_detected_at: string | null;
9
+ };
10
+ //# sourceMappingURL=executive-settings.d.ts.map
@@ -0,0 +1,43 @@
1
+ /** Shared Executive identity and the desktop's default-on rider preference. */
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import * as os from "node:os";
5
+ import { effectiveBotKind } from "./config.js";
6
+ import { tailBotEvents } from "./events.js";
7
+ export function executiveRiderEnabled(home = os.homedir()) {
8
+ try {
9
+ const settings = JSON.parse(fs.readFileSync(path.join(home, ".hq/menubar.json"), "utf8"));
10
+ return settings?.executiveMeetingRider?.enabled !== false;
11
+ }
12
+ catch (err) {
13
+ return err.code === "ENOENT";
14
+ }
15
+ }
16
+ export function isExecutiveBot(config, home = os.homedir()) {
17
+ if (effectiveBotKind(config) !== "personal")
18
+ return false;
19
+ let name = "setup";
20
+ try {
21
+ const value = JSON.parse(fs.readFileSync(path.join(home, ".hq/executive.json"), "utf8"));
22
+ if (value?.bot != null) {
23
+ if (typeof value.bot !== "string" || !value.bot.trim())
24
+ return false;
25
+ name = value.bot;
26
+ }
27
+ }
28
+ catch (err) {
29
+ if (err.code !== "ENOENT")
30
+ return false;
31
+ }
32
+ return config.name === name;
33
+ }
34
+ export function executiveStatus(config, dir, home = os.homedir()) {
35
+ const last = tailBotEvents(dir, (event) => event.event === "meeting.detected").at(-1);
36
+ return {
37
+ is_executive: isExecutiveBot(config, home), rider_enabled: executiveRiderEnabled(home),
38
+ // The first desktop producer did not set source.
39
+ detect_source_last: last ? (last.payload.source === "poll" ? "poll" : "desktop") : null,
40
+ last_detected_at: last?.at ?? null,
41
+ };
42
+ }
43
+ //# sourceMappingURL=executive-settings.js.map
@@ -169,6 +169,10 @@ export function migrateLegacyJobsToFleet(dir, config, ops, now = new Date()) {
169
169
  const skipped = [];
170
170
  const moved = new Set();
171
171
  for (const job of legacy) {
172
+ if (job.trigger || !job.cron) {
173
+ skipped.push(job.jobId);
174
+ continue;
175
+ }
172
176
  let schedule;
173
177
  try {
174
178
  schedule = /^(cron|rate)\(/i.test(job.cron.trim()) ? fleetScheduleFromLegacy(job.cron) : job.cron.trim();
@@ -79,15 +79,15 @@ export function sharedRuntimeLayout(root, id, platform) {
79
79
  }
80
80
  export const HQ_FLEET_LOCAL_RELEASE = {
81
81
  repository: "indigoai-us/hq-fleet-local",
82
- tag: "v0.4.22",
83
- version: "0.4.22",
84
- artifact: "hq-fleet-local-0.4.22.zip",
85
- artifactSha256: "b12c01d48c8add7609de557f02987eaa7570223751446915835f52dc9b69d1db",
86
- runtimeArtifact: "hq-fleet-runtime-f17ab10aa598.tar",
87
- runtimeArtifactSha256: "a2296305a82af7f834804f4240705d791f0c08fa52257955dae3dce091d39ee8",
88
- manifestSha256: "f3c62e4e64f91e0aeab32d0999a106c89122144c83757874c81781fb417c0392",
89
- signatureSha256: "f07502b61b35431292c9d656f9e602f5230f8fb8ca093993ec92d4353077b4ba",
90
- runtimeCommit: "f17ab10aa598d5915f47bf0b83d332a5cc243f98",
82
+ tag: "v0.4.24",
83
+ version: "0.4.24",
84
+ artifact: "hq-fleet-local-0.4.24.zip",
85
+ artifactSha256: "daa74b7aaf7f26d8c069f33aa94a453369fb2d7ae8c122e7fbe48a5b38e73f31",
86
+ runtimeArtifact: "hq-fleet-runtime-d257fdd1171f.tar",
87
+ runtimeArtifactSha256: "8262c5f28773d35c04f7f9ce6f61ed19a88e601847f9d12d5ab11fb5d8e87fb0",
88
+ manifestSha256: "302d2206957b815a846db583a49e7862948fd8a8a36aaff8e0e8a6e0aa7a1b9c",
89
+ signatureSha256: "efee90b00c6eb1d0f53a76e93a756435975c9da6c70c9c0b505aafcce79a586a",
90
+ runtimeCommit: "d257fdd1171f1edd5e381b3bad7baacab0cb00d3",
91
91
  };
92
92
  /** hq-fleet-local trust/release-public.pem (rotated 2026-10-30; signed v0.4.17). */
93
93
  export const HQ_FLEET_LOCAL_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
@@ -1717,7 +1717,7 @@ async function installSharedFleetRuntime(installation, root, python, platform, r
1717
1717
  function writeNativeProfileMarker(profileDir, shared, engine) {
1718
1718
  const marker = path.join(profileDir, "bootstrap.json");
1719
1719
  const temporary = `${marker}.${process.pid}.${crypto.randomUUID()}`;
1720
- fs.writeFileSync(temporary, `${JSON.stringify({ schemaVersion: 2, runtimeId: shared.id, engine }, null, 2)}\n`, { mode: 0o600 });
1720
+ fs.writeFileSync(temporary, `${JSON.stringify({ schemaVersion: 2, runtimeId: shared.id, engine, cliVersion: CLI_VERSION }, null, 2)}\n`, { mode: 0o600 });
1721
1721
  fs.renameSync(temporary, marker);
1722
1722
  }
1723
1723
  function removeEmbeddedRuntime(profileDir, platform = process.platform) {
@@ -215,7 +215,7 @@ export function planJobRuns(items, handled) {
215
215
  const byJob = new Map();
216
216
  for (const item of items) {
217
217
  const job = readJobRun(item);
218
- if (!job)
218
+ if (!job || item.messageId.startsWith("localjob-event-"))
219
219
  continue;
220
220
  const list = byJob.get(job.jobId);
221
221
  if (list)
@@ -48,6 +48,12 @@ export declare const HOT_SIGNAL_SYNC_JOB_KIND = "hot-signal-sync";
48
48
  * own schedule in `jobs.json` is the one that runs.
49
49
  */
50
50
  export declare const HOT_SIGNAL_SYNC_DEFAULT_CRON = "cron(30 6 * * ? *)";
51
+ interface BuiltinJobsMarker {
52
+ v: 1;
53
+ registered: Record<string, string>;
54
+ }
55
+ declare function readMarker(dir: string): BuiltinJobsMarker;
56
+ declare function writeMarker(dir: string, marker: BuiltinJobsMarker): void;
51
57
  export declare function hotSignalSyncRegistered(dir: string, company: string): boolean;
52
58
  export declare function fleetHotSignalSyncRegistered(dir: string, company: string): boolean;
53
59
  export declare function markFleetHotSignalSyncRegistered(dir: string, company: string, now?: Date): void;
@@ -210,4 +216,6 @@ export interface HotSignalSyncRunOutcome {
210
216
  /** Run the scheduled sync once and describe it in plain words. Never throws. */
211
217
  export declare function runHotSignalSyncJob(input: HotSignalSyncRunInput): Promise<HotSignalSyncRunOutcome>;
212
218
  export declare function describeSyncReport(company: string, report: OnDemandSyncReport): string;
219
+ /** Shared registration marker for additive built-in event jobs. */
220
+ export { readMarker as readBuiltinJobsMarker, writeMarker as writeBuiltinJobsMarker };
213
221
  //# sourceMappingURL=hot-signal-sync.d.ts.map
@@ -430,4 +430,6 @@ export function describeSyncReport(company, report) {
430
430
  lines.push(notice);
431
431
  return lines.join("\n");
432
432
  }
433
+ /** Shared registration marker for additive built-in event jobs. */
434
+ export { readMarker as readBuiltinJobsMarker, writeMarker as writeBuiltinJobsMarker };
433
435
  //# sourceMappingURL=hot-signal-sync.js.map
@@ -0,0 +1,28 @@
1
+ import { type LiveMeetingBot } from "../../live-meetings.js";
2
+ import type { BotConfig } from "../config.js";
3
+ export declare function detectIntervalSeconds(hqRoot: string): number;
4
+ /** Desktop events carry company UIDs; local rider files use manifest slugs. */
5
+ export declare function normalizeDetectedCompany(hqRoot: string, payload: unknown): unknown;
6
+ export interface MeetingDetectDeps {
7
+ hqRoot: string;
8
+ dir: string;
9
+ enabled: () => boolean;
10
+ companies: () => string[];
11
+ list: (company: string, signal: AbortSignal) => Promise<{
12
+ companyId: string;
13
+ bots: LiveMeetingBot[];
14
+ }>;
15
+ intervalSeconds?: () => number;
16
+ now?: () => Date;
17
+ log: (message: string) => void;
18
+ }
19
+ /** Live reads are keyed by Recall ID; the desktop may emit a saved meeting ID or SDK window ID. */
20
+ export declare function resolveDetectedMeeting(hqRoot: string, payload: unknown, list: MeetingDetectDeps["list"]): Promise<unknown>;
21
+ export declare function createMeetingDetect(deps: MeetingDetectDeps): {
22
+ /** Called by the inbox loop; network work never holds up inbox delivery. */
23
+ tick(): void;
24
+ settled(): Promise<void>;
25
+ stop(): Promise<void>;
26
+ };
27
+ export declare function defaultMeetingDetectDeps(config: BotConfig, dir: string, log: (message: string) => void): MeetingDetectDeps;
28
+ //# sourceMappingURL=meeting-detect.d.ts.map