@indigoai-us/hq-cli 5.345.178 → 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 +19 -0
- package/dist/command-catalog.generated.d.ts +71 -1
- package/dist/command-catalog.generated.js +86 -1
- package/dist/commands/bot-jobs.d.ts +11 -9
- package/dist/commands/bot-jobs.js +56 -21
- package/dist/commands/bot.d.ts +8 -0
- package/dist/commands/bot.js +44 -0
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +15 -4
- package/dist/commands/integrations-core.d.ts +2 -0
- package/dist/commands/integrations-core.js +1 -0
- package/dist/commands/sources.d.ts +89 -0
- package/dist/commands/sources.js +363 -1
- package/dist/lib/bot/fleet-local.d.ts +1 -1
- package/dist/lib/bot/fleet-local.js +7 -7
- 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-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/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/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.179] — 2026-10-29
|
|
6
|
+
- Conduct loop lanes now count a phase's absolute `deadline` from when the lane starts the phase, so time the envelope spent waiting in the queue no longer eats its run budget. An unparsable `deadline` now falls back to the phase's normal budget instead of an invalid one.
|
|
7
|
+
- Pins hq-fleet-local v0.4.13 so a personal bot's first-start kickoff waits for HQ Fleet Runtime to attach before delivery instead of being dropped during startup.
|
|
8
|
+
- `hq sources sync` runs every hot channel now for the signed-in person: each adapter once per integration connection the person created or was granted, through the HQ integration gateway. It prints sources landed per adapter and connection. A signed-out session or a revoked connection prints a reconnect prompt naming the connection, exits non-zero, and leaves the cursor unchanged. Use `--adapter <id>` to run one adapter and `--company <slug>` to pick the company. Catalog channels with no adapter in this build report "adapter not available in this hq version".
|
|
9
|
+
- `hq sources runs --today` shows the hot-channel run log for the signed-in person: per adapter, the sources each run landed and the signals it generated, with start and finish times and any error. Cursors live at `sources/_cursors/@{personKey}/` and each run appends to `sources/_runs/@{personKey}/{day}.jsonl`, where the person key is a short hash of the person's email.
|
|
10
|
+
- `hq bot run` registers a built-in hot-channel sync job for the bot's company: daily at 06:30 in the computer's time zone, run inside the bot with no model turn. If the computer was asleep at 06:30, the job runs once when it wakes and the ledger records `catchUp: true`. No crontab or launchd entry is written. Change the time with the new `hq bot jobs <bot> --reschedule <jobId> --every "..."` (or `--cron`); a removed job is not added back.
|
|
11
|
+
- Only one hot-channel sync runs at a time per person and company. If the bot's scheduled run and `hq sources sync` overlap, the second one lands nothing, says another sync is running, and writes a `skipped` line to the run log.
|
|
12
|
+
- Linear: issues assigned to you that changed since the last sync land as source documents.
|
|
13
|
+
- Slack: threads HQ has saved to your company vault that you are part of become one signal each, pointing at the thread with no message text copied. A Slack rate-limit pause is reported with a prompt to retry or reconnect.
|
|
14
|
+
- Attio: activity HQ has saved to your company vault becomes signals only when it is about you (changes you made, records you own, comments or notes that mention or are assigned to you), with no payload copied.
|
|
15
|
+
- Meetings: transcripts already in `sources/meetings/` that you attended land as short pointer documents under `sources/meeting-events/`, with no transcript text copied.
|
|
16
|
+
- Gmail ships behind the `hq.hot-channel-gmail` flag. While the flag is off, Gmail is listed as `gated` and never called. With it on, new INBOX mail lands as header-only pointers (never the body or snippet) with a read-only scope.
|
|
17
|
+
- Newly landed sources are turned into signals with your own logged-in `claude`: up to 20 documents per run, each signal linked to its source and listed in the day's signal index. Signals from meetings, Slack and Attio use the full saved content each pointer names (up to 200 KB). A logged-out `claude`, a timeout, bad output, or any other error in one adapter's signals step marks that adapter failed in the run log and the run continues with the next adapter.
|
|
18
|
+
- One malformed event from a channel is skipped and counted as rejected in the run log, with the reason, instead of stopping that channel on every later run.
|
|
19
|
+
- Hot-channel syncs alert when sources land but no signals come out: the bot DMs you and `hq sources sync` prints the alert, at most once an hour per connection. Set `signals_alert_channel: <Slack channel id>` in the company's `settings/signals/preferences.yaml` to also post it to Slack.
|
|
20
|
+
- Contributors: adding a hot channel is one adapter file plus one catalog entry, with end-to-end tests for both run triggers.
|
|
21
|
+
- On Windows, `hq daemon install` now registers a scheduled task named for the current user (`ai.getindigo.hq-daemon-<SID>`, or a sanitised username when the SID cannot be read), so two people who share a PC no longer overwrite each other's daemon. Status, uninstall, `hq doctor` and the handoff and legacy lookups use the same per-user name. An older shared `ai.getindigo.hq-daemon` task that runs as the current user is moved to the new name and kept running; one that belongs to another Windows user, or whose owner cannot be read, is left untouched and `hq` says so.
|
|
22
|
+
- `hq daemon uninstall` on Windows now works for users who upgraded from the shared task and have not re-run install: it checks schtasks for the per-user task instead of trusting the shared XML file, treats a per-user task that is already gone as removed, and also removes this user's task registered under the username form. A task owner stored as a bare user name is no longer treated as `DOMAIN\user`, so another domain's user of the same name keeps their task.
|
|
23
|
+
|
|
5
24
|
## [5.345.178] — 2026-10-29
|
|
6
25
|
- `hq dm send --attach` now uploads the bytes it read with their exact length, so a file changed after the size check no longer stalls and fails with `Upload failed (HTTP 400)`. A refused upload now names the storage error (for example `RequestTimeout`).
|
|
7
26
|
- `hq daemon disable mesh` now keeps `mesh` off on machines where it is not on by default,
|
|
@@ -5150,6 +5150,72 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
5150
5150
|
readonly hidden: false;
|
|
5151
5151
|
}];
|
|
5152
5152
|
readonly subcommands: readonly [];
|
|
5153
|
+
}, {
|
|
5154
|
+
readonly name: "runs";
|
|
5155
|
+
readonly description: "Show the hot-channel run log: sources landed and signals generated per adapter";
|
|
5156
|
+
readonly aliases: readonly [];
|
|
5157
|
+
readonly hidden: false;
|
|
5158
|
+
readonly helpOption: true;
|
|
5159
|
+
readonly helpCommand: false;
|
|
5160
|
+
readonly usage: "[options]";
|
|
5161
|
+
readonly helpText: readonly [];
|
|
5162
|
+
readonly arguments: readonly [];
|
|
5163
|
+
readonly options: readonly [{
|
|
5164
|
+
readonly flags: "--today";
|
|
5165
|
+
readonly description: "Show today's runs (UTC); the default";
|
|
5166
|
+
readonly hidden: false;
|
|
5167
|
+
}, {
|
|
5168
|
+
readonly flags: "--date <YYYY-MM-DD>";
|
|
5169
|
+
readonly description: "Show runs for this UTC day";
|
|
5170
|
+
readonly hidden: false;
|
|
5171
|
+
}, {
|
|
5172
|
+
readonly flags: "--company <slug>";
|
|
5173
|
+
readonly description: "Company slug (defaults to .hq/config.json activeCompany)";
|
|
5174
|
+
readonly hidden: false;
|
|
5175
|
+
}, {
|
|
5176
|
+
readonly flags: "--email <address>";
|
|
5177
|
+
readonly description: "Whose runs to show (defaults to the signed-in person)";
|
|
5178
|
+
readonly hidden: false;
|
|
5179
|
+
}, {
|
|
5180
|
+
readonly flags: "--format <fmt>";
|
|
5181
|
+
readonly description: "Output format: table | json (default: table for TTY, json piped)";
|
|
5182
|
+
readonly hidden: false;
|
|
5183
|
+
}, {
|
|
5184
|
+
readonly flags: "--hq-root <path>";
|
|
5185
|
+
readonly description: "Local HQ tree root (default: <runtime-path>)";
|
|
5186
|
+
readonly hidden: false;
|
|
5187
|
+
readonly defaultValue: "<runtime-path>";
|
|
5188
|
+
}];
|
|
5189
|
+
readonly subcommands: readonly [];
|
|
5190
|
+
}, {
|
|
5191
|
+
readonly name: "sync";
|
|
5192
|
+
readonly description: "Run every hot channel now: each adapter once per connection you can use";
|
|
5193
|
+
readonly aliases: readonly [];
|
|
5194
|
+
readonly hidden: false;
|
|
5195
|
+
readonly helpOption: true;
|
|
5196
|
+
readonly helpCommand: false;
|
|
5197
|
+
readonly usage: "[options]";
|
|
5198
|
+
readonly helpText: readonly [];
|
|
5199
|
+
readonly arguments: readonly [];
|
|
5200
|
+
readonly options: readonly [{
|
|
5201
|
+
readonly flags: "--adapter <id>";
|
|
5202
|
+
readonly description: "Run one adapter only (a catalog provider or adapter id)";
|
|
5203
|
+
readonly hidden: false;
|
|
5204
|
+
}, {
|
|
5205
|
+
readonly flags: "--company <slug>";
|
|
5206
|
+
readonly description: "Company slug (defaults to .hq/config.json activeCompany)";
|
|
5207
|
+
readonly hidden: false;
|
|
5208
|
+
}, {
|
|
5209
|
+
readonly flags: "--json";
|
|
5210
|
+
readonly description: "Print the run report as JSON";
|
|
5211
|
+
readonly hidden: false;
|
|
5212
|
+
}, {
|
|
5213
|
+
readonly flags: "--hq-root <path>";
|
|
5214
|
+
readonly description: "Local HQ tree root (default: <runtime-path>)";
|
|
5215
|
+
readonly hidden: false;
|
|
5216
|
+
readonly defaultValue: "<runtime-path>";
|
|
5217
|
+
}];
|
|
5218
|
+
readonly subcommands: readonly [];
|
|
5153
5219
|
}];
|
|
5154
5220
|
}, {
|
|
5155
5221
|
readonly name: "signals";
|
|
@@ -18691,7 +18757,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
18691
18757
|
readonly subcommands: readonly [];
|
|
18692
18758
|
}, {
|
|
18693
18759
|
readonly name: "jobs";
|
|
18694
|
-
readonly description: "Show, schedule, or remove a bot's recurring jobs (
|
|
18760
|
+
readonly description: "Show, schedule, reschedule, or remove a bot's recurring jobs (they run on this computer while the bot runs)";
|
|
18695
18761
|
readonly aliases: readonly [];
|
|
18696
18762
|
readonly hidden: false;
|
|
18697
18763
|
readonly helpOption: true;
|
|
@@ -18728,6 +18794,10 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
18728
18794
|
readonly flags: "--rm <jobId>";
|
|
18729
18795
|
readonly description: "Remove a scheduled job";
|
|
18730
18796
|
readonly hidden: false;
|
|
18797
|
+
}, {
|
|
18798
|
+
readonly flags: "--reschedule <jobId>";
|
|
18799
|
+
readonly description: "Change a job's schedule (with --every or --cron)";
|
|
18800
|
+
readonly hidden: false;
|
|
18731
18801
|
}, {
|
|
18732
18802
|
readonly flags: "--json";
|
|
18733
18803
|
readonly description: "Print machine-readable JSON";
|
|
@@ -6243,6 +6243,86 @@ export const COMMAND_CATALOG = [
|
|
|
6243
6243
|
}
|
|
6244
6244
|
],
|
|
6245
6245
|
"subcommands": []
|
|
6246
|
+
},
|
|
6247
|
+
{
|
|
6248
|
+
"name": "runs",
|
|
6249
|
+
"description": "Show the hot-channel run log: sources landed and signals generated per adapter",
|
|
6250
|
+
"aliases": [],
|
|
6251
|
+
"hidden": false,
|
|
6252
|
+
"helpOption": true,
|
|
6253
|
+
"helpCommand": false,
|
|
6254
|
+
"usage": "[options]",
|
|
6255
|
+
"helpText": [],
|
|
6256
|
+
"arguments": [],
|
|
6257
|
+
"options": [
|
|
6258
|
+
{
|
|
6259
|
+
"flags": "--today",
|
|
6260
|
+
"description": "Show today's runs (UTC); the default",
|
|
6261
|
+
"hidden": false
|
|
6262
|
+
},
|
|
6263
|
+
{
|
|
6264
|
+
"flags": "--date <YYYY-MM-DD>",
|
|
6265
|
+
"description": "Show runs for this UTC day",
|
|
6266
|
+
"hidden": false
|
|
6267
|
+
},
|
|
6268
|
+
{
|
|
6269
|
+
"flags": "--company <slug>",
|
|
6270
|
+
"description": "Company slug (defaults to .hq/config.json activeCompany)",
|
|
6271
|
+
"hidden": false
|
|
6272
|
+
},
|
|
6273
|
+
{
|
|
6274
|
+
"flags": "--email <address>",
|
|
6275
|
+
"description": "Whose runs to show (defaults to the signed-in person)",
|
|
6276
|
+
"hidden": false
|
|
6277
|
+
},
|
|
6278
|
+
{
|
|
6279
|
+
"flags": "--format <fmt>",
|
|
6280
|
+
"description": "Output format: table | json (default: table for TTY, json piped)",
|
|
6281
|
+
"hidden": false
|
|
6282
|
+
},
|
|
6283
|
+
{
|
|
6284
|
+
"flags": "--hq-root <path>",
|
|
6285
|
+
"description": "Local HQ tree root (default: <runtime-path>)",
|
|
6286
|
+
"hidden": false,
|
|
6287
|
+
"defaultValue": "<runtime-path>"
|
|
6288
|
+
}
|
|
6289
|
+
],
|
|
6290
|
+
"subcommands": []
|
|
6291
|
+
},
|
|
6292
|
+
{
|
|
6293
|
+
"name": "sync",
|
|
6294
|
+
"description": "Run every hot channel now: each adapter once per connection you can use",
|
|
6295
|
+
"aliases": [],
|
|
6296
|
+
"hidden": false,
|
|
6297
|
+
"helpOption": true,
|
|
6298
|
+
"helpCommand": false,
|
|
6299
|
+
"usage": "[options]",
|
|
6300
|
+
"helpText": [],
|
|
6301
|
+
"arguments": [],
|
|
6302
|
+
"options": [
|
|
6303
|
+
{
|
|
6304
|
+
"flags": "--adapter <id>",
|
|
6305
|
+
"description": "Run one adapter only (a catalog provider or adapter id)",
|
|
6306
|
+
"hidden": false
|
|
6307
|
+
},
|
|
6308
|
+
{
|
|
6309
|
+
"flags": "--company <slug>",
|
|
6310
|
+
"description": "Company slug (defaults to .hq/config.json activeCompany)",
|
|
6311
|
+
"hidden": false
|
|
6312
|
+
},
|
|
6313
|
+
{
|
|
6314
|
+
"flags": "--json",
|
|
6315
|
+
"description": "Print the run report as JSON",
|
|
6316
|
+
"hidden": false
|
|
6317
|
+
},
|
|
6318
|
+
{
|
|
6319
|
+
"flags": "--hq-root <path>",
|
|
6320
|
+
"description": "Local HQ tree root (default: <runtime-path>)",
|
|
6321
|
+
"hidden": false,
|
|
6322
|
+
"defaultValue": "<runtime-path>"
|
|
6323
|
+
}
|
|
6324
|
+
],
|
|
6325
|
+
"subcommands": []
|
|
6246
6326
|
}
|
|
6247
6327
|
]
|
|
6248
6328
|
},
|
|
@@ -22774,7 +22854,7 @@ export const COMMAND_CATALOG = [
|
|
|
22774
22854
|
},
|
|
22775
22855
|
{
|
|
22776
22856
|
"name": "jobs",
|
|
22777
|
-
"description": "Show, schedule, or remove a bot's recurring jobs (
|
|
22857
|
+
"description": "Show, schedule, reschedule, or remove a bot's recurring jobs (they run on this computer while the bot runs)",
|
|
22778
22858
|
"aliases": [],
|
|
22779
22859
|
"hidden": false,
|
|
22780
22860
|
"helpOption": true,
|
|
@@ -22820,6 +22900,11 @@ export const COMMAND_CATALOG = [
|
|
|
22820
22900
|
"description": "Remove a scheduled job",
|
|
22821
22901
|
"hidden": false
|
|
22822
22902
|
},
|
|
22903
|
+
{
|
|
22904
|
+
"flags": "--reschedule <jobId>",
|
|
22905
|
+
"description": "Change a job's schedule (with --every or --cron)",
|
|
22906
|
+
"hidden": false
|
|
22907
|
+
},
|
|
22823
22908
|
{
|
|
22824
22909
|
"flags": "--json",
|
|
22825
22910
|
"description": "Print machine-readable JSON",
|
|
@@ -5,17 +5,17 @@
|
|
|
5
5
|
* hq bot jobs <botName> --add "<what to do>" --every "<plain english>"
|
|
6
6
|
* hq bot jobs <botName> --add "<what to do>" --cron "0 9 * * 1-5"
|
|
7
7
|
* hq bot jobs <botName> --rm <jobId>
|
|
8
|
+
* hq bot jobs <botName> --reschedule <jobId> --every "<plain english>"
|
|
8
9
|
*
|
|
9
|
-
* The schedule lives
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* a
|
|
13
|
-
*
|
|
14
|
-
* The routes are self-only per agent, so every call uses the BOT's own machine
|
|
15
|
-
* identity — the same credential path its turns use — not the owner's session.
|
|
10
|
+
* The schedule lives with the bot: its jobs sit in the bot's own folder
|
|
11
|
+
* (lib/bot/local-jobs.ts) and the bot's inbox loop fires them while it runs
|
|
12
|
+
* (see lib/bot/job-run.ts). Nothing here writes a crontab, a launchd plist, or
|
|
13
|
+
* a systemd unit — by rule. Schedules are still written in the EventBridge form
|
|
14
|
+
* and held to the same limits, so a job reads the same wherever it runs.
|
|
16
15
|
*/
|
|
17
16
|
import type { Command } from "commander";
|
|
18
|
-
import {
|
|
17
|
+
import { type LocalJobsApi } from "../lib/bot/local-jobs.js";
|
|
18
|
+
import { type BotJobRecord, type CreateJobInput } from "../lib/bot/api.js";
|
|
19
19
|
import { type BotConfig, type BotKind } from "../lib/bot/config.js";
|
|
20
20
|
/** Mirrors hq-pro-agents `MIN_RECURRING_INTERVAL_SECONDS`. */
|
|
21
21
|
export declare const MIN_RECURRING_INTERVAL_SECONDS: number;
|
|
@@ -100,7 +100,7 @@ export interface BotJobsDeps {
|
|
|
100
100
|
};
|
|
101
101
|
botTokenSupplier(dir: string): () => Promise<string>;
|
|
102
102
|
/** Test seam: the jobs client for a bot directory. */
|
|
103
|
-
apiFor?(dir: string):
|
|
103
|
+
apiFor?(dir: string): LocalJobsApi;
|
|
104
104
|
parseSchedule?: typeof parseScheduleText;
|
|
105
105
|
timezone?: () => string;
|
|
106
106
|
out?: (line: string) => void;
|
|
@@ -113,6 +113,8 @@ export interface BotJobsOptions {
|
|
|
113
113
|
cron?: string;
|
|
114
114
|
tz?: string;
|
|
115
115
|
rm?: string;
|
|
116
|
+
/** Change an existing job's schedule, keeping its id, instruction and history. */
|
|
117
|
+
reschedule?: string;
|
|
116
118
|
json?: boolean;
|
|
117
119
|
quiet?: boolean;
|
|
118
120
|
}
|
|
@@ -5,14 +5,13 @@
|
|
|
5
5
|
* hq bot jobs <botName> --add "<what to do>" --every "<plain english>"
|
|
6
6
|
* hq bot jobs <botName> --add "<what to do>" --cron "0 9 * * 1-5"
|
|
7
7
|
* hq bot jobs <botName> --rm <jobId>
|
|
8
|
+
* hq bot jobs <botName> --reschedule <jobId> --every "<plain english>"
|
|
8
9
|
*
|
|
9
|
-
* The schedule lives
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* a
|
|
13
|
-
*
|
|
14
|
-
* The routes are self-only per agent, so every call uses the BOT's own machine
|
|
15
|
-
* identity — the same credential path its turns use — not the owner's session.
|
|
10
|
+
* The schedule lives with the bot: its jobs sit in the bot's own folder
|
|
11
|
+
* (lib/bot/local-jobs.ts) and the bot's inbox loop fires them while it runs
|
|
12
|
+
* (see lib/bot/job-run.ts). Nothing here writes a crontab, a launchd plist, or
|
|
13
|
+
* a systemd unit — by rule. Schedules are still written in the EventBridge form
|
|
14
|
+
* and held to the same limits, so a job reads the same wherever it runs.
|
|
16
15
|
*/
|
|
17
16
|
import chalk from "chalk";
|
|
18
17
|
import { localJobsClient } from "../lib/bot/local-jobs.js";
|
|
@@ -365,12 +364,56 @@ export async function runBotJobs(nameArg, opts, deps) {
|
|
|
365
364
|
client = deps.apiFor ? deps.apiFor(dir) : localJobsClient(dir);
|
|
366
365
|
return client;
|
|
367
366
|
};
|
|
368
|
-
if (opts.add
|
|
369
|
-
refuse("Add
|
|
367
|
+
if ([opts.add, opts.rm, opts.reschedule].filter((value) => value !== undefined).length > 1) {
|
|
368
|
+
refuse("Add, remove, or reschedule one job at a time, not several in the same command.");
|
|
370
369
|
}
|
|
371
370
|
if (opts.quiet && opts.add === undefined) {
|
|
372
371
|
refuse("Use --quiet with --add to set a scheduled job's delivery option.");
|
|
373
372
|
}
|
|
373
|
+
// Shared by --add and --reschedule: one schedule from --every or --cron, in
|
|
374
|
+
// the stored form, held to the same floor.
|
|
375
|
+
const scheduleFromOptions = () => {
|
|
376
|
+
if (!opts.every && !opts.cron) {
|
|
377
|
+
refuse(`Say how often it should run: --every "every weekday at 9am", or --cron "0 9 * * 1-5".`);
|
|
378
|
+
}
|
|
379
|
+
if (opts.every && opts.cron)
|
|
380
|
+
refuse("Use --every or --cron, not both.");
|
|
381
|
+
const timezone = opts.tz?.trim() || (deps.timezone ?? machineTimezone)();
|
|
382
|
+
// An unknown zone would throw RangeError on every inbox poll; refuse it now.
|
|
383
|
+
try {
|
|
384
|
+
new Intl.DateTimeFormat("en-US", { timeZone: timezone });
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
refuse(`"${timezone}" is not a known IANA time zone (for example America/New_York or Europe/London).`);
|
|
388
|
+
}
|
|
389
|
+
const parse = deps.parseSchedule ?? parseScheduleText;
|
|
390
|
+
return opts.every ? parse(opts.every, { timezone }) : { cron: opts.cron, timezone };
|
|
391
|
+
};
|
|
392
|
+
if (opts.reschedule !== undefined) {
|
|
393
|
+
const jobId = opts.reschedule.trim();
|
|
394
|
+
if (!jobId)
|
|
395
|
+
refuse("Say which job to reschedule: --reschedule <jobId>");
|
|
396
|
+
const schedule = scheduleFromOptions();
|
|
397
|
+
const cron = toEventBridgeCron(schedule.cron);
|
|
398
|
+
assertScheduleWithinFloor(cron);
|
|
399
|
+
const api = await jobsApi();
|
|
400
|
+
if (!api.rescheduleJob)
|
|
401
|
+
refuse(`${name}'s jobs cannot be rescheduled in place. Remove the job and add it again.`);
|
|
402
|
+
let updated;
|
|
403
|
+
try {
|
|
404
|
+
updated = await api.rescheduleJob(config.agentUid, jobId, { cron, timezone: schedule.timezone });
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
refuse(jobsErrorMessage(err, ctx));
|
|
408
|
+
}
|
|
409
|
+
if (opts.json) {
|
|
410
|
+
out(JSON.stringify({ ok: true, job: updated }, null, 2));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const words = schedule.human || describeSchedule(updated);
|
|
414
|
+
out(chalk.green(`Rescheduled ${updated.jobId}: ${name} now runs it ${words}.`));
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
374
417
|
if (opts.rm !== undefined) {
|
|
375
418
|
const jobId = opts.rm.trim();
|
|
376
419
|
if (!jobId)
|
|
@@ -389,16 +432,7 @@ export async function runBotJobs(nameArg, opts, deps) {
|
|
|
389
432
|
if (opts.quiet && !(await quietOptionEnabled())) {
|
|
390
433
|
refuse(`--quiet requires the ${BOT_JOB_QUIET_OPTION_FLAG} flag, which is currently disabled.`);
|
|
391
434
|
}
|
|
392
|
-
|
|
393
|
-
refuse(`Say how often it should run: --every "every weekday at 9am", or --cron "0 9 * * 1-5".`);
|
|
394
|
-
}
|
|
395
|
-
if (opts.every && opts.cron)
|
|
396
|
-
refuse("Use --every or --cron, not both.");
|
|
397
|
-
const timezone = opts.tz?.trim() || (deps.timezone ?? machineTimezone)();
|
|
398
|
-
const parse = deps.parseSchedule ?? parseScheduleText;
|
|
399
|
-
const schedule = opts.every
|
|
400
|
-
? parse(opts.every, { timezone })
|
|
401
|
-
: { cron: opts.cron, timezone };
|
|
435
|
+
const schedule = scheduleFromOptions();
|
|
402
436
|
const body = buildCreateJobBody({ prompt: opts.add, cron: schedule.cron, timezone: schedule.timezone, quiet: opts.quiet });
|
|
403
437
|
const api = await jobsApi();
|
|
404
438
|
let created;
|
|
@@ -412,7 +446,7 @@ export async function runBotJobs(nameArg, opts, deps) {
|
|
|
412
446
|
out(JSON.stringify({ ok: true, job: created }, null, 2));
|
|
413
447
|
return;
|
|
414
448
|
}
|
|
415
|
-
const words =
|
|
449
|
+
const words = schedule.human || describeSchedule(created);
|
|
416
450
|
out(chalk.green(`Scheduled ${created.jobId}: ${name} will ${body.prompt.replace(/\s+/g, " ").slice(0, 120)} — ${words}.`));
|
|
417
451
|
out(chalk.gray(`It runs on this computer while ${name} is running. If a run is due while the computer is asleep or ` +
|
|
418
452
|
`${name} is stopped, it runs once, as a catch-up, the next time ${name} is running.`));
|
|
@@ -432,13 +466,14 @@ export async function runBotJobs(nameArg, opts, deps) {
|
|
|
432
466
|
export function registerBotJobsCommand(bot, deps) {
|
|
433
467
|
bot
|
|
434
468
|
.command("jobs <name>")
|
|
435
|
-
.description("Show, schedule, or remove a bot's recurring jobs (
|
|
469
|
+
.description("Show, schedule, reschedule, or remove a bot's recurring jobs (they run on this computer while the bot runs)")
|
|
436
470
|
.option("--add <what>", "Schedule new work, in plain language")
|
|
437
471
|
.option("--every <when>", 'How often, in plain language (e.g. "every weekday at 9am")')
|
|
438
472
|
.option("--cron <expr>", 'How often, as a cron expression (e.g. "0 9 * * 1-5")')
|
|
439
473
|
.option("--tz <zone>", "IANA timezone for the schedule (default: this computer's)")
|
|
440
474
|
.option("--quiet", "Skip owner DMs after successful runs (requires rollout flag)")
|
|
441
475
|
.option("--rm <jobId>", "Remove a scheduled job")
|
|
476
|
+
.option("--reschedule <jobId>", "Change a job's schedule (with --every or --cron)")
|
|
442
477
|
.option("--json", "Print machine-readable JSON")
|
|
443
478
|
.action(async (name, opts) => {
|
|
444
479
|
try {
|
package/dist/commands/bot.d.ts
CHANGED
|
@@ -379,4 +379,12 @@ export interface RemoteBotRow {
|
|
|
379
379
|
ownerUid: string | null;
|
|
380
380
|
}
|
|
381
381
|
export declare function registerBotCommand(program: Command): void;
|
|
382
|
+
/**
|
|
383
|
+
* The company a bot's hot-signal-sync job syncs: the bot's own company, else
|
|
384
|
+
* the first company it serves. Null = none, and no job is registered. There is
|
|
385
|
+
* no fallback to the HQ's active company: a personal bot syncs a company only
|
|
386
|
+
* when it was given one explicitly (`--company`), so the job never writes into
|
|
387
|
+
* a vault the bot does not belong to.
|
|
388
|
+
*/
|
|
389
|
+
export declare function hotSignalSyncCompany(config: Pick<BotConfig, "companySlug" | "kind" | "companies" | "workerId">): string | null;
|
|
382
390
|
//# sourceMappingURL=bot.d.ts.map
|
package/dist/commands/bot.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { withLocalJobs } from "../lib/bot/local-jobs.js";
|
|
2
|
+
import { currentHotSignalSyncCompany, ensureHotSignalSyncJob, removeStaleHotSignalSyncJobs, runHotSignalSyncJob } from "../lib/bot/jobs/hot-signal-sync.js";
|
|
3
|
+
import { createClaudeSignalsPass } from "../lib/hot-channels/signals-pass.js";
|
|
4
|
+
import { machineTimeZone } from "../lib/bot/job-run.js";
|
|
5
|
+
import { runCompanyHotChannelSync } from "./sources.js";
|
|
2
6
|
import { OwnerContextCache, lookupOwnerContext } from "../lib/bot/owner-context.js";
|
|
3
7
|
import { hasActivePromotion } from "../lib/bot/promotion-receipt.js";
|
|
4
8
|
import { readInflightTurns } from "../lib/bot/inflight.js";
|
|
@@ -1274,6 +1278,27 @@ async function runBotResident(nameArg) {
|
|
|
1274
1278
|
const token = botTokenSupplier(dir);
|
|
1275
1279
|
// The bot's own schedule lives in its folder and fires from this loop.
|
|
1276
1280
|
const api = withLocalJobs(new BotApi({ token }), dir);
|
|
1281
|
+
// The built-in hot-signal-sync job, registered once in the bot's own
|
|
1282
|
+
// jobs.json (06:30 daily in this computer's time zone until the owner
|
|
1283
|
+
// reschedules it with `hq bot jobs`). It runs in-process, with no model turn.
|
|
1284
|
+
const syncCompany = hotSignalSyncCompany(config);
|
|
1285
|
+
try {
|
|
1286
|
+
// A job for a company the bot no longer serves is removed before anything runs.
|
|
1287
|
+
for (const stale of removeStaleHotSignalSyncJobs(dir, syncCompany)) {
|
|
1288
|
+
console.log(`Removed the hot-channel sync job ${stale.jobId}: this bot no longer syncs that company.`);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
catch (err) {
|
|
1292
|
+
console.error(`Could not check the hot-channel sync jobs: ${err instanceof Error ? err.message : String(err)}`);
|
|
1293
|
+
}
|
|
1294
|
+
if (syncCompany) {
|
|
1295
|
+
try {
|
|
1296
|
+
ensureHotSignalSyncJob(dir, { company: syncCompany, timezone: machineTimeZone() });
|
|
1297
|
+
}
|
|
1298
|
+
catch (err) {
|
|
1299
|
+
console.error(`Could not register the hot-channel sync job for ${syncCompany}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1277
1302
|
const owner = new OwnerContextCache(() => lookupOwnerContext(config.ownerUid), Date.now, undefined, config.ownerUid);
|
|
1278
1303
|
const handle = await runBot({
|
|
1279
1304
|
dir,
|
|
@@ -1281,6 +1306,15 @@ async function runBotResident(nameArg) {
|
|
|
1281
1306
|
api,
|
|
1282
1307
|
runtime: runtimeFor(config.runtime),
|
|
1283
1308
|
ownerContext: () => owner.get(),
|
|
1309
|
+
hotSignalSync: ({ company, catchUp, now, sendAlert }) => runHotSignalSyncJob({
|
|
1310
|
+
company,
|
|
1311
|
+
catchUp,
|
|
1312
|
+
now,
|
|
1313
|
+
sendAlert,
|
|
1314
|
+
sync: (input) => runCompanyHotChannelSync({ hqRoot: config.hqRoot, ...input }),
|
|
1315
|
+
// A new pass per run: the login preflight and the 20-document cap are per run.
|
|
1316
|
+
signalsPass: createClaudeSignalsPass({ hqRoot: config.hqRoot, company }),
|
|
1317
|
+
}),
|
|
1284
1318
|
// US-005: decision picks in the owner DM, with the bot's own machine token
|
|
1285
1319
|
// (never the owner's login).
|
|
1286
1320
|
decisions: new DecisionsClient(token),
|
|
@@ -2641,4 +2675,14 @@ export function registerBotCommand(program) {
|
|
|
2641
2675
|
console.log(opts.json ? JSON.stringify({ ok: true, ...r }, null, 2) : r.message);
|
|
2642
2676
|
})());
|
|
2643
2677
|
}
|
|
2678
|
+
/**
|
|
2679
|
+
* The company a bot's hot-signal-sync job syncs: the bot's own company, else
|
|
2680
|
+
* the first company it serves. Null = none, and no job is registered. There is
|
|
2681
|
+
* no fallback to the HQ's active company: a personal bot syncs a company only
|
|
2682
|
+
* when it was given one explicitly (`--company`), so the job never writes into
|
|
2683
|
+
* a vault the bot does not belong to.
|
|
2684
|
+
*/
|
|
2685
|
+
export function hotSignalSyncCompany(config) {
|
|
2686
|
+
return currentHotSignalSyncCompany(config);
|
|
2687
|
+
}
|
|
2644
2688
|
//# sourceMappingURL=bot.js.map
|
|
@@ -59,6 +59,8 @@ export interface DaemonCommandDeps {
|
|
|
59
59
|
/** Reads stdin for the one-shot daemon machine login; never reads env or argv credentials. */
|
|
60
60
|
readStdin?: () => Promise<string>;
|
|
61
61
|
windowsUser?: string;
|
|
62
|
+
/** The Windows user's SID; the daemon's Scheduled Task name carries it so each user on a PC has their own task. */
|
|
63
|
+
windowsSid?: string;
|
|
62
64
|
windowsHeadlessLauncher?: string;
|
|
63
65
|
systemUnitDir?: string;
|
|
64
66
|
systemRecordPath?: string;
|
package/dist/commands/daemon.js
CHANGED
|
@@ -24,7 +24,7 @@ import { DEFAULT_SERVICES, SERVICE_DESCRIPTIONS, enabledServices, hqCloudUnits }
|
|
|
24
24
|
import { applyServiceToggle, effectiveSyncOwner, readDaemonConfig, writeDaemonConfig } from "../lib/daemon/config.js";
|
|
25
25
|
import { daemonUnitInstalled } from "../lib/daemon/handoff.js";
|
|
26
26
|
import { adoptLegacyUnits, readAdoptedUnits, restoreLegacyUnits } from "../lib/daemon/migrate.js";
|
|
27
|
-
import {
|
|
27
|
+
import { ROOT_REFUSAL, SYSTEM_UNIT_DIR, WINDOWS_START_WAIT_MS, daemonRootRefusal, installDaemonUnit, installScopeFor, classifyWindowsTask, legacyWindowsTask, readWindowsSid, uninstallDaemonUnit, windowsInteractiveSession, windowsTaskName, } from "../lib/daemon/install.js";
|
|
28
28
|
import { describeLegacyUnit, findLegacyUnits } from "../lib/daemon/legacy.js";
|
|
29
29
|
import { isDaemonProcess, liveDaemonLock, runningDaemonPid } from "../lib/daemon/lock.js";
|
|
30
30
|
import { bundledHqCloud, managedHqCloud, managedHqCloudRoot, readHqCloudCopy, readHqCloudUpdateState } from "../lib/daemon/hq-cloud.js";
|
|
@@ -132,7 +132,10 @@ async function loadDefaults(over) {
|
|
|
132
132
|
existsSync: fs.existsSync,
|
|
133
133
|
listDir: (dir) => fs.readdirSync(dir),
|
|
134
134
|
launchdJobLoaded: (label) => run("launchctl", ["print", `gui/${process.getuid?.() ?? 0}/${label}`]).status === 0,
|
|
135
|
-
|
|
135
|
+
windowsTaskOwner: (task) => classifyWindowsTask(run, task, {
|
|
136
|
+
windowsSid: over.windowsSid ?? readWindowsSid(run),
|
|
137
|
+
windowsUser: over.windowsUser ?? mesh.resolveWindowsUser(env),
|
|
138
|
+
}).state,
|
|
136
139
|
})),
|
|
137
140
|
resolveHqRoot: over.resolveHqRoot ?? ((flag) => resolveDaemonHqRoot(flag, { env, cwd: process.cwd(), home })),
|
|
138
141
|
userHome: over.userHome ?? ((user) => loginHome(user, run)),
|
|
@@ -140,6 +143,7 @@ async function loadDefaults(over) {
|
|
|
140
143
|
sleep: over.sleep,
|
|
141
144
|
interactiveSession: over.interactiveSession,
|
|
142
145
|
windowsUser: over.windowsUser ?? (platform === "win32" ? mesh.resolveWindowsUser(env) : ""),
|
|
146
|
+
windowsSid: over.windowsSid ?? (platform === "win32" ? readWindowsSid(run) : undefined),
|
|
143
147
|
windowsHeadlessLauncher: over.windowsHeadlessLauncher ?? (platform === "win32" ? mesh.resolveWindowsHeadlessLauncher(env) : undefined),
|
|
144
148
|
systemUnitDir: over.systemUnitDir ?? SYSTEM_UNIT_DIR,
|
|
145
149
|
syncModeClient: over.syncModeClient,
|
|
@@ -196,6 +200,7 @@ function hostFor(d, paths, pathEnv) {
|
|
|
196
200
|
uid: d.uid,
|
|
197
201
|
isRoot: d.isRoot,
|
|
198
202
|
windowsUser: d.windowsUser ?? "",
|
|
203
|
+
windowsSid: d.windowsSid,
|
|
199
204
|
windowsHeadlessLauncher: d.windowsHeadlessLauncher,
|
|
200
205
|
systemUnitDir: d.systemUnitDir ?? SYSTEM_UNIT_DIR,
|
|
201
206
|
env,
|
|
@@ -306,6 +311,8 @@ async function runInstall(d, opts) {
|
|
|
306
311
|
runningDaemon: () => liveDaemonLock(paths.lockPath, d.daemonAlive),
|
|
307
312
|
});
|
|
308
313
|
handOver(d, { paths, detected, runAsUser });
|
|
314
|
+
if (result.notice)
|
|
315
|
+
d.out(result.notice);
|
|
309
316
|
if (result.warning)
|
|
310
317
|
d.err(result.warning);
|
|
311
318
|
if (result.kind === "manual") {
|
|
@@ -333,7 +340,7 @@ async function runInstall(d, opts) {
|
|
|
333
340
|
? `Installed ${result.unitPath}, but the daemon did not start within ${seconds} seconds although you are signed in to Windows.`
|
|
334
341
|
: `Installed ${result.unitPath}, but the daemon did not start within ${seconds} seconds, and HQ could not check whether you are signed in to Windows.`,
|
|
335
342
|
"Check it with: hq daemon status",
|
|
336
|
-
`The task's last result: powershell -NoProfile -Command "(Get-ScheduledTaskInfo -TaskName '${
|
|
343
|
+
`The task's last result: powershell -NoProfile -Command "(Get-ScheduledTaskInfo -TaskName '${windowsTaskName({ windowsSid: d.windowsSid, windowsUser: d.windowsUser ?? "" })}').LastTaskResult"`,
|
|
337
344
|
].join("\n"));
|
|
338
345
|
}
|
|
339
346
|
if (!result.running) {
|
|
@@ -386,6 +393,8 @@ async function runUninstall(d, opts) {
|
|
|
386
393
|
}
|
|
387
394
|
const result = uninstallDaemonUnit(hostFor(d, paths, false), { scope: installScopeFor(detected.type) }, { run: d.run });
|
|
388
395
|
d.out(result.removed ? `Removed ${result.unitPath}` : "hq daemon was not installed.");
|
|
396
|
+
if (result.notice)
|
|
397
|
+
d.out(result.notice);
|
|
389
398
|
if (result.warning)
|
|
390
399
|
d.err(result.warning);
|
|
391
400
|
if (opts.restoreLegacy) {
|
|
@@ -869,7 +878,9 @@ async function runMigrate(d, opts) {
|
|
|
869
878
|
platform: d.platform,
|
|
870
879
|
home: d.home,
|
|
871
880
|
systemUnitDir: d.systemUnitDir,
|
|
881
|
+
windowsTaskName: windowsTaskName({ windowsSid: d.windowsSid, windowsUser: d.windowsUser ?? "" }),
|
|
872
882
|
windowsTaskExists: (task) => d.run("schtasks.exe", ["/Query", "/TN", task]).status === 0,
|
|
883
|
+
windowsLegacyTaskIsMine: () => legacyWindowsTask(d.run, { windowsSid: d.windowsSid, windowsUser: d.windowsUser ?? "" }).state === "mine",
|
|
873
884
|
});
|
|
874
885
|
if (!installed) {
|
|
875
886
|
throw new DaemonCommandError("hq daemon is not installed here yet. Run `hq daemon install` first, then `hq daemon migrate --apply`.");
|
|
@@ -893,7 +904,7 @@ async function runMigrate(d, opts) {
|
|
|
893
904
|
if (u.migratable)
|
|
894
905
|
d.out(`Would take over ${u.unitId} from ${describeLegacyUnit(u)}${u.path ? ` (${u.path})` : ""}`);
|
|
895
906
|
else
|
|
896
|
-
d.out(`Leaving ${u.unitId} to ${describeLegacyUnit(u)} (owned by the HQ desktop app)`);
|
|
907
|
+
d.out(`Leaving ${u.unitId} to ${describeLegacyUnit(u)} (${u.notMineReason ?? "owned by the HQ desktop app"})`);
|
|
897
908
|
}
|
|
898
909
|
d.out("Nothing was changed. `hq daemon migrate --apply` takes them over.");
|
|
899
910
|
}
|
|
@@ -299,6 +299,8 @@ export interface UsableApp {
|
|
|
299
299
|
* provider, else `--connection <id>`.
|
|
300
300
|
*/
|
|
301
301
|
selector: string;
|
|
302
|
+
/** The caller created this connection (`viewerAccess.isCreator`). */
|
|
303
|
+
ownedByCaller: boolean;
|
|
302
304
|
}
|
|
303
305
|
export interface UsableAppsResult {
|
|
304
306
|
companyUid: string;
|
|
@@ -529,6 +529,7 @@ export async function fetchUsableApps(token, companyUid) {
|
|
|
529
529
|
status: row.status,
|
|
530
530
|
providerArg,
|
|
531
531
|
selector: shared ? `--connection ${row.id}` : `--provider ${providerArg}`,
|
|
532
|
+
ownedByCaller: row.viewerAccess?.isCreator === true,
|
|
532
533
|
};
|
|
533
534
|
})
|
|
534
535
|
.sort((a, b) => compareNames(a.name, b.name))
|