@indigoai-us/hq-cli 5.345.110 → 5.345.111
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 +26 -0
- package/dist/command-catalog.generated.d.ts +17 -5
- package/dist/command-catalog.generated.js +20 -5
- package/dist/commands/capacity.d.ts +26 -0
- package/dist/commands/capacity.js +64 -2
- package/dist/commands/db-provision.js +31 -10
- package/dist/commands/db-remote-deps.js +12 -0
- package/dist/commands/db-sql.js +42 -9
- package/dist/commands/db-status.d.ts +2 -1
- package/dist/commands/db-status.js +33 -7
- package/dist/lib/bot/brief-job.d.ts +22 -0
- package/dist/lib/bot/brief-job.js +38 -0
- package/dist/lib/bot/run.d.ts +10 -0
- package/dist/lib/bot/run.js +54 -9
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +225 -2
- package/dist/lib/capacity/collectors/claude-oauth.js +1053 -43
- package/dist/lib/capacity/ledger.d.ts +10 -1
- package/dist/lib/capacity/ledger.js +8 -1
- package/dist/lib/capacity/project.js +3 -0
- package/dist/lib/db/control-plane.d.ts +22 -0
- package/dist/lib/db/control-plane.js +50 -0
- package/dist/lib/db/remote-engine.d.ts +9 -1
- package/dist/lib/db/remote-engine.js +32 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.111] — 2026-10-13
|
|
6
|
+
- The local bot runs a Create brief job from the desktop app in a fresh model session of its own instead of resuming the owner's whole DM conversation, and schedules it like a DM rather than behind the job slot. A resumed session that fails twice in a row with the model CLI's "An unknown error occurred (Unexpected)" is cleared so the next turn starts fresh; a single failure never clears it.
|
|
7
|
+
|
|
8
|
+
- Add `--personal` support to `hq db provision`, `status`, and `sql` for a person's remote database.
|
|
9
|
+
- `hq capacity` now keeps its Claude OAuth sessions awake on its own: when a
|
|
10
|
+
token is about to expire it refreshes in the background, with per-home
|
|
11
|
+
backoff so busy lanes do not thrash the auth endpoint. If a refresh is
|
|
12
|
+
rejected (revoked or invalid grant), the account is reported as "signed
|
|
13
|
+
out: sign-in revoked" and the printed fix is `hq capacity login claude
|
|
14
|
+
--account <id> --repair` for a managed non-default home, or `claude auth
|
|
15
|
+
login` for the default home.
|
|
16
|
+
- Refreshes fail closed when the live-lane or running-process gates cannot
|
|
17
|
+
be verified (unexpected `hq lanes list --json` shape, `ps` failure, or
|
|
18
|
+
darwin credential pre-read failure with a prior refresh token), so a
|
|
19
|
+
rotation never races a running Claude Code.
|
|
20
|
+
- Doctor's refresh error record stores only the HTTP status and a fixed
|
|
21
|
+
classification (`invalid_grant`, `revoked`, `server_error`, `timeout`),
|
|
22
|
+
never a response body.
|
|
23
|
+
- An expired Claude account whose newest usage reading predates the token
|
|
24
|
+
expiry is now flagged as stale, so the router does not trust an old
|
|
25
|
+
reading as live headroom.
|
|
26
|
+
- `hq capacity doctor` adds a per-account Claude table showing token age,
|
|
27
|
+
time to expiry, last refresh attempt and outcome, and any backoff
|
|
28
|
+
window. All strings in the report are passed through a token-redaction
|
|
29
|
+
scrubber before display.
|
|
30
|
+
|
|
5
31
|
## [5.345.110] — 2026-10-13
|
|
6
32
|
- The bundled sync runner now uses hq-cloud 6.18.69, which compacts the sync-state write-ahead log into a snapshot once the log grows past a size-scaled ratio of the snapshot, so large companies stop accumulating oversized sync-state files.
|
|
7
33
|
- Remove the three policy-family shell rollback copies after the native route's clean release soak. `hq core policy age-report`, `policy retire`, and `policy frontmatter` now use the native route regardless of the retained `core-native-policy` flag, which still gates `knowledge-prefs`.
|
|
@@ -1916,7 +1916,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
1916
1916
|
readonly options: readonly [];
|
|
1917
1917
|
readonly subcommands: readonly [{
|
|
1918
1918
|
readonly name: "status";
|
|
1919
|
-
readonly description: "Show local
|
|
1919
|
+
readonly description: "Show company local/remote status or your personal database status";
|
|
1920
1920
|
readonly aliases: readonly [];
|
|
1921
1921
|
readonly hidden: false;
|
|
1922
1922
|
readonly helpOption: true;
|
|
@@ -1926,7 +1926,11 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
1926
1926
|
readonly arguments: readonly [];
|
|
1927
1927
|
readonly options: readonly [{
|
|
1928
1928
|
readonly flags: "--company <slug>";
|
|
1929
|
-
readonly description: "Company slug (tenant scope
|
|
1929
|
+
readonly description: "Company slug (tenant scope)";
|
|
1930
|
+
readonly hidden: false;
|
|
1931
|
+
}, {
|
|
1932
|
+
readonly flags: "--personal";
|
|
1933
|
+
readonly description: "Show your personal database status";
|
|
1930
1934
|
readonly hidden: false;
|
|
1931
1935
|
}, {
|
|
1932
1936
|
readonly flags: "--home <path>";
|
|
@@ -1945,7 +1949,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
1945
1949
|
readonly subcommands: readonly [];
|
|
1946
1950
|
}, {
|
|
1947
1951
|
readonly name: "sql";
|
|
1948
|
-
readonly description: "Run SQL against
|
|
1952
|
+
readonly description: "Run read-only SQL against a company or personal database; use --write for changes";
|
|
1949
1953
|
readonly aliases: readonly [];
|
|
1950
1954
|
readonly hidden: false;
|
|
1951
1955
|
readonly helpOption: true;
|
|
@@ -1960,7 +1964,11 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
1960
1964
|
}];
|
|
1961
1965
|
readonly options: readonly [{
|
|
1962
1966
|
readonly flags: "--company <slug>";
|
|
1963
|
-
readonly description: "Company slug (tenant scope
|
|
1967
|
+
readonly description: "Company slug (tenant scope)";
|
|
1968
|
+
readonly hidden: false;
|
|
1969
|
+
}, {
|
|
1970
|
+
readonly flags: "--personal";
|
|
1971
|
+
readonly description: "Run against your personal remote database";
|
|
1964
1972
|
readonly hidden: false;
|
|
1965
1973
|
}, {
|
|
1966
1974
|
readonly flags: "--app <app>";
|
|
@@ -2027,7 +2035,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
2027
2035
|
readonly subcommands: readonly [];
|
|
2028
2036
|
}, {
|
|
2029
2037
|
readonly name: "provision";
|
|
2030
|
-
readonly description: "Create
|
|
2038
|
+
readonly description: "Create a company remote database or your personal database";
|
|
2031
2039
|
readonly aliases: readonly [];
|
|
2032
2040
|
readonly hidden: false;
|
|
2033
2041
|
readonly helpOption: true;
|
|
@@ -2039,6 +2047,10 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
2039
2047
|
readonly flags: "--company <slug>";
|
|
2040
2048
|
readonly description: "Company slug";
|
|
2041
2049
|
readonly hidden: false;
|
|
2050
|
+
}, {
|
|
2051
|
+
readonly flags: "--personal";
|
|
2052
|
+
readonly description: "Provision your personal database";
|
|
2053
|
+
readonly hidden: false;
|
|
2042
2054
|
}, {
|
|
2043
2055
|
readonly flags: "--region <region>";
|
|
2044
2056
|
readonly description: "AWS region";
|
|
@@ -2298,7 +2298,7 @@ export const COMMAND_CATALOG = [
|
|
|
2298
2298
|
"subcommands": [
|
|
2299
2299
|
{
|
|
2300
2300
|
"name": "status",
|
|
2301
|
-
"description": "Show local
|
|
2301
|
+
"description": "Show company local/remote status or your personal database status",
|
|
2302
2302
|
"aliases": [],
|
|
2303
2303
|
"hidden": false,
|
|
2304
2304
|
"helpOption": true,
|
|
@@ -2309,7 +2309,12 @@ export const COMMAND_CATALOG = [
|
|
|
2309
2309
|
"options": [
|
|
2310
2310
|
{
|
|
2311
2311
|
"flags": "--company <slug>",
|
|
2312
|
-
"description": "Company slug (tenant scope
|
|
2312
|
+
"description": "Company slug (tenant scope)",
|
|
2313
|
+
"hidden": false
|
|
2314
|
+
},
|
|
2315
|
+
{
|
|
2316
|
+
"flags": "--personal",
|
|
2317
|
+
"description": "Show your personal database status",
|
|
2313
2318
|
"hidden": false
|
|
2314
2319
|
},
|
|
2315
2320
|
{
|
|
@@ -2333,7 +2338,7 @@ export const COMMAND_CATALOG = [
|
|
|
2333
2338
|
},
|
|
2334
2339
|
{
|
|
2335
2340
|
"name": "sql",
|
|
2336
|
-
"description": "Run SQL against
|
|
2341
|
+
"description": "Run read-only SQL against a company or personal database; use --write for changes",
|
|
2337
2342
|
"aliases": [],
|
|
2338
2343
|
"hidden": false,
|
|
2339
2344
|
"helpOption": true,
|
|
@@ -2351,7 +2356,12 @@ export const COMMAND_CATALOG = [
|
|
|
2351
2356
|
"options": [
|
|
2352
2357
|
{
|
|
2353
2358
|
"flags": "--company <slug>",
|
|
2354
|
-
"description": "Company slug (tenant scope
|
|
2359
|
+
"description": "Company slug (tenant scope)",
|
|
2360
|
+
"hidden": false
|
|
2361
|
+
},
|
|
2362
|
+
{
|
|
2363
|
+
"flags": "--personal",
|
|
2364
|
+
"description": "Run against your personal remote database",
|
|
2355
2365
|
"hidden": false
|
|
2356
2366
|
},
|
|
2357
2367
|
{
|
|
@@ -2433,7 +2443,7 @@ export const COMMAND_CATALOG = [
|
|
|
2433
2443
|
},
|
|
2434
2444
|
{
|
|
2435
2445
|
"name": "provision",
|
|
2436
|
-
"description": "Create
|
|
2446
|
+
"description": "Create a company remote database or your personal database",
|
|
2437
2447
|
"aliases": [],
|
|
2438
2448
|
"hidden": false,
|
|
2439
2449
|
"helpOption": true,
|
|
@@ -2447,6 +2457,11 @@ export const COMMAND_CATALOG = [
|
|
|
2447
2457
|
"description": "Company slug",
|
|
2448
2458
|
"hidden": false
|
|
2449
2459
|
},
|
|
2460
|
+
{
|
|
2461
|
+
"flags": "--personal",
|
|
2462
|
+
"description": "Provision your personal database",
|
|
2463
|
+
"hidden": false
|
|
2464
|
+
},
|
|
2450
2465
|
{
|
|
2451
2466
|
"flags": "--region <region>",
|
|
2452
2467
|
"description": "AWS region",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { Command } from "commander";
|
|
12
12
|
import { type CapacityState, type LedgerEvent, type LedgerOptions, type NowView, type ParserFailureEvent, type Reading, type ReadingSource } from "../lib/capacity/ledger.js";
|
|
13
13
|
import { type CapacityAccount } from "../lib/capacity/accounts.js";
|
|
14
|
+
import { type ClaudeRefreshLastEntry } from "../lib/capacity/collectors/claude-oauth.js";
|
|
14
15
|
export declare const CAPACITY_SCHEMA_VERSION: "hq-capacity-now-view.v1";
|
|
15
16
|
export declare const DOCTOR_SCHEMA_VERSION = "hq-capacity-doctor.v1";
|
|
16
17
|
export declare const PARSER_FAILURE_WINDOW_MS: number;
|
|
@@ -40,6 +41,30 @@ export interface DoctorVersion {
|
|
|
40
41
|
installed: string | null;
|
|
41
42
|
match: boolean;
|
|
42
43
|
}
|
|
44
|
+
export interface DoctorClaudeAccount {
|
|
45
|
+
/** Account label from the capacity registry (eg "default"). */
|
|
46
|
+
label: string;
|
|
47
|
+
/** Resolved credential home directory. */
|
|
48
|
+
home: string;
|
|
49
|
+
/** ISO timestamp of the last refresh attempt, redacted. Null when nothing was tried for this home. */
|
|
50
|
+
lastRefreshAttempt: string | null;
|
|
51
|
+
/** Outcome of the last refresh attempt. Null when nothing was tried. */
|
|
52
|
+
lastRefreshOutcome: ClaudeRefreshLastEntry["outcome"] | null;
|
|
53
|
+
/** ms since the last refresh attempt. Null when nothing was tried. */
|
|
54
|
+
tokenAgeMs: number | null;
|
|
55
|
+
/** ms until the token's recorded expiry. Negative when past expiry. Null when the collector has not recorded one. */
|
|
56
|
+
timeToExpiryMs: number | null;
|
|
57
|
+
/** ISO expiry of the token the entry describes. Null when unknown. */
|
|
58
|
+
tokenExpiresAt: string | null;
|
|
59
|
+
/** ISO backoff window end, when the last outcome set one. Null otherwise. */
|
|
60
|
+
backoffUntil: string | null;
|
|
61
|
+
/** Short, token-free diagnostic from the last attempt. Null otherwise. Never contains a response body. */
|
|
62
|
+
errorMessage: string | null;
|
|
63
|
+
/** Fixed classification string for the last error outcome. Null otherwise. */
|
|
64
|
+
errorClass: ClaudeRefreshLastEntry["errorClass"] | null;
|
|
65
|
+
/** HTTP status from the last attempt that reached the token endpoint. Null otherwise. */
|
|
66
|
+
httpStatus: number | null;
|
|
67
|
+
}
|
|
43
68
|
export interface DoctorReport {
|
|
44
69
|
schemaVersion: typeof DOCTOR_SCHEMA_VERSION;
|
|
45
70
|
collector: "on" | "off";
|
|
@@ -50,6 +75,7 @@ export interface DoctorReport {
|
|
|
50
75
|
provider: string;
|
|
51
76
|
state: CapacityState;
|
|
52
77
|
}[];
|
|
78
|
+
claudeAccounts: DoctorClaudeAccount[];
|
|
53
79
|
ok: boolean;
|
|
54
80
|
stale: boolean;
|
|
55
81
|
ageMs: number;
|
|
@@ -3,9 +3,9 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import * as readline from "node:readline";
|
|
6
|
-
import { LedgerCorrupt, LedgerMissing, READING_SOURCES, capacityOptionsFromEnv, capacityRoot, nowViewJsonSchema, readCollectorMark, readLedgerEvents, readNow, rebuildNow, setCollectorEnabled, } from "../lib/capacity/ledger.js";
|
|
6
|
+
import { LedgerCorrupt, LedgerMissing, READING_SOURCES, capacityOptionsFromEnv, capacityRoot, nowViewJsonSchema, readCollectorMark, readLedgerEvents, readNow, rebuildNow, redactProviderTokens, setCollectorEnabled, } from "../lib/capacity/ledger.js";
|
|
7
7
|
import { listCapacityAccounts, registryAccountIds } from "../lib/capacity/accounts.js";
|
|
8
|
-
import { claudeIdentityKeys } from "../lib/capacity/collectors/claude-oauth.js";
|
|
8
|
+
import { claudeIdentityKeys, readClaudeRefreshLast } from "../lib/capacity/collectors/claude-oauth.js";
|
|
9
9
|
import { projectCapacityView } from "../lib/capacity/project.js";
|
|
10
10
|
import { capacityWidgetTimeZone, renderCapacityWidget } from "../lib/capacity/widget.js";
|
|
11
11
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
@@ -185,6 +185,7 @@ export function buildDoctorReport(options) {
|
|
|
185
185
|
provider: provider.provider,
|
|
186
186
|
state: provider.state,
|
|
187
187
|
}));
|
|
188
|
+
const claudeAccounts = buildClaudeAccountDoctor(resolved.options, resolved.nowMs);
|
|
188
189
|
const freshness = capacityFreshness(view.generatedAt, resolved.nowMs);
|
|
189
190
|
return {
|
|
190
191
|
schemaVersion: DOCTOR_SCHEMA_VERSION,
|
|
@@ -193,11 +194,56 @@ export function buildDoctorReport(options) {
|
|
|
193
194
|
versions,
|
|
194
195
|
parserFailures,
|
|
195
196
|
providers,
|
|
197
|
+
claudeAccounts,
|
|
196
198
|
ok: providers.every((provider) => provider.state !== "unknown"),
|
|
197
199
|
stale: freshness.stale,
|
|
198
200
|
ageMs: freshness.ageMs,
|
|
199
201
|
};
|
|
200
202
|
}
|
|
203
|
+
function buildClaudeAccountDoctor(options, nowMs) {
|
|
204
|
+
const env = options.env ?? process.env;
|
|
205
|
+
const home = options.home ?? env.HOME;
|
|
206
|
+
const root = options.root ?? capacityRoot(env, home);
|
|
207
|
+
const accounts = listCapacityAccounts({ env, hqRoot: env.HQ_ROOT, home })
|
|
208
|
+
.filter((account) => account.provider === "claude");
|
|
209
|
+
return accounts.map((account) => {
|
|
210
|
+
const entry = readClaudeRefreshLast(root, account.home);
|
|
211
|
+
if (!entry) {
|
|
212
|
+
return {
|
|
213
|
+
label: account.label,
|
|
214
|
+
home: account.home,
|
|
215
|
+
lastRefreshAttempt: null,
|
|
216
|
+
lastRefreshOutcome: null,
|
|
217
|
+
tokenAgeMs: null,
|
|
218
|
+
timeToExpiryMs: null,
|
|
219
|
+
tokenExpiresAt: null,
|
|
220
|
+
backoffUntil: null,
|
|
221
|
+
errorMessage: null,
|
|
222
|
+
errorClass: null,
|
|
223
|
+
httpStatus: null,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const attemptedAtMs = Date.parse(entry.attemptedAt);
|
|
227
|
+
const expiresAtMs = entry.tokenExpiresAt ? Date.parse(entry.tokenExpiresAt) : NaN;
|
|
228
|
+
const tokenAgeMs = entry.outcome === "refreshed" && Number.isFinite(attemptedAtMs)
|
|
229
|
+
? nowMs - attemptedAtMs
|
|
230
|
+
: null;
|
|
231
|
+
const timeToExpiryMs = Number.isFinite(expiresAtMs) ? expiresAtMs - nowMs : null;
|
|
232
|
+
return {
|
|
233
|
+
label: account.label,
|
|
234
|
+
home: account.home,
|
|
235
|
+
lastRefreshAttempt: redactProviderTokens(entry.attemptedAt),
|
|
236
|
+
lastRefreshOutcome: entry.outcome,
|
|
237
|
+
tokenAgeMs,
|
|
238
|
+
timeToExpiryMs,
|
|
239
|
+
tokenExpiresAt: entry.tokenExpiresAt ?? null,
|
|
240
|
+
backoffUntil: entry.backoffUntil ?? null,
|
|
241
|
+
errorMessage: entry.errorMessage ? redactProviderTokens(entry.errorMessage) : null,
|
|
242
|
+
errorClass: entry.errorClass ?? null,
|
|
243
|
+
httpStatus: typeof entry.httpStatus === "number" ? entry.httpStatus : null,
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
}
|
|
201
247
|
export function formatDoctor(report) {
|
|
202
248
|
const sources = formatTable(["SOURCE", "LAST SUCCESS"], report.sources.map((source) => [source.source, source.lastSuccess ?? "—"]));
|
|
203
249
|
const versions = formatTable(["PROVIDER", "PINNED", "INSTALLED", "MATCH"], report.versions.map((version) => [
|
|
@@ -212,6 +258,16 @@ export function formatDoctor(report) {
|
|
|
212
258
|
const states = report.providers.length === 0
|
|
213
259
|
? "none"
|
|
214
260
|
: report.providers.map((provider) => `${provider.provider} ${provider.state}`).join("\n");
|
|
261
|
+
const claudeAccounts = report.claudeAccounts.length === 0
|
|
262
|
+
? "none"
|
|
263
|
+
: formatTable(["ACCOUNT", "LAST ATTEMPT", "OUTCOME", "TOKEN AGE", "EXPIRES IN", "BACKOFF UNTIL"], report.claudeAccounts.map((account) => [
|
|
264
|
+
account.label,
|
|
265
|
+
account.lastRefreshAttempt ?? "—",
|
|
266
|
+
account.lastRefreshOutcome ?? "—",
|
|
267
|
+
account.tokenAgeMs === null ? "—" : formatSignedSpan(account.tokenAgeMs),
|
|
268
|
+
account.timeToExpiryMs === null ? "—" : formatSignedSpan(account.timeToExpiryMs),
|
|
269
|
+
account.backoffUntil ?? "—",
|
|
270
|
+
]));
|
|
215
271
|
const warning = report.stale
|
|
216
272
|
? report.ageMs === 0
|
|
217
273
|
? "collector not running, numbers may be stale; last update unknown"
|
|
@@ -226,8 +282,14 @@ export function formatDoctor(report) {
|
|
|
226
282
|
failures,
|
|
227
283
|
"provider state",
|
|
228
284
|
states,
|
|
285
|
+
"claude accounts",
|
|
286
|
+
claudeAccounts,
|
|
229
287
|
].join("\n");
|
|
230
288
|
}
|
|
289
|
+
function formatSignedSpan(ms) {
|
|
290
|
+
const span = formatSpan(Math.floor(Math.abs(ms) / 1000));
|
|
291
|
+
return ms < 0 ? `-${span}` : span;
|
|
292
|
+
}
|
|
231
293
|
export function registerCapacityCommand(program) {
|
|
232
294
|
const capacity = program
|
|
233
295
|
.command("capacity")
|
|
@@ -9,24 +9,43 @@ import { defaultRemoteDeps, describeRemoteError, } from "./db-remote-deps.js";
|
|
|
9
9
|
const defaultDeps = defaultRemoteDeps;
|
|
10
10
|
export function registerDbProvisionCommand(db, depsFactory = defaultDeps) {
|
|
11
11
|
db.command("provision")
|
|
12
|
-
.description("Create
|
|
13
|
-
.
|
|
12
|
+
.description("Create a company remote database or your personal database")
|
|
13
|
+
.option("--company <slug>", "Company slug")
|
|
14
|
+
.option("--personal", "Provision your personal database")
|
|
14
15
|
.option("--region <region>", "AWS region", "us-east-1")
|
|
15
16
|
.action(async (opts) => {
|
|
16
17
|
try {
|
|
17
18
|
const deps = depsFactory();
|
|
18
19
|
const company = String(opts.company ?? "").trim();
|
|
19
|
-
if (
|
|
20
|
-
console.error(chalk.red("Error: --
|
|
20
|
+
if (opts.personal && opts.company !== undefined) {
|
|
21
|
+
console.error(chalk.red("Error: --personal cannot be used with --company"));
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (!opts.personal && !company) {
|
|
26
|
+
console.error(chalk.red("error: required option '--company <slug>' not specified"));
|
|
21
27
|
process.exitCode = 1;
|
|
22
28
|
return;
|
|
23
29
|
}
|
|
24
|
-
const resolved = await deps.resolveCompany(company);
|
|
25
30
|
const client = new ControlPlaneDbClient({
|
|
26
31
|
baseUrl: deps.apiBaseUrl,
|
|
27
32
|
getAccessToken: deps.getAccessToken,
|
|
28
33
|
fetchImpl: deps.fetchImpl,
|
|
29
34
|
});
|
|
35
|
+
if (opts.personal) {
|
|
36
|
+
const result = await client.personalProvision({ region: opts.region });
|
|
37
|
+
if (result.status === "provisioning") {
|
|
38
|
+
console.log("Your personal database is still being set up. Run this again in a minute.");
|
|
39
|
+
}
|
|
40
|
+
else if (result.idempotent || result.status === "existing") {
|
|
41
|
+
console.log(`Your personal database already exists and is ready (${result.region}).`);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
console.log(`Your personal database is ready (${result.region}).`);
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const resolved = await deps.resolveCompany(company);
|
|
30
49
|
const result = await client.provision({
|
|
31
50
|
companyUid: resolved.companyUid,
|
|
32
51
|
companySlug: resolved.companySlug,
|
|
@@ -50,11 +69,13 @@ export function registerDbProvisionCommand(db, depsFactory = defaultDeps) {
|
|
|
50
69
|
const status = error.status;
|
|
51
70
|
if (described) {
|
|
52
71
|
console.error(chalk.red("Error:"), described);
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
if (!opts.personal) {
|
|
73
|
+
if (status === 501 || error.code === "REMOTE_DB_NOT_AVAILABLE") {
|
|
74
|
+
console.error(chalk.dim("Local hq db status|sql|migrate still work offline."));
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
console.error(chalk.dim("Local databases still work: hq db status|sql|migrate."));
|
|
78
|
+
}
|
|
58
79
|
}
|
|
59
80
|
}
|
|
60
81
|
else if (status === 404) {
|
|
@@ -54,6 +54,18 @@ export function describeRemoteError(error) {
|
|
|
54
54
|
return error.message;
|
|
55
55
|
const status = error.status;
|
|
56
56
|
const code = error.code;
|
|
57
|
+
if (code === "PERSONAL_DB_NOT_ENABLED") {
|
|
58
|
+
return "Personal databases are not turned on for your account.";
|
|
59
|
+
}
|
|
60
|
+
if (code === "PERSONAL_DB_NOT_PROVISIONED") {
|
|
61
|
+
return "You don't have a personal database yet. Run: hq db provision --personal";
|
|
62
|
+
}
|
|
63
|
+
if (code === "PERSONAL_DB_APP_LIMIT") {
|
|
64
|
+
return "Your personal database already holds 8 apps, which is the limit.";
|
|
65
|
+
}
|
|
66
|
+
if (status === 404 && error.personalRoute) {
|
|
67
|
+
return "The HQ API does not support personal databases yet. Update the CLI or try again later.";
|
|
68
|
+
}
|
|
57
69
|
if (code === "REMOTE_DB_NOT_AVAILABLE" || status === 501) {
|
|
58
70
|
const reason = error.reason;
|
|
59
71
|
const message = error.message;
|
package/dist/commands/db-sql.js
CHANGED
|
@@ -8,10 +8,10 @@ import { Option } from "commander";
|
|
|
8
8
|
import chalk from "chalk";
|
|
9
9
|
import fs from "node:fs";
|
|
10
10
|
import { safeErrorMessage } from "../lib/db/redact.js";
|
|
11
|
-
import { formatRemoteResults, openRemoteAppConnection, runRemoteSql, } from "../lib/db/remote-engine.js";
|
|
11
|
+
import { formatRemoteResults, openRemoteAppConnection, openRemoteSessionConnection, runRemoteSql, } from "../lib/db/remote-engine.js";
|
|
12
12
|
import { formatSqlResult, runLocalSql } from "../lib/db/sql.js";
|
|
13
13
|
import { isPlanGateError } from "../utils/plan-gate-error.js";
|
|
14
|
-
import { defaultRemoteDeps, describeRemoteError, remoteEngineDeps, resolveAppId, } from "./db-remote-deps.js";
|
|
14
|
+
import { defaultRemoteDeps, controlPlaneClient, describeRemoteError, remoteEngineDeps, resolveAppId, } from "./db-remote-deps.js";
|
|
15
15
|
import { describeSessionGap } from "./db-dump.js";
|
|
16
16
|
function readSqlArg(sqlArgs, stdinFlag) {
|
|
17
17
|
if (stdinFlag || sqlArgs.length === 0) {
|
|
@@ -24,8 +24,9 @@ function readSqlArg(sqlArgs, stdinFlag) {
|
|
|
24
24
|
}
|
|
25
25
|
export function registerDbSqlCommand(db, remoteDepsFactory = defaultRemoteDeps) {
|
|
26
26
|
db.command("sql")
|
|
27
|
-
.description("Run SQL against
|
|
28
|
-
.
|
|
27
|
+
.description("Run read-only SQL against a company or personal database; use --write for changes")
|
|
28
|
+
.option("--company <slug>", "Company slug (tenant scope)")
|
|
29
|
+
.option("--personal", "Run against your personal remote database")
|
|
29
30
|
.option("--app <app>", "Run against this app's remote database (app id, name or subdomain)")
|
|
30
31
|
.option("--write", "Allow write/DDL statements (discouraged for ad-hoc DDL)", false)
|
|
31
32
|
.addOption(new Option("--remote", "Remote SQL is not available in this CLI build yet; local queries work without --remote").hideHelp())
|
|
@@ -38,8 +39,26 @@ export function registerDbSqlCommand(db, remoteDepsFactory = defaultRemoteDeps)
|
|
|
38
39
|
.action(async (sqlParts, opts) => {
|
|
39
40
|
try {
|
|
40
41
|
const company = String(opts.company ?? "").trim();
|
|
41
|
-
if (
|
|
42
|
-
console.error(chalk.red("Error: --
|
|
42
|
+
if (opts.personal && opts.company !== undefined) {
|
|
43
|
+
console.error(chalk.red("Error: --personal cannot be used with --company"));
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (opts.personal) {
|
|
48
|
+
const conflicts = [
|
|
49
|
+
opts.app !== undefined && "--app",
|
|
50
|
+
!!opts.remote && "--remote",
|
|
51
|
+
opts.dbPath !== undefined && "--db-path",
|
|
52
|
+
!!opts.allowCrossCompanyPath && "--allow-cross-company-path",
|
|
53
|
+
].filter(Boolean);
|
|
54
|
+
if (conflicts.length > 0) {
|
|
55
|
+
console.error(chalk.red(`Error: --personal cannot be used with ${conflicts.join(", ")}`));
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!opts.personal && !company) {
|
|
61
|
+
console.error(chalk.red("error: required option '--company <slug>' not specified"));
|
|
43
62
|
process.exitCode = 1;
|
|
44
63
|
return;
|
|
45
64
|
}
|
|
@@ -50,8 +69,12 @@ export function registerDbSqlCommand(db, remoteDepsFactory = defaultRemoteDeps)
|
|
|
50
69
|
}
|
|
51
70
|
const sql = readSqlArg(sqlParts, !!opts.stdin).trim();
|
|
52
71
|
const format = opts.format === "table" ? "table" : "jsonl";
|
|
72
|
+
if (opts.personal) {
|
|
73
|
+
await runPersonalRemote(remoteDepsFactory(), sql, !!opts.write, format);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
53
76
|
if (opts.app) {
|
|
54
|
-
await runRemote(remoteDepsFactory(), company, opts.app, sql, !!opts.write);
|
|
77
|
+
await runRemote(remoteDepsFactory(), company, opts.app, sql, !!opts.write, format);
|
|
55
78
|
return;
|
|
56
79
|
}
|
|
57
80
|
const result = runLocalSql({
|
|
@@ -73,7 +96,17 @@ export function registerDbSqlCommand(db, remoteDepsFactory = defaultRemoteDeps)
|
|
|
73
96
|
}
|
|
74
97
|
});
|
|
75
98
|
}
|
|
76
|
-
async function
|
|
99
|
+
async function runPersonalRemote(deps, sql, write, format) {
|
|
100
|
+
const session = await controlPlaneClient(deps).personalSession();
|
|
101
|
+
const conn = await openRemoteSessionConnection(remoteEngineDeps(deps), session, (schema) => schema === "personal");
|
|
102
|
+
try {
|
|
103
|
+
console.log(formatRemoteResults(await runRemoteSql(conn, sql, write), format));
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
await conn.close();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function runRemote(deps, company, app, sql, write, format) {
|
|
77
110
|
const resolved = await deps.resolveCompany(company);
|
|
78
111
|
const appId = await resolveAppId(deps, resolved.companySlug, app);
|
|
79
112
|
const conn = await openRemoteAppConnection(remoteEngineDeps(deps), {
|
|
@@ -81,7 +114,7 @@ async function runRemote(deps, company, app, sql, write) {
|
|
|
81
114
|
appId,
|
|
82
115
|
});
|
|
83
116
|
try {
|
|
84
|
-
console.log(formatRemoteResults(await runRemoteSql(conn, sql, write)));
|
|
117
|
+
console.log(formatRemoteResults(await runRemoteSql(conn, sql, write), format));
|
|
85
118
|
}
|
|
86
119
|
finally {
|
|
87
120
|
await conn.close();
|
|
@@ -3,5 +3,6 @@
|
|
|
3
3
|
* Optionally reports remote tier when control plane returns a binding (US-009).
|
|
4
4
|
*/
|
|
5
5
|
import { Command } from "commander";
|
|
6
|
-
|
|
6
|
+
import { type DbRemoteCommandDeps } from "./db-remote-deps.js";
|
|
7
|
+
export declare function registerDbStatusCommand(db: Command, depsFactory?: () => DbRemoteCommandDeps): void;
|
|
7
8
|
//# sourceMappingURL=db-status.d.ts.map
|
|
@@ -5,21 +5,47 @@
|
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import { ControlPlaneDbClient } from "../lib/db/control-plane.js";
|
|
7
7
|
import { ensureAndStatusLocalDb, formatLocalDbStatus, } from "../lib/db/local.js";
|
|
8
|
-
|
|
8
|
+
import { safeErrorMessage } from "../lib/db/redact.js";
|
|
9
|
+
import { controlPlaneClient, defaultRemoteDeps, describeRemoteError, } from "./db-remote-deps.js";
|
|
10
|
+
export function registerDbStatusCommand(db, depsFactory = defaultRemoteDeps) {
|
|
9
11
|
db.command("status")
|
|
10
|
-
.description("Show local
|
|
11
|
-
.
|
|
12
|
+
.description("Show company local/remote status or your personal database status")
|
|
13
|
+
.option("--company <slug>", "Company slug (tenant scope)")
|
|
14
|
+
.option("--personal", "Show your personal database status")
|
|
12
15
|
.option("--home <path>", "Override home directory for local DB root (tests / advanced only)")
|
|
13
16
|
.option("--remote", "Also query control plane for remote tier status (requires auth)", false)
|
|
14
17
|
.option("--company-uid <uid>", "Company uid for remote status (with --remote)")
|
|
15
18
|
.action(async (opts) => {
|
|
16
19
|
try {
|
|
17
20
|
const company = String(opts.company ?? "").trim();
|
|
18
|
-
if (
|
|
19
|
-
console.error(chalk.red("Error: --
|
|
21
|
+
if (opts.personal && opts.company !== undefined) {
|
|
22
|
+
console.error(chalk.red("Error: --personal cannot be used with --company"));
|
|
20
23
|
process.exitCode = 1;
|
|
21
24
|
return;
|
|
22
25
|
}
|
|
26
|
+
if (opts.personal && (opts.remote || opts.companyUid !== undefined)) {
|
|
27
|
+
console.error(chalk.red("Error: --personal cannot be used with --remote or --company-uid"));
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (!opts.personal && !company) {
|
|
32
|
+
console.error(chalk.red("error: required option '--company <slug>' not specified"));
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (opts.personal) {
|
|
37
|
+
const remote = await controlPlaneClient(depsFactory()).personalStatus();
|
|
38
|
+
if (!remote.remote) {
|
|
39
|
+
console.log("remote: (none)");
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
console.log(`remote: ${remote.remote.status}`);
|
|
43
|
+
console.log(`remoteEngine: ${remote.remote.engineId}`);
|
|
44
|
+
console.log(`remoteRegion: ${remote.remote.region}`);
|
|
45
|
+
console.log(`remoteResourceArn: ${remote.remote.resourceArn}`);
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
23
49
|
const status = ensureAndStatusLocalDb(company, opts.home ? { home: opts.home } : undefined);
|
|
24
50
|
// Never print remote connection material.
|
|
25
51
|
console.log(formatLocalDbStatus(status));
|
|
@@ -54,7 +80,7 @@ export function registerDbStatusCommand(db) {
|
|
|
54
80
|
}
|
|
55
81
|
}
|
|
56
82
|
catch (e) {
|
|
57
|
-
const msg = e
|
|
83
|
+
const msg = describeRemoteError(e) ?? safeErrorMessage(e);
|
|
58
84
|
console.log(`remote: error (${msg})`);
|
|
59
85
|
}
|
|
60
86
|
}
|
|
@@ -63,7 +89,7 @@ export function registerDbStatusCommand(db) {
|
|
|
63
89
|
}
|
|
64
90
|
}
|
|
65
91
|
catch (error) {
|
|
66
|
-
console.error(chalk.red("Error:"), error
|
|
92
|
+
console.error(chalk.red("Error:"), describeRemoteError(error) ?? safeErrorMessage(error));
|
|
67
93
|
process.exitCode = 1;
|
|
68
94
|
}
|
|
69
95
|
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Create brief job the desktop app sends its local bot as an owner DM. The
|
|
3
|
+
* body starts with the skill invocation and names the run:
|
|
4
|
+
*
|
|
5
|
+
* /daily-brief <company> --run-id <run_id>
|
|
6
|
+
*
|
|
7
|
+
* Such a turn is a job, not a reply: it runs the daily-brief skill once and
|
|
8
|
+
* reports through workspace/brief/<run_id>/status.json. Resuming the owner's
|
|
9
|
+
* DM conversation for it replayed the whole history on every brief (hundreds
|
|
10
|
+
* of thousands of tokens on 2026-10-12), so a brief job gets a fresh model
|
|
11
|
+
* session of its own and never writes the owner's DM session file. The DM
|
|
12
|
+
* history still records the job message and the bot's posts, so the owner's
|
|
13
|
+
* next fresh session knows the brief ran. Unlike a scheduled job, a brief is
|
|
14
|
+
* a person's click: it is scheduled like a DM, not held behind the job slot.
|
|
15
|
+
*/
|
|
16
|
+
import { type BotSessionScope } from "./paths.js";
|
|
17
|
+
/** The run id when the text is a brief job message, else null. */
|
|
18
|
+
export declare function briefJobRunId(text: string | null | undefined): string | null;
|
|
19
|
+
/** One fresh session per brief run; the session file is never kept. */
|
|
20
|
+
export declare function briefJobSessionScope(runId: string): BotSessionScope;
|
|
21
|
+
export declare function isBriefJobSessionScope(scope: BotSessionScope): boolean;
|
|
22
|
+
//# sourceMappingURL=brief-job.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Create brief job the desktop app sends its local bot as an owner DM. The
|
|
3
|
+
* body starts with the skill invocation and names the run:
|
|
4
|
+
*
|
|
5
|
+
* /daily-brief <company> --run-id <run_id>
|
|
6
|
+
*
|
|
7
|
+
* Such a turn is a job, not a reply: it runs the daily-brief skill once and
|
|
8
|
+
* reports through workspace/brief/<run_id>/status.json. Resuming the owner's
|
|
9
|
+
* DM conversation for it replayed the whole history on every brief (hundreds
|
|
10
|
+
* of thousands of tokens on 2026-10-12), so a brief job gets a fresh model
|
|
11
|
+
* session of its own and never writes the owner's DM session file. The DM
|
|
12
|
+
* history still records the job message and the bot's posts, so the owner's
|
|
13
|
+
* next fresh session knows the brief ran. Unlike a scheduled job, a brief is
|
|
14
|
+
* a person's click: it is scheduled like a DM, not held behind the job slot.
|
|
15
|
+
*/
|
|
16
|
+
import { jobSessionScope } from "./paths.js";
|
|
17
|
+
const BRIEF_JOB_RUN = /^\s*\/daily-brief\b[^\n]*?--run-id[\s=]+([A-Za-z0-9][A-Za-z0-9_-]*)/;
|
|
18
|
+
/** The run id when the text is a brief job message, else null. */
|
|
19
|
+
export function briefJobRunId(text) {
|
|
20
|
+
if (!text)
|
|
21
|
+
return null;
|
|
22
|
+
const m = BRIEF_JOB_RUN.exec(text);
|
|
23
|
+
return m ? m[1] : null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Reserved so no cloud job id can land in a brief scope: cloud ids are
|
|
27
|
+
* `job_...` and this token contains a character the session path never gets
|
|
28
|
+
* from them. The path sanitizer maps it to an underscore in the file name.
|
|
29
|
+
*/
|
|
30
|
+
const BRIEF_JOB_PREFIX = "brief-run:";
|
|
31
|
+
/** One fresh session per brief run; the session file is never kept. */
|
|
32
|
+
export function briefJobSessionScope(runId) {
|
|
33
|
+
return jobSessionScope(`${BRIEF_JOB_PREFIX}${runId}`);
|
|
34
|
+
}
|
|
35
|
+
export function isBriefJobSessionScope(scope) {
|
|
36
|
+
return scope.startsWith(`job:${BRIEF_JOB_PREFIX}`);
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=brief-job.js.map
|