@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.
@@ -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: refreshes the next time Claude Code is used with this account";
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: refreshes the next time Claude Code is used with this account";
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
- return new pg.Client(config);
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
- if (!SCHEMA_PATTERN.test(session.schema)) {
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.query(sql);
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.110",
3
+ "version": "5.345.111",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {