@indigoai-us/hq-cli 5.345.177 → 5.345.179
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 +50 -0
- package/dist/command-catalog.generated.d.ts +79 -1
- package/dist/command-catalog.generated.js +96 -1
- package/dist/commands/bot-jobs.d.ts +11 -9
- package/dist/commands/bot-jobs.js +56 -21
- package/dist/commands/bot.d.ts +13 -0
- package/dist/commands/bot.js +230 -21
- package/dist/commands/cloud.d.ts +49 -0
- package/dist/commands/cloud.js +155 -15
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +15 -4
- package/dist/commands/dm-attach.js +15 -3
- package/dist/commands/integrations-core.d.ts +2 -0
- package/dist/commands/integrations-core.js +1 -0
- package/dist/commands/library-add.d.ts +9 -0
- package/dist/commands/library-add.js +19 -2
- package/dist/commands/run.js +9 -1
- package/dist/commands/sources.d.ts +89 -0
- package/dist/commands/sources.js +363 -1
- package/dist/lib/bot/config.d.ts +2 -0
- package/dist/lib/bot/fleet-default-flag.d.ts +31 -0
- package/dist/lib/bot/fleet-default-flag.js +127 -0
- package/dist/lib/bot/fleet-local.d.ts +30 -1
- package/dist/lib/bot/fleet-local.js +86 -11
- package/dist/lib/bot/index.d.ts +1 -0
- package/dist/lib/bot/index.js +1 -0
- package/dist/lib/bot/jobs/hot-signal-sync.d.ts +109 -0
- package/dist/lib/bot/jobs/hot-signal-sync.js +177 -0
- package/dist/lib/bot/local-config.js +1 -1
- package/dist/lib/bot/local-jobs.d.ts +40 -4
- package/dist/lib/bot/local-jobs.js +72 -5
- package/dist/lib/bot/run.d.ts +15 -0
- package/dist/lib/bot/run.js +61 -0
- package/dist/lib/daemon/catalog.d.ts +2 -0
- package/dist/lib/daemon/catalog.js +7 -2
- package/dist/lib/daemon/config.d.ts +2 -1
- package/dist/lib/daemon/config.js +4 -3
- package/dist/lib/daemon/handoff.d.ts +4 -0
- package/dist/lib/daemon/handoff.js +6 -2
- package/dist/lib/daemon/install.d.ts +70 -2
- package/dist/lib/daemon/install.js +185 -27
- package/dist/lib/daemon/legacy.d.ts +9 -3
- package/dist/lib/daemon/legacy.js +11 -4
- package/dist/lib/daemon/migrate.js +1 -1
- package/dist/lib/daemon/run.d.ts +3 -2
- package/dist/lib/daemon/run.js +10 -6
- package/dist/lib/doctor/checks/anywhere.d.ts +6 -0
- package/dist/lib/doctor/checks/anywhere.js +35 -6
- package/dist/lib/hot-channels/adapters/__fixtures__/dummy.d.ts +33 -0
- package/dist/lib/hot-channels/adapters/__fixtures__/dummy.js +53 -0
- package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.d.ts +38 -0
- package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.js +64 -0
- package/dist/lib/hot-channels/adapters/attio.d.ts +67 -0
- package/dist/lib/hot-channels/adapters/attio.js +374 -0
- package/dist/lib/hot-channels/adapters/gmail.d.ts +111 -0
- package/dist/lib/hot-channels/adapters/gmail.js +413 -0
- package/dist/lib/hot-channels/adapters/index.d.ts +23 -0
- package/dist/lib/hot-channels/adapters/index.js +39 -0
- package/dist/lib/hot-channels/adapters/linear.d.ts +79 -0
- package/dist/lib/hot-channels/adapters/linear.js +321 -0
- package/dist/lib/hot-channels/adapters/meeting-providers.d.ts +86 -0
- package/dist/lib/hot-channels/adapters/meeting-providers.js +253 -0
- package/dist/lib/hot-channels/adapters/meetings.d.ts +81 -0
- package/dist/lib/hot-channels/adapters/meetings.js +523 -0
- package/dist/lib/hot-channels/adapters/slack.d.ts +70 -0
- package/dist/lib/hot-channels/adapters/slack.js +277 -0
- package/dist/lib/hot-channels/alert.d.ts +78 -0
- package/dist/lib/hot-channels/alert.js +179 -0
- package/dist/lib/hot-channels/audience.d.ts +15 -0
- package/dist/lib/hot-channels/audience.js +25 -0
- package/dist/lib/hot-channels/contract.d.ts +140 -0
- package/dist/lib/hot-channels/contract.js +20 -0
- package/dist/lib/hot-channels/cursors.d.ts +41 -0
- package/dist/lib/hot-channels/cursors.js +84 -0
- package/dist/lib/hot-channels/index.d.ts +10 -0
- package/dist/lib/hot-channels/index.js +10 -0
- package/dist/lib/hot-channels/landing.d.ts +83 -0
- package/dist/lib/hot-channels/landing.js +175 -0
- package/dist/lib/hot-channels/ledger.d.ts +101 -0
- package/dist/lib/hot-channels/ledger.js +186 -0
- package/dist/lib/hot-channels/loop.d.ts +74 -0
- package/dist/lib/hot-channels/loop.js +219 -0
- package/dist/lib/hot-channels/prompts/signals-pass.md +22 -0
- package/dist/lib/hot-channels/registry.d.ts +28 -0
- package/dist/lib/hot-channels/registry.js +121 -0
- package/dist/lib/hot-channels/run.d.ts +137 -0
- package/dist/lib/hot-channels/run.js +324 -0
- package/dist/lib/hot-channels/signals-pass.d.ts +150 -0
- package/dist/lib/hot-channels/signals-pass.js +581 -0
- package/dist/lib/hot-channels/sync-lock.d.ts +51 -0
- package/dist/lib/hot-channels/sync-lock.js +224 -0
- package/dist/lib/install/hook-target.d.ts +6 -2
- package/dist/lib/install/hook-target.js +9 -4
- package/dist/lib/lanes/loop.d.ts +10 -0
- package/dist/lib/lanes/loop.js +18 -1
- package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +1 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.js +26 -6
- package/dist/run/hq-plugin.d.ts +3 -0
- package/dist/run/hq-plugin.js +18 -0
- package/package.json +7 -7
|
@@ -7,20 +7,46 @@ import { HQ_FLEET_RUNTIME_ENGINE_ID, } from "./config.js";
|
|
|
7
7
|
import { ensureFleetPython, fleetUvEnv } from "./fleet-python.js";
|
|
8
8
|
import { ensureCognitoToken } from "../../utils/cognito-session.js";
|
|
9
9
|
import { vaultApiFetch } from "../../utils/vault-api.js";
|
|
10
|
+
/** The model the HQ Fleet Runtime asks for when a Claude bot names none. */
|
|
11
|
+
export const FLEET_CLAUDE_DEFAULT_MODEL = "claude-opus-5-5";
|
|
12
|
+
/**
|
|
13
|
+
* The runtime provider for a bot created from a model CLI choice. Claude uses
|
|
14
|
+
* the runtime's native Anthropic provider, which reads the owner's Claude Code
|
|
15
|
+
* login, so it needs no sign-in of its own; ChatGPT and Grok sign in with
|
|
16
|
+
* `hq bot auth`.
|
|
17
|
+
*/
|
|
18
|
+
export function fleetProviderForRuntime(runtime, model) {
|
|
19
|
+
if (runtime === "claude") {
|
|
20
|
+
const requested = model?.trim() ?? "";
|
|
21
|
+
return { id: "anthropic", model: /^claude-[a-z0-9.-]+$/.test(requested) ? requested : FLEET_CLAUDE_DEFAULT_MODEL };
|
|
22
|
+
}
|
|
23
|
+
const preset = providerProfiles[runtime === "grok" ? "grok" : "chatgpt"];
|
|
24
|
+
return {
|
|
25
|
+
id: preset.provider,
|
|
26
|
+
model: model?.trim() || preset.model,
|
|
27
|
+
credentialProvider: preset.runtimeProvider,
|
|
28
|
+
baseUrl: preset.baseUrl,
|
|
29
|
+
transport: preset.transport,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** True when a bot with this provider still needs `hq bot auth` before it can answer. */
|
|
33
|
+
export function fleetProviderNeedsSignIn(provider) {
|
|
34
|
+
return provider.id !== "anthropic";
|
|
35
|
+
}
|
|
10
36
|
export const HQ_FLEET_LOCAL_RELEASE = {
|
|
11
37
|
repository: "indigoai-us/hq-fleet-local",
|
|
12
|
-
tag: "v0.4.
|
|
13
|
-
version: "0.4.
|
|
14
|
-
artifact: "hq-fleet-local-0.4.
|
|
15
|
-
artifactSha256: "
|
|
38
|
+
tag: "v0.4.13",
|
|
39
|
+
version: "0.4.13",
|
|
40
|
+
artifact: "hq-fleet-local-0.4.13.zip",
|
|
41
|
+
artifactSha256: "81b129ac129578240af9091b06ae190f47f6be38ff933b35ae5459d5b78c72df",
|
|
16
42
|
runtimeArtifact: "hq-fleet-runtime-abccac13bd9e.tar",
|
|
17
43
|
runtimeArtifactSha256: "29bcfe718ad7fbd3636afd00623772f2a25bc84c4f6f10d8d57f38e549d528d5",
|
|
18
|
-
manifestSha256: "
|
|
19
|
-
signatureSha256: "
|
|
44
|
+
manifestSha256: "42654f91264099ce173ce3a9f42e207f566e1412ea4b550dc21775d764327bbb",
|
|
45
|
+
signatureSha256: "93cf9b0a94d75c532454243e3391e421e2743f59f8e65989a5a56ec9152e2fa4",
|
|
20
46
|
runtimeCommit: "abccac13bd9ea593b24c8cd9c0ba28d5c6050cc9",
|
|
21
47
|
};
|
|
22
48
|
export const HQ_FLEET_LOCAL_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
|
23
|
-
|
|
49
|
+
MCowBQYDK2VwAyEAbYvc1pwOmiCUPJv8yIa3E9RXfniFA84nH2+ROmLMGZU=
|
|
24
50
|
-----END PUBLIC KEY-----
|
|
25
51
|
`;
|
|
26
52
|
function digest(value) {
|
|
@@ -417,7 +443,9 @@ function replaceProfileProvider(profilePath, agentUid, provider) {
|
|
|
417
443
|
const credentialProviders = Array.isArray(profile.credentialProviders)
|
|
418
444
|
? profile.credentialProviders.filter((value) => typeof value === "string")
|
|
419
445
|
: [];
|
|
420
|
-
profile.credentialProviders = [
|
|
446
|
+
profile.credentialProviders = [
|
|
447
|
+
...new Set([...credentialProviders, ...(provider.credentialProvider ? [provider.credentialProvider] : [])]),
|
|
448
|
+
];
|
|
421
449
|
const temporary = `${profilePath}.${process.pid}.${crypto.randomUUID()}`;
|
|
422
450
|
fs.writeFileSync(temporary, `${JSON.stringify(profile, null, 2)}\n`, { mode: 0o600 });
|
|
423
451
|
fs.renameSync(temporary, profilePath);
|
|
@@ -498,6 +526,16 @@ export function authenticateFleetLocalBot(input, deps = {}) {
|
|
|
498
526
|
if (!/^[a-z0-9][a-z0-9._:-]*$/.test(requested)) {
|
|
499
527
|
throw expectedError("HQ Fleet provider identifier is invalid.");
|
|
500
528
|
}
|
|
529
|
+
if ((requested === "claude" || requested === "anthropic") && !input.apiKey) {
|
|
530
|
+
const files = fleetLocalExecutables(input.agentUid, deps);
|
|
531
|
+
const run = deps.run ?? execFileSync;
|
|
532
|
+
const native = fleetProviderForRuntime("claude", input.model);
|
|
533
|
+
replaceProfileProvider(files.profile, input.agentUid, native);
|
|
534
|
+
runJson(run, files.helper, ["render-profile", "--spec", files.profile, "--mode", "update"], { stdio: ["ignore", "pipe", "pipe"] }, "profile update");
|
|
535
|
+
runJson(run, files.helper, ["doctor-profile", "--profile", files.profileDir, "--runtime", files.runtime], { stdio: ["ignore", "pipe", "pipe"] }, "profile validation");
|
|
536
|
+
const restarted = controlFleetLocalBot(input.agentUid, input.installService ? "install" : "restart", deps).ok === true;
|
|
537
|
+
return { provider: requested, runtimeProvider: "anthropic", model: native.model, authenticated: true, restarted };
|
|
538
|
+
}
|
|
501
539
|
const known = providerProfiles[requested];
|
|
502
540
|
const provider = known ?? {
|
|
503
541
|
provider: `custom:personal-${requested}`,
|
|
@@ -558,7 +596,7 @@ export function authenticateFleetLocalBot(input, deps = {}) {
|
|
|
558
596
|
});
|
|
559
597
|
runJson(run, files.helper, ["render-profile", "--spec", files.profile, "--mode", "update"], { stdio: ["ignore", "pipe", "pipe"] }, "profile update");
|
|
560
598
|
runJson(run, files.helper, ["doctor-profile", "--profile", files.profileDir, "--runtime", files.runtime], { stdio: ["ignore", "pipe", "pipe"] }, "profile validation");
|
|
561
|
-
const restarted = controlFleetLocalBot(input.agentUid, "restart", deps).ok === true;
|
|
599
|
+
const restarted = controlFleetLocalBot(input.agentUid, input.installService ? "install" : "restart", deps).ok === true;
|
|
562
600
|
return {
|
|
563
601
|
provider: requested,
|
|
564
602
|
runtimeProvider: provider.runtimeProvider,
|
|
@@ -583,7 +621,8 @@ export function writeFleetLocalProfile(input, deps = {}) {
|
|
|
583
621
|
schemaVersion: 1,
|
|
584
622
|
botId: input.agentUid,
|
|
585
623
|
ownerUid: input.ownerUid,
|
|
586
|
-
|
|
624
|
+
...(input.ownerCompanies?.length ? { ownerCompanies: [...new Set(input.ownerCompanies)].sort() } : {}),
|
|
625
|
+
provider: input.provider ?? { id: "custom:personal-chatgpt", model: "gpt-5.6-sol" },
|
|
587
626
|
hqRoot: path.resolve(input.hqRoot),
|
|
588
627
|
profileDir: path.resolve(dir),
|
|
589
628
|
runtime: { commit: input.installation.release.runtimeCommit, path: path.resolve(runtimeDir) },
|
|
@@ -599,6 +638,24 @@ export function writeFleetLocalProfile(input, deps = {}) {
|
|
|
599
638
|
fs.renameSync(temporary, destination);
|
|
600
639
|
if (input.systemPrompt !== undefined)
|
|
601
640
|
writeFleetLocalSoul(input.agentUid, input.systemPrompt, { fleetHome: root });
|
|
641
|
+
if (input.firstStart && (input.firstStart.intro || input.firstStart.kickoff)) {
|
|
642
|
+
writeFleetLocalFirstStart(dir, input.firstStart);
|
|
643
|
+
}
|
|
644
|
+
return destination;
|
|
645
|
+
}
|
|
646
|
+
/** The runtime's inbox plugin sends the intro and runs the kickoff once, then stamps this file. */
|
|
647
|
+
export function writeFleetLocalFirstStart(profileDir, firstStart) {
|
|
648
|
+
const destination = path.join(profileDir, "first-start.json");
|
|
649
|
+
if (fs.existsSync(destination))
|
|
650
|
+
return destination;
|
|
651
|
+
const temporary = path.join(profileDir, `.first-start-${process.pid}-${crypto.randomUUID()}`);
|
|
652
|
+
const record = {
|
|
653
|
+
schemaVersion: 1,
|
|
654
|
+
...(firstStart.intro ? { intro: firstStart.intro } : {}),
|
|
655
|
+
...(firstStart.kickoff ? { kickoff: firstStart.kickoff } : {}),
|
|
656
|
+
};
|
|
657
|
+
fs.writeFileSync(temporary, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 });
|
|
658
|
+
fs.renameSync(temporary, destination);
|
|
602
659
|
return destination;
|
|
603
660
|
}
|
|
604
661
|
export function writeFleetLocalSoul(agentUid, systemPrompt, deps = {}) {
|
|
@@ -669,6 +726,24 @@ function checkedRun(run, executable, args, options, label) {
|
|
|
669
726
|
throw expectedError(`HQ Fleet Local ${label} failed.`);
|
|
670
727
|
}
|
|
671
728
|
}
|
|
729
|
+
/** Moves a fresh machine credential into the bot's secure store and deletes the file. */
|
|
730
|
+
export function importFleetLocalMachineCredential(agentUid, machineCredentials, deps = {}) {
|
|
731
|
+
const run = deps.run ?? execFileSync;
|
|
732
|
+
const files = fleetLocalExecutables(agentUid, deps);
|
|
733
|
+
const machineValue = fs.readFileSync(machineCredentials, "utf8").trim();
|
|
734
|
+
if (!machineValue) {
|
|
735
|
+
throw expectedError(`HQ Fleet Local machine credential file is empty: ${machineCredentials}`);
|
|
736
|
+
}
|
|
737
|
+
checkedRun(run, files.helper, [
|
|
738
|
+
"machine-credential",
|
|
739
|
+
"import",
|
|
740
|
+
"--profile",
|
|
741
|
+
files.profileDir,
|
|
742
|
+
"--bot-id",
|
|
743
|
+
agentUid,
|
|
744
|
+
], { input: machineValue, stdio: ["pipe", "ignore", "pipe"] }, "machine credential import");
|
|
745
|
+
fs.rmSync(machineCredentials, { force: true });
|
|
746
|
+
}
|
|
672
747
|
export async function bootstrapFleetLocalBot(input, deps = {}) {
|
|
673
748
|
const run = deps.run ?? execFileSync;
|
|
674
749
|
const platform = deps.platform ?? process.platform;
|
|
@@ -716,7 +791,7 @@ export async function bootstrapFleetLocalBot(input, deps = {}) {
|
|
|
716
791
|
"--python",
|
|
717
792
|
runtimePython,
|
|
718
793
|
"--editable",
|
|
719
|
-
`${runtimeSource}[
|
|
794
|
+
`${runtimeSource}[mcp,anthropic]`,
|
|
720
795
|
distributionSource,
|
|
721
796
|
], { stdio: ["ignore", 2, 2], env: uvEnv }, "package installation");
|
|
722
797
|
fs.writeFileSync(marker, `${JSON.stringify({ schemaVersion: 1, engine: input.installation.engine }, null, 2)}\n`, { mode: 0o600 });
|
package/dist/lib/bot/index.d.ts
CHANGED
package/dist/lib/bot/index.js
CHANGED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bot's built-in hot-signal-sync job.
|
|
3
|
+
*
|
|
4
|
+
* A local bot that is bound to a company registers one recurring job in its
|
|
5
|
+
* own `jobs.json` the first time it starts (`ensureHotSignalSyncJob`). The job
|
|
6
|
+
* fires from the bot's inbox loop like every other local job
|
|
7
|
+
* (lib/bot/local-jobs.ts): no crontab, launchd or systemd entry is written.
|
|
8
|
+
* The default schedule is 06:30 daily in the machine's time zone, ahead of a
|
|
9
|
+
* morning brief; the owner changes it with
|
|
10
|
+
* `hq bot jobs <bot> --reschedule <jobId> --every "<when>"`, or removes it with
|
|
11
|
+
* `--rm`, and a removed job is not added back on the next start.
|
|
12
|
+
*
|
|
13
|
+
* When the job fires the bot runs the sync in-process, with no model turn
|
|
14
|
+
* (`runHotSignalSyncJob`): the same hot-channel sync `hq sources sync` runs,
|
|
15
|
+
* with `trigger: "scheduled"` and `catchUp` set when the run fired late after
|
|
16
|
+
* a missed window. Catch-up is bounded by each adapter's `catchUpWindow`, and
|
|
17
|
+
* per-connection cursors keep a redelivered run from landing anything twice.
|
|
18
|
+
* The claude signals pass (US-011) runs after each adapter when the bot wires
|
|
19
|
+
* one in; with none the signals count is 0.
|
|
20
|
+
*/
|
|
21
|
+
import type { HotChannelRunResult, WriteGuard } from "../../hot-channels/loop.js";
|
|
22
|
+
import type { RunTrigger } from "../../hot-channels/ledger.js";
|
|
23
|
+
import type { OnDemandSyncReport } from "../../hot-channels/run.js";
|
|
24
|
+
import { type BotConfig } from "../config.js";
|
|
25
|
+
import { type LocalJob } from "../local-jobs.js";
|
|
26
|
+
export declare const HOT_SIGNAL_SYNC_JOB_KIND = "hot-signal-sync";
|
|
27
|
+
/**
|
|
28
|
+
* 06:30 every day, in the job's time zone (the stored EventBridge form of
|
|
29
|
+
* "30 6 * * *"). Only the first registration uses it; after that the job's
|
|
30
|
+
* own schedule in `jobs.json` is the one that runs.
|
|
31
|
+
*/
|
|
32
|
+
export declare const HOT_SIGNAL_SYNC_DEFAULT_CRON = "cron(30 6 * * ? *)";
|
|
33
|
+
/** The private context that marks a job as this built-in, for one company. */
|
|
34
|
+
export declare function hotSignalSyncJobContext(company: string): Record<string, unknown>;
|
|
35
|
+
/** The company a job item syncs, when its context marks it as this built-in; else null. */
|
|
36
|
+
export declare function hotSignalSyncJobCompany(context: unknown): string | null;
|
|
37
|
+
/**
|
|
38
|
+
* The company a bot's hot-signal-sync job syncs: the bot's own company, else
|
|
39
|
+
* the first company it serves. Null = none, and no job is registered. There is
|
|
40
|
+
* no fallback to the HQ's active company: a personal bot syncs a company only
|
|
41
|
+
* when it was given one explicitly (`--company`), so the job never writes into
|
|
42
|
+
* a vault the bot does not belong to. Checked again each time the job fires.
|
|
43
|
+
*/
|
|
44
|
+
export declare function currentHotSignalSyncCompany(config: Pick<BotConfig, "companySlug" | "kind" | "companies" | "workerId">): string | null;
|
|
45
|
+
/**
|
|
46
|
+
* Remove every hot-signal-sync job whose company is not `current` (all of them
|
|
47
|
+
* when `current` is null), and forget their registration so the company can be
|
|
48
|
+
* registered again if the bot is given it back. Returns the removed jobs.
|
|
49
|
+
*/
|
|
50
|
+
export declare function removeStaleHotSignalSyncJobs(dir: string, current: string | null): LocalJob[];
|
|
51
|
+
export interface EnsureHotSignalSyncJobResult {
|
|
52
|
+
/** The job, or null when the owner removed it after it was first registered. */
|
|
53
|
+
job: LocalJob | null;
|
|
54
|
+
created: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Register the hot-signal-sync job for `company` once. Safe to call on every
|
|
58
|
+
* bot start: an existing job is left as it is (including a changed schedule),
|
|
59
|
+
* and a job the owner removed is not added again.
|
|
60
|
+
*/
|
|
61
|
+
export declare function ensureHotSignalSyncJob(dir: string, input: {
|
|
62
|
+
company: string;
|
|
63
|
+
timezone: string;
|
|
64
|
+
}, now?: Date): EnsureHotSignalSyncJobResult;
|
|
65
|
+
/**
|
|
66
|
+
* The signals pass over one adapter's landed sources: how many signals it
|
|
67
|
+
* generated. The bot wires the local `claude -p` pass
|
|
68
|
+
* (`createClaudeSignalsPass`); `noLocalSignalsPass` is the default when no
|
|
69
|
+
* pass is given.
|
|
70
|
+
*/
|
|
71
|
+
export type LocalSignalsPass = (result: HotChannelRunResult, guard?: WriteGuard) => Promise<number>;
|
|
72
|
+
export declare const noLocalSignalsPass: LocalSignalsPass;
|
|
73
|
+
/** One company's hot-channel sync, as `runCompanyHotChannelSync` runs it (minus the HQ root). */
|
|
74
|
+
export type HotSignalSyncFn = (input: {
|
|
75
|
+
company: string;
|
|
76
|
+
trigger: RunTrigger;
|
|
77
|
+
catchUp: boolean;
|
|
78
|
+
signalsGenerated: LocalSignalsPass;
|
|
79
|
+
now: () => Date;
|
|
80
|
+
/** The owner DM for the signals-failure alert (US-012); unset = no alert check. */
|
|
81
|
+
signalsAlert?: {
|
|
82
|
+
sendOwnerDm: (text: string) => Promise<void>;
|
|
83
|
+
log?: (message: string) => void;
|
|
84
|
+
};
|
|
85
|
+
}) => Promise<OnDemandSyncReport>;
|
|
86
|
+
export interface HotSignalSyncRunInput {
|
|
87
|
+
company: string;
|
|
88
|
+
/** The run fired late, after a missed window (job-run.ts `isCatchUpRun`). */
|
|
89
|
+
catchUp: boolean;
|
|
90
|
+
now: () => Date;
|
|
91
|
+
sync: HotSignalSyncFn;
|
|
92
|
+
signalsPass?: LocalSignalsPass;
|
|
93
|
+
/**
|
|
94
|
+
* Sends the signals-failure alert to the owner as its own message, even when
|
|
95
|
+
* the job is quiet. Throws when the message was not delivered.
|
|
96
|
+
*/
|
|
97
|
+
sendAlert?: (text: string) => Promise<void>;
|
|
98
|
+
log?: (message: string) => void;
|
|
99
|
+
}
|
|
100
|
+
export interface HotSignalSyncRunOutcome {
|
|
101
|
+
ok: boolean;
|
|
102
|
+
/** Plain report for the owner's DM and the job's run output. */
|
|
103
|
+
text: string;
|
|
104
|
+
report: OnDemandSyncReport | null;
|
|
105
|
+
}
|
|
106
|
+
/** Run the scheduled sync once and describe it in plain words. Never throws. */
|
|
107
|
+
export declare function runHotSignalSyncJob(input: HotSignalSyncRunInput): Promise<HotSignalSyncRunOutcome>;
|
|
108
|
+
export declare function describeSyncReport(company: string, report: OnDemandSyncReport): string;
|
|
109
|
+
//# sourceMappingURL=hot-signal-sync.d.ts.map
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bot's built-in hot-signal-sync job.
|
|
3
|
+
*
|
|
4
|
+
* A local bot that is bound to a company registers one recurring job in its
|
|
5
|
+
* own `jobs.json` the first time it starts (`ensureHotSignalSyncJob`). The job
|
|
6
|
+
* fires from the bot's inbox loop like every other local job
|
|
7
|
+
* (lib/bot/local-jobs.ts): no crontab, launchd or systemd entry is written.
|
|
8
|
+
* The default schedule is 06:30 daily in the machine's time zone, ahead of a
|
|
9
|
+
* morning brief; the owner changes it with
|
|
10
|
+
* `hq bot jobs <bot> --reschedule <jobId> --every "<when>"`, or removes it with
|
|
11
|
+
* `--rm`, and a removed job is not added back on the next start.
|
|
12
|
+
*
|
|
13
|
+
* When the job fires the bot runs the sync in-process, with no model turn
|
|
14
|
+
* (`runHotSignalSyncJob`): the same hot-channel sync `hq sources sync` runs,
|
|
15
|
+
* with `trigger: "scheduled"` and `catchUp` set when the run fired late after
|
|
16
|
+
* a missed window. Catch-up is bounded by each adapter's `catchUpWindow`, and
|
|
17
|
+
* per-connection cursors keep a redelivered run from landing anything twice.
|
|
18
|
+
* The claude signals pass (US-011) runs after each adapter when the bot wires
|
|
19
|
+
* one in; with none the signals count is 0.
|
|
20
|
+
*/
|
|
21
|
+
import * as fs from "node:fs";
|
|
22
|
+
import * as path from "node:path";
|
|
23
|
+
import { JOB_RUN_OPTIONS_CONTEXT_KEY } from "../job-run.js";
|
|
24
|
+
import { effectiveBotCompanies } from "../config.js";
|
|
25
|
+
import { ensureLocalJob, readLocalJobs, removeLocalJobsWhere, withJobsLock } from "../local-jobs.js";
|
|
26
|
+
export const HOT_SIGNAL_SYNC_JOB_KIND = "hot-signal-sync";
|
|
27
|
+
/**
|
|
28
|
+
* 06:30 every day, in the job's time zone (the stored EventBridge form of
|
|
29
|
+
* "30 6 * * *"). Only the first registration uses it; after that the job's
|
|
30
|
+
* own schedule in `jobs.json` is the one that runs.
|
|
31
|
+
*/
|
|
32
|
+
export const HOT_SIGNAL_SYNC_DEFAULT_CRON = "cron(30 6 * * ? *)";
|
|
33
|
+
/** Which built-in jobs this bot has registered once, so a removed one stays removed. */
|
|
34
|
+
const BUILTIN_JOBS_FILE = "builtin-jobs.json";
|
|
35
|
+
function markerKey(company) {
|
|
36
|
+
return `${HOT_SIGNAL_SYNC_JOB_KIND}:${company}`;
|
|
37
|
+
}
|
|
38
|
+
function readMarker(dir) {
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(dir, BUILTIN_JOBS_FILE), "utf-8"));
|
|
41
|
+
if (parsed && parsed.v === 1 && parsed.registered && typeof parsed.registered === "object") {
|
|
42
|
+
return { v: 1, registered: { ...parsed.registered } };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Missing or unreadable: nothing registered yet.
|
|
47
|
+
}
|
|
48
|
+
return { v: 1, registered: {} };
|
|
49
|
+
}
|
|
50
|
+
function writeMarker(dir, marker) {
|
|
51
|
+
const file = path.join(dir, BUILTIN_JOBS_FILE);
|
|
52
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
53
|
+
fs.writeFileSync(tmp, JSON.stringify(marker, null, 2) + "\n", { mode: 0o600 });
|
|
54
|
+
fs.renameSync(tmp, file);
|
|
55
|
+
}
|
|
56
|
+
/** The private context that marks a job as this built-in, for one company. */
|
|
57
|
+
export function hotSignalSyncJobContext(company) {
|
|
58
|
+
return { [JOB_RUN_OPTIONS_CONTEXT_KEY]: { builtin: HOT_SIGNAL_SYNC_JOB_KIND, company } };
|
|
59
|
+
}
|
|
60
|
+
/** The company a job item syncs, when its context marks it as this built-in; else null. */
|
|
61
|
+
export function hotSignalSyncJobCompany(context) {
|
|
62
|
+
if (!context || typeof context !== "object" || Array.isArray(context))
|
|
63
|
+
return null;
|
|
64
|
+
const options = context[JOB_RUN_OPTIONS_CONTEXT_KEY];
|
|
65
|
+
if (!options || typeof options !== "object" || Array.isArray(options))
|
|
66
|
+
return null;
|
|
67
|
+
const { builtin, company } = options;
|
|
68
|
+
if (builtin !== HOT_SIGNAL_SYNC_JOB_KIND)
|
|
69
|
+
return null;
|
|
70
|
+
return typeof company === "string" && company.trim() ? company.trim() : null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The company a bot's hot-signal-sync job syncs: the bot's own company, else
|
|
74
|
+
* the first company it serves. Null = none, and no job is registered. There is
|
|
75
|
+
* no fallback to the HQ's active company: a personal bot syncs a company only
|
|
76
|
+
* when it was given one explicitly (`--company`), so the job never writes into
|
|
77
|
+
* a vault the bot does not belong to. Checked again each time the job fires.
|
|
78
|
+
*/
|
|
79
|
+
export function currentHotSignalSyncCompany(config) {
|
|
80
|
+
return config.companySlug ?? effectiveBotCompanies(config)[0] ?? null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Remove every hot-signal-sync job whose company is not `current` (all of them
|
|
84
|
+
* when `current` is null), and forget their registration so the company can be
|
|
85
|
+
* registered again if the bot is given it back. Returns the removed jobs.
|
|
86
|
+
*/
|
|
87
|
+
export function removeStaleHotSignalSyncJobs(dir, current) {
|
|
88
|
+
const removed = removeLocalJobsWhere(dir, (job) => {
|
|
89
|
+
const company = hotSignalSyncJobCompany(job.context);
|
|
90
|
+
return company !== null && company !== current;
|
|
91
|
+
});
|
|
92
|
+
const keep = current ? markerKey(current) : null;
|
|
93
|
+
withJobsLock(dir, () => {
|
|
94
|
+
const marker = readMarker(dir);
|
|
95
|
+
let changed = false;
|
|
96
|
+
for (const key of Object.keys(marker.registered)) {
|
|
97
|
+
if (key.startsWith(HOT_SIGNAL_SYNC_JOB_KIND + ":") && key !== keep) {
|
|
98
|
+
delete marker.registered[key];
|
|
99
|
+
changed = true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (changed)
|
|
103
|
+
writeMarker(dir, marker);
|
|
104
|
+
});
|
|
105
|
+
return removed;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Register the hot-signal-sync job for `company` once. Safe to call on every
|
|
109
|
+
* bot start: an existing job is left as it is (including a changed schedule),
|
|
110
|
+
* and a job the owner removed is not added again.
|
|
111
|
+
*/
|
|
112
|
+
export function ensureHotSignalSyncJob(dir, input, now = new Date()) {
|
|
113
|
+
const key = markerKey(input.company);
|
|
114
|
+
const matches = (job) => hotSignalSyncJobCompany(job.context) === input.company;
|
|
115
|
+
if (readMarker(dir).registered[key]) {
|
|
116
|
+
return { job: readLocalJobs(dir).find(matches) ?? null, created: false };
|
|
117
|
+
}
|
|
118
|
+
const { job, created } = ensureLocalJob(dir, matches, {
|
|
119
|
+
prompt: `Sync hot channels for ${input.company} and run the signals pass (runs in the bot, no model turn).`,
|
|
120
|
+
cron: HOT_SIGNAL_SYNC_DEFAULT_CRON,
|
|
121
|
+
timezone: input.timezone,
|
|
122
|
+
context: hotSignalSyncJobContext(input.company),
|
|
123
|
+
}, now);
|
|
124
|
+
withJobsLock(dir, () => {
|
|
125
|
+
const marker = readMarker(dir);
|
|
126
|
+
if (!marker.registered[key]) {
|
|
127
|
+
marker.registered[key] = now.toISOString();
|
|
128
|
+
writeMarker(dir, marker);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
return { job, created };
|
|
132
|
+
}
|
|
133
|
+
export const noLocalSignalsPass = async () => 0;
|
|
134
|
+
/** Run the scheduled sync once and describe it in plain words. Never throws. */
|
|
135
|
+
export async function runHotSignalSyncJob(input) {
|
|
136
|
+
const signalsPass = input.signalsPass ?? noLocalSignalsPass;
|
|
137
|
+
let report;
|
|
138
|
+
try {
|
|
139
|
+
report = await input.sync({
|
|
140
|
+
company: input.company,
|
|
141
|
+
trigger: "scheduled",
|
|
142
|
+
catchUp: input.catchUp,
|
|
143
|
+
signalsGenerated: signalsPass,
|
|
144
|
+
now: input.now,
|
|
145
|
+
...(input.sendAlert
|
|
146
|
+
? { signalsAlert: { sendOwnerDm: input.sendAlert, ...(input.log ? { log: input.log } : {}) } }
|
|
147
|
+
: {}),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
152
|
+
return { ok: false, text: `The hot-channel sync for ${input.company} failed: ${reason}`, report: null };
|
|
153
|
+
}
|
|
154
|
+
return { ok: report.exitCode === 0 && !report.signInRequired, text: describeSyncReport(input.company, report), report };
|
|
155
|
+
}
|
|
156
|
+
export function describeSyncReport(company, report) {
|
|
157
|
+
if (report.signInRequired) {
|
|
158
|
+
return `The hot-channel sync for ${company} did not run: ${report.notices.join(" ")}`;
|
|
159
|
+
}
|
|
160
|
+
if (report.skipped)
|
|
161
|
+
return report.notices.join(" ");
|
|
162
|
+
const landed = report.outcomes.reduce((sum, o) => sum + o.landed, 0);
|
|
163
|
+
const signals = report.entries.reduce((sum, e) => sum + e.signalsGenerated, 0);
|
|
164
|
+
const lines = [
|
|
165
|
+
`Hot-channel sync for ${company}: ${landed} new source${landed === 1 ? "" : "s"}, ${signals} signal${signals === 1 ? "" : "s"}.`,
|
|
166
|
+
];
|
|
167
|
+
for (const outcome of report.outcomes) {
|
|
168
|
+
if (outcome.status === "ok")
|
|
169
|
+
continue;
|
|
170
|
+
const label = outcome.appName ?? outcome.adapter;
|
|
171
|
+
lines.push(`- ${label}: ${outcome.reconnect?.prompt ?? outcome.message ?? outcome.status}`);
|
|
172
|
+
}
|
|
173
|
+
for (const notice of report.notices)
|
|
174
|
+
lines.push(notice);
|
|
175
|
+
return lines.join("\n");
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=hot-signal-sync.js.map
|
|
@@ -41,7 +41,7 @@ export function buildBotLocalConfig(config) {
|
|
|
41
41
|
const companies = effectiveBotCompanies(config);
|
|
42
42
|
const local = {
|
|
43
43
|
v: 1,
|
|
44
|
-
|
|
44
|
+
runtime: config.runtime,
|
|
45
45
|
...(config.engine ? { engine: config.engine } : {}),
|
|
46
46
|
workerSource: config.workerSource ?? "scaffold",
|
|
47
47
|
...(config.workerId ? { workerId: config.workerId } : {}),
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* - A fire is recorded before it is handed over, so a crash never runs the
|
|
19
19
|
* same due time twice.
|
|
20
20
|
*/
|
|
21
|
-
import { type BotApi, type InboxItem } from "./api.js";
|
|
21
|
+
import { type BotApi, type BotJobRecord, type InboxItem } from "./api.js";
|
|
22
22
|
export interface LocalJob {
|
|
23
23
|
jobId: string;
|
|
24
24
|
prompt: string;
|
|
@@ -27,6 +27,12 @@ export interface LocalJob {
|
|
|
27
27
|
timezone: string;
|
|
28
28
|
context?: Record<string, unknown>;
|
|
29
29
|
createdAt: string;
|
|
30
|
+
/**
|
|
31
|
+
* When the schedule was last changed (`hq bot jobs --reschedule`). Fire
|
|
32
|
+
* times before it are never treated as missed, so a new schedule does not
|
|
33
|
+
* fire a catch-up for a time that only matches the new expression.
|
|
34
|
+
*/
|
|
35
|
+
scheduledFrom?: string;
|
|
30
36
|
/** The newest due time already handed to the bot. */
|
|
31
37
|
lastFiredAt?: string;
|
|
32
38
|
/** The run in progress, if any. */
|
|
@@ -72,6 +78,29 @@ export declare function addLocalJob(dir: string, input: {
|
|
|
72
78
|
timezone: string;
|
|
73
79
|
context?: Record<string, unknown>;
|
|
74
80
|
}, now?: Date): LocalJob;
|
|
81
|
+
/**
|
|
82
|
+
* Add a job unless one already matches, as one locked read-and-write, so two
|
|
83
|
+
* bot starts racing each other still leave a single job.
|
|
84
|
+
*/
|
|
85
|
+
export declare function ensureLocalJob(dir: string, matches: (job: LocalJob) => boolean, input: {
|
|
86
|
+
prompt: string;
|
|
87
|
+
cron: string;
|
|
88
|
+
timezone: string;
|
|
89
|
+
context?: Record<string, unknown>;
|
|
90
|
+
}, now?: Date): {
|
|
91
|
+
job: LocalJob;
|
|
92
|
+
created: boolean;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Change a job's schedule in place, keeping its id, instruction, context and
|
|
96
|
+
* run history. Returns null when the bot has no job with that id.
|
|
97
|
+
*/
|
|
98
|
+
export declare function rescheduleLocalJob(dir: string, jobId: string, schedule: {
|
|
99
|
+
cron: string;
|
|
100
|
+
timezone: string;
|
|
101
|
+
}, now?: Date): LocalJob | null;
|
|
102
|
+
/** Remove every job `matches` picks, as one locked read-and-write. Returns the removed jobs. */
|
|
103
|
+
export declare function removeLocalJobsWhere(dir: string, matches: (job: LocalJob) => boolean): LocalJob[];
|
|
75
104
|
/** Forget the once-per-minute check (tests, and a bot restarted in-process). */
|
|
76
105
|
export declare function resetLocalJobChecks(): void;
|
|
77
106
|
export declare function takeDueLocalJobRuns(dir: string, now?: Date): InboxItem[];
|
|
@@ -81,9 +110,16 @@ export declare function takeDueLocalJobRuns(dir: string, now?: Date): InboxItem[
|
|
|
81
110
|
* locally instead of being sent to the cloud.
|
|
82
111
|
*/
|
|
83
112
|
export declare function withLocalJobs(api: BotApi, dir: string, clock?: () => Date): BotApi;
|
|
113
|
+
/** What `hq bot jobs` uses: list, add, remove, and (local bots) reschedule. */
|
|
114
|
+
export type LocalJobsApi = Pick<BotApi, "listJobs" | "createJob" | "cancelJob"> & {
|
|
115
|
+
rescheduleJob?(agentUid: string, jobId: string, schedule: {
|
|
116
|
+
cron: string;
|
|
117
|
+
timezone: string;
|
|
118
|
+
}): Promise<BotJobRecord>;
|
|
119
|
+
};
|
|
84
120
|
/**
|
|
85
|
-
* `hq bot jobs` against the bot's own folder: the same
|
|
86
|
-
*
|
|
121
|
+
* `hq bot jobs` against the bot's own folder: the same calls the cloud client
|
|
122
|
+
* offers, plus reschedule, answered from `jobs.json`.
|
|
87
123
|
*/
|
|
88
|
-
export declare function localJobsClient(dir: string, clock?: () => Date):
|
|
124
|
+
export declare function localJobsClient(dir: string, clock?: () => Date): Required<LocalJobsApi>;
|
|
89
125
|
//# sourceMappingURL=local-jobs.d.ts.map
|
|
@@ -237,10 +237,14 @@ function rateIntervalMs(expr) {
|
|
|
237
237
|
function isRunnableSchedule(expr) {
|
|
238
238
|
return rateIntervalMs(expr) !== null || parseCron(expr) !== null;
|
|
239
239
|
}
|
|
240
|
+
/** Where the current schedule starts counting: its last change, else creation. */
|
|
241
|
+
function scheduleOrigin(job) {
|
|
242
|
+
return Date.parse(job.scheduledFrom ?? job.createdAt);
|
|
243
|
+
}
|
|
240
244
|
export function latestDueFire(job, now) {
|
|
241
245
|
const interval = rateIntervalMs(job.cron);
|
|
242
246
|
if (interval) {
|
|
243
|
-
const origin =
|
|
247
|
+
const origin = scheduleOrigin(job);
|
|
244
248
|
const steps = Math.floor((now.getTime() - origin) / interval);
|
|
245
249
|
if (steps < 1)
|
|
246
250
|
return null;
|
|
@@ -252,7 +256,7 @@ export function latestDueFire(job, now) {
|
|
|
252
256
|
return null;
|
|
253
257
|
const start = new Date(now);
|
|
254
258
|
start.setUTCSeconds(0, 0);
|
|
255
|
-
const floor = Math.max(job.lastFiredAt ? Date.parse(job.lastFiredAt) : Date.parse(job.createdAt), start.getTime() - LOOKBACK_MINUTES * 60_000);
|
|
259
|
+
const floor = Math.max(job.lastFiredAt ? Date.parse(job.lastFiredAt) : Date.parse(job.createdAt), job.scheduledFrom ? Date.parse(job.scheduledFrom) : 0, start.getTime() - LOOKBACK_MINUTES * 60_000);
|
|
256
260
|
for (let t = start.getTime(); t > floor; t -= 60_000) {
|
|
257
261
|
const at = new Date(t);
|
|
258
262
|
if (cronMatches(cron, at, job.timezone))
|
|
@@ -264,7 +268,7 @@ export function latestDueFire(job, now) {
|
|
|
264
268
|
export function nextDueFire(job, now) {
|
|
265
269
|
const interval = rateIntervalMs(job.cron);
|
|
266
270
|
if (interval) {
|
|
267
|
-
const origin =
|
|
271
|
+
const origin = scheduleOrigin(job);
|
|
268
272
|
const steps = Math.max(0, Math.floor((now.getTime() - origin) / interval)) + 1;
|
|
269
273
|
return new Date(origin + steps * interval);
|
|
270
274
|
}
|
|
@@ -310,6 +314,53 @@ export function addLocalJob(dir, input, now = new Date()) {
|
|
|
310
314
|
withJobsLock(dir, () => writeLocalJobs(dir, [...readLocalJobs(dir), job]));
|
|
311
315
|
return job;
|
|
312
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Add a job unless one already matches, as one locked read-and-write, so two
|
|
319
|
+
* bot starts racing each other still leave a single job.
|
|
320
|
+
*/
|
|
321
|
+
export function ensureLocalJob(dir, matches, input, now = new Date()) {
|
|
322
|
+
if (!isRunnableSchedule(input.cron))
|
|
323
|
+
throw new Error(`"${input.cron}" is not a schedule this bot can run.`);
|
|
324
|
+
return withJobsLock(dir, () => {
|
|
325
|
+
const jobs = readLocalJobs(dir);
|
|
326
|
+
const existing = jobs.find(matches);
|
|
327
|
+
if (existing)
|
|
328
|
+
return { job: existing, created: false };
|
|
329
|
+
const job = {
|
|
330
|
+
jobId: `job_${randomUUID().replace(/-/g, "").slice(0, 20)}`,
|
|
331
|
+
prompt: input.prompt,
|
|
332
|
+
cron: input.cron,
|
|
333
|
+
timezone: input.timezone,
|
|
334
|
+
...(input.context ? { context: input.context } : {}),
|
|
335
|
+
createdAt: now.toISOString(),
|
|
336
|
+
};
|
|
337
|
+
writeLocalJobs(dir, [...jobs, job]);
|
|
338
|
+
return { job, created: true };
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Change a job's schedule in place, keeping its id, instruction, context and
|
|
343
|
+
* run history. Returns null when the bot has no job with that id.
|
|
344
|
+
*/
|
|
345
|
+
export function rescheduleLocalJob(dir, jobId, schedule, now = new Date()) {
|
|
346
|
+
if (!isRunnableSchedule(schedule.cron))
|
|
347
|
+
throw new Error(`"${schedule.cron}" is not a schedule this bot can run.`);
|
|
348
|
+
return withJobsLock(dir, () => {
|
|
349
|
+
const jobs = readLocalJobs(dir);
|
|
350
|
+
const index = jobs.findIndex((job) => job.jobId === jobId);
|
|
351
|
+
if (index < 0)
|
|
352
|
+
return null;
|
|
353
|
+
const updated = {
|
|
354
|
+
...jobs[index],
|
|
355
|
+
cron: schedule.cron,
|
|
356
|
+
timezone: schedule.timezone,
|
|
357
|
+
scheduledFrom: now.toISOString(),
|
|
358
|
+
};
|
|
359
|
+
jobs[index] = updated;
|
|
360
|
+
writeLocalJobs(dir, jobs);
|
|
361
|
+
return updated;
|
|
362
|
+
});
|
|
363
|
+
}
|
|
313
364
|
/** Remove a job. Returns false when the bot has no job with that id. */
|
|
314
365
|
function removeLocalJob(dir, jobId) {
|
|
315
366
|
return withJobsLock(dir, () => {
|
|
@@ -321,6 +372,16 @@ function removeLocalJob(dir, jobId) {
|
|
|
321
372
|
return true;
|
|
322
373
|
});
|
|
323
374
|
}
|
|
375
|
+
/** Remove every job `matches` picks, as one locked read-and-write. Returns the removed jobs. */
|
|
376
|
+
export function removeLocalJobsWhere(dir, matches) {
|
|
377
|
+
return withJobsLock(dir, () => {
|
|
378
|
+
const jobs = readLocalJobs(dir);
|
|
379
|
+
const removed = jobs.filter(matches);
|
|
380
|
+
if (removed.length > 0)
|
|
381
|
+
writeLocalJobs(dir, jobs.filter((job) => !matches(job)));
|
|
382
|
+
return removed;
|
|
383
|
+
});
|
|
384
|
+
}
|
|
324
385
|
// ─── Firing ──────────────────────────────────────────────────────────────────
|
|
325
386
|
/**
|
|
326
387
|
* Hand over every job that is due now. Each fire is recorded (lastFiredAt and
|
|
@@ -430,8 +491,8 @@ export function withLocalJobs(api, dir, clock = () => new Date()) {
|
|
|
430
491
|
/** The most jobs one bot may hold (the same cap the cloud uses). */
|
|
431
492
|
const MAX_LOCAL_JOBS = 25;
|
|
432
493
|
/**
|
|
433
|
-
* `hq bot jobs` against the bot's own folder: the same
|
|
434
|
-
*
|
|
494
|
+
* `hq bot jobs` against the bot's own folder: the same calls the cloud client
|
|
495
|
+
* offers, plus reschedule, answered from `jobs.json`.
|
|
435
496
|
*/
|
|
436
497
|
export function localJobsClient(dir, clock = () => new Date()) {
|
|
437
498
|
return {
|
|
@@ -454,6 +515,12 @@ export function localJobsClient(dir, clock = () => new Date()) {
|
|
|
454
515
|
throw new BotApiError(404, "local jobs", "JOB_NOT_FOUND", `Job '${jobId}' not found`);
|
|
455
516
|
}
|
|
456
517
|
},
|
|
518
|
+
async rescheduleJob(_agentUid, jobId, schedule) {
|
|
519
|
+
const job = rescheduleLocalJob(dir, jobId, schedule, clock());
|
|
520
|
+
if (!job)
|
|
521
|
+
throw new BotApiError(404, "local jobs", "JOB_NOT_FOUND", `Job '${jobId}' not found`);
|
|
522
|
+
return toJobRecord(job, clock());
|
|
523
|
+
},
|
|
457
524
|
};
|
|
458
525
|
}
|
|
459
526
|
//# sourceMappingURL=local-jobs.js.map
|