@indigoai-us/hq-cli 5.117.1 → 5.117.3
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 +41 -0
- package/assets/bot-workers/setup/context/USER-GUIDE.md +44 -3
- package/assets/bot-workers/setup/context/quick-reference.md +1 -1
- package/dist/command-catalog.generated.d.ts +5 -2
- package/dist/command-catalog.generated.js +6 -2
- package/dist/commands/agent-kit.js +16 -1
- package/dist/commands/agent-probe.d.ts +1 -3
- package/dist/commands/agent-probe.js +11 -29
- package/dist/commands/billing.js +1 -1
- package/dist/commands/db-provision.js +4 -4
- package/dist/commands/meetings.js +2 -2
- package/dist/commands/whoami.d.ts +7 -0
- package/dist/commands/whoami.js +55 -1
- package/dist/lib/agent-kit/run/inbox.js +2 -2
- package/dist/lib/agent-kit/run/mesh-listener.js +11 -2
- package/dist/lib/agent-kit/skills.js +2 -2
- package/dist/lib/billing/plan-lock.d.ts +99 -0
- package/dist/lib/billing/plan-lock.js +230 -0
- package/dist/lib/doctor/__testing__/fake-hq-tree.d.ts +1 -1
- package/dist/lib/doctor/__testing__/fake-hq-tree.js +1 -1
- package/dist/lib/doctor/checks/claude-wiring.js +61 -1
- package/dist/lib/doctor/compat.js +1 -0
- package/dist/lib/doctor/fix/apply.js +21 -22
- package/dist/lib/doctor/fix/remediation.d.ts +7 -4
- package/dist/lib/doctor/fix/remediation.js +15 -7
- package/dist/lib/doctor/registry.js +43 -0
- package/dist/lib/doctor/stray-gate-entries.d.ts +35 -0
- package/dist/lib/doctor/stray-gate-entries.js +83 -0
- package/dist/lib/plan-limit-nag.js +1 -1
- package/dist/utils/plan-gate-error.js +9 -9
- package/dist/utils/team-upgrade.d.ts +1 -1
- package/dist/utils/team-upgrade.js +3 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.117.3] — 2026-09-16
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `hq doctor --fix` no longer breaks Claude Code sessions. Since HQ moved to a single master hook, the doctor saw every hook script as unregistered. It then added one hook entry per script with no tool filter, so guards such as the Glob path check and core protection ran on every tool call and blocked Bash, Skill and Read with "Glob needs a path" or "Edit to locked path is not allowed". An HQ update could then move those entries into `.claude/settings.local.json`. The doctor no longer auto-registers orphan scripts. It now detects the stray entries in both settings files and `hq doctor --fix` removes them, keeping your permissions and any hooks you added yourself. Scripts listed in `.claude/hooks/hook-registry.json` also no longer show as orphans.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `hq doctor` now keeps its hook-health compatibility verdict in step with
|
|
14
|
+
`check-hq-hooks.sh` when either required command hook is missing.
|
|
15
|
+
|
|
16
|
+
## [5.117.2] — 2026-09-16
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- External agents: the work-mesh listener no longer fails every refresh with
|
|
21
|
+
"Agent callers may only use DM, channel comms, and reaction routes". It now
|
|
22
|
+
re-reads the agent's own inbox on each doorbell instead of the person-only
|
|
23
|
+
conversation routes.
|
|
24
|
+
- External agents: `hq agent probe` no longer waits for a DM to itself to show up
|
|
25
|
+
in the inbox, which the server never delivers. The dm check passes when the
|
|
26
|
+
send is accepted and the agent inbox is readable.
|
|
27
|
+
- External agents: kit service logs no longer write every line twice when the
|
|
28
|
+
services run under a supervisor.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- HQ now calls the paid plan by its real name everywhere you see it. If your
|
|
33
|
+
workspace went over its Starter limit, the notice, the error you got when you
|
|
34
|
+
tried to write something, and the upgrade prompt used to give three different
|
|
35
|
+
names for the same plan — "HQ Workforce", "HQ Team", and "Team" — which made
|
|
36
|
+
it genuinely unclear what you were being asked to buy. They all say HQ
|
|
37
|
+
Workforce now. The free plan is called Starter rather than "free plan", to
|
|
38
|
+
match the pricing page.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- `hq doctor` now reports failed hook wiring when `.claude/settings.json` has
|
|
43
|
+
no usable command hook for `SessionStart` or `PreToolUse`. The file can still
|
|
44
|
+
be present and valid JSON, but it no longer reads as healthy when nothing runs.
|
|
45
|
+
|
|
5
46
|
## [5.117.1] — 2026-09-16
|
|
6
47
|
|
|
7
48
|
### Fixed
|
|
@@ -109,9 +109,9 @@ Structured storage for agent and app state — not a replacement for markdown kn
|
|
|
109
109
|
| `hq db status --company {co}` | Create/open local SQLite at `~/.hq/db/{co}/vault.db` (WAL) |
|
|
110
110
|
| `hq db sql --company {co} -- 'SELECT …'` | Query the company local DB (read-only by default) |
|
|
111
111
|
| `hq db migrate --company {co} --hq-root {HQ}` | Apply reviewable SQL under `companies/{co}/db/migrations/` |
|
|
112
|
-
| `hq db provision --company {co}` | Remote DB on **HQ
|
|
112
|
+
| `hq db provision --company {co}` | Remote DB on **HQ Workforce** ($500/mo) or Enterprise only (secrets never printed) |
|
|
113
113
|
|
|
114
|
-
Use local for single-machine agent skills and implementer standards. Use
|
|
114
|
+
Use local for single-machine agent skills and implementer standards. Use the remote DB for multi-machine shared data and deploy SecretBinding. Never commit `*.db` files into the vault tree.
|
|
115
115
|
|
|
116
116
|
### HQ CLI: Direct messages (`hq dm`)
|
|
117
117
|
|
|
@@ -318,6 +318,47 @@ HQ/
|
|
|
318
318
|
└── threads/ # Session threads + handoff.json
|
|
319
319
|
```
|
|
320
320
|
|
|
321
|
+
## Plans and limits
|
|
322
|
+
|
|
323
|
+
HQ has three plans: **Starter**, **HQ Workforce** ($500/month per workspace), and
|
|
324
|
+
**Enterprise**.
|
|
325
|
+
|
|
326
|
+
Starter is free. It holds up to 5 members, and it does not include integrations
|
|
327
|
+
or MCP, Atlas, or telemetry. Everything else — knowledge, projects, policies,
|
|
328
|
+
workers, secrets, vault storage, `hq deploy`, and `hq share` — works the same as
|
|
329
|
+
on the paid plans.
|
|
330
|
+
|
|
331
|
+
### When a workspace goes over
|
|
332
|
+
|
|
333
|
+
A Starter workspace locks as soon as it goes over 5 members. There is no grace
|
|
334
|
+
period and no countdown.
|
|
335
|
+
|
|
336
|
+
A locked workspace is read-only. Nothing is deleted or hidden, and everyone can
|
|
337
|
+
still read everything. Writes are refused until the workspace is back inside the
|
|
338
|
+
limits.
|
|
339
|
+
|
|
340
|
+
There are two ways out, and only the workspace owner can do either:
|
|
341
|
+
|
|
342
|
+
1. Remove members until there are 5 or fewer. Removing someone from a workspace
|
|
343
|
+
does not delete their personal HQ or the work they saved into the workspace.
|
|
344
|
+
2. Upgrade to HQ Workforce at $500/month from the workspace billing page.
|
|
345
|
+
|
|
346
|
+
Either one unlocks the workspace right away.
|
|
347
|
+
|
|
348
|
+
Existing integration connections on a locked Starter workspace are disabled, not
|
|
349
|
+
deleted. Upgrading turns them back on without reconnecting anything.
|
|
350
|
+
|
|
351
|
+
### What you'll see
|
|
352
|
+
|
|
353
|
+
The owner gets one email when the workspace locks, a reminder at most once every
|
|
354
|
+
7 days while it stays locked, and one confirmation when it unlocks. Billing mail
|
|
355
|
+
comes from billing@hqforwork.com, and billing links go to hq.computer, which is
|
|
356
|
+
the console.
|
|
357
|
+
|
|
358
|
+
In the console you'll see a lock banner, an upgrade wall, and a team roster that
|
|
359
|
+
counts members against the limit. In a Claude Code session, `/startwork` shows a
|
|
360
|
+
short notice on every turn while the workspace is over its limits.
|
|
361
|
+
|
|
321
362
|
## Meeting notes, signals & ontology
|
|
322
363
|
|
|
323
364
|
HQ captures these **natively, per company** — check HQ first, not your email or a third-party notetaker.
|
|
@@ -334,7 +375,7 @@ HQ captures these **natively, per company** — check HQ first, not your email o
|
|
|
334
375
|
|
|
335
376
|
**Your preference for "meeting notes":** defaults to HQ-native. To point a company at email instead, set `meeting_notes_source: email` in `companies/{co}/settings/knowledge/preferences.yaml` (global default lives in `personal/settings/knowledge-preferences.yaml`).
|
|
336
377
|
|
|
337
|
-
> Signals extraction and the ontology gardener run on HQ cloud and
|
|
378
|
+
> Signals extraction and the ontology gardener run on HQ cloud and are part of HQ Workforce ($500/mo) and Enterprise. They are not included in the free Starter plan. Cloud-back a company with `/designate-team` to turn them on. Reference: `core/knowledge/public/hq-core/native-knowledge-stores.md`.
|
|
338
379
|
|
|
339
380
|
## Typical Session
|
|
340
381
|
|
|
@@ -148,7 +148,7 @@ Share-session URLs are encrypted single-use 15-minute capabilities — never per
|
|
|
148
148
|
|
|
149
149
|
## CLI: `hq db` (vault databases)
|
|
150
150
|
|
|
151
|
-
Local SQLite per company (always) + optional remote Postgres-class on **HQ
|
|
151
|
+
Local SQLite per company (always, Starter included) + optional remote Postgres-class on **HQ Workforce** ($500/mo) or Enterprise. Guide: `core/knowledge/public/hq-core/vault-databases.md`. Requires `@indigoai-us/hq-cli` ≥ 5.62.0.
|
|
152
152
|
|
|
153
153
|
| Command | Use |
|
|
154
154
|
|---------|-----|
|
|
@@ -800,6 +800,9 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
800
800
|
readonly options: readonly [{
|
|
801
801
|
readonly flags: "--json";
|
|
802
802
|
readonly description: "Output identity metadata as JSON (never tokens)";
|
|
803
|
+
}, {
|
|
804
|
+
readonly flags: "--company <slug>";
|
|
805
|
+
readonly description: "Also report this company's workspace plan state (Starter lock)";
|
|
803
806
|
}];
|
|
804
807
|
readonly subcommands: readonly [];
|
|
805
808
|
}, {
|
|
@@ -1276,7 +1279,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
1276
1279
|
readonly subcommands: readonly [];
|
|
1277
1280
|
}, {
|
|
1278
1281
|
readonly name: "provision";
|
|
1279
|
-
readonly description: "Provision (or re-bind) the company remote vault DB via HQ control plane (
|
|
1282
|
+
readonly description: "Provision (or re-bind) the company remote vault DB via HQ control plane (HQ Workforce plan)";
|
|
1280
1283
|
readonly aliases: readonly [];
|
|
1281
1284
|
readonly hidden: false;
|
|
1282
1285
|
readonly usage: "[options]";
|
|
@@ -4603,7 +4606,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
4603
4606
|
readonly subcommands: readonly [];
|
|
4604
4607
|
}, {
|
|
4605
4608
|
readonly name: "upgrade";
|
|
4606
|
-
readonly description: "Open Stripe Checkout to upgrade this company to HQ
|
|
4609
|
+
readonly description: "Open Stripe Checkout to upgrade this company to HQ Workforce ($500/mo)";
|
|
4607
4610
|
readonly aliases: readonly [];
|
|
4608
4611
|
readonly hidden: false;
|
|
4609
4612
|
readonly usage: "[options]";
|
|
@@ -1021,6 +1021,10 @@ export const COMMAND_CATALOG = [
|
|
|
1021
1021
|
{
|
|
1022
1022
|
"flags": "--json",
|
|
1023
1023
|
"description": "Output identity metadata as JSON (never tokens)"
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
"flags": "--company <slug>",
|
|
1027
|
+
"description": "Also report this company's workspace plan state (Starter lock)"
|
|
1024
1028
|
}
|
|
1025
1029
|
],
|
|
1026
1030
|
"subcommands": []
|
|
@@ -1628,7 +1632,7 @@ export const COMMAND_CATALOG = [
|
|
|
1628
1632
|
},
|
|
1629
1633
|
{
|
|
1630
1634
|
"name": "provision",
|
|
1631
|
-
"description": "Provision (or re-bind) the company remote vault DB via HQ control plane (
|
|
1635
|
+
"description": "Provision (or re-bind) the company remote vault DB via HQ control plane (HQ Workforce plan)",
|
|
1632
1636
|
"aliases": [],
|
|
1633
1637
|
"hidden": false,
|
|
1634
1638
|
"usage": "[options]",
|
|
@@ -5951,7 +5955,7 @@ export const COMMAND_CATALOG = [
|
|
|
5951
5955
|
},
|
|
5952
5956
|
{
|
|
5953
5957
|
"name": "upgrade",
|
|
5954
|
-
"description": "Open Stripe Checkout to upgrade this company to HQ
|
|
5958
|
+
"description": "Open Stripe Checkout to upgrade this company to HQ Workforce ($500/mo)",
|
|
5955
5959
|
"aliases": [],
|
|
5956
5960
|
"hidden": false,
|
|
5957
5961
|
"usage": "[options]",
|
|
@@ -124,7 +124,7 @@ export function buildKitRuntime(service) {
|
|
|
124
124
|
process.env.HQ_MACHINE_CREDS_FILE = paths.machineCredsPath;
|
|
125
125
|
process.env.HQ_REQUIRE_MACHINE_IDENTITY = "1";
|
|
126
126
|
process.env.HQ_VAULT_API_URL = creds.apiBaseUrl;
|
|
127
|
-
const log = createKitLogger(paths, service, { echo: true });
|
|
127
|
+
const log = createKitLogger(paths, service, { echo: process.stdout.isTTY === true });
|
|
128
128
|
const getToken = () => ensureCognitoToken({ tokenSource: "machine", interactive: false });
|
|
129
129
|
return { paths, creds, config, getToken, log };
|
|
130
130
|
}
|
|
@@ -168,6 +168,21 @@ export async function runKitService(service, rt) {
|
|
|
168
168
|
refreshMs: rt.config.meshRefreshMs,
|
|
169
169
|
getToken: rt.getToken,
|
|
170
170
|
log: rt.log,
|
|
171
|
+
// Any doorbell (and the periodic refresh) re-polls the agent inbox, the
|
|
172
|
+
// only conversation source agent callers are authorized to read.
|
|
173
|
+
refetch: async () => {
|
|
174
|
+
const r = await pollInboxOnce({
|
|
175
|
+
paths: rt.paths,
|
|
176
|
+
agentUid: rt.creds.entityUid,
|
|
177
|
+
apiBaseUrl: rt.creds.apiBaseUrl,
|
|
178
|
+
pollMs: rt.config.inboxPollMs,
|
|
179
|
+
ack: rt.config.inboxAck,
|
|
180
|
+
getToken: rt.getToken,
|
|
181
|
+
log: rt.log,
|
|
182
|
+
});
|
|
183
|
+
if (!r.ok)
|
|
184
|
+
throw new Error("agent inbox poll failed; see inbox.log");
|
|
185
|
+
},
|
|
171
186
|
// An inbox doorbell means "poll now" rather than waiting for the loop.
|
|
172
187
|
onInboxDoorbell: () => pollInboxOnce({
|
|
173
188
|
paths: rt.paths,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* presence is live: a fresh last-heartbeat.json plus an ok
|
|
10
10
|
* component-mesh stamp from the doorbell listener. (The company
|
|
11
11
|
* roster route is owner/admin-only and 404s for agt_ callers.)
|
|
12
|
-
* dm send a DM to self via POST /v1/notify/dm and read
|
|
12
|
+
* dm send a DM to self via POST /v1/notify/dm (accepted) and read the agent inbox (200)
|
|
13
13
|
* through GET /v1/agents/{uid}/inbox (the agent read surface;
|
|
14
14
|
* /v1/notify/thread is intentionally closed to agents)
|
|
15
15
|
* secrets `GET /secrets/{companyUid}` answers 200
|
|
@@ -38,8 +38,6 @@ export interface ProbeResult {
|
|
|
38
38
|
/** Seconds since the kit's last successful heartbeat, or null if never. */
|
|
39
39
|
heartbeatAgeSeconds: number | null;
|
|
40
40
|
}
|
|
41
|
-
export declare const DM_ROUNDTRIP_TIMEOUT_MS = 20000;
|
|
42
|
-
export declare const DM_ROUNDTRIP_POLL_MS = 2000;
|
|
43
41
|
/** A heartbeat older than this does not count as live presence (3 missed 60 s beats). */
|
|
44
42
|
export declare const PRESENCE_FRESH_SECONDS = 180;
|
|
45
43
|
export interface ProbeDeps {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* presence is live: a fresh last-heartbeat.json plus an ok
|
|
10
10
|
* component-mesh stamp from the doorbell listener. (The company
|
|
11
11
|
* roster route is owner/admin-only and 404s for agt_ callers.)
|
|
12
|
-
* dm send a DM to self via POST /v1/notify/dm and read
|
|
12
|
+
* dm send a DM to self via POST /v1/notify/dm (accepted) and read the agent inbox (200)
|
|
13
13
|
* through GET /v1/agents/{uid}/inbox (the agent read surface;
|
|
14
14
|
* /v1/notify/thread is intentionally closed to agents)
|
|
15
15
|
* secrets `GET /secrets/{companyUid}` answers 200
|
|
@@ -41,8 +41,6 @@ import { readKitConfig } from "../lib/agent-kit/kit-config.js";
|
|
|
41
41
|
import { readLastHeartbeat } from "../lib/agent-kit/run/heartbeat.js";
|
|
42
42
|
import { defaultRunPull, syncPullArgs } from "../lib/agent-kit/run/sync.js";
|
|
43
43
|
import { requireExternalCreds } from "./agent-kit.js";
|
|
44
|
-
export const DM_ROUNDTRIP_TIMEOUT_MS = 20_000;
|
|
45
|
-
export const DM_ROUNDTRIP_POLL_MS = 2_000;
|
|
46
44
|
/** A heartbeat older than this does not count as live presence (3 missed 60 s beats). */
|
|
47
45
|
export const PRESENCE_FRESH_SECONDS = 180;
|
|
48
46
|
function errText(err) {
|
|
@@ -59,7 +57,6 @@ export function heartbeatAgeSeconds(paths, now = () => new Date()) {
|
|
|
59
57
|
}
|
|
60
58
|
export async function runProbe(deps) {
|
|
61
59
|
const now = deps.now ?? (() => new Date());
|
|
62
|
-
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
63
60
|
const nonce = (deps.nonce ?? (() => randomBytes(6).toString("hex")))();
|
|
64
61
|
const self = deps.creds.entityUid;
|
|
65
62
|
const checks = [];
|
|
@@ -153,37 +150,22 @@ export async function runProbe(deps) {
|
|
|
153
150
|
else {
|
|
154
151
|
checks.push({ name: "work-mesh", ok: false, detail: "skipped: no token/company" });
|
|
155
152
|
}
|
|
156
|
-
// 4. dm
|
|
153
|
+
// 4. dm: the send is accepted and the agent inbox is readable.
|
|
154
|
+
// The server never delivers a self-DM into the sender's own inbox
|
|
155
|
+
// (hq-pro-core notify-dm skips inbox delivery when recipient === sender),
|
|
156
|
+
// so a self round-trip can never succeed. Inbound delivery is proven by a
|
|
157
|
+
// teammate's DM, which the kit's inbox poller mirrors to inbox.jsonl.
|
|
157
158
|
if (token) {
|
|
158
159
|
try {
|
|
159
160
|
const body = `hq agent probe ${nonce}`;
|
|
160
161
|
await deps.sendDm(token, self, body);
|
|
161
|
-
const
|
|
162
|
-
let found = false;
|
|
163
|
-
let lastStatus = 0;
|
|
164
|
-
for (;;) {
|
|
165
|
-
const res = await deps.readInbox(token);
|
|
166
|
-
lastStatus = res.status;
|
|
167
|
-
if (res.status === 200 && inboxContainsNonce(res.body, nonce)) {
|
|
168
|
-
found = true;
|
|
169
|
-
break;
|
|
170
|
-
}
|
|
171
|
-
// A 4xx will not heal by polling (auth/route refusal): stop now.
|
|
172
|
-
if (res.status >= 400 && res.status < 500)
|
|
173
|
-
break;
|
|
174
|
-
if (now().getTime() >= deadline)
|
|
175
|
-
break;
|
|
176
|
-
await sleep(DM_ROUNDTRIP_POLL_MS);
|
|
177
|
-
}
|
|
178
|
-
const secs = Math.round((deps.dmTimeoutMs ?? DM_ROUNDTRIP_TIMEOUT_MS) / 1000);
|
|
162
|
+
const res = await deps.readInbox(token);
|
|
179
163
|
checks.push({
|
|
180
164
|
name: "dm",
|
|
181
|
-
ok:
|
|
182
|
-
detail:
|
|
183
|
-
? "
|
|
184
|
-
:
|
|
185
|
-
? `sent but not visible in /v1/agents/{uid}/inbox after ${secs}s`
|
|
186
|
-
: `sent but /v1/agents/{uid}/inbox answered ${lastStatus}`,
|
|
165
|
+
ok: res.status === 200,
|
|
166
|
+
detail: res.status === 200
|
|
167
|
+
? "send accepted by /v1/notify/dm; /v1/agents/{uid}/inbox → 200 (self-DMs are not delivered to the inbox; a DM from a teammate confirms inbound)"
|
|
168
|
+
: `send accepted but /v1/agents/{uid}/inbox answered ${res.status}`,
|
|
187
169
|
});
|
|
188
170
|
}
|
|
189
171
|
catch (err) {
|
package/dist/commands/billing.js
CHANGED
|
@@ -110,7 +110,7 @@ export function registerBillingCommand(program) {
|
|
|
110
110
|
});
|
|
111
111
|
billing
|
|
112
112
|
.command("upgrade")
|
|
113
|
-
.description("Open Stripe Checkout to upgrade this company to HQ
|
|
113
|
+
.description("Open Stripe Checkout to upgrade this company to HQ Workforce ($500/mo)")
|
|
114
114
|
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
115
115
|
.option("--no-browser", "Print the Stripe Checkout URL instead of opening it")
|
|
116
116
|
.option("--json", "Emit the Checkout URL as JSON")
|
|
@@ -28,7 +28,7 @@ const defaultDeps = () => ({
|
|
|
28
28
|
});
|
|
29
29
|
export function registerDbProvisionCommand(db, depsFactory = defaultDeps) {
|
|
30
30
|
db.command("provision")
|
|
31
|
-
.description("Provision (or re-bind) the company remote vault DB via HQ control plane (
|
|
31
|
+
.description("Provision (or re-bind) the company remote vault DB via HQ control plane (HQ Workforce plan)")
|
|
32
32
|
.requiredOption("--company <slug>", "Company slug")
|
|
33
33
|
.option("--region <region>", "AWS region", "us-east-1")
|
|
34
34
|
.action(async (opts) => {
|
|
@@ -65,9 +65,9 @@ export function registerDbProvisionCommand(db, depsFactory = defaultDeps) {
|
|
|
65
65
|
throw error;
|
|
66
66
|
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
67
67
|
const status = error.status;
|
|
68
|
-
if (status === 402 || /PLAN_REQUIRED|Team plan|\$500/i.test(msg)) {
|
|
69
|
-
console.error(chalk.red("Error:"), "Remote vault DB requires the HQ
|
|
70
|
-
console.error(chalk.dim("Local databases still work: hq db status|sql|migrate — no
|
|
68
|
+
if (status === 402 || /PLAN_REQUIRED|Team plan|Workforce|\$500/i.test(msg)) {
|
|
69
|
+
console.error(chalk.red("Error:"), "Remote vault DB requires the HQ Workforce plan ($500/mo).");
|
|
70
|
+
console.error(chalk.dim("Local databases still work: hq db status|sql|migrate — no HQ Workforce plan required."));
|
|
71
71
|
}
|
|
72
72
|
else if (status === 404 || /Unknown route|Not Found/i.test(msg)) {
|
|
73
73
|
console.error(chalk.red("Error:"), "Remote DB control plane is not available yet (routes not deployed).");
|
|
@@ -111,8 +111,8 @@ async function handleApiError(res, json = false) {
|
|
|
111
111
|
console.error(chalk.red(body.error ?? "Not found"));
|
|
112
112
|
}
|
|
113
113
|
else if (res.status === 402) {
|
|
114
|
-
console.error(chalk.red("Meeting notetaker bots require the $500/mo HQ
|
|
115
|
-
console.error(chalk.dim(body.message ?? body.error ?? "Upgrade your company to HQ
|
|
114
|
+
console.error(chalk.red("Meeting notetaker bots require the $500/mo HQ Workforce plan."));
|
|
115
|
+
console.error(chalk.dim(body.message ?? body.error ?? "Upgrade your company to HQ Workforce to record meetings."));
|
|
116
116
|
}
|
|
117
117
|
else {
|
|
118
118
|
console.error(chalk.red(`API error (${res.status}): ${body.error ?? res.statusText}`));
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* hq whoami — displays current user or 'not logged in'
|
|
3
3
|
*/
|
|
4
4
|
import { Command } from 'commander';
|
|
5
|
+
import { type PlanLockStatus } from "../lib/billing/plan-lock.js";
|
|
5
6
|
export interface WhoamiTokenIdentity {
|
|
6
7
|
email?: string;
|
|
7
8
|
sub?: string;
|
|
@@ -19,5 +20,11 @@ export interface WhoamiDisplayIdentity {
|
|
|
19
20
|
* email with the token subject of the delegating machine.
|
|
20
21
|
*/
|
|
21
22
|
export declare function resolveWhoamiIdentity(identity: WhoamiTokenIdentity): WhoamiDisplayIdentity;
|
|
23
|
+
/**
|
|
24
|
+
* Human rendering of the plan state: the full WORKSPACE LOCKED block when the
|
|
25
|
+
* workspace is locked, the one-line `Plan: Starter — …` orientation when it is
|
|
26
|
+
* Starter and healthy, and nothing at all otherwise.
|
|
27
|
+
*/
|
|
28
|
+
export declare function renderWhoamiPlanBlock(status: PlanLockStatus): string | null;
|
|
22
29
|
export declare function registerWhoamiCommand(program: Command): void;
|
|
23
30
|
//# sourceMappingURL=whoami.d.ts.map
|
package/dist/commands/whoami.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { loadCachedTokens, isExpiring, loadMachineCreds, } from '@indigoai-us/hq-cloud';
|
|
6
6
|
import { peekIdToken as decodeIdToken } from "../utils/id-token.js";
|
|
7
|
-
import { loadMachineCachedTokens, resolveCognitoTokenSource } from "../utils/cognito-session.js";
|
|
7
|
+
import { ensureCognitoToken, loadMachineCachedTokens, resolveCognitoTokenSource, } from "../utils/cognito-session.js";
|
|
8
|
+
import { colorizePlanLockNotice, fetchPlanLockStatus, renderPlanLine, renderPlanLockNotice, } from "../lib/billing/plan-lock.js";
|
|
8
9
|
/**
|
|
9
10
|
* Resolves the person represented by an ID token without pairing a delegated
|
|
10
11
|
* email with the token subject of the delegating machine.
|
|
@@ -40,11 +41,58 @@ function peekIdToken(idToken) {
|
|
|
40
41
|
: undefined,
|
|
41
42
|
};
|
|
42
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* starter-plan-hard-limits / US-011 — read the workspace plan lock for
|
|
46
|
+
* `--company <slug>`.
|
|
47
|
+
*
|
|
48
|
+
* hq-pro owns the decision (`planLock` on GET /membership/me); this only reads
|
|
49
|
+
* it. Best-effort by construction: no session, a network failure, an older
|
|
50
|
+
* backend that sends no `planLock`, or a company the caller is not a member of
|
|
51
|
+
* all resolve to null — reported as UNKNOWN and rendered as nothing, never as
|
|
52
|
+
* a guessed lock or a guessed all-clear, and never as a command failure. The
|
|
53
|
+
* command exists to answer "who am I"; the plan line is decoration on top.
|
|
54
|
+
*/
|
|
55
|
+
async function readPlanLock(company) {
|
|
56
|
+
const slug = company?.trim();
|
|
57
|
+
if (!slug)
|
|
58
|
+
return null;
|
|
59
|
+
try {
|
|
60
|
+
// NEVER interactive: this runs inside `--json` (before the authenticated
|
|
61
|
+
// check) and inside hq-core's per-turn plan-lock refresh hook. With the
|
|
62
|
+
// default `interactive: true`, a box with no cached session would launch a
|
|
63
|
+
// browser sign-in and block the caller instead of resolving to UNKNOWN.
|
|
64
|
+
const token = await ensureCognitoToken({ interactive: false });
|
|
65
|
+
return await fetchPlanLockStatus(token, slug);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Human rendering of the plan state: the full WORKSPACE LOCKED block when the
|
|
73
|
+
* workspace is locked, the one-line `Plan: Starter — …` orientation when it is
|
|
74
|
+
* Starter and healthy, and nothing at all otherwise.
|
|
75
|
+
*/
|
|
76
|
+
export function renderWhoamiPlanBlock(status) {
|
|
77
|
+
if (status.lock.locked) {
|
|
78
|
+
return colorizePlanLockNotice(renderPlanLockNotice(status));
|
|
79
|
+
}
|
|
80
|
+
return renderPlanLine(status);
|
|
81
|
+
}
|
|
82
|
+
async function printPlanBlock(company) {
|
|
83
|
+
const status = await readPlanLock(company);
|
|
84
|
+
if (!status)
|
|
85
|
+
return;
|
|
86
|
+
const block = renderWhoamiPlanBlock(status);
|
|
87
|
+
if (block)
|
|
88
|
+
console.log(block);
|
|
89
|
+
}
|
|
43
90
|
export function registerWhoamiCommand(program) {
|
|
44
91
|
program
|
|
45
92
|
.command('whoami')
|
|
46
93
|
.description('Show the currently authenticated user')
|
|
47
94
|
.option('--json', 'Output identity metadata as JSON (never tokens)')
|
|
95
|
+
.option('--company <slug>', 'Also report this company\'s workspace plan state (Starter lock)')
|
|
48
96
|
.action(async (opts) => {
|
|
49
97
|
try {
|
|
50
98
|
// Effective source (same as auth status / ensureCognitoToken): env
|
|
@@ -67,6 +115,9 @@ export function registerWhoamiCommand(program) {
|
|
|
67
115
|
const agentUid = machineCreds?.entityUid !== undefined
|
|
68
116
|
? (machineCreds.entityUid.startsWith('agt_') ? machineCreds.entityUid : null)
|
|
69
117
|
: boundLegacyAgent;
|
|
118
|
+
// planLock is additive: absent-or-null means UNKNOWN, which is what
|
|
119
|
+
// every consumer already sees from a backend that predates it.
|
|
120
|
+
const planLock = await readPlanLock(opts.company);
|
|
70
121
|
console.log(JSON.stringify({
|
|
71
122
|
schemaVersion: 1,
|
|
72
123
|
authenticated: machine ? Boolean(machineCreds) : Boolean(cached && !isExpiring(cached, 0)),
|
|
@@ -75,6 +126,7 @@ export function registerWhoamiCommand(program) {
|
|
|
75
126
|
personUid: !machine && identity.entityType === 'person' ? identity.entityUid ?? null : null,
|
|
76
127
|
agentUid,
|
|
77
128
|
username: machineCreds?.username ?? null,
|
|
129
|
+
planLock,
|
|
78
130
|
}, null, 2));
|
|
79
131
|
return;
|
|
80
132
|
}
|
|
@@ -88,6 +140,7 @@ export function registerWhoamiCommand(program) {
|
|
|
88
140
|
: undefined;
|
|
89
141
|
console.log(`Machine identity ${username}${entityUid ? ` (agent ${entityUid})` : ''} — sessions mint automatically`);
|
|
90
142
|
console.log(`token source: machine`);
|
|
143
|
+
await printPlanBlock(opts.company);
|
|
91
144
|
return;
|
|
92
145
|
}
|
|
93
146
|
if (!cached) {
|
|
@@ -101,6 +154,7 @@ export function registerWhoamiCommand(program) {
|
|
|
101
154
|
}
|
|
102
155
|
const { email, sub } = resolveWhoamiIdentity(peekIdToken(cached.idToken));
|
|
103
156
|
console.log(`Logged in as ${email ?? 'unknown'}${sub ? ` (${sub})` : ''}`);
|
|
157
|
+
await printPlanBlock(opts.company);
|
|
104
158
|
}
|
|
105
159
|
catch (error) {
|
|
106
160
|
console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error');
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
import * as fs from "node:fs";
|
|
19
19
|
import * as path from "node:path";
|
|
20
20
|
import { vaultApiFetch } from "../../../utils/vault-api.js";
|
|
21
|
-
import { warmMeshConversationCache } from "../../mesh/api.js";
|
|
22
21
|
import { writeComponentStatus } from "../creds.js";
|
|
23
22
|
export const SEEN_IDS_NAME = "seen-ids.json";
|
|
24
23
|
export const INBOX_JSONL_NAME = "inbox.jsonl";
|
|
@@ -115,7 +114,8 @@ export function defaultAckItem(agentUid, apiBaseUrl) {
|
|
|
115
114
|
export async function pollInboxOnce(deps) {
|
|
116
115
|
const fetchInbox = deps.fetchInbox ?? defaultFetchInbox(deps.agentUid, deps.apiBaseUrl);
|
|
117
116
|
const ackItem = deps.ackItem ?? defaultAckItem(deps.agentUid, deps.apiBaseUrl);
|
|
118
|
-
|
|
117
|
+
// No default cache warm: the human conversation routes 403 for agent callers.
|
|
118
|
+
const warmCache = deps.warmCache ?? (async () => { });
|
|
119
119
|
let token;
|
|
120
120
|
try {
|
|
121
121
|
token = await deps.getToken();
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* is stamped ok while subscribed and refreshing, error otherwise.
|
|
21
21
|
*/
|
|
22
22
|
import mqtt from "mqtt";
|
|
23
|
-
import {
|
|
23
|
+
import { defaultFetchInbox } from "./inbox.js";
|
|
24
24
|
import { createPersonalRealtimeFetcher, MQTT_KEEPALIVE_SECONDS, renewalDelayMs, } from "../../mesh/live/daemon/credentials.js";
|
|
25
25
|
import { presignIotWssUrl } from "../../mesh/live/daemon/presign.js";
|
|
26
26
|
import { writeComponentStatus } from "../creds.js";
|
|
@@ -54,7 +54,16 @@ export async function startMeshListener(deps) {
|
|
|
54
54
|
const setT = deps.setTimeout ?? ((fn, ms) => setTimeout(fn, ms));
|
|
55
55
|
const clearT = deps.clearTimeout ?? ((h) => clearTimeout(h));
|
|
56
56
|
const connect = deps.connect ?? ((url, opts) => mqtt.connect(url, opts));
|
|
57
|
-
|
|
57
|
+
// Agent callers are refused by the human conversation routes
|
|
58
|
+
// (/v1/notify/inbox|contacts|thread → 403 AGENT_ROUTE_NOT_SUPPORTED), so the
|
|
59
|
+
// default refetch reads the agent's own inbox, the agent-authorized source.
|
|
60
|
+
const refetch = deps.refetch ??
|
|
61
|
+
(async (token, uid) => {
|
|
62
|
+
const res = await defaultFetchInbox(uid, deps.apiBaseUrl)(token);
|
|
63
|
+
if (res.status !== 200) {
|
|
64
|
+
throw new Error(`GET ${res.url ?? "agent inbox"} → ${res.status}${res.bodyText ? ` body=${res.bodyText}` : ""}`);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
58
67
|
const fetchCredentials = deps.fetchCredentials ??
|
|
59
68
|
(async () => {
|
|
60
69
|
const token = await deps.getToken();
|
|
@@ -114,8 +114,8 @@ Rules:
|
|
|
114
114
|
body: `# Work mesh status
|
|
115
115
|
|
|
116
116
|
The work mesh is HQ's live board of who is working on what. It is the source
|
|
117
|
-
of truth for project stories and active sessions
|
|
118
|
-
|
|
117
|
+
of truth for project stories and active sessions. The kit's listener re-polls
|
|
118
|
+
this agent's inbox whenever a doorbell rings.
|
|
119
119
|
|
|
120
120
|
\`\`\`bash
|
|
121
121
|
hq mesh session status --company <slug> # who is working on what right now
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `plan-lock` (starter-plan-hard-limits / US-011) — the CLI-side read + render
|
|
3
|
+
* of the workspace plan lock.
|
|
4
|
+
*
|
|
5
|
+
* Starter (free) workspaces are capped at 5 members and 0 integrations. Going
|
|
6
|
+
* over locks the workspace immediately: it becomes read-only until the owner
|
|
7
|
+
* trims back under the caps or upgrades to HQ Workforce. The lock decision is
|
|
8
|
+
* NOT made here — hq-pro's `src/billing/plan-lock.ts` is the single source of
|
|
9
|
+
* truth and ships the answer on `GET /membership/me` as a per-company
|
|
10
|
+
* `planLock` object. This module only reads that field and renders it.
|
|
11
|
+
*
|
|
12
|
+
* Member counts are decoration, never a second opinion: the count comes from
|
|
13
|
+
* `GET /v1/billing/usage-limits` on a best-effort basis and its absence only
|
|
14
|
+
* removes the "n of 5" detail from the notice. An absent field is UNKNOWN, so
|
|
15
|
+
* nothing here ever infers a lock (or an unlock) from missing data
|
|
16
|
+
* (hq-absent-field-never-means-constraining-value).
|
|
17
|
+
*/
|
|
18
|
+
/** Mirror of hq-pro's `PlanLockReason`. */
|
|
19
|
+
export type PlanLockReason = "users" | "integrations";
|
|
20
|
+
/** Mirror of hq-pro's `PlanLock` wire shape (see src/billing/plan-lock.ts). */
|
|
21
|
+
export interface PlanLock {
|
|
22
|
+
locked: boolean;
|
|
23
|
+
reasons: PlanLockReason[];
|
|
24
|
+
upgradeUrl: string;
|
|
25
|
+
fixOptions: {
|
|
26
|
+
removeMembersTo: number;
|
|
27
|
+
disconnectIntegrations: boolean;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Optional member decoration for the notice ("7 of 5 members"). */
|
|
31
|
+
export interface PlanLockMembers {
|
|
32
|
+
used: number;
|
|
33
|
+
limit: number;
|
|
34
|
+
}
|
|
35
|
+
export interface PlanLockStatus {
|
|
36
|
+
companySlug: string;
|
|
37
|
+
companyUid?: string;
|
|
38
|
+
lock: PlanLock;
|
|
39
|
+
members?: PlanLockMembers;
|
|
40
|
+
/**
|
|
41
|
+
* Resolved plan id, when the usage endpoint answered. UNKNOWN (absent) when
|
|
42
|
+
* it did not — surfaces that gate on "is this Starter?" must then stay
|
|
43
|
+
* silent rather than assume either way.
|
|
44
|
+
*/
|
|
45
|
+
plan?: "free" | "paid" | "enterprise";
|
|
46
|
+
checkedAt: string;
|
|
47
|
+
}
|
|
48
|
+
/** Starter member cap quoted when the server did not send `removeMembersTo`. */
|
|
49
|
+
export declare const STARTER_MEMBER_TARGET = 5;
|
|
50
|
+
/** Upgrade destination quoted when the server did not send one. */
|
|
51
|
+
export declare const DEFAULT_UPGRADE_URL = "https://hq.computer/billing";
|
|
52
|
+
/** The paid plan the lock wall sends owners to. Copy lives in ONE place. */
|
|
53
|
+
export declare const WORKFORCE_PLAN_LABEL = "HQ Workforce ($500/mo)";
|
|
54
|
+
/**
|
|
55
|
+
* Defensively parse a `planLock` payload. Anything malformed returns null —
|
|
56
|
+
* the caller then behaves exactly as if the server had sent nothing, which is
|
|
57
|
+
* "unknown", not "locked".
|
|
58
|
+
*/
|
|
59
|
+
export declare function parsePlanLock(value: unknown): PlanLock | null;
|
|
60
|
+
/**
|
|
61
|
+
* Pick the membership row for `companySlug` (or `cmp_…` uid) out of a decoded
|
|
62
|
+
* `/membership/me` body and return its parsed lock. No matching row — or no
|
|
63
|
+
* `planLock` on it — is UNKNOWN, so this returns null rather than guessing.
|
|
64
|
+
*/
|
|
65
|
+
export declare function selectPlanLock(body: unknown, companyRef: string): {
|
|
66
|
+
lock: PlanLock;
|
|
67
|
+
companyUid?: string;
|
|
68
|
+
} | null;
|
|
69
|
+
/** Parse the resolved plan id out of a usage-limits body. Absent → null. */
|
|
70
|
+
export declare function selectPlan(body: unknown): "free" | "paid" | "enterprise" | null;
|
|
71
|
+
/** Parse the `users` dimension out of a usage-limits body. Absent → null. */
|
|
72
|
+
export declare function selectMemberUsage(body: unknown): PlanLockMembers | null;
|
|
73
|
+
/**
|
|
74
|
+
* Read the live lock for one company: `planLock` from `/membership/me`, plus a
|
|
75
|
+
* best-effort member count for the notice. Returns null when the server did not
|
|
76
|
+
* answer with a lock for this company — callers must then say nothing.
|
|
77
|
+
*/
|
|
78
|
+
export declare function fetchPlanLockStatus(token: string, companyRef: string, opts?: {
|
|
79
|
+
timeoutMs?: number;
|
|
80
|
+
}): Promise<PlanLockStatus | null>;
|
|
81
|
+
/**
|
|
82
|
+
* The full WORKSPACE LOCKED block: why it locked, where the workspace stands
|
|
83
|
+
* against the cap, and the two fixes. Plain text — colour is applied by the
|
|
84
|
+
* caller so scripts capturing stdout get a clean block.
|
|
85
|
+
*/
|
|
86
|
+
export declare function renderPlanLockNotice(status: PlanLockStatus): string;
|
|
87
|
+
/**
|
|
88
|
+
* The one-line form injected on every turn while the workspace stays locked.
|
|
89
|
+
* Kept to a single line on purpose — it repeats each turn.
|
|
90
|
+
*/
|
|
91
|
+
export declare function renderPlanLockLine(status: PlanLockStatus): string;
|
|
92
|
+
/**
|
|
93
|
+
* The `Plan: …` orientation line. Starter only: a paid or enterprise
|
|
94
|
+
* workspace — and an UNKNOWN plan — gets no line at all.
|
|
95
|
+
*/
|
|
96
|
+
export declare function renderPlanLine(status: PlanLockStatus): string | null;
|
|
97
|
+
/** Colourised block for interactive output. */
|
|
98
|
+
export declare function colorizePlanLockNotice(notice: string): string;
|
|
99
|
+
//# sourceMappingURL=plan-lock.d.ts.map
|