@indigoai-us/hq-cli 5.345.110 → 5.345.112
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 +29 -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/hq-cloud-sync-runner/bin/sync-runner.js +180 -180
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- 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 +2 -2
|
@@ -174,6 +174,13 @@ export interface NowAccount {
|
|
|
174
174
|
observedAt?: string | null;
|
|
175
175
|
reason: string | null;
|
|
176
176
|
fixCommand: string | null;
|
|
177
|
+
/**
|
|
178
|
+
* True when the account is expired and the newest usage reading predates the
|
|
179
|
+
* token expiry. The router must not trust `limits` on this account: the
|
|
180
|
+
* numbers were recorded before the token lapsed and the collector has not
|
|
181
|
+
* been able to refresh them since.
|
|
182
|
+
*/
|
|
183
|
+
usageStale?: boolean;
|
|
177
184
|
limits: NowLimit[];
|
|
178
185
|
}
|
|
179
186
|
export interface NowProvider {
|
|
@@ -372,7 +379,9 @@ export interface LedgerOptions {
|
|
|
372
379
|
export declare function capacityOptionsFromEnv(env?: NodeJS.ProcessEnv): LedgerOptions;
|
|
373
380
|
export declare function capacityRoot(env?: NodeJS.ProcessEnv, home?: string): string;
|
|
374
381
|
/** Shown for a credential that exists and is past expiry. Never a login. */
|
|
375
|
-
export declare const EXPIRED_REASON = "expired:
|
|
382
|
+
export declare const EXPIRED_REASON = "expired: hq capacity renews it on the next collect";
|
|
383
|
+
/** The token endpoint rejected the stored refresh token. The human must sign in again. */
|
|
384
|
+
export declare const SIGN_IN_REVOKED_REASON = "signed out: sign-in revoked";
|
|
376
385
|
export declare function isExpiredReason(reason: string | null | undefined): boolean;
|
|
377
386
|
export declare function isAccountFailureReason(reason: string): boolean;
|
|
378
387
|
/** Exact command for an account that has no usable credential. */
|
|
@@ -189,12 +189,17 @@ const SIGNED_OUT_PREFIX = "signed out:";
|
|
|
189
189
|
const EXPIRED_PREFIX = "expired:";
|
|
190
190
|
/** Ledger text written before expired and signed-out were different states. */
|
|
191
191
|
const LEGACY_EXPIRED_REASON = "signed out: token expired";
|
|
192
|
+
/** Ledger text written before hq capacity refreshed Claude tokens itself. */
|
|
193
|
+
const LEGACY_EXPIRED_REASON_V2 = "expired: refreshes the next time Claude Code is used with this account";
|
|
192
194
|
/** Shown for a credential that exists and is past expiry. Never a login. */
|
|
193
|
-
export const EXPIRED_REASON = "expired:
|
|
195
|
+
export const EXPIRED_REASON = "expired: hq capacity renews it on the next collect";
|
|
196
|
+
/** The token endpoint rejected the stored refresh token. The human must sign in again. */
|
|
197
|
+
export const SIGN_IN_REVOKED_REASON = "signed out: sign-in revoked";
|
|
194
198
|
export function isExpiredReason(reason) {
|
|
195
199
|
if (!reason)
|
|
196
200
|
return false;
|
|
197
201
|
return reason === LEGACY_EXPIRED_REASON
|
|
202
|
+
|| reason === LEGACY_EXPIRED_REASON_V2
|
|
198
203
|
|| reason === "unavailable: token_expired"
|
|
199
204
|
|| reason.startsWith(EXPIRED_PREFIX);
|
|
200
205
|
}
|
|
@@ -1034,6 +1039,7 @@ function clusterAccounts(provider, events) {
|
|
|
1034
1039
|
? "ok"
|
|
1035
1040
|
: "unknown";
|
|
1036
1041
|
const home = homes[0] ?? null;
|
|
1042
|
+
const usageStale = expired && (!Number.isFinite(latestReadingAt) || (problem?.at ?? 0) > latestReadingAt);
|
|
1037
1043
|
accounts.push({
|
|
1038
1044
|
accountId,
|
|
1039
1045
|
labels: ordered.length > 0 ? ordered : [primary],
|
|
@@ -1049,6 +1055,7 @@ function clusterAccounts(provider, events) {
|
|
|
1049
1055
|
: isSignedOut
|
|
1050
1056
|
? capacityLoginCommand(provider.provider, primary)
|
|
1051
1057
|
: null,
|
|
1058
|
+
usageStale: usageStale ? true : undefined,
|
|
1052
1059
|
limits,
|
|
1053
1060
|
});
|
|
1054
1061
|
}
|
|
@@ -186,6 +186,7 @@ function mergeGroup(group, homes, labels, provider) {
|
|
|
186
186
|
const observedAt = latestObserved(group.map((piece) => piece.account));
|
|
187
187
|
const organization = latestOrganization(group.map((piece) => piece.account));
|
|
188
188
|
const accountId = (ok[0] ?? group[0])?.account.accountId ?? labels[0] ?? provider;
|
|
189
|
+
const usageStale = group.some((piece) => piece.account.usageStale === true);
|
|
189
190
|
if (ok.length > 0) {
|
|
190
191
|
let state = "ok";
|
|
191
192
|
for (const limit of limits) {
|
|
@@ -203,6 +204,7 @@ function mergeGroup(group, homes, labels, provider) {
|
|
|
203
204
|
observedAt,
|
|
204
205
|
reason: null,
|
|
205
206
|
fixCommand: null,
|
|
207
|
+
usageStale: usageStale ? true : undefined,
|
|
206
208
|
limits,
|
|
207
209
|
};
|
|
208
210
|
}
|
|
@@ -220,6 +222,7 @@ function mergeGroup(group, homes, labels, provider) {
|
|
|
220
222
|
observedAt,
|
|
221
223
|
reason: problem?.reason ?? null,
|
|
222
224
|
fixCommand: problem?.fixCommand ?? null,
|
|
225
|
+
usageStale: usageStale ? true : undefined,
|
|
223
226
|
limits,
|
|
224
227
|
};
|
|
225
228
|
}
|
|
@@ -107,6 +107,23 @@ export interface RemoteAppSession {
|
|
|
107
107
|
expiration?: string;
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
|
+
export interface PersonalProvisionResponse {
|
|
111
|
+
ok: boolean;
|
|
112
|
+
personUid: string;
|
|
113
|
+
status: "ready" | "provisioning" | "existing";
|
|
114
|
+
region: string;
|
|
115
|
+
idempotent: boolean;
|
|
116
|
+
}
|
|
117
|
+
export interface PersonalStatusResponse {
|
|
118
|
+
personUid: string;
|
|
119
|
+
remote: {
|
|
120
|
+
engineId: string;
|
|
121
|
+
region: string;
|
|
122
|
+
status: string;
|
|
123
|
+
resourceArn: string;
|
|
124
|
+
} | null;
|
|
125
|
+
}
|
|
126
|
+
export type PersonalSession = RemoteAppSession;
|
|
110
127
|
export interface RemoteDestroyAppResponse {
|
|
111
128
|
ok: boolean;
|
|
112
129
|
appId: string;
|
|
@@ -129,6 +146,11 @@ export declare class ControlPlaneDbClient {
|
|
|
129
146
|
companySlug: string;
|
|
130
147
|
region?: string;
|
|
131
148
|
}): Promise<RemoteProvisionResponse>;
|
|
149
|
+
personalProvision(input?: {
|
|
150
|
+
region?: string;
|
|
151
|
+
}): Promise<PersonalProvisionResponse>;
|
|
152
|
+
personalStatus(): Promise<PersonalStatusResponse>;
|
|
153
|
+
personalSession(): Promise<PersonalSession>;
|
|
132
154
|
status(companyUid: string): Promise<RemoteStatusResponse>;
|
|
133
155
|
appUsage(companyUid: string, appId: string): Promise<RemoteAppUsageResponse>;
|
|
134
156
|
usage(companyUid: string): Promise<RemoteUsageResponse>;
|
|
@@ -54,6 +54,53 @@ export class ControlPlaneDbClient {
|
|
|
54
54
|
}
|
|
55
55
|
return JSON.parse(text);
|
|
56
56
|
}
|
|
57
|
+
async personalProvision(input = {}) {
|
|
58
|
+
const token = await this.getAccessToken();
|
|
59
|
+
const res = await this.fetchImpl(`${this.baseUrl}/v1/db/personal/provision`, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: {
|
|
62
|
+
Authorization: `Bearer ${token}`,
|
|
63
|
+
"Content-Type": "application/json",
|
|
64
|
+
},
|
|
65
|
+
body: JSON.stringify(input),
|
|
66
|
+
signal: AbortSignal.timeout(DB_PROVISION_TIMEOUT_MS),
|
|
67
|
+
});
|
|
68
|
+
const text = await res.text();
|
|
69
|
+
assertNoPostgresUrl("personal provision", text);
|
|
70
|
+
if (!res.ok)
|
|
71
|
+
throw personalHttpError("personal provision", res.status, text);
|
|
72
|
+
return JSON.parse(text);
|
|
73
|
+
}
|
|
74
|
+
async personalStatus() {
|
|
75
|
+
const token = await this.getAccessToken();
|
|
76
|
+
const res = await this.fetchImpl(`${this.baseUrl}/v1/db/personal/status`, {
|
|
77
|
+
method: "GET",
|
|
78
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
79
|
+
signal: AbortSignal.timeout(DB_STATUS_TIMEOUT_MS),
|
|
80
|
+
});
|
|
81
|
+
const text = await res.text();
|
|
82
|
+
assertNoPostgresUrl("personal status", text);
|
|
83
|
+
if (!res.ok)
|
|
84
|
+
throw personalHttpError("personal status", res.status, text);
|
|
85
|
+
return JSON.parse(text);
|
|
86
|
+
}
|
|
87
|
+
async personalSession() {
|
|
88
|
+
const token = await this.getAccessToken();
|
|
89
|
+
const res = await this.fetchImpl(`${this.baseUrl}/v1/db/personal/session`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: {
|
|
92
|
+
Authorization: `Bearer ${token}`,
|
|
93
|
+
"Content-Type": "application/json",
|
|
94
|
+
},
|
|
95
|
+
body: JSON.stringify({}),
|
|
96
|
+
signal: AbortSignal.timeout(DB_STATUS_TIMEOUT_MS),
|
|
97
|
+
});
|
|
98
|
+
const text = await res.text();
|
|
99
|
+
assertNoPostgresUrl("personal session", text);
|
|
100
|
+
if (!res.ok)
|
|
101
|
+
throw personalHttpError("personal session", res.status, text);
|
|
102
|
+
return JSON.parse(text);
|
|
103
|
+
}
|
|
57
104
|
async status(companyUid) {
|
|
58
105
|
const token = await this.getAccessToken();
|
|
59
106
|
const url = `${this.baseUrl}/v1/db/status?companyUid=${encodeURIComponent(companyUid)}`;
|
|
@@ -168,4 +215,7 @@ function httpError(label, status, text) {
|
|
|
168
215
|
err.reason = reason;
|
|
169
216
|
return err;
|
|
170
217
|
}
|
|
218
|
+
function personalHttpError(label, status, text) {
|
|
219
|
+
return Object.assign(httpError(label, status, text), { personalRoute: true });
|
|
220
|
+
}
|
|
171
221
|
//# sourceMappingURL=control-plane.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type QueryConfig } from "pg";
|
|
1
2
|
import type { ControlPlaneDbClient, RemoteAppSession } from "./control-plane.js";
|
|
2
3
|
/**
|
|
3
4
|
* Source of short-lived, app-scoped database credentials. The control plane
|
|
@@ -20,6 +21,7 @@ export interface RemoteQueryResult {
|
|
|
20
21
|
export interface RemoteClient {
|
|
21
22
|
connect(): Promise<void>;
|
|
22
23
|
query(sql: string, params?: unknown[]): Promise<RemoteQueryResult | RemoteQueryResult[]>;
|
|
24
|
+
querySingleStatement(sql: string): Promise<RemoteQueryResult | RemoteQueryResult[]>;
|
|
23
25
|
end(): Promise<void>;
|
|
24
26
|
}
|
|
25
27
|
export interface RemoteClientConfig {
|
|
@@ -37,10 +39,14 @@ export interface RemoteEngineDeps {
|
|
|
37
39
|
tokenFactory?: (session: RemoteAppSession) => Promise<string>;
|
|
38
40
|
clientFactory?: (config: RemoteClientConfig) => RemoteClient;
|
|
39
41
|
}
|
|
42
|
+
export declare function extendedQueryConfig(sql: string): QueryConfig & {
|
|
43
|
+
queryMode: "extended";
|
|
44
|
+
};
|
|
40
45
|
export declare function quoteIdent(name: string): string;
|
|
41
46
|
export interface RemoteAppConnection {
|
|
42
47
|
schema: string;
|
|
43
48
|
query(sql: string, params?: unknown[]): Promise<RemoteQueryResult[]>;
|
|
49
|
+
querySingleStatement(sql: string): Promise<RemoteQueryResult[]>;
|
|
44
50
|
close(): Promise<void>;
|
|
45
51
|
}
|
|
46
52
|
/** Open a connection as the app role with `search_path` set to the app schema. */
|
|
@@ -48,13 +54,15 @@ export declare function openRemoteAppConnection(deps: RemoteEngineDeps, input: {
|
|
|
48
54
|
companyUid: string;
|
|
49
55
|
appId: string;
|
|
50
56
|
}): Promise<RemoteAppConnection>;
|
|
57
|
+
/** Open a DSQL connection from a control-plane session with a caller-selected schema guard. */
|
|
58
|
+
export declare function openRemoteSessionConnection(deps: RemoteEngineDeps, session: RemoteAppSession, isAllowedSchema: (schema: string) => boolean): Promise<RemoteAppConnection>;
|
|
51
59
|
export interface RemoteSqlOutcome {
|
|
52
60
|
results: RemoteQueryResult[];
|
|
53
61
|
readonly: boolean;
|
|
54
62
|
}
|
|
55
63
|
/** Run SQL in the app schema. Without `write`, the SQL runs inside BEGIN READ ONLY and is rolled back. */
|
|
56
64
|
export declare function runRemoteSql(conn: RemoteAppConnection, sql: string, write: boolean): Promise<RemoteSqlOutcome>;
|
|
57
|
-
export declare function formatRemoteResults(outcome: RemoteSqlOutcome): string;
|
|
65
|
+
export declare function formatRemoteResults(outcome: RemoteSqlOutcome, format?: "jsonl" | "table"): string;
|
|
58
66
|
export interface DumpSummary {
|
|
59
67
|
tables: number;
|
|
60
68
|
rows: number;
|
|
@@ -18,7 +18,15 @@ async function defaultToken(session) {
|
|
|
18
18
|
return signer.getDbConnectAuthToken();
|
|
19
19
|
}
|
|
20
20
|
function defaultClient(config) {
|
|
21
|
-
|
|
21
|
+
const client = new pg.Client(config);
|
|
22
|
+
return Object.assign(client, {
|
|
23
|
+
querySingleStatement(sql) {
|
|
24
|
+
return client.query(extendedQueryConfig(sql));
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export function extendedQueryConfig(sql) {
|
|
29
|
+
return { text: sql, queryMode: "extended" };
|
|
22
30
|
}
|
|
23
31
|
export function quoteIdent(name) {
|
|
24
32
|
return `"${name.replace(/"/g, '""')}"`;
|
|
@@ -26,7 +34,11 @@ export function quoteIdent(name) {
|
|
|
26
34
|
/** Open a connection as the app role with `search_path` set to the app schema. */
|
|
27
35
|
export async function openRemoteAppConnection(deps, input) {
|
|
28
36
|
const session = await deps.provider.getSession(input);
|
|
29
|
-
|
|
37
|
+
return openRemoteSessionConnection(deps, session, (schema) => SCHEMA_PATTERN.test(schema));
|
|
38
|
+
}
|
|
39
|
+
/** Open a DSQL connection from a control-plane session with a caller-selected schema guard. */
|
|
40
|
+
export async function openRemoteSessionConnection(deps, session, isAllowedSchema) {
|
|
41
|
+
if (!isAllowedSchema(session.schema)) {
|
|
30
42
|
throw new Error("Control plane returned an unexpected schema name");
|
|
31
43
|
}
|
|
32
44
|
const password = await (deps.tokenFactory ?? defaultToken)(session);
|
|
@@ -52,6 +64,10 @@ export async function openRemoteAppConnection(deps, input) {
|
|
|
52
64
|
const res = await client.query(sql, params);
|
|
53
65
|
return Array.isArray(res) ? res : [res];
|
|
54
66
|
},
|
|
67
|
+
async querySingleStatement(sql) {
|
|
68
|
+
const res = await client.querySingleStatement(sql);
|
|
69
|
+
return Array.isArray(res) ? res : [res];
|
|
70
|
+
},
|
|
55
71
|
close: () => client.end(),
|
|
56
72
|
};
|
|
57
73
|
}
|
|
@@ -59,7 +75,7 @@ export async function openRemoteAppConnection(deps, input) {
|
|
|
59
75
|
export async function runRemoteSql(conn, sql, write) {
|
|
60
76
|
await conn.query(write ? "BEGIN" : "BEGIN READ ONLY");
|
|
61
77
|
try {
|
|
62
|
-
const results = await conn.
|
|
78
|
+
const results = await conn.querySingleStatement(sql);
|
|
63
79
|
await conn.query(write ? "COMMIT" : "ROLLBACK");
|
|
64
80
|
return { results, readonly: !write };
|
|
65
81
|
}
|
|
@@ -68,7 +84,19 @@ export async function runRemoteSql(conn, sql, write) {
|
|
|
68
84
|
throw error;
|
|
69
85
|
}
|
|
70
86
|
}
|
|
71
|
-
export function formatRemoteResults(outcome) {
|
|
87
|
+
export function formatRemoteResults(outcome, format = "jsonl") {
|
|
88
|
+
if (format === "table") {
|
|
89
|
+
return outcome.results.map((result) => {
|
|
90
|
+
const columns = result.fields.map((field) => field.name);
|
|
91
|
+
if (columns.length === 0)
|
|
92
|
+
return "(no columns)";
|
|
93
|
+
const header = columns.join("\t");
|
|
94
|
+
const rows = result.rows
|
|
95
|
+
.map((row) => columns.map((column) => String(row[column] ?? "")).join("\t"))
|
|
96
|
+
.join("\n");
|
|
97
|
+
return rows ? `${header}\n${rows}` : header;
|
|
98
|
+
}).join("\n");
|
|
99
|
+
}
|
|
72
100
|
const blocks = outcome.results.map((r) => {
|
|
73
101
|
if (r.fields.length === 0)
|
|
74
102
|
return JSON.stringify({ rows: r.rowCount ?? 0 });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.345.
|
|
3
|
+
"version": "5.345.112",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@aws-sdk/client-iot-data-plane": "^3.1096.0",
|
|
49
49
|
"@aws-sdk/client-s3": "^3.1049.0",
|
|
50
50
|
"@aws-sdk/dsql-signer": "^3.1146.0",
|
|
51
|
-
"@indigoai-us/hq-cloud": "~6.18.
|
|
51
|
+
"@indigoai-us/hq-cloud": "~6.18.71",
|
|
52
52
|
"@indigoai-us/hq-flags-client": "^0.1.2",
|
|
53
53
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
54
54
|
"@sentry/node": "^10.49.0",
|