@indigoai-us/hq-cli 5.109.16 → 5.110.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/assets/bot-workers/setup/context/USER-GUIDE.md +363 -0
  3. package/assets/bot-workers/setup/context/quick-reference.md +199 -0
  4. package/assets/bot-workers/setup/skills/first-company.md +71 -0
  5. package/assets/bot-workers/setup/skills/standing-help.md +74 -0
  6. package/assets/bot-workers/setup/worker.yaml +422 -0
  7. package/dist/commands/bot-continuity.d.ts +28 -0
  8. package/dist/commands/bot-continuity.js +68 -0
  9. package/dist/commands/bot.d.ts +73 -0
  10. package/dist/commands/bot.js +776 -0
  11. package/dist/commands/workers.d.ts +2 -14
  12. package/dist/commands/workers.js +2 -8
  13. package/dist/lib/bot/api.d.ts +202 -0
  14. package/dist/lib/bot/api.js +202 -0
  15. package/dist/lib/bot/company-bind.d.ts +27 -0
  16. package/dist/lib/bot/company-bind.js +62 -0
  17. package/dist/lib/bot/config.d.ts +106 -0
  18. package/dist/lib/bot/config.js +141 -0
  19. package/dist/lib/bot/continuity-download.d.ts +28 -0
  20. package/dist/lib/bot/continuity-download.js +75 -0
  21. package/dist/lib/bot/continuity-install.d.ts +14 -0
  22. package/dist/lib/bot/continuity-install.js +101 -0
  23. package/dist/lib/bot/continuity.d.ts +66 -0
  24. package/dist/lib/bot/continuity.js +301 -0
  25. package/dist/lib/bot/creds.d.ts +24 -0
  26. package/dist/lib/bot/creds.js +51 -0
  27. package/dist/lib/bot/daemon.d.ts +75 -0
  28. package/dist/lib/bot/daemon.js +316 -0
  29. package/dist/lib/bot/inbox-state.d.ts +18 -0
  30. package/dist/lib/bot/inbox-state.js +51 -0
  31. package/dist/lib/bot/index.d.ts +16 -0
  32. package/dist/lib/bot/index.js +16 -0
  33. package/dist/lib/bot/inflight.d.ts +40 -0
  34. package/dist/lib/bot/inflight.js +44 -0
  35. package/dist/lib/bot/log.d.ts +13 -0
  36. package/dist/lib/bot/log.js +59 -0
  37. package/dist/lib/bot/owner-context.d.ts +75 -0
  38. package/dist/lib/bot/owner-context.js +151 -0
  39. package/dist/lib/bot/paths.d.ts +61 -0
  40. package/dist/lib/bot/paths.js +103 -0
  41. package/dist/lib/bot/progress.d.ts +84 -0
  42. package/dist/lib/bot/progress.js +167 -0
  43. package/dist/lib/bot/promote.d.ts +16 -0
  44. package/dist/lib/bot/promote.js +106 -0
  45. package/dist/lib/bot/promotion-hold.d.ts +24 -0
  46. package/dist/lib/bot/promotion-hold.js +103 -0
  47. package/dist/lib/bot/promotion-receipt.d.ts +9 -0
  48. package/dist/lib/bot/promotion-receipt.js +56 -0
  49. package/dist/lib/bot/promotion-upload.d.ts +16 -0
  50. package/dist/lib/bot/promotion-upload.js +65 -0
  51. package/dist/lib/bot/prompt.d.ts +103 -0
  52. package/dist/lib/bot/prompt.js +329 -0
  53. package/dist/lib/bot/room-policy.d.ts +53 -0
  54. package/dist/lib/bot/room-policy.js +73 -0
  55. package/dist/lib/bot/run.d.ts +98 -0
  56. package/dist/lib/bot/run.js +787 -0
  57. package/dist/lib/bot/runtime/claude.d.ts +49 -0
  58. package/dist/lib/bot/runtime/claude.js +151 -0
  59. package/dist/lib/bot/runtime/codex.d.ts +28 -0
  60. package/dist/lib/bot/runtime/codex.js +147 -0
  61. package/dist/lib/bot/runtime/grok.d.ts +16 -0
  62. package/dist/lib/bot/runtime/grok.js +67 -0
  63. package/dist/lib/bot/runtime/index.d.ts +35 -0
  64. package/dist/lib/bot/runtime/index.js +279 -0
  65. package/dist/lib/bot/runtime/messages-stream.d.ts +27 -0
  66. package/dist/lib/bot/runtime/messages-stream.js +85 -0
  67. package/dist/lib/bot/runtime/types.d.ts +136 -0
  68. package/dist/lib/bot/runtime/types.js +51 -0
  69. package/dist/lib/bot/scaffold.d.ts +38 -0
  70. package/dist/lib/bot/scaffold.js +94 -0
  71. package/dist/lib/bot/session.d.ts +19 -0
  72. package/dist/lib/bot/session.js +39 -0
  73. package/dist/lib/bot/status.d.ts +40 -0
  74. package/dist/lib/bot/status.js +66 -0
  75. package/dist/lib/bot/worker-source.d.ts +66 -0
  76. package/dist/lib/bot/worker-source.js +283 -0
  77. package/dist/lib/workers-registry/read.d.ts +15 -0
  78. package/dist/lib/workers-registry/read.js +17 -0
  79. package/dist/register-all.js +2 -0
  80. package/package.json +2 -1
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Owner context: who the bot's owner is and which companies they belong to.
3
+ *
4
+ * A local bot runs under its OWN HQ identity (per-bot machine credentials set
5
+ * for the whole resident process, and inherited by the model subprocess). So
6
+ * any membership lookup the model makes with `hq` describes the bot, and a bot
7
+ * usually belongs to no company. Bots were reading that empty answer as "your
8
+ * owner has no company".
9
+ *
10
+ * The resident process (trusted code, not the model) looks the owner up with
11
+ * the owner's own cached HQ session, strictly non-interactively, and verifies
12
+ * the session really is the owner before trusting it. Only the result (company
13
+ * slugs, uids, roles) reaches the model; the owner's token never does. A failed
14
+ * lookup is reported as "could not check", never as "no companies".
15
+ */
16
+ import { ensureCognitoToken } from "../../utils/cognito-session.js";
17
+ import { resolveCallerPersonUid, vaultApiFetch } from "../../utils/vault-api.js";
18
+ function message(err) {
19
+ return err instanceof Error ? err.message : String(err);
20
+ }
21
+ export const defaultOwnerLookupIo = {
22
+ // tokenSource "person" never uses the bot's machine creds, even though
23
+ // HQ_MACHINE_CREDS_FILE is set for this process.
24
+ personToken: () => ensureCognitoToken({ tokenSource: "person", interactive: false }),
25
+ callerPersonUid: (token) => resolveCallerPersonUid(token),
26
+ memberships: async (token) => {
27
+ const res = await vaultApiFetch({ token, path: "/membership/me", signal: AbortSignal.timeout(15_000) });
28
+ if (!res.ok)
29
+ throw new Error(`membership lookup returned ${res.status}`);
30
+ const data = (await res.json());
31
+ if (!Array.isArray(data.memberships))
32
+ throw new Error("membership lookup returned no list");
33
+ return data.memberships;
34
+ },
35
+ };
36
+ export async function lookupOwnerContext(ownerUid, io = defaultOwnerLookupIo) {
37
+ const checkedAt = (io.now ?? (() => new Date()))().toISOString();
38
+ let token;
39
+ try {
40
+ token = await io.personToken();
41
+ }
42
+ catch (err) {
43
+ return { status: "unavailable", ownerUid, reason: `your owner's HQ sign-in on this computer is not usable (${message(err)})`, checkedAt };
44
+ }
45
+ let signedInAs;
46
+ try {
47
+ signedInAs = await io.callerPersonUid(token);
48
+ }
49
+ catch (err) {
50
+ return { status: "unavailable", ownerUid, reason: `could not confirm who is signed in (${message(err)})`, checkedAt };
51
+ }
52
+ if (signedInAs !== ownerUid)
53
+ return { status: "mismatch", ownerUid, signedInAs, checkedAt };
54
+ try {
55
+ const rows = await io.memberships(token);
56
+ const companies = rows
57
+ .filter((m) => m.status === "active")
58
+ .map((m) => ({ companyUid: m.companyUid, ...(m.companySlug ? { companySlug: m.companySlug } : {}), role: m.role }));
59
+ return { status: "ok", ownerUid, companies, checkedAt };
60
+ }
61
+ catch (err) {
62
+ return { status: "unavailable", ownerUid, reason: `could not read your owner's companies (${message(err)})`, checkedAt };
63
+ }
64
+ }
65
+ /** Successful lookups are reused for 5 minutes, failures retried after 1 minute. */
66
+ export const OWNER_CONTEXT_TTL_MS = 5 * 60_000;
67
+ export const OWNER_CONTEXT_RETRY_MS = 60_000;
68
+ /** A turn never waits longer than this for a refresh; the lookup keeps going in the background. */
69
+ export const OWNER_CONTEXT_WAIT_MS = 8_000;
70
+ export class OwnerContextCache {
71
+ lookup;
72
+ nowMs;
73
+ waitMs;
74
+ ownerUid;
75
+ value = null;
76
+ at = 0;
77
+ pending = null;
78
+ constructor(lookup, nowMs = Date.now, waitMs = OWNER_CONTEXT_WAIT_MS, ownerUid = "") {
79
+ this.lookup = lookup;
80
+ this.nowMs = nowMs;
81
+ this.waitMs = waitMs;
82
+ this.ownerUid = ownerUid;
83
+ }
84
+ async get() {
85
+ const age = this.nowMs() - this.at;
86
+ if (this.value && age < (this.value.status === "ok" ? OWNER_CONTEXT_TTL_MS : OWNER_CONTEXT_RETRY_MS))
87
+ return this.value;
88
+ if (!this.pending) {
89
+ this.pending = this.lookup()
90
+ .then((v) => {
91
+ this.value = v;
92
+ this.at = this.nowMs();
93
+ return v;
94
+ })
95
+ .finally(() => {
96
+ this.pending = null;
97
+ });
98
+ this.pending.catch(() => undefined);
99
+ }
100
+ let timer;
101
+ const slow = new Promise((resolve) => {
102
+ timer = setTimeout(() => resolve(null), this.waitMs);
103
+ });
104
+ try {
105
+ const got = await Promise.race([this.pending, slow]);
106
+ if (got)
107
+ return got;
108
+ }
109
+ finally {
110
+ if (timer)
111
+ clearTimeout(timer);
112
+ }
113
+ // Slow refresh: the last answer (it carries its own checkedAt) beats none.
114
+ if (this.value)
115
+ return this.value;
116
+ return {
117
+ status: "unavailable",
118
+ ownerUid: this.ownerUid,
119
+ reason: "the check is taking too long",
120
+ checkedAt: new Date(this.nowMs()).toISOString(),
121
+ };
122
+ }
123
+ }
124
+ /**
125
+ * The block prepended to every owner DM turn. It names its subject explicitly so the
126
+ * model cannot confuse the owner's memberships with its own.
127
+ */
128
+ export function ownerContextBlock(ctx, agentUid) {
129
+ const head = `[Owner context, checked ${ctx.checkedAt} by this bot's runtime with your owner's own HQ sign-in. It describes your OWNER (${ctx.ownerUid}), not you (${agentUid}).]`;
130
+ let body;
131
+ if (ctx.status === "ok") {
132
+ body =
133
+ ctx.companies.length === 0
134
+ ? "Your owner has no active company memberships. They work in a personal HQ."
135
+ : `Your owner's active companies:\n${ctx.companies
136
+ .map((c) => `- ${c.companySlug ?? c.companyUid} (${c.companyUid}), role: ${c.role}`)
137
+ .join("\n")}`;
138
+ }
139
+ else if (ctx.status === "mismatch") {
140
+ body =
141
+ `The HQ sign-in on this computer belongs to ${ctx.signedInAs}, not your owner, so your owner's companies could not be checked. ` +
142
+ "Do not say which companies your owner belongs to; tell them you could not check and why.";
143
+ }
144
+ else {
145
+ body =
146
+ `Your owner's companies could not be checked: ${ctx.reason}. ` +
147
+ "Do not guess and do not say they have no company; tell them you could not check right now.";
148
+ }
149
+ return `${head}\n${body}\n[End owner context]`;
150
+ }
151
+ //# sourceMappingURL=owner-context.js.map
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Local bot on-disk layout (local-bots US-003/US-005).
3
+ *
4
+ * ~/.hq/bots/<name>/
5
+ * bot.json config (name, agt_ uid, owner, runtime, hqRoot)
6
+ * memory/ the bot's memory when created with --memory local
7
+ * machine-creds.json 0600 Cognito machine creds (never printed)
8
+ * token-state/ per-bot machine token cache (HQ_MACHINE_TOKEN_STATE_DIR)
9
+ * status.json pid, state, restarts, last heartbeat
10
+ * inbox-state.json processed messageIds (idempotency across restarts)
11
+ * session.json model-CLI session id for resume (DM conversation)
12
+ * session-room-<channelId>.json one session per room the bot answers in
13
+ * pid single-file pid lock
14
+ * logs/bot.log rotated at 5MB
15
+ *
16
+ * Injectable root (HQ_BOTS_ROOT) so tests never touch the real HOME.
17
+ */
18
+ export declare const BOTS_DIRNAME = "bots";
19
+ export declare const BOT_CONFIG_NAME = "bot.json";
20
+ export declare const BOT_CREDS_NAME = "machine-creds.json";
21
+ export declare const BOT_STATUS_NAME = "status.json";
22
+ export declare const BOT_INBOX_STATE_NAME = "inbox-state.json";
23
+ export declare const BOT_SESSION_NAME = "session.json";
24
+ export declare const BOT_TOKEN_STATE_DIRNAME = "token-state";
25
+ export declare const BOT_LOGS_DIRNAME = "logs";
26
+ export declare const BOT_LOG_NAME = "bot.log";
27
+ export declare const BOT_LOG_MAX_BYTES: number;
28
+ export declare const LAUNCHD_LABEL_PREFIX = "ai.getindigo.hq-bot";
29
+ export declare function isValidBotName(name: string): boolean;
30
+ export declare function validateBotName(name: string): string;
31
+ export declare function botsRoot(home?: string, env?: NodeJS.ProcessEnv): string;
32
+ export declare function botDir(name: string, root?: string): string;
33
+ export declare function botConfigPath(dir: string): string;
34
+ export declare function botCredsPath(dir: string): string;
35
+ export declare function botStatusPath(dir: string): string;
36
+ export declare function botInboxStatePath(dir: string): string;
37
+ /**
38
+ * Which model-CLI conversation an inbox item belongs to: the owner's DM thread
39
+ * ("dm") or one room ("room:<channelId>"). DM context stays private to the
40
+ * owner; each room gets its own coherent context.
41
+ */
42
+ export type BotSessionScope = "dm" | `room:${string}`;
43
+ export declare function roomSessionScope(channelId: string): BotSessionScope;
44
+ export declare function botSessionPath(dir: string, scope?: BotSessionScope): string;
45
+ export declare function botTokenStateDir(dir: string): string;
46
+ /**
47
+ * Scratch working directory for model turns. Deliberately OUTSIDE the HQ git
48
+ * tree: Claude/Grok/Codex scan the working tree on every turn, and HQ's tree
49
+ * (tens of thousands of entries, a huge `git status`) cost ~20 s per reply on
50
+ * 2026-09-11. The system prompt tells the bot where HQ is; it `cd`s there when
51
+ * a request needs the files or `hq` commands.
52
+ */
53
+ export declare function botTurnCwd(dir: string): string;
54
+ /** ~/.hq/bots/<name>/memory — "local" memory, outside the synced HQ tree. */
55
+ export declare function botLocalMemoryDir(dir: string): string;
56
+ export declare function botLogsDir(dir: string): string;
57
+ export declare function botLogPath(dir: string): string;
58
+ export declare function launchdLabel(name: string): string;
59
+ /** personal/workers/<name>/ relative to the HQ root. */
60
+ export declare function botWorkerRelDir(name: string): string;
61
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Local bot on-disk layout (local-bots US-003/US-005).
3
+ *
4
+ * ~/.hq/bots/<name>/
5
+ * bot.json config (name, agt_ uid, owner, runtime, hqRoot)
6
+ * memory/ the bot's memory when created with --memory local
7
+ * machine-creds.json 0600 Cognito machine creds (never printed)
8
+ * token-state/ per-bot machine token cache (HQ_MACHINE_TOKEN_STATE_DIR)
9
+ * status.json pid, state, restarts, last heartbeat
10
+ * inbox-state.json processed messageIds (idempotency across restarts)
11
+ * session.json model-CLI session id for resume (DM conversation)
12
+ * session-room-<channelId>.json one session per room the bot answers in
13
+ * pid single-file pid lock
14
+ * logs/bot.log rotated at 5MB
15
+ *
16
+ * Injectable root (HQ_BOTS_ROOT) so tests never touch the real HOME.
17
+ */
18
+ import * as os from "node:os";
19
+ import * as path from "node:path";
20
+ export const BOTS_DIRNAME = "bots";
21
+ export const BOT_CONFIG_NAME = "bot.json";
22
+ export const BOT_CREDS_NAME = "machine-creds.json";
23
+ export const BOT_STATUS_NAME = "status.json";
24
+ export const BOT_INBOX_STATE_NAME = "inbox-state.json";
25
+ export const BOT_SESSION_NAME = "session.json";
26
+ export const BOT_TOKEN_STATE_DIRNAME = "token-state";
27
+ export const BOT_LOGS_DIRNAME = "logs";
28
+ export const BOT_LOG_NAME = "bot.log";
29
+ export const BOT_LOG_MAX_BYTES = 5 * 1024 * 1024;
30
+ export const LAUNCHD_LABEL_PREFIX = "ai.getindigo.hq-bot";
31
+ /** Lowercase slug: letters, digits, single hyphens; 1–40 chars. */
32
+ const BOT_NAME_RE = /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/;
33
+ export function isValidBotName(name) {
34
+ return BOT_NAME_RE.test(name) && !name.includes("--");
35
+ }
36
+ export function validateBotName(name) {
37
+ const trimmed = (name ?? "").trim();
38
+ if (!isValidBotName(trimmed)) {
39
+ throw Object.assign(new Error(`Invalid bot name "${name}": use 1–40 lowercase letters, digits, and single hyphens (e.g. "assistant", "scout-2").`), { expected: true });
40
+ }
41
+ return trimmed;
42
+ }
43
+ export function botsRoot(home = os.homedir(), env = process.env) {
44
+ const override = env.HQ_BOTS_ROOT?.trim();
45
+ if (override)
46
+ return override;
47
+ return path.join(home, ".hq", BOTS_DIRNAME);
48
+ }
49
+ export function botDir(name, root) {
50
+ return path.join(root ?? botsRoot(), name);
51
+ }
52
+ export function botConfigPath(dir) {
53
+ return path.join(dir, BOT_CONFIG_NAME);
54
+ }
55
+ export function botCredsPath(dir) {
56
+ return path.join(dir, BOT_CREDS_NAME);
57
+ }
58
+ export function botStatusPath(dir) {
59
+ return path.join(dir, BOT_STATUS_NAME);
60
+ }
61
+ export function botInboxStatePath(dir) {
62
+ return path.join(dir, BOT_INBOX_STATE_NAME);
63
+ }
64
+ export function roomSessionScope(channelId) {
65
+ return `room:${channelId}`;
66
+ }
67
+ export function botSessionPath(dir, scope = "dm") {
68
+ if (scope === "dm")
69
+ return path.join(dir, BOT_SESSION_NAME);
70
+ const channelId = scope.slice("room:".length).replace(/[^A-Za-z0-9_-]/g, "_");
71
+ return path.join(dir, `session-room-${channelId}.json`);
72
+ }
73
+ export function botTokenStateDir(dir) {
74
+ return path.join(dir, BOT_TOKEN_STATE_DIRNAME);
75
+ }
76
+ /**
77
+ * Scratch working directory for model turns. Deliberately OUTSIDE the HQ git
78
+ * tree: Claude/Grok/Codex scan the working tree on every turn, and HQ's tree
79
+ * (tens of thousands of entries, a huge `git status`) cost ~20 s per reply on
80
+ * 2026-09-11. The system prompt tells the bot where HQ is; it `cd`s there when
81
+ * a request needs the files or `hq` commands.
82
+ */
83
+ export function botTurnCwd(dir) {
84
+ return path.join(dir, "cwd");
85
+ }
86
+ /** ~/.hq/bots/<name>/memory — "local" memory, outside the synced HQ tree. */
87
+ export function botLocalMemoryDir(dir) {
88
+ return path.join(dir, "memory");
89
+ }
90
+ export function botLogsDir(dir) {
91
+ return path.join(dir, BOT_LOGS_DIRNAME);
92
+ }
93
+ export function botLogPath(dir) {
94
+ return path.join(dir, BOT_LOGS_DIRNAME, BOT_LOG_NAME);
95
+ }
96
+ export function launchdLabel(name) {
97
+ return `${LAUNCHD_LABEL_PREFIX}.${name}`;
98
+ }
99
+ /** personal/workers/<name>/ relative to the HQ root. */
100
+ export function botWorkerRelDir(name) {
101
+ return path.join("personal", "workers", name);
102
+ }
103
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1,84 @@
1
+ /**
2
+ * In-between messages for one bot turn.
3
+ *
4
+ * While the model works, each finished assistant message ("I'll look up the
5
+ * page first.", "Found it — publishing now.") is posted to the same place the
6
+ * final reply goes, instead of being held back and glued into one reply at the
7
+ * end. Rules:
8
+ *
9
+ * - whole messages only (the runtime parsers never pass deltas, reasoning,
10
+ * tool arguments or tool results); empty text is skipped
11
+ * - messages arriving close together are joined with a blank line into one
12
+ * post, and posts are spaced at least `minGapMs` apart
13
+ * - if nothing has been said `workingNoticeMs` into the turn, ONE short
14
+ * "working on it" note goes out
15
+ * - the final answer is posted only when it adds something: text already
16
+ * posted (or about to be) is never repeated, including Grok's final result
17
+ * that is just every message concatenated
18
+ *
19
+ * Posts are sent strictly in order. A failed post is logged by the caller's
20
+ * `send` and does not stop the turn.
21
+ */
22
+ export declare const PROGRESS_COALESCE_MS = 2000;
23
+ export declare const PROGRESS_MIN_GAP_MS = 4000;
24
+ export declare const WORKING_NOTICE_MS = 60000;
25
+ export declare const WORKING_NOTICE_TEXT = "Working on it \u2014 I'll reply here when it's done.";
26
+ export interface ProgressPosterOptions {
27
+ /** Post one message; resolve true when it was delivered. */
28
+ send: (body: string) => Promise<boolean>;
29
+ now?: () => number;
30
+ setTimer?: (fn: () => void, ms: number) => unknown;
31
+ clearTimer?: (handle: unknown) => void;
32
+ coalesceMs?: number;
33
+ minGapMs?: number;
34
+ workingNoticeMs?: number;
35
+ workingNoticeText?: string;
36
+ }
37
+ /** Whitespace-insensitive form used to recognise repeated text. */
38
+ export declare function normalizeForCompare(text: string): string;
39
+ export declare class ProgressPoster {
40
+ private readonly send;
41
+ private readonly now;
42
+ private readonly setTimer;
43
+ private readonly clearTimer;
44
+ private readonly coalesceMs;
45
+ private readonly minGapMs;
46
+ private readonly workingNoticeMs;
47
+ private readonly workingNoticeText;
48
+ private pending;
49
+ /** Every message the model has said this turn (posted or pending), in order. */
50
+ private readonly said;
51
+ private flushTimer;
52
+ private noticeTimer;
53
+ private lastPostAt;
54
+ private chain;
55
+ private finished;
56
+ private noticeSent;
57
+ private delivered;
58
+ constructor(opts: ProgressPosterOptions);
59
+ /** Arm the one-time "working on it" note. Call when the turn starts. */
60
+ start(): void;
61
+ /** One finished assistant message from the model. */
62
+ message(text: string): void;
63
+ /** Posts that actually reached the chat (progress, notice and final). */
64
+ get postedCount(): number;
65
+ /** True once any message from the model was said this turn (not counting the notice). */
66
+ get saidAnything(): boolean;
67
+ /**
68
+ * The turn succeeded: post whatever is pending, then the final answer unless
69
+ * it repeats what was already said. Resolves once everything is sent.
70
+ */
71
+ finish(finalText: string): Promise<void>;
72
+ /**
73
+ * The turn failed: post what the model already said (it is real progress),
74
+ * then stop. The caller posts its own "did not finish" message afterwards.
75
+ */
76
+ abandon(): Promise<void>;
77
+ private repeatsSaid;
78
+ private scheduleFlush;
79
+ private flushNow;
80
+ private enqueue;
81
+ private cancelFlush;
82
+ private cancelNotice;
83
+ }
84
+ //# sourceMappingURL=progress.d.ts.map
@@ -0,0 +1,167 @@
1
+ /**
2
+ * In-between messages for one bot turn.
3
+ *
4
+ * While the model works, each finished assistant message ("I'll look up the
5
+ * page first.", "Found it — publishing now.") is posted to the same place the
6
+ * final reply goes, instead of being held back and glued into one reply at the
7
+ * end. Rules:
8
+ *
9
+ * - whole messages only (the runtime parsers never pass deltas, reasoning,
10
+ * tool arguments or tool results); empty text is skipped
11
+ * - messages arriving close together are joined with a blank line into one
12
+ * post, and posts are spaced at least `minGapMs` apart
13
+ * - if nothing has been said `workingNoticeMs` into the turn, ONE short
14
+ * "working on it" note goes out
15
+ * - the final answer is posted only when it adds something: text already
16
+ * posted (or about to be) is never repeated, including Grok's final result
17
+ * that is just every message concatenated
18
+ *
19
+ * Posts are sent strictly in order. A failed post is logged by the caller's
20
+ * `send` and does not stop the turn.
21
+ */
22
+ export const PROGRESS_COALESCE_MS = 2_000;
23
+ export const PROGRESS_MIN_GAP_MS = 4_000;
24
+ export const WORKING_NOTICE_MS = 60_000;
25
+ export const WORKING_NOTICE_TEXT = "Working on it — I'll reply here when it's done.";
26
+ /** Whitespace-insensitive form used to recognise repeated text. */
27
+ export function normalizeForCompare(text) {
28
+ return text.replace(/\s+/g, "");
29
+ }
30
+ export class ProgressPoster {
31
+ send;
32
+ now;
33
+ setTimer;
34
+ clearTimer;
35
+ coalesceMs;
36
+ minGapMs;
37
+ workingNoticeMs;
38
+ workingNoticeText;
39
+ pending = [];
40
+ /** Every message the model has said this turn (posted or pending), in order. */
41
+ said = [];
42
+ flushTimer = null;
43
+ noticeTimer = null;
44
+ lastPostAt = Number.NEGATIVE_INFINITY;
45
+ chain = Promise.resolve();
46
+ finished = false;
47
+ noticeSent = false;
48
+ delivered = 0;
49
+ constructor(opts) {
50
+ this.send = opts.send;
51
+ this.now = opts.now ?? Date.now;
52
+ this.setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
53
+ this.clearTimer = opts.clearTimer ?? ((h) => clearTimeout(h));
54
+ this.coalesceMs = opts.coalesceMs ?? PROGRESS_COALESCE_MS;
55
+ this.minGapMs = opts.minGapMs ?? PROGRESS_MIN_GAP_MS;
56
+ this.workingNoticeMs = opts.workingNoticeMs ?? WORKING_NOTICE_MS;
57
+ this.workingNoticeText = opts.workingNoticeText ?? WORKING_NOTICE_TEXT;
58
+ }
59
+ /** Arm the one-time "working on it" note. Call when the turn starts. */
60
+ start() {
61
+ if (this.finished || this.noticeTimer !== null || this.workingNoticeMs <= 0)
62
+ return;
63
+ this.noticeTimer = this.setTimer(() => {
64
+ this.noticeTimer = null;
65
+ if (this.finished || this.said.length > 0 || this.noticeSent)
66
+ return;
67
+ this.noticeSent = true;
68
+ this.enqueue(this.workingNoticeText);
69
+ }, this.workingNoticeMs);
70
+ }
71
+ /** One finished assistant message from the model. */
72
+ message(text) {
73
+ const t = text.trim();
74
+ if (this.finished || !t)
75
+ return;
76
+ this.cancelNotice();
77
+ this.said.push(t);
78
+ this.pending.push(t);
79
+ this.scheduleFlush();
80
+ }
81
+ /** Posts that actually reached the chat (progress, notice and final). */
82
+ get postedCount() {
83
+ return this.delivered;
84
+ }
85
+ /** True once any message from the model was said this turn (not counting the notice). */
86
+ get saidAnything() {
87
+ return this.said.length > 0;
88
+ }
89
+ /**
90
+ * The turn succeeded: post whatever is pending, then the final answer unless
91
+ * it repeats what was already said. Resolves once everything is sent.
92
+ */
93
+ async finish(finalText) {
94
+ if (this.finished)
95
+ return this.chain;
96
+ this.finished = true;
97
+ this.cancelNotice();
98
+ this.cancelFlush();
99
+ const final = finalText.trim();
100
+ if (final && !this.repeatsSaid(final))
101
+ this.pending.push(final);
102
+ if (this.pending.length === 0 && this.delivered === 0 && !final)
103
+ this.pending.push("(no reply)");
104
+ this.flushNow();
105
+ return this.chain;
106
+ }
107
+ /**
108
+ * The turn failed: post what the model already said (it is real progress),
109
+ * then stop. The caller posts its own "did not finish" message afterwards.
110
+ */
111
+ async abandon() {
112
+ if (this.finished)
113
+ return this.chain;
114
+ this.finished = true;
115
+ this.cancelNotice();
116
+ this.cancelFlush();
117
+ this.flushNow();
118
+ return this.chain;
119
+ }
120
+ repeatsSaid(final) {
121
+ const f = normalizeForCompare(final);
122
+ if (!f)
123
+ return true;
124
+ if (this.said.some((s) => normalizeForCompare(s) === f))
125
+ return true;
126
+ // Grok-style: the final result is every message joined, with or without separators.
127
+ return this.said.length > 1 && normalizeForCompare(this.said.join("")) === f;
128
+ }
129
+ scheduleFlush() {
130
+ this.cancelFlush();
131
+ const at = Math.max(this.now() + this.coalesceMs, this.lastPostAt + this.minGapMs);
132
+ this.flushTimer = this.setTimer(() => {
133
+ this.flushTimer = null;
134
+ this.flushNow();
135
+ }, Math.max(0, at - this.now()));
136
+ }
137
+ flushNow() {
138
+ if (this.pending.length === 0)
139
+ return;
140
+ const body = this.pending.join("\n\n");
141
+ this.pending = [];
142
+ this.enqueue(body);
143
+ }
144
+ enqueue(body) {
145
+ this.lastPostAt = this.now();
146
+ this.chain = this.chain.then(async () => {
147
+ try {
148
+ if (await this.send(body))
149
+ this.delivered += 1;
150
+ }
151
+ catch {
152
+ /* the caller's send logs; a failed post never breaks the turn */
153
+ }
154
+ });
155
+ }
156
+ cancelFlush() {
157
+ if (this.flushTimer !== null)
158
+ this.clearTimer(this.flushTimer);
159
+ this.flushTimer = null;
160
+ }
161
+ cancelNotice() {
162
+ if (this.noticeTimer !== null)
163
+ this.clearTimer(this.noticeTimer);
164
+ this.noticeTimer = null;
165
+ }
166
+ }
167
+ //# sourceMappingURL=progress.js.map
@@ -0,0 +1,16 @@
1
+ import type { BotApi, BotPromotionState } from "./api.js";
2
+ import type { BotConfig } from "./config.js";
3
+ /** One recoverable local-to-cloud step, used by CLI and desktop. Never kills the
4
+ * local runtime, recopies provider login files, or changes another bot.
5
+ */
6
+ export declare function promoteLocalBot(input: {
7
+ dir: string;
8
+ config: BotConfig;
9
+ companyUid: string;
10
+ api: BotApi;
11
+ drainTimeoutMs?: number;
12
+ }): Promise<{
13
+ promotion: BotPromotionState;
14
+ pending: boolean;
15
+ }>;
16
+ //# sourceMappingURL=promote.d.ts.map