@indigoai-us/hq-cli 5.345.187 → 5.345.189
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 +111 -0
- package/dist/command-catalog.generated.d.ts +34 -0
- package/dist/command-catalog.generated.js +42 -0
- package/dist/commands/agent-inbox.js +43 -11
- package/dist/commands/agent-mcp.js +5 -2
- package/dist/commands/bot.js +15 -3
- package/dist/commands/core-company-index-scope-flag.d.ts +2 -1
- package/dist/commands/core-company-index-scope-flag.js +2 -1
- package/dist/commands/core.js +0 -5
- package/dist/commands/dm-attach.d.ts +73 -0
- package/dist/commands/dm-attach.js +104 -0
- package/dist/commands/dm.d.ts +10 -0
- package/dist/commands/dm.js +114 -2
- package/dist/commands/sources.js +15 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +57330 -63803
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/inbox-fetch.d.ts +92 -0
- package/dist/lib/agent-kit/inbox-fetch.js +135 -0
- package/dist/lib/agent-kit/mcp/tools.d.ts +11 -2
- package/dist/lib/agent-kit/mcp/tools.js +17 -3
- package/dist/lib/bot/fleet-local.d.ts +3 -1
- package/dist/lib/bot/fleet-local.js +9 -3
- package/dist/lib/bot/fleet-python.d.ts +6 -0
- package/dist/lib/bot/fleet-python.js +24 -5
- package/dist/lib/bot/probe.d.ts +7 -1
- package/dist/lib/bot/probe.js +11 -3
- package/dist/lib/bot/run.d.ts +9 -0
- package/dist/lib/bot/run.js +114 -8
- package/dist/lib/bot/runtime-failure.d.ts +3 -0
- package/dist/lib/bot/runtime-failure.js +20 -2
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/index-render/session-scope.d.ts +3 -3
- package/dist/lib/index-render/session-scope.js +11 -9
- package/dist/lib/index-render/shared.d.ts +6 -4
- package/dist/lib/index-render/shared.js +33 -4
- package/dist/utils/cognito-session.d.ts +5 -3
- package/dist/utils/cognito-session.js +43 -46
- package/dist/utils/hq-root-resolver.d.ts +27 -1
- package/dist/utils/hq-root-resolver.js +65 -33
- package/package.json +15 -15
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,117 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.189] — 2026-11-01
|
|
6
|
+
- A local bot that cannot reach HQ when it starts now retries the identity check up to five times
|
|
7
|
+
(waits of 5 s, 15 s, 45 s and 2 min) on a network error (connection refused, reset, DNS failure,
|
|
8
|
+
timeout), 408, 429 or 5xx. Before, one network error at startup failed the bot with exit 0, which
|
|
9
|
+
launchd never restarts, so bots stayed down until someone restarted them. After the last try the bot
|
|
10
|
+
stops in the failed state with a plain reason that says to check the network and run
|
|
11
|
+
`hq bot start <name>`. Other 4xx answers, sign-in errors and internal errors fail at once, as before.
|
|
12
|
+
A stop during a retry wait ends the bot cleanly as stopped.
|
|
13
|
+
- Hosted agent Docker image now runs on Node 26 (was Node 22).
|
|
14
|
+
- Updated 16 bundled libraries to their latest minor and patch releases. The local search engine stays on its current version so installing HQ still works on machines without build tools.
|
|
15
|
+
- On Windows, the CLI's last-resort HQ folder is now `C:\hq` (`%SystemDrive%\hq`) instead of
|
|
16
|
+
`%USERPROFILE%\hq`. `$HQ_ROOT`, the HQ folder you are inside, and the location recorded in
|
|
17
|
+
`~/.hq/menubar.json` or `~/.hq/config.json` still win. Discovery checks the home folders first
|
|
18
|
+
(`C:\Users\<you>\hq` and the usual variants), then `C:\hq`, then `%LOCALAPPDATA%\hq`, so an
|
|
19
|
+
existing home install keeps working and is preferred over a stray `C:\hq`.
|
|
20
|
+
macOS and Linux are unchanged.
|
|
21
|
+
- `hq bot create` no longer hangs before it reaches the server. The Fleet
|
|
22
|
+
Runtime setup steps (`gh auth token`, `tar`, `uv python install`,
|
|
23
|
+
`uv python find`) now time out and report "timed out after Ns".
|
|
24
|
+
- Under `--json`, any create failure prints a `{ ok: false, reason:
|
|
25
|
+
"create-failed" }` document on stdout, so HQ Desktop can show it.
|
|
26
|
+
- `hq bot probe` and bot failure messages now recognize a coding tool's plan
|
|
27
|
+
usage limit (for example Claude Code's "You've hit your weekly limit · resets
|
|
28
|
+
9pm"). They used to call it `unknown`. It is now classed `transient`, the
|
|
29
|
+
probe's JSON gains `usageLimit: true` and `resetsAt` when the tool gives a
|
|
30
|
+
reset time, and the detail says the plan hit its limit and when it resets.
|
|
31
|
+
- `hq dm attachment <eventId|vaultPath>` downloads a file attached to a DM.
|
|
32
|
+
- The recipient can now open a file held in the sender's vault.
|
|
33
|
+
- The sender can open its own attachment.
|
|
34
|
+
- Several files with the same name on one message are saved as
|
|
35
|
+
name.ext, name-1.ext, and so on.
|
|
36
|
+
- Every destination is checked before any file is written.
|
|
37
|
+
- A company's `INDEX.md` now shows a project's own recorded status instead of listing every unfinished project as active. `hq core rebuild-index companies` reads the project's `state.json` status first, then `prd.json` `status` or `metadata.status`, and only then falls back to the story-pass check.
|
|
38
|
+
- `hq core rebuild-index companies` in a company-bound session now rebuilds only that session's companies, even when the company-scope kill switch is off. Before, it could rewrite every company's INDEX.md. `--company <slug>` works again for a company inside the bound set.
|
|
39
|
+
- `hq sources schedule --at HH:MM` now turns the daily hot-channel sync on when a company doesn't have it yet. It registers the sync on your personal setup bot and sets the time. Before this change, the command failed with "No hot-channel sync job", so the desktop's Change time couldn't start the sync.
|
|
40
|
+
- `hq-hook dispatch <EVENT>` runs a hook event the way master-hook.sh does,
|
|
41
|
+
in one process (US-007).
|
|
42
|
+
- Registry hooks come from the compiled index, through the hook-gate
|
|
43
|
+
profiles, `HQ_DISABLED_HOOKS` and the prefilters. Folder hooks run after
|
|
44
|
+
them in master-hook's order.
|
|
45
|
+
- A fail-closed PreToolUse guard that is missing, crashes or times out
|
|
46
|
+
blocks the event and names the guard. Every hook failure is recorded in
|
|
47
|
+
`~/.hq/logs/hook-engine.jsonl`.
|
|
48
|
+
- Built-in hook ports run in-process under the bridge flags; a port that
|
|
49
|
+
panics makes the bash hook run.
|
|
50
|
+
- Events the engine cannot reproduce exactly (session start, per-agent
|
|
51
|
+
scope, the timeout sentry, HQ Anywhere routing) run through
|
|
52
|
+
master-hook.sh with the same input.
|
|
53
|
+
- Nothing is registered to use it yet; exec-form registration (US-015) and
|
|
54
|
+
the cutover (US-017) come later.
|
|
55
|
+
- `hq-hook` gains a compiled hook index (US-006): one memory-mapped file per
|
|
56
|
+
HQ root and policy-directory list with the hook registry rows, the
|
|
57
|
+
`policy_vocab` prefilter vocabulary and the policy injector's parsed records.
|
|
58
|
+
- It rebuilds only when a source file's metadata changes. A symlink is
|
|
59
|
+
tracked by the file it points to, so re-linking the personal overlay does
|
|
60
|
+
not rebuild it.
|
|
61
|
+
- Each company's index holds only that company's policies.
|
|
62
|
+
- `hq-hook index records|vocab|registry` print the same bytes as the shell
|
|
63
|
+
code they replace, which the new parity test checks.
|
|
64
|
+
- `hq-hook index probe|bench` measure the per-event cost for the
|
|
65
|
+
go/no-go report. Nothing in the hook path uses the index yet; the
|
|
66
|
+
dispatcher (US-007) will.
|
|
67
|
+
- `hq-hook run block-credential-file-read` is now a full native port of the
|
|
68
|
+
bash guard and its arithmetic library, under flag
|
|
69
|
+
`hook-engine-port-block-credential-file-read`, which stays off.
|
|
70
|
+
- It answers every call itself and never exits 75 (hand back to bash).
|
|
71
|
+
- Exit code, stdout and stderr match the guard byte for byte on the guard's
|
|
72
|
+
corpus (about 1,500 payloads), 330 recorded real calls and about 6,800
|
|
73
|
+
fuzz payloads.
|
|
74
|
+
- The bridge now also pins `bash-arith-subst-guard-lib.sh`. A changed or
|
|
75
|
+
missing library sends the call to bash, as a changed guard does.
|
|
76
|
+
- `hq-hook run block-credential-file-read` is the second hook port
|
|
77
|
+
master-hook.sh can bridge (US-022, flag
|
|
78
|
+
`hook-engine-port-block-credential-file-read`, default off).
|
|
79
|
+
- It answers natively the Read and Bash calls that the guard's own fast path
|
|
80
|
+
allows and that name no credential file, pass through no symlink and use
|
|
81
|
+
no substitution.
|
|
82
|
+
- Every other call exits 75 and the bash guard decides, keeping its
|
|
83
|
+
fail-closed behavior.
|
|
84
|
+
- The port never blocks.
|
|
85
|
+
- It is pinned to the guard version it was checked against.
|
|
86
|
+
- The guard's 400-payload corpus shows zero verdict differences with the
|
|
87
|
+
port in front.
|
|
88
|
+
- `hq-hook`: the `block-credential-file-read` port accepts the guard script on
|
|
89
|
+
hq-core-staging main (9be16758, #1727). The guard's fast path gained the
|
|
90
|
+
arithmetic guard, the split-name rule, the `git show <ref>:<path>` rule and
|
|
91
|
+
an xargs/parallel prefilter term since the port was written; the port now
|
|
92
|
+
declines every command those can refuse (`$`, `((`, declaration keywords,
|
|
93
|
+
the new prefilter term) and keeps answering only what bash's fast path
|
|
94
|
+
allows. The vendored fixture and corpus are refreshed to that version.
|
|
95
|
+
- `hq-hook`: the `policy-enforcement-gate` port accepts the gate script from
|
|
96
|
+
hq-core-staging #1717 (read-only `hq dm` subcommands skipped by the outbound
|
|
97
|
+
scan). Without this pin the bridge logged `script-mismatch` on every current
|
|
98
|
+
HQ and ran bash alone. The port's allow set is unchanged: it already leaves
|
|
99
|
+
every command naming `hq` and `dm` to bash. The vendored fixture is refreshed
|
|
100
|
+
to that version and the pin test covers it.
|
|
101
|
+
|
|
102
|
+
## [5.345.188] — 2026-10-30
|
|
103
|
+
- `hq agent inbox` and the `hq_inbox_read` MCP tool now bound the server read (token mint plus GET) at 30 s
|
|
104
|
+
(`HQ_AGENT_INBOX_TIMEOUT_MS`, floor 1000) and retry it once after a short delay, honouring the server's
|
|
105
|
+
numeric `Retry-After` up to 5 s, when the token mint is held by another process, the request times out or
|
|
106
|
+
fails in transport, or the server answers 408, 425, 429, 500, 502 or 503. A 504 is left to the existing
|
|
107
|
+
single retry inside the API client, and a 401, 403, 404 or an invalid 200 body is not retried. The
|
|
108
|
+
deadline now reaches the hq-cloud mint, so a retry no longer queues behind a first mint that is still
|
|
109
|
+
waiting on another process's lock. The list command sets `HQ_MACHINE_CREDS_FILE`,
|
|
110
|
+
`HQ_REQUIRE_MACHINE_IDENTITY` and `HQ_VAULT_API_URL` before minting, as `hq agent inbox done` already did.
|
|
111
|
+
The JSON output gains `stale: true|false` next to `source`; whenever a retry happened, including one that
|
|
112
|
+
succeeded, it also carries `attempts: 2`, and a note that still reports a fallback ends with
|
|
113
|
+
"(after 2 attempts)", so automation can tell a hard failure from a single blip (DEV-4065).
|
|
114
|
+
- Keep downloaded revisions current and retry locally deleted files until the cloud confirms their removal.
|
|
115
|
+
|
|
5
116
|
## [5.345.187] — 2026-10-30
|
|
6
117
|
- The HQ Anywhere person gate now follows the desktop Settings key `hq-anywhere-opt-out`. Turning HQ Anywhere on in the app enables `hq mcp serve` without a separate `hq-anywhere` write. Older servers that only have `hq-anywhere` still work.
|
|
7
118
|
|
|
@@ -4175,6 +4175,40 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
4175
4175
|
readonly hidden: false;
|
|
4176
4176
|
}];
|
|
4177
4177
|
readonly subcommands: readonly [];
|
|
4178
|
+
}, {
|
|
4179
|
+
readonly name: "attachment";
|
|
4180
|
+
readonly description: "Download a file attached to a DM. REF is the message eventId from `hq dm inbox` (downloads every file on it) or the attachment's vault path (chat/attachments/...). The file is read from the sender's vault; only the two people in the conversation can open a DM attachment.";
|
|
4181
|
+
readonly aliases: readonly [];
|
|
4182
|
+
readonly hidden: false;
|
|
4183
|
+
readonly helpOption: true;
|
|
4184
|
+
readonly helpCommand: false;
|
|
4185
|
+
readonly usage: "[options] <ref>";
|
|
4186
|
+
readonly helpText: readonly [];
|
|
4187
|
+
readonly arguments: readonly [{
|
|
4188
|
+
readonly name: "ref";
|
|
4189
|
+
readonly required: true;
|
|
4190
|
+
readonly variadic: false;
|
|
4191
|
+
readonly description: "";
|
|
4192
|
+
}];
|
|
4193
|
+
readonly options: readonly [{
|
|
4194
|
+
readonly flags: "--out <path>";
|
|
4195
|
+
readonly description: "Where to write: a file path for one attachment, else a directory (default: current directory)";
|
|
4196
|
+
readonly hidden: false;
|
|
4197
|
+
}, {
|
|
4198
|
+
readonly flags: "--company <uid>";
|
|
4199
|
+
readonly description: "Vault scope the file lives in (the companyUid shown on the message). Derived from the DM pair when omitted";
|
|
4200
|
+
readonly hidden: false;
|
|
4201
|
+
}, {
|
|
4202
|
+
readonly flags: "--force";
|
|
4203
|
+
readonly description: "Overwrite an existing file";
|
|
4204
|
+
readonly hidden: false;
|
|
4205
|
+
}, {
|
|
4206
|
+
readonly flags: "--limit <n>";
|
|
4207
|
+
readonly description: "How many recent inbox messages to search for the eventId";
|
|
4208
|
+
readonly hidden: false;
|
|
4209
|
+
readonly defaultValue: "100";
|
|
4210
|
+
}];
|
|
4211
|
+
readonly subcommands: readonly [];
|
|
4178
4212
|
}, {
|
|
4179
4213
|
readonly name: "thread";
|
|
4180
4214
|
readonly description: "Show your two-way conversation with a person (email, personUid, or agentUid). Reads oldest-first and marks their messages read unless --no-ack.";
|
|
@@ -5059,6 +5059,48 @@ export const COMMAND_CATALOG = [
|
|
|
5059
5059
|
],
|
|
5060
5060
|
"subcommands": []
|
|
5061
5061
|
},
|
|
5062
|
+
{
|
|
5063
|
+
"name": "attachment",
|
|
5064
|
+
"description": "Download a file attached to a DM. REF is the message eventId from `hq dm inbox` (downloads every file on it) or the attachment's vault path (chat/attachments/...). The file is read from the sender's vault; only the two people in the conversation can open a DM attachment.",
|
|
5065
|
+
"aliases": [],
|
|
5066
|
+
"hidden": false,
|
|
5067
|
+
"helpOption": true,
|
|
5068
|
+
"helpCommand": false,
|
|
5069
|
+
"usage": "[options] <ref>",
|
|
5070
|
+
"helpText": [],
|
|
5071
|
+
"arguments": [
|
|
5072
|
+
{
|
|
5073
|
+
"name": "ref",
|
|
5074
|
+
"required": true,
|
|
5075
|
+
"variadic": false,
|
|
5076
|
+
"description": ""
|
|
5077
|
+
}
|
|
5078
|
+
],
|
|
5079
|
+
"options": [
|
|
5080
|
+
{
|
|
5081
|
+
"flags": "--out <path>",
|
|
5082
|
+
"description": "Where to write: a file path for one attachment, else a directory (default: current directory)",
|
|
5083
|
+
"hidden": false
|
|
5084
|
+
},
|
|
5085
|
+
{
|
|
5086
|
+
"flags": "--company <uid>",
|
|
5087
|
+
"description": "Vault scope the file lives in (the companyUid shown on the message). Derived from the DM pair when omitted",
|
|
5088
|
+
"hidden": false
|
|
5089
|
+
},
|
|
5090
|
+
{
|
|
5091
|
+
"flags": "--force",
|
|
5092
|
+
"description": "Overwrite an existing file",
|
|
5093
|
+
"hidden": false
|
|
5094
|
+
},
|
|
5095
|
+
{
|
|
5096
|
+
"flags": "--limit <n>",
|
|
5097
|
+
"description": "How many recent inbox messages to search for the eventId",
|
|
5098
|
+
"hidden": false,
|
|
5099
|
+
"defaultValue": "100"
|
|
5100
|
+
}
|
|
5101
|
+
],
|
|
5102
|
+
"subcommands": []
|
|
5103
|
+
},
|
|
5062
5104
|
{
|
|
5063
5105
|
"name": "thread",
|
|
5064
5106
|
"description": "Show your two-way conversation with a person (email, personUid, or agentUid). Reads oldest-first and marks their messages read unless --no-ack.",
|
|
@@ -8,6 +8,7 @@ import chalk from "chalk";
|
|
|
8
8
|
import * as os from "node:os";
|
|
9
9
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
10
10
|
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
11
|
+
import { annotateInboxAttempts, fetchServerInboxWithRetry, parseRetryAfterSeconds, resolveAgentInboxTimeoutMs } from "../lib/agent-kit/inbox-fetch.js";
|
|
11
12
|
import { redactAgentInboxItem } from "../lib/agent-kit/inbox-redact.js";
|
|
12
13
|
import { markInboxDone, mergeAgentInbox, readDoneIds, summarizeInboxEntry } from "../lib/agent-kit/inbox-state.js";
|
|
13
14
|
import { agentKitPaths } from "../lib/agent-kit/paths.js";
|
|
@@ -23,25 +24,56 @@ export function registerAgentInboxCommand(agent) {
|
|
|
23
24
|
const paths = agentKitPaths(os.homedir(), process.env);
|
|
24
25
|
const creds = requireExternalCreds(paths);
|
|
25
26
|
const done = readDoneIds(paths);
|
|
27
|
+
// Pin the enrolled machine identity and API, as `done` does below, so the
|
|
28
|
+
// mint cannot fall through to a person session or another API base.
|
|
29
|
+
process.env.HQ_MACHINE_CREDS_FILE = paths.machineCredsPath;
|
|
30
|
+
process.env.HQ_REQUIRE_MACHINE_IDENTITY = "1";
|
|
31
|
+
process.env.HQ_VAULT_API_URL = creds.apiBaseUrl;
|
|
26
32
|
// Merged with the server, never the local mirror alone: a dead kit
|
|
27
33
|
// poller would otherwise make this command answer confidently with
|
|
28
|
-
// yesterday's mail and no sign anything was wrong.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
// yesterday's mail and no sign anything was wrong. The server read is
|
|
35
|
+
// bounded and retried once (inbox-fetch.ts) so a shared-lock mint, a
|
|
36
|
+
// 503 with Retry-After or a hung socket does not become a fallback.
|
|
37
|
+
let attempts = 1;
|
|
38
|
+
const merged = await mergeAgentInbox(paths, async () => {
|
|
39
|
+
const outcome = await fetchServerInboxWithRetry({
|
|
40
|
+
timeoutMs: resolveAgentInboxTimeoutMs(process.env),
|
|
41
|
+
mint: (signal) => ensureCognitoToken({ tokenSource: "machine", interactive: false, signal }),
|
|
42
|
+
fetch: async (token, signal) => {
|
|
43
|
+
const res = await vaultApiFetch({
|
|
44
|
+
token,
|
|
45
|
+
baseUrl: creds.apiBaseUrl,
|
|
46
|
+
path: `/v1/agents/${encodeURIComponent(creds.entityUid)}/inbox`,
|
|
47
|
+
signal,
|
|
48
|
+
});
|
|
49
|
+
const retryAfterSeconds = parseRetryAfterSeconds(res.headers?.get?.("retry-after"));
|
|
50
|
+
// Preserve a decode failure as an invalid body for mergeAgentInbox to
|
|
51
|
+
// classify. Parser errors can contain response bytes: do not echo them.
|
|
52
|
+
return {
|
|
53
|
+
status: res.status,
|
|
54
|
+
body: await res.json().catch(() => undefined),
|
|
55
|
+
...(retryAfterSeconds !== undefined ? { retryAfterSeconds } : {}),
|
|
56
|
+
};
|
|
57
|
+
},
|
|
35
58
|
});
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
59
|
+
attempts = outcome.attempts;
|
|
60
|
+
if (!outcome.ok)
|
|
61
|
+
throw outcome.error;
|
|
62
|
+
return outcome.response;
|
|
39
63
|
});
|
|
64
|
+
const { entries, note, source, stale, attempts: attemptsMade } = annotateInboxAttempts(merged, attempts);
|
|
40
65
|
const rows = entries
|
|
41
66
|
.filter((e) => opts.all || !done.has(e.id))
|
|
42
67
|
.map((e) => summarizeInboxEntry(redactAgentInboxItem(e), done.has(e.id)));
|
|
43
68
|
if (opts.json) {
|
|
44
|
-
console.log(JSON.stringify({
|
|
69
|
+
console.log(JSON.stringify({
|
|
70
|
+
count: rows.length,
|
|
71
|
+
source,
|
|
72
|
+
stale,
|
|
73
|
+
...(attemptsMade !== undefined ? { attempts: attemptsMade } : {}),
|
|
74
|
+
messages: rows,
|
|
75
|
+
...(note ? { note } : {}),
|
|
76
|
+
}, null, 2));
|
|
45
77
|
return;
|
|
46
78
|
}
|
|
47
79
|
if (note)
|
|
@@ -13,6 +13,7 @@ import * as os from "node:os";
|
|
|
13
13
|
import { CLI_VERSION } from "../cli-version.js";
|
|
14
14
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
15
15
|
import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
|
|
16
|
+
import { parseRetryAfterSeconds } from "../lib/agent-kit/inbox-fetch.js";
|
|
16
17
|
import { McpServer } from "../lib/agent-kit/mcp/jsonrpc.js";
|
|
17
18
|
import { createVaultObjectDownloader } from "../lib/agent-kit/attachment-download.js";
|
|
18
19
|
import { buildAgentMcpTools } from "../lib/agent-kit/mcp/tools.js";
|
|
@@ -59,7 +60,7 @@ export function defaultMcpClients(creds, credsPath, paths = agentKitPaths(os.hom
|
|
|
59
60
|
HQ_VAULT_API_URL: creds.apiBaseUrl,
|
|
60
61
|
NO_COLOR: "1",
|
|
61
62
|
};
|
|
62
|
-
const getToken = () => ensureCognitoToken({ tokenSource: "machine", interactive: false });
|
|
63
|
+
const getToken = (signal) => ensureCognitoToken({ tokenSource: "machine", interactive: false, signal });
|
|
63
64
|
let companyUid;
|
|
64
65
|
return {
|
|
65
66
|
creds,
|
|
@@ -89,9 +90,11 @@ export function defaultMcpClients(creds, credsPath, paths = agentKitPaths(os.hom
|
|
|
89
90
|
method: init?.method,
|
|
90
91
|
body: init?.body,
|
|
91
92
|
query: init?.query,
|
|
93
|
+
signal: init?.signal,
|
|
92
94
|
});
|
|
93
95
|
const body = await res.json().catch(() => ({}));
|
|
94
|
-
|
|
96
|
+
const retryAfterSeconds = parseRetryAfterSeconds(res.headers.get("retry-after"));
|
|
97
|
+
return { status: res.status, body, ...(retryAfterSeconds !== undefined ? { retryAfterSeconds } : {}) };
|
|
95
98
|
},
|
|
96
99
|
};
|
|
97
100
|
}
|
package/dist/commands/bot.js
CHANGED
|
@@ -665,8 +665,20 @@ async function promptFleetProvider() {
|
|
|
665
665
|
* its half-written folder.
|
|
666
666
|
*/
|
|
667
667
|
export async function runBotCreate(nameArg, opts) {
|
|
668
|
-
|
|
669
|
-
|
|
668
|
+
try {
|
|
669
|
+
const name = validateBotName(nameArg);
|
|
670
|
+
await withBotLock(name, () => runBotCreateLocked(name, opts), { command: "hq bot create" });
|
|
671
|
+
}
|
|
672
|
+
catch (err) {
|
|
673
|
+
// Under --json the desktop app reads stdout: a failure, including one in
|
|
674
|
+
// the local steps before the create reaches HQ, must leave a document
|
|
675
|
+
// there too, not only a line on stderr.
|
|
676
|
+
if (opts.json && !err?.jsonReported) {
|
|
677
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
678
|
+
console.log(JSON.stringify({ ok: false, name: nameArg, reason: "create-failed", message }, null, 2));
|
|
679
|
+
}
|
|
680
|
+
throw err;
|
|
681
|
+
}
|
|
670
682
|
}
|
|
671
683
|
/**
|
|
672
684
|
* Write a bot's new name to the cloud. Never fails the caller's command when
|
|
@@ -880,7 +892,7 @@ async function runBotCreateLocked(name, opts) {
|
|
|
880
892
|
return false;
|
|
881
893
|
if (opts.json)
|
|
882
894
|
console.log(JSON.stringify({ ok: false, ...failure }, null, 2));
|
|
883
|
-
throw Object.assign(new Error(failure.message), { expected: true });
|
|
895
|
+
throw Object.assign(new Error(failure.message), { expected: true, jsonReported: opts.json === true });
|
|
884
896
|
}
|
|
885
897
|
const matches = matchRemoteBots(mine, name);
|
|
886
898
|
if (matches.length > 1) {
|
|
@@ -4,7 +4,8 @@ export { CORE_COMPANY_INDEX_SCOPE_FLAG };
|
|
|
4
4
|
* Kill switch for company-scoped index rebuilds, default ON
|
|
5
5
|
* (indigo-feature-flags-default-on-as-fail-safes). An absent, archived or
|
|
6
6
|
* unreadable flag row keeps scoping on; only an explicit false turns it off,
|
|
7
|
-
* and then `companies`
|
|
7
|
+
* and then an unbound `companies` rebuild writes every company as it did
|
|
8
|
+
* before scoping. A bound session stays scoped either way.
|
|
8
9
|
*/
|
|
9
10
|
export declare function coreCompanyIndexScopeEnabled(): boolean;
|
|
10
11
|
//# sourceMappingURL=core-company-index-scope-flag.d.ts.map
|
|
@@ -5,7 +5,8 @@ export { CORE_COMPANY_INDEX_SCOPE_FLAG };
|
|
|
5
5
|
* Kill switch for company-scoped index rebuilds, default ON
|
|
6
6
|
* (indigo-feature-flags-default-on-as-fail-safes). An absent, archived or
|
|
7
7
|
* unreadable flag row keeps scoping on; only an explicit false turns it off,
|
|
8
|
-
* and then `companies`
|
|
8
|
+
* and then an unbound `companies` rebuild writes every company as it did
|
|
9
|
+
* before scoping. A bound session stays scoped either way.
|
|
9
10
|
*/
|
|
10
11
|
export function coreCompanyIndexScopeEnabled() {
|
|
11
12
|
return resolveProcessFlagGate(CORE_COMPANY_INDEX_SCOPE_FLAG, { fallback: true }, () => true, undefined, { acceptStaleSnapshot: true, killSwitchWithoutCompany: true });
|
package/dist/commands/core.js
CHANGED
|
@@ -668,7 +668,6 @@ export function registerCoreCommands(program) {
|
|
|
668
668
|
const passthrough = operands.slice(1);
|
|
669
669
|
const force = passthrough.includes("--accept");
|
|
670
670
|
const rendererArgs = passthrough.filter((arg) => arg !== "--accept");
|
|
671
|
-
const isCompaniesTarget = entry.renderer === "companies";
|
|
672
671
|
const wantsHelp = rendererArgs.some((arg) => arg === "--help" || arg === "-h");
|
|
673
672
|
if (wantsHelp && ["companies", "company-knowledge", "projects"].includes(entry.renderer)) {
|
|
674
673
|
renderIndexTarget(entry.renderer, { root: "", output: (message) => process.stdout.write(message) }, rendererArgs);
|
|
@@ -681,10 +680,6 @@ export function registerCoreCommands(program) {
|
|
|
681
680
|
process.stdout.write(`Usage: hq core rebuild-index ${entry.target} [--accept]\n\n${entry.summary}. Help performs no writes.\n`);
|
|
682
681
|
return;
|
|
683
682
|
}
|
|
684
|
-
const requestsCompanyScope = isCompaniesTarget && rendererArgs.some((arg) => arg === "--company" || arg.startsWith("--company="));
|
|
685
|
-
if (requestsCompanyScope && !coreCompanyIndexScopeEnabled()) {
|
|
686
|
-
throw expectedUserError("rebuild-index companies --company is disabled; enable cli.rebuild-index-company-scope for a canary.");
|
|
687
|
-
}
|
|
688
683
|
const hqRoot = resolveLiveRoot({ hqRoot: scope.hqRoot });
|
|
689
684
|
const refused = [];
|
|
690
685
|
try {
|
|
@@ -103,4 +103,77 @@ export declare function attachmentScopeForChannel(channel: {
|
|
|
103
103
|
export declare function personalScopeForCaller(callerUid: string | undefined): string;
|
|
104
104
|
/** Render the confirmation lines listing where each attachment landed. */
|
|
105
105
|
export declare function formatUploadedAttachments(files: readonly DmAttachmentWire[]): string;
|
|
106
|
+
/** A parsed chat attachment key: chat/attachments/<dm|chan>/<scopeId>/<file>. */
|
|
107
|
+
export interface ParsedChatAttachmentKey {
|
|
108
|
+
scope: "dm" | "chan";
|
|
109
|
+
scopeId: string;
|
|
110
|
+
file: string;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Parse a chat attachment vault key. Returns null for anything that is not a
|
|
114
|
+
* single file directly under a conversation folder (no nesting, no `..`). Pure.
|
|
115
|
+
*/
|
|
116
|
+
export declare function parseChatAttachmentKey(key: string): ParsedChatAttachmentKey | null;
|
|
117
|
+
/** One attachment reference as it appears on a message row. */
|
|
118
|
+
export interface AttachmentRef {
|
|
119
|
+
vaultPath: string;
|
|
120
|
+
companyUid?: string;
|
|
121
|
+
name?: string;
|
|
122
|
+
/** Who sent the message carrying this attachment, when known. */
|
|
123
|
+
senderUid?: string;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The vault scopes to try, in order, for a READ of a chat attachment. An
|
|
127
|
+
* explicit `companyUid` (message metadata or `--company`) wins. Otherwise a
|
|
128
|
+
* 1:1 DM attachment lives in the SENDER's personal vault (see
|
|
129
|
+
* `personalScopeForCaller`): when `senderUid` is known and in the pair, that
|
|
130
|
+
* is the only scope. With no sender (a bare vault path), the pair id is sorted
|
|
131
|
+
* and does not say who sent it, so the counterparty's vault is tried first and
|
|
132
|
+
* then the caller's own (the caller may be the sender). The server enforces
|
|
133
|
+
* that the caller is one of the two; this only picks the vault. Throws when no
|
|
134
|
+
* scope can be decided. Pure.
|
|
135
|
+
*/
|
|
136
|
+
export declare function attachmentReadScopes(args: {
|
|
137
|
+
vaultPath: string;
|
|
138
|
+
companyUid?: string;
|
|
139
|
+
callerUid?: string;
|
|
140
|
+
senderUid?: string;
|
|
141
|
+
}): string[];
|
|
142
|
+
/** The first (most likely) read scope from `attachmentReadScopes`. Pure. */
|
|
143
|
+
export declare function attachmentReadScope(args: {
|
|
144
|
+
vaultPath: string;
|
|
145
|
+
companyUid?: string;
|
|
146
|
+
callerUid?: string;
|
|
147
|
+
senderUid?: string;
|
|
148
|
+
}): string;
|
|
149
|
+
/**
|
|
150
|
+
* The attachments on the message with `eventId` (or `messageId`) in a list of
|
|
151
|
+
* message rows. Returns null when no row matches. Pure.
|
|
152
|
+
*/
|
|
153
|
+
export declare function findMessageAttachments(rows: ReadonlyArray<{
|
|
154
|
+
eventId?: string;
|
|
155
|
+
messageId?: string;
|
|
156
|
+
fromPersonUid?: string;
|
|
157
|
+
attachments?: AttachmentRef[];
|
|
158
|
+
attachment?: AttachmentRef;
|
|
159
|
+
}>, eventId: string): AttachmentRef[] | null;
|
|
160
|
+
export interface AttachmentDownloadDeps {
|
|
161
|
+
/** POST /v1/files/presign with `{ company, keys }`. */
|
|
162
|
+
presign: (token: string, body: Record<string, unknown>) => Promise<Response>;
|
|
163
|
+
/** GET the presigned URL. */
|
|
164
|
+
get: (url: string) => Promise<Response>;
|
|
165
|
+
}
|
|
166
|
+
export declare const defaultAttachmentDownloadDeps: AttachmentDownloadDeps;
|
|
167
|
+
/**
|
|
168
|
+
* Presign a GET for one chat attachment in `scopeUid` and return its bytes.
|
|
169
|
+
* hq-pro files-presign admits a counterparty read of the sender's personal
|
|
170
|
+
* vault only when the caller is a member of the DM pair; a refusal (whole
|
|
171
|
+
* request or per key) surfaces as an Error.
|
|
172
|
+
*/
|
|
173
|
+
export declare function downloadChatAttachment(args: {
|
|
174
|
+
token: string;
|
|
175
|
+
scopeUid: string;
|
|
176
|
+
vaultPath: string;
|
|
177
|
+
deps?: AttachmentDownloadDeps;
|
|
178
|
+
}): Promise<Buffer>;
|
|
106
179
|
//# sourceMappingURL=dm-attach.d.ts.map
|
|
@@ -193,4 +193,108 @@ export function formatUploadedAttachments(files) {
|
|
|
193
193
|
.map((f) => ` [file] ${f.name} (${formatBytes(f.sizeBytes)})\n vault: ${f.vaultPath}`)
|
|
194
194
|
.join("\n");
|
|
195
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Parse a chat attachment vault key. Returns null for anything that is not a
|
|
198
|
+
* single file directly under a conversation folder (no nesting, no `..`). Pure.
|
|
199
|
+
*/
|
|
200
|
+
export function parseChatAttachmentKey(key) {
|
|
201
|
+
const parts = key.trim().split("/");
|
|
202
|
+
if (parts.length !== 5)
|
|
203
|
+
return null;
|
|
204
|
+
const [a, b, scope, scopeId, file] = parts;
|
|
205
|
+
if (a !== "chat" || b !== "attachments")
|
|
206
|
+
return null;
|
|
207
|
+
if (scope !== "dm" && scope !== "chan")
|
|
208
|
+
return null;
|
|
209
|
+
if (!scopeId || scopeId === "." || scopeId === "..")
|
|
210
|
+
return null;
|
|
211
|
+
if (!file || file === "." || file === "..")
|
|
212
|
+
return null;
|
|
213
|
+
return { scope, scopeId, file };
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* The vault scopes to try, in order, for a READ of a chat attachment. An
|
|
217
|
+
* explicit `companyUid` (message metadata or `--company`) wins. Otherwise a
|
|
218
|
+
* 1:1 DM attachment lives in the SENDER's personal vault (see
|
|
219
|
+
* `personalScopeForCaller`): when `senderUid` is known and in the pair, that
|
|
220
|
+
* is the only scope. With no sender (a bare vault path), the pair id is sorted
|
|
221
|
+
* and does not say who sent it, so the counterparty's vault is tried first and
|
|
222
|
+
* then the caller's own (the caller may be the sender). The server enforces
|
|
223
|
+
* that the caller is one of the two; this only picks the vault. Throws when no
|
|
224
|
+
* scope can be decided. Pure.
|
|
225
|
+
*/
|
|
226
|
+
export function attachmentReadScopes(args) {
|
|
227
|
+
const parsed = parseChatAttachmentKey(args.vaultPath);
|
|
228
|
+
if (!parsed) {
|
|
229
|
+
throw new Error(`Not a chat attachment path: ${args.vaultPath}. Expected chat/attachments/dm/<uidA--uidB>/<file> or chat/attachments/chan/<channelId>/<file>.`);
|
|
230
|
+
}
|
|
231
|
+
const explicit = args.companyUid?.trim();
|
|
232
|
+
if (explicit)
|
|
233
|
+
return [explicit];
|
|
234
|
+
if (parsed.scope === "chan") {
|
|
235
|
+
throw new Error("A channel attachment needs its vault scope. Pass --company <uid> with the companyUid shown on the message.");
|
|
236
|
+
}
|
|
237
|
+
const pair = parsed.scopeId.split("--").filter(Boolean);
|
|
238
|
+
const me = args.callerUid?.trim();
|
|
239
|
+
if (pair.length !== 2 || !me || !pair.includes(me)) {
|
|
240
|
+
throw new Error("This DM attachment is not from a conversation you are part of, or your uid could not be read. Pass --company <uid> with the companyUid shown on the message.");
|
|
241
|
+
}
|
|
242
|
+
const sender = args.senderUid?.trim();
|
|
243
|
+
if (sender && pair.includes(sender))
|
|
244
|
+
return [sender];
|
|
245
|
+
const other = pair[0] === me ? pair[1] : pair[0];
|
|
246
|
+
return [other, me];
|
|
247
|
+
}
|
|
248
|
+
/** The first (most likely) read scope from `attachmentReadScopes`. Pure. */
|
|
249
|
+
export function attachmentReadScope(args) {
|
|
250
|
+
return attachmentReadScopes(args)[0];
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* The attachments on the message with `eventId` (or `messageId`) in a list of
|
|
254
|
+
* message rows. Returns null when no row matches. Pure.
|
|
255
|
+
*/
|
|
256
|
+
export function findMessageAttachments(rows, eventId) {
|
|
257
|
+
const id = eventId.trim();
|
|
258
|
+
const row = rows.find((r) => r.eventId === id || r.messageId === id);
|
|
259
|
+
if (!row)
|
|
260
|
+
return null;
|
|
261
|
+
const list = row.attachments?.length ? row.attachments : row.attachment ? [row.attachment] : [];
|
|
262
|
+
const sender = row.fromPersonUid?.trim();
|
|
263
|
+
return list
|
|
264
|
+
.filter((a) => Boolean(a?.vaultPath))
|
|
265
|
+
.map((a) => (sender && !a.senderUid ? { ...a, senderUid: sender } : a));
|
|
266
|
+
}
|
|
267
|
+
export const defaultAttachmentDownloadDeps = {
|
|
268
|
+
presign: (token, body) => vaultApiFetch({ token, path: "/v1/files/presign", method: "POST", body }),
|
|
269
|
+
get: (url) => fetch(url),
|
|
270
|
+
};
|
|
271
|
+
/**
|
|
272
|
+
* Presign a GET for one chat attachment in `scopeUid` and return its bytes.
|
|
273
|
+
* hq-pro files-presign admits a counterparty read of the sender's personal
|
|
274
|
+
* vault only when the caller is a member of the DM pair; a refusal (whole
|
|
275
|
+
* request or per key) surfaces as an Error.
|
|
276
|
+
*/
|
|
277
|
+
export async function downloadChatAttachment(args) {
|
|
278
|
+
const deps = args.deps ?? defaultAttachmentDownloadDeps;
|
|
279
|
+
const res = await deps.presign(args.token, {
|
|
280
|
+
company: args.scopeUid,
|
|
281
|
+
keys: [{ key: args.vaultPath, op: "get" }],
|
|
282
|
+
});
|
|
283
|
+
if (!res.ok) {
|
|
284
|
+
const err = (await res.json().catch(() => ({})));
|
|
285
|
+
const message = typeof err.error === "string" ? err.error : typeof err.message === "string" ? err.message : res.statusText;
|
|
286
|
+
throw new Error(`Could not open the attachment (HTTP ${res.status}): ${message}`);
|
|
287
|
+
}
|
|
288
|
+
const data = (await res.json().catch(() => ({})));
|
|
289
|
+
const row = (data.results ?? []).find((r) => Boolean(r) && typeof r === "object" && r.key === args.vaultPath);
|
|
290
|
+
if (!row || typeof row.url !== "string") {
|
|
291
|
+
const reason = typeof row?.error === "string" ? row.error : "the vault refused the read";
|
|
292
|
+
throw new Error(`Attachment refused: ${args.vaultPath}: ${reason}`);
|
|
293
|
+
}
|
|
294
|
+
const got = await deps.get(row.url);
|
|
295
|
+
if (!got.ok) {
|
|
296
|
+
throw new Error(`Download failed for ${args.vaultPath} (HTTP ${got.status}).`);
|
|
297
|
+
}
|
|
298
|
+
return Buffer.from(await got.arrayBuffer());
|
|
299
|
+
}
|
|
196
300
|
//# sourceMappingURL=dm-attach.js.map
|
package/dist/commands/dm.d.ts
CHANGED
|
@@ -443,5 +443,15 @@ export declare function formatChannelTranscript(messages: ChannelMessageItem[],
|
|
|
443
443
|
*/
|
|
444
444
|
export declare function agentCallerUid(token: string, machine?: () => boolean): string | null;
|
|
445
445
|
export declare const AGENT_THREAD_UNAVAILABLE: string;
|
|
446
|
+
/**
|
|
447
|
+
* Where to write each downloaded attachment. One file and an `--out` that is
|
|
448
|
+
* not an existing directory: write exactly there. Otherwise `--out` (default
|
|
449
|
+
* cwd) is a directory and each file keeps its sanitized name. Pure apart from
|
|
450
|
+
* the injected `isDir`.
|
|
451
|
+
*/
|
|
452
|
+
export declare function attachmentOutputPaths(refs: ReadonlyArray<{
|
|
453
|
+
vaultPath: string;
|
|
454
|
+
name?: string;
|
|
455
|
+
}>, out: string | undefined, isDir: (p: string) => boolean): string[];
|
|
446
456
|
export declare function registerDmCommand(program: Command): void;
|
|
447
457
|
//# sourceMappingURL=dm.d.ts.map
|