@indigoai-us/hq-cli 5.345.92 → 5.345.93
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 +14 -0
- package/dist/command-catalog.generated.d.ts +252 -0
- package/dist/command-catalog.generated.js +306 -0
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/capacity.d.ts +71 -0
- package/dist/commands/capacity.js +666 -0
- package/dist/commands/integrations-connect.js +64 -5
- package/dist/commands/lanes-accounts.d.ts +1 -0
- package/dist/commands/lanes-accounts.js +6 -0
- package/dist/lib/capacity/accounts.d.ts +36 -0
- package/dist/lib/capacity/accounts.js +90 -0
- package/dist/lib/capacity/collect.d.ts +33 -0
- package/dist/lib/capacity/collect.js +164 -0
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +145 -0
- package/dist/lib/capacity/collectors/claude-oauth.js +1211 -0
- package/dist/lib/capacity/collectors/claude-statusline.d.ts +72 -0
- package/dist/lib/capacity/collectors/claude-statusline.js +436 -0
- package/dist/lib/capacity/collectors/codex.d.ts +120 -0
- package/dist/lib/capacity/collectors/codex.js +1134 -0
- package/dist/lib/capacity/collectors/grok.d.ts +86 -0
- package/dist/lib/capacity/collectors/grok.js +588 -0
- package/dist/lib/capacity/lane-login.d.ts +29 -0
- package/dist/lib/capacity/lane-login.js +91 -0
- package/dist/lib/capacity/ledger.d.ts +399 -0
- package/dist/lib/capacity/ledger.js +1441 -0
- package/dist/lib/capacity/project.d.ts +12 -0
- package/dist/lib/capacity/project.js +264 -0
- package/dist/lib/capacity/read.d.ts +62 -0
- package/dist/lib/capacity/read.js +264 -0
- package/dist/lib/capacity/refusal.d.ts +30 -0
- package/dist/lib/capacity/refusal.js +183 -0
- package/dist/lib/capacity/scheduler.d.ts +76 -0
- package/dist/lib/capacity/scheduler.js +800 -0
- package/dist/lib/capacity/sign-in.d.ts +103 -0
- package/dist/lib/capacity/sign-in.js +517 -0
- package/dist/lib/capacity/widget.d.ts +22 -0
- package/dist/lib/capacity/widget.js +418 -0
- package/dist/lib/lanes/spawn.js +12 -0
- package/dist/lib/mesh/live/daemon/run.d.ts +5 -0
- package/dist/lib/mesh/live/daemon/run.js +29 -1
- package/package.json +1 -1
|
@@ -305,6 +305,57 @@ async function resolveKey(opts, appLabel) {
|
|
|
305
305
|
}
|
|
306
306
|
throw new IntegrationsCliError(`${appLabel} needs an API key. Pass it with --token-stdin (recommended, keeps it out of shell history) or --token <key>.`, { expected: true });
|
|
307
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* Remote MCP endpoints the integrations.sh catalog lists as MCP without a URL.
|
|
310
|
+
*
|
|
311
|
+
* Gmail's documented server is `https://gmailmcp.googleapis.com/mcp/v1`
|
|
312
|
+
* (Google Workspace "Configure the Gmail MCP server"). Catalog connect and
|
|
313
|
+
* reconnect otherwise POST domain/query, and hq-pro throws
|
|
314
|
+
* FactoryInstallationUnsupported — printed as a failure that still exited 0
|
|
315
|
+
* when the command returned instead of throwing.
|
|
316
|
+
*/
|
|
317
|
+
const DOCUMENTED_REMOTE_MCP_BY_DOMAIN = {
|
|
318
|
+
"gmail.com": { mcpUrl: "https://gmailmcp.googleapis.com/mcp/v1" },
|
|
319
|
+
};
|
|
320
|
+
function documentedRemoteMcp(domainOrName) {
|
|
321
|
+
const want = domainOrName?.trim().toLowerCase() ?? "";
|
|
322
|
+
if (!want)
|
|
323
|
+
return undefined;
|
|
324
|
+
const direct = DOCUMENTED_REMOTE_MCP_BY_DOMAIN[want];
|
|
325
|
+
if (direct)
|
|
326
|
+
return { mcpUrl: direct.mcpUrl, domain: want };
|
|
327
|
+
for (const [domain, overlay] of Object.entries(DOCUMENTED_REMOTE_MCP_BY_DOMAIN)) {
|
|
328
|
+
if (domainLabel(domain) === want)
|
|
329
|
+
return { mcpUrl: overlay.mcpUrl, domain };
|
|
330
|
+
}
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* When the catalog names an MCP app but has no server URL, use the documented
|
|
335
|
+
* endpoint and drop catalog/query handles so hq-pro takes the direct `mcpUrl`
|
|
336
|
+
* path. Keep `domain` + `provider` so a revoked factory:gmail row is revived
|
|
337
|
+
* instead of creating a second connection keyed on gmailmcp.googleapis.com.
|
|
338
|
+
*/
|
|
339
|
+
function withDocumentedRemoteMcp(target, domainHint) {
|
|
340
|
+
if (target.ref.mcpUrl || target.ref.discoveryReceiptId)
|
|
341
|
+
return target;
|
|
342
|
+
const overlay = documentedRemoteMcp(domainHint) ?? documentedRemoteMcp(target.ref.domain);
|
|
343
|
+
if (!overlay)
|
|
344
|
+
return target;
|
|
345
|
+
return {
|
|
346
|
+
...target,
|
|
347
|
+
ref: {
|
|
348
|
+
mcpUrl: overlay.mcpUrl,
|
|
349
|
+
domain: overlay.domain,
|
|
350
|
+
provider: target.ref.provider ?? domainLabel(overlay.domain),
|
|
351
|
+
displayName: target.ref.displayName ?? target.label,
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function unsupportedCatalogSurfaceError(label, cause) {
|
|
356
|
+
return new IntegrationsCliError(`${label} cannot be connected because the app catalog has no MCP server URL. ` +
|
|
357
|
+
"Re-run with --mcp-url <endpoint> (find it with `hq integrations inspect`).", { expected: true, status: cause.status, code: cause.code });
|
|
358
|
+
}
|
|
308
359
|
/**
|
|
309
360
|
* Turn the caller's arguments into one unambiguous target. Precedence is
|
|
310
361
|
* most-specific-first so an explicit flag always wins over the positional
|
|
@@ -354,13 +405,13 @@ async function resolveTarget(token, companyUid, app, opts, preserveDomain = fals
|
|
|
354
405
|
// hq-pro. A catalog entry id is useful auth metadata, but replacing the
|
|
355
406
|
// domain with it can create a distinct connection instead of reviving
|
|
356
407
|
// the original acct_ row.
|
|
357
|
-
return {
|
|
408
|
+
return withDocumentedRemoteMcp({
|
|
358
409
|
ref: { domain: app },
|
|
359
410
|
...(match?.authClass ? { authClass: match.authClass } : {}),
|
|
360
411
|
label: redactUrlsInText(match?.name || app),
|
|
361
|
-
};
|
|
412
|
+
}, app);
|
|
362
413
|
}
|
|
363
|
-
return catalogEntryToTarget(match, { ref: { domain: app }, label: redactUrlsInText(app) });
|
|
414
|
+
return withDocumentedRemoteMcp(catalogEntryToTarget(match, { ref: { domain: app }, label: redactUrlsInText(app) }), match?.domain ?? app);
|
|
364
415
|
}
|
|
365
416
|
// A bare name (`notion`, `atlassian`) is how the catalog reads to a person —
|
|
366
417
|
// the rows are titled `notion.com`, `atlassian.com`. Resolve it to that row
|
|
@@ -370,9 +421,9 @@ async function resolveTarget(token, companyUid, app, opts, preserveDomain = fals
|
|
|
370
421
|
// unmatched → fall back to the server-side query resolver.
|
|
371
422
|
const named = await findCatalogEntryByName(token, companyUid, app);
|
|
372
423
|
if (named) {
|
|
373
|
-
return catalogEntryToTarget(named, { ref: { query: app }, label: redactUrlsInText(app) });
|
|
424
|
+
return withDocumentedRemoteMcp(catalogEntryToTarget(named, { ref: { query: app }, label: redactUrlsInText(app) }), named.domain);
|
|
374
425
|
}
|
|
375
|
-
return { ref: { query: app }, label: redactUrlsInText(app) };
|
|
426
|
+
return withDocumentedRemoteMcp({ ref: { query: app }, label: redactUrlsInText(app) }, app);
|
|
376
427
|
}
|
|
377
428
|
/**
|
|
378
429
|
* Turn a matched catalog entry into a connect target, preferring its opaque
|
|
@@ -1065,6 +1116,10 @@ async function connectApp(token, companyUid, app, opts, expectedRevivedConnectio
|
|
|
1065
1116
|
}
|
|
1066
1117
|
return;
|
|
1067
1118
|
}
|
|
1119
|
+
if (err instanceof IntegrationsCliError &&
|
|
1120
|
+
err.code === "INTEGRATION_FACTORY_UNSUPPORTED_SURFACE") {
|
|
1121
|
+
throw unsupportedCatalogSurfaceError(target.label, err);
|
|
1122
|
+
}
|
|
1068
1123
|
throw err;
|
|
1069
1124
|
}
|
|
1070
1125
|
}
|
|
@@ -1407,6 +1462,10 @@ export function registerConnectCommands(integrations) {
|
|
|
1407
1462
|
reportInstall(result, opts);
|
|
1408
1463
|
return;
|
|
1409
1464
|
}
|
|
1465
|
+
if (err instanceof IntegrationsCliError &&
|
|
1466
|
+
err.code === "INTEGRATION_FACTORY_UNSUPPORTED_SURFACE") {
|
|
1467
|
+
throw unsupportedCatalogSurfaceError(target.label, err);
|
|
1468
|
+
}
|
|
1410
1469
|
throw err;
|
|
1411
1470
|
}
|
|
1412
1471
|
});
|
|
@@ -12,6 +12,7 @@ export declare function createLaneAccountsHandlers(deps: LaneAccountsHandlerDeps
|
|
|
12
12
|
home?: string;
|
|
13
13
|
deviceAuth?: boolean;
|
|
14
14
|
company?: string;
|
|
15
|
+
capacityOnly?: boolean;
|
|
15
16
|
}) => Promise<void>;
|
|
16
17
|
runAccountsSetHome: (ctx: Ctx, providerRaw: string, accountIdRaw: string, homeRaw: string, opts: {
|
|
17
18
|
host?: string;
|
|
@@ -8,6 +8,7 @@ import { AccountResolutionError, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, PROVIDER
|
|
|
8
8
|
import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/config.js";
|
|
9
9
|
import { resolveLaneEffort } from "../lib/lanes/effort.js";
|
|
10
10
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
11
|
+
import { CAPACITY_ONLY_REASON } from "../lib/capacity/accounts.js";
|
|
11
12
|
import { accountQuotaView, clearAccountQuota, loadQuotaState } from "../lib/lanes/quota.js";
|
|
12
13
|
import { resolveLaneModel } from "../lib/lanes/spawn.js";
|
|
13
14
|
import { leadLaneIds } from "../lib/lanes/admission.js";
|
|
@@ -362,6 +363,11 @@ export function createLaneAccountsHandlers(deps) {
|
|
|
362
363
|
}
|
|
363
364
|
}
|
|
364
365
|
delete nextAccount.disabled_reason;
|
|
366
|
+
if (opts.capacityOnly && registeredAccount?.enabled !== true) {
|
|
367
|
+
const prior = registeredAccount?.disabled_reason;
|
|
368
|
+
nextAccount.enabled = false;
|
|
369
|
+
nextAccount.disabled_reason = prior && prior !== CAPACITY_ONLY_REASON ? prior : CAPACITY_ONLY_REASON;
|
|
370
|
+
}
|
|
365
371
|
assertUniqueAccountHomes(provider, {
|
|
366
372
|
...config.providers[provider].accounts,
|
|
367
373
|
[id]: nextAccount,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capacity account list (US-011).
|
|
3
|
+
*
|
|
4
|
+
* The lane account registry plus each provider's default home. A missing
|
|
5
|
+
* config file is an empty registry: this module does not call
|
|
6
|
+
* documentedDefaultConfig, which would invent an enabled account "a".
|
|
7
|
+
*/
|
|
8
|
+
import { type LaneProvider, type LanesConfig } from "../lanes/types.js";
|
|
9
|
+
export type CapacityAccountSource = "default" | "registry" | "host";
|
|
10
|
+
export interface CapacityAccount {
|
|
11
|
+
provider: LaneProvider;
|
|
12
|
+
label: string;
|
|
13
|
+
home: string;
|
|
14
|
+
source: CapacityAccountSource;
|
|
15
|
+
/** False for a capacity-only account: read for usage, never used by lanes. */
|
|
16
|
+
lanes?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* `disabled_reason` that marks a registry account as capacity-only. Signing in
|
|
20
|
+
* for usage visibility must not opt an account into lane work, because lanes
|
|
21
|
+
* spread work across every enabled account and the accounts can belong to
|
|
22
|
+
* different companies. `hq lanes accounts enable` opts one in.
|
|
23
|
+
*/
|
|
24
|
+
export declare const CAPACITY_ONLY_REASON = "capacity-only: read for usage, not used by lanes";
|
|
25
|
+
export interface ListCapacityAccountsOptions {
|
|
26
|
+
home?: string;
|
|
27
|
+
hqRoot?: string;
|
|
28
|
+
hostname?: string;
|
|
29
|
+
/** Injected config. `null` means the registry is empty. Omit to read the file. */
|
|
30
|
+
config?: LanesConfig | null;
|
|
31
|
+
env?: NodeJS.ProcessEnv;
|
|
32
|
+
}
|
|
33
|
+
export declare function defaultProviderHome(provider: LaneProvider, home?: string): string;
|
|
34
|
+
export declare function listCapacityAccounts(options?: ListCapacityAccountsOptions): CapacityAccount[];
|
|
35
|
+
export declare function registryAccountIds(hqRoot: string, provider: string): string[];
|
|
36
|
+
//# sourceMappingURL=accounts.d.ts.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capacity account list (US-011).
|
|
3
|
+
*
|
|
4
|
+
* The lane account registry plus each provider's default home. A missing
|
|
5
|
+
* config file is an empty registry: this module does not call
|
|
6
|
+
* documentedDefaultConfig, which would invent an enabled account "a".
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { effectiveAccountHome } from "../lanes/account-home.js";
|
|
12
|
+
import { lanesConfigPath, loadLanesConfig } from "../lanes/config.js";
|
|
13
|
+
import { resolveLanesHqRoot } from "../lanes/hq-root.js";
|
|
14
|
+
import { LANE_PROVIDERS } from "../lanes/types.js";
|
|
15
|
+
/**
|
|
16
|
+
* `disabled_reason` that marks a registry account as capacity-only. Signing in
|
|
17
|
+
* for usage visibility must not opt an account into lane work, because lanes
|
|
18
|
+
* spread work across every enabled account and the accounts can belong to
|
|
19
|
+
* different companies. `hq lanes accounts enable` opts one in.
|
|
20
|
+
*/
|
|
21
|
+
export const CAPACITY_ONLY_REASON = "capacity-only: read for usage, not used by lanes";
|
|
22
|
+
const DEFAULT_DIR = {
|
|
23
|
+
claude: ".claude",
|
|
24
|
+
codex: ".codex",
|
|
25
|
+
grok: ".grok",
|
|
26
|
+
};
|
|
27
|
+
export function defaultProviderHome(provider, home = os.homedir()) {
|
|
28
|
+
return path.join(home, DEFAULT_DIR[provider]);
|
|
29
|
+
}
|
|
30
|
+
export function listCapacityAccounts(options = {}) {
|
|
31
|
+
const home = options.home ?? os.homedir();
|
|
32
|
+
const hostname = options.hostname ?? os.hostname();
|
|
33
|
+
const config = resolveConfig(options);
|
|
34
|
+
const accounts = [];
|
|
35
|
+
for (const provider of LANE_PROVIDERS) {
|
|
36
|
+
accounts.push({
|
|
37
|
+
provider,
|
|
38
|
+
label: "default",
|
|
39
|
+
home: defaultProviderHome(provider, home),
|
|
40
|
+
source: "default",
|
|
41
|
+
});
|
|
42
|
+
const configured = config?.providers[provider]?.accounts ?? {};
|
|
43
|
+
for (const [label, account] of Object.entries(configured)) {
|
|
44
|
+
if (!account)
|
|
45
|
+
continue;
|
|
46
|
+
const capacityOnly = !account.enabled && account.disabled_reason === CAPACITY_ONLY_REASON;
|
|
47
|
+
if (!account.enabled && !capacityOnly)
|
|
48
|
+
continue;
|
|
49
|
+
const effective = effectiveAccountHome(account, hostname);
|
|
50
|
+
accounts.push({
|
|
51
|
+
provider,
|
|
52
|
+
label,
|
|
53
|
+
home: effective.home,
|
|
54
|
+
source: effective.source === "host" ? "host" : "registry",
|
|
55
|
+
lanes: account.enabled,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return accounts;
|
|
60
|
+
}
|
|
61
|
+
export function registryAccountIds(hqRoot, provider) {
|
|
62
|
+
if (!LANE_PROVIDERS.includes(provider))
|
|
63
|
+
return [];
|
|
64
|
+
const filePath = lanesConfigPath(hqRoot);
|
|
65
|
+
if (!fs.existsSync(filePath))
|
|
66
|
+
return [];
|
|
67
|
+
const config = loadLanesConfig(hqRoot);
|
|
68
|
+
return Object.keys(config.providers[provider].accounts);
|
|
69
|
+
}
|
|
70
|
+
function resolveConfig(options) {
|
|
71
|
+
if (options.config === null)
|
|
72
|
+
return null;
|
|
73
|
+
if (options.config)
|
|
74
|
+
return options.config;
|
|
75
|
+
const explicit = options.hqRoot?.trim() || options.env?.HQ_ROOT?.trim() || "";
|
|
76
|
+
let hqRoot = explicit;
|
|
77
|
+
if (!hqRoot) {
|
|
78
|
+
try {
|
|
79
|
+
hqRoot = resolveLanesHqRoot({ env: options.env });
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const filePath = lanesConfigPath(hqRoot);
|
|
86
|
+
if (!fs.existsSync(filePath))
|
|
87
|
+
return null;
|
|
88
|
+
return loadLanesConfig(hqRoot);
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=accounts.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot capacity collection (US-011).
|
|
3
|
+
*
|
|
4
|
+
* The daemon is the resident collector. This runs the same readers once
|
|
5
|
+
* into whatever ledger root the caller passes, so a check never writes
|
|
6
|
+
* ~/.hq/capacity unless that root is requested.
|
|
7
|
+
*/
|
|
8
|
+
import { type CapacityAccount } from "./accounts.js";
|
|
9
|
+
import { type CodexAppServerSpawn } from "./collectors/codex.js";
|
|
10
|
+
import { type CollectClaudeOAuthOptions } from "./collectors/claude-oauth.js";
|
|
11
|
+
import { type CollectGrokOptions } from "./collectors/grok.js";
|
|
12
|
+
import { type NowView } from "./ledger.js";
|
|
13
|
+
export interface CollectCapacityOnceOptions {
|
|
14
|
+
root?: string;
|
|
15
|
+
home?: string;
|
|
16
|
+
hqRoot?: string;
|
|
17
|
+
hostname?: string;
|
|
18
|
+
env?: NodeJS.ProcessEnv;
|
|
19
|
+
now?: () => Date;
|
|
20
|
+
/** Replaces registry discovery. Tests pass fixture homes here. */
|
|
21
|
+
accounts?: readonly CapacityAccount[];
|
|
22
|
+
claude?: Pick<CollectClaudeOAuthOptions, "platform" | "securityExec" | "fetchImpl" | "cliVersion" | "readVersion" | "endpoint">;
|
|
23
|
+
grok?: Pick<CollectGrokOptions, "fetchImpl" | "cliVersion" | "readVersion" | "endpoint">;
|
|
24
|
+
codex?: {
|
|
25
|
+
spawn?: CodexAppServerSpawn;
|
|
26
|
+
cliVersion?: string;
|
|
27
|
+
skipAppServer?: boolean;
|
|
28
|
+
};
|
|
29
|
+
/** Login passes false so a partial account list cannot label the whole spool. */
|
|
30
|
+
includeSpool?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export declare function collectCapacityOnce(options?: CollectCapacityOnceOptions): Promise<NowView>;
|
|
33
|
+
//# sourceMappingURL=collect.d.ts.map
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot capacity collection (US-011).
|
|
3
|
+
*
|
|
4
|
+
* The daemon is the resident collector. This runs the same readers once
|
|
5
|
+
* into whatever ledger root the caller passes, so a check never writes
|
|
6
|
+
* ~/.hq/capacity unless that root is requested.
|
|
7
|
+
*/
|
|
8
|
+
import { execFile } from "node:child_process";
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { listCapacityAccounts } from "./accounts.js";
|
|
12
|
+
import { ingestCodexResult, isCodexProcessRunning, latestCodexSessionWriteAt, readCodexAppServer, readInstalledCodexVersion, shouldReadAppServer, tailCodexSessions, } from "./collectors/codex.js";
|
|
13
|
+
import { collectClaudeOAuth, } from "./collectors/claude-oauth.js";
|
|
14
|
+
import { consumeClaudeStatuslineSpool } from "./collectors/claude-statusline.js";
|
|
15
|
+
import { collectGrok } from "./collectors/grok.js";
|
|
16
|
+
import { capacityRoot, rebuildNow, } from "./ledger.js";
|
|
17
|
+
export async function collectCapacityOnce(options = {}) {
|
|
18
|
+
const env = options.env ?? process.env;
|
|
19
|
+
const root = options.root ?? capacityRoot(env, options.home);
|
|
20
|
+
const ledger = {
|
|
21
|
+
root,
|
|
22
|
+
env,
|
|
23
|
+
home: options.home,
|
|
24
|
+
...(options.now ? { now: options.now } : {}),
|
|
25
|
+
};
|
|
26
|
+
const accounts = options.accounts
|
|
27
|
+
? [...options.accounts]
|
|
28
|
+
: listCapacityAccounts({
|
|
29
|
+
home: options.home,
|
|
30
|
+
hqRoot: options.hqRoot,
|
|
31
|
+
hostname: options.hostname,
|
|
32
|
+
env,
|
|
33
|
+
});
|
|
34
|
+
const claude = accounts.filter((account) => account.provider === "claude");
|
|
35
|
+
const codex = accounts.filter((account) => account.provider === "codex");
|
|
36
|
+
const grok = accounts.filter((account) => account.provider === "grok");
|
|
37
|
+
await collectClaudeOAuth({
|
|
38
|
+
ledger,
|
|
39
|
+
now: options.now,
|
|
40
|
+
allowDisabled: true,
|
|
41
|
+
accounts: claude.map((account) => ({ label: account.label, home: account.home })),
|
|
42
|
+
...options.claude,
|
|
43
|
+
});
|
|
44
|
+
await collectCodexHomes(codex, ledger, options);
|
|
45
|
+
await collectGrok({
|
|
46
|
+
ledger,
|
|
47
|
+
now: options.now,
|
|
48
|
+
allowDisabled: true,
|
|
49
|
+
accounts: grok.map((account) => ({ label: account.label, home: account.home })),
|
|
50
|
+
...options.grok,
|
|
51
|
+
});
|
|
52
|
+
if (options.includeSpool !== false) {
|
|
53
|
+
consumeClaudeStatuslineSpool({
|
|
54
|
+
root,
|
|
55
|
+
now: options.now ? options.now() : new Date(),
|
|
56
|
+
accounts: claude.map((account) => ({ label: account.label, home: account.home })),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return rebuildNow(ledger);
|
|
60
|
+
}
|
|
61
|
+
async function collectCodexHomes(accounts, ledger, options) {
|
|
62
|
+
const groups = new Map();
|
|
63
|
+
for (const account of accounts) {
|
|
64
|
+
const key = path.resolve(account.home);
|
|
65
|
+
const group = groups.get(key) ?? { home: account.home, labels: [] };
|
|
66
|
+
if (!group.labels.includes(account.label))
|
|
67
|
+
group.labels.push(account.label);
|
|
68
|
+
groups.set(key, group);
|
|
69
|
+
}
|
|
70
|
+
let cliVersion = options.codex?.cliVersion;
|
|
71
|
+
if (!cliVersion) {
|
|
72
|
+
try {
|
|
73
|
+
cliVersion = await readInstalledCodexVersion(options.codex?.spawn);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
cliVersion = "unknown";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const now = options.now ? options.now() : new Date();
|
|
80
|
+
const processes = options.codex?.skipAppServer ? "codex" : await processListAsync();
|
|
81
|
+
const root = ledger.root;
|
|
82
|
+
let state = root ? readTailState(root) : undefined;
|
|
83
|
+
for (const group of groups.values()) {
|
|
84
|
+
const label = group.labels.find((item) => item !== "default") ?? group.labels[0] ?? "default";
|
|
85
|
+
const sessionsDir = path.join(group.home, "sessions");
|
|
86
|
+
try {
|
|
87
|
+
const tailed = tailCodexSessions({
|
|
88
|
+
sessionsDir,
|
|
89
|
+
state,
|
|
90
|
+
cliVersion,
|
|
91
|
+
now,
|
|
92
|
+
recentDays: 2,
|
|
93
|
+
accountId: `home:${path.resolve(group.home)}`,
|
|
94
|
+
accountLabel: label,
|
|
95
|
+
accountLabels: group.labels,
|
|
96
|
+
accountHome: group.home,
|
|
97
|
+
});
|
|
98
|
+
ingestCodexResult(tailed, ledger);
|
|
99
|
+
state = tailed.state;
|
|
100
|
+
if (root)
|
|
101
|
+
writeTailState(root, state);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// One home's tail must not stop the others.
|
|
105
|
+
}
|
|
106
|
+
if (options.codex?.skipAppServer)
|
|
107
|
+
continue;
|
|
108
|
+
const lastWrite = latestCodexSessionWriteAt(sessionsDir);
|
|
109
|
+
if (!shouldReadAppServer({
|
|
110
|
+
lastSessionWriteAt: lastWrite,
|
|
111
|
+
codexProcessRunning: isCodexProcessRunning(processes),
|
|
112
|
+
now: now.getTime(),
|
|
113
|
+
}))
|
|
114
|
+
continue;
|
|
115
|
+
try {
|
|
116
|
+
const result = await readCodexAppServer({
|
|
117
|
+
cliVersion,
|
|
118
|
+
now,
|
|
119
|
+
home: group.home,
|
|
120
|
+
accountId: `home:${path.resolve(group.home)}`,
|
|
121
|
+
accountLabel: label,
|
|
122
|
+
accountLabels: group.labels,
|
|
123
|
+
accountHome: group.home,
|
|
124
|
+
spawn: options.codex?.spawn,
|
|
125
|
+
timeoutMs: 20_000,
|
|
126
|
+
});
|
|
127
|
+
ingestCodexResult(result, ledger);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// A refused or busy app-server is not a credential read.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const TAIL_STATE_FILE = "codex-tail-state.json";
|
|
135
|
+
function readTailState(root) {
|
|
136
|
+
try {
|
|
137
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(root, TAIL_STATE_FILE), "utf8"));
|
|
138
|
+
if (!parsed || typeof parsed !== "object" || !parsed.files)
|
|
139
|
+
return undefined;
|
|
140
|
+
return parsed;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function writeTailState(root, state) {
|
|
147
|
+
const dest = path.join(root, TAIL_STATE_FILE);
|
|
148
|
+
const tmp = `${dest}.${process.pid}.tmp`;
|
|
149
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
150
|
+
fs.writeFileSync(tmp, `${JSON.stringify(state)}\n`, { mode: 0o600 });
|
|
151
|
+
fs.renameSync(tmp, dest);
|
|
152
|
+
fs.chmodSync(dest, 0o600);
|
|
153
|
+
}
|
|
154
|
+
function processListAsync() {
|
|
155
|
+
return new Promise((resolve) => {
|
|
156
|
+
execFile("ps", ["-ax", "-o", "command="], { timeout: 5_000, encoding: "utf8" }, (error, stdout) => {
|
|
157
|
+
if (error)
|
|
158
|
+
resolve("");
|
|
159
|
+
else
|
|
160
|
+
resolve(typeof stdout === "string" ? stdout : "");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=collect.js.map
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude OAuth usage collector (US-005).
|
|
3
|
+
*
|
|
4
|
+
* Reads the Claude Code credential read-only (macOS Keychain item
|
|
5
|
+
* `Claude Code-credentials`, plus `Claude Code-credentials-<hash>` seats,
|
|
6
|
+
* or the credentials JSON under ~/.claude/ on Linux). GETs
|
|
7
|
+
* https://api.anthropic.com/api/oauth/usage with the oauth-2025-04-20 beta
|
|
8
|
+
* header. This module does not refresh the token, write the Keychain or
|
|
9
|
+
* ~/.claude, call an OAuth token endpoint, or invoke the claude CLI except
|
|
10
|
+
* `claude --version` when the caller does not pass cliVersion.
|
|
11
|
+
*
|
|
12
|
+
* The legacy shape (seven_day_opus populated, no limits[]) is drift.
|
|
13
|
+
* observedAt is the response time. One collect appends every account's
|
|
14
|
+
* readings and failures in one rebuild.
|
|
15
|
+
*/
|
|
16
|
+
import { type LedgerOptions, type NowView, type ParserFailureEvent, type ReadingInput } from "../ledger.js";
|
|
17
|
+
export declare const CLAUDE_OAUTH_PARSER_VERSION = "claude-oauth-usage.1";
|
|
18
|
+
export declare const CLAUDE_OAUTH_PROVIDER = "claude";
|
|
19
|
+
export declare const CLAUDE_OAUTH_SOURCE: "oauth-usage";
|
|
20
|
+
export declare const CLAUDE_OAUTH_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
21
|
+
export declare const CLAUDE_OAUTH_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
|
|
22
|
+
export declare const CLAUDE_OAUTH_BETA = "oauth-2025-04-20";
|
|
23
|
+
export declare const CLAUDE_ORG_CACHE_MS: number;
|
|
24
|
+
export declare const CLAUDE_ORG_CACHE_FILE = "claude-org-cache.json";
|
|
25
|
+
export declare const CLAUDE_OAUTH_POLL_MS: number;
|
|
26
|
+
/** Freshness is shorter than the 5 minute tick so the next tick is not skipped. */
|
|
27
|
+
export declare const CLAUDE_OAUTH_FRESH_MS: number;
|
|
28
|
+
export declare const CLAUDE_OAUTH_KEYCHAIN_BACKOFF_MS: number;
|
|
29
|
+
export declare const CLAUDE_OAUTH_FETCH_TIMEOUT_MS = 10000;
|
|
30
|
+
export declare const CLAUDE_OAUTH_SECURITY_TIMEOUT_MS = 5000;
|
|
31
|
+
export declare const CLAUDE_OAUTH_POLL_FILE = "claude-oauth-last-call.json";
|
|
32
|
+
export declare const CLAUDE_KEYCHAIN_SERVICE = "Claude Code-credentials";
|
|
33
|
+
export interface ClaudeOAuthFetchInit {
|
|
34
|
+
method: "GET";
|
|
35
|
+
headers: Record<string, string>;
|
|
36
|
+
redirect: "manual";
|
|
37
|
+
signal?: AbortSignal;
|
|
38
|
+
}
|
|
39
|
+
export type ClaudeOAuthFetch = (url: string, init: ClaudeOAuthFetchInit) => Promise<{
|
|
40
|
+
status: number;
|
|
41
|
+
text: () => Promise<string>;
|
|
42
|
+
headers?: {
|
|
43
|
+
get(name: string): string | null;
|
|
44
|
+
};
|
|
45
|
+
}>;
|
|
46
|
+
export interface SecurityExecResult {
|
|
47
|
+
status: number | null;
|
|
48
|
+
stdout: string;
|
|
49
|
+
stderr: string;
|
|
50
|
+
timedOut: boolean;
|
|
51
|
+
}
|
|
52
|
+
export type SecurityExec = (args: readonly string[], timeoutMs: number) => SecurityExecResult;
|
|
53
|
+
export type ClaudeKeychainStatus = "no-prompt" | "prompt" | "timeout" | "error";
|
|
54
|
+
export interface ClaudeCredentialAccount {
|
|
55
|
+
token: string;
|
|
56
|
+
/** Already hashed. Raw org ids and keychain account names are not accepted here. */
|
|
57
|
+
accountId: string;
|
|
58
|
+
expiresAt?: number | null;
|
|
59
|
+
scopes?: readonly string[] | null;
|
|
60
|
+
planType?: string | null;
|
|
61
|
+
}
|
|
62
|
+
export interface ReadClaudeCredentialsResult {
|
|
63
|
+
keychain: ClaudeKeychainStatus;
|
|
64
|
+
accounts: ClaudeCredentialAccount[];
|
|
65
|
+
}
|
|
66
|
+
export type ReadClaudeCredentials = () => ReadClaudeCredentialsResult;
|
|
67
|
+
export interface ClaudeOAuthCollectResult {
|
|
68
|
+
readings: ReadingInput[];
|
|
69
|
+
failures: ParserFailureEvent[];
|
|
70
|
+
view: NowView | null;
|
|
71
|
+
skipped?: "disabled" | "rate-limit" | "keychain-backoff";
|
|
72
|
+
keychain: ClaudeKeychainStatus;
|
|
73
|
+
httpStatus: number | null;
|
|
74
|
+
accountIds: string[];
|
|
75
|
+
}
|
|
76
|
+
export interface CollectClaudeOAuthOptions {
|
|
77
|
+
/** Linux credentials JSON. When set, the Keychain is not read. */
|
|
78
|
+
credentialsPath?: string;
|
|
79
|
+
/** Replaces Keychain and credentials-file discovery. */
|
|
80
|
+
readCredentials?: ReadClaudeCredentials;
|
|
81
|
+
/** Replaces `security`. Used by the default Keychain reader. */
|
|
82
|
+
securityExec?: SecurityExec;
|
|
83
|
+
endpoint?: string;
|
|
84
|
+
cliVersion?: string;
|
|
85
|
+
readVersion?: () => string;
|
|
86
|
+
now?: () => Date;
|
|
87
|
+
fetchImpl?: ClaudeOAuthFetch;
|
|
88
|
+
ledger?: LedgerOptions;
|
|
89
|
+
platform?: NodeJS.Platform;
|
|
90
|
+
/** Keychain account attribute used when discovery is not available. */
|
|
91
|
+
keychainAccount?: string;
|
|
92
|
+
/**
|
|
93
|
+
* When set, read each home instead of the unscoped keychain dump.
|
|
94
|
+
* One entry per label. Homes that resolve to the same path are one read.
|
|
95
|
+
*/
|
|
96
|
+
accounts?: readonly {
|
|
97
|
+
label: string;
|
|
98
|
+
home: string;
|
|
99
|
+
}[];
|
|
100
|
+
/** One-shot collect reads while the resident collector stays disabled. */
|
|
101
|
+
allowDisabled?: boolean;
|
|
102
|
+
}
|
|
103
|
+
interface MapOptions {
|
|
104
|
+
observedAt: string;
|
|
105
|
+
cliVersion: string;
|
|
106
|
+
accountId: string;
|
|
107
|
+
planType: string | null;
|
|
108
|
+
accountLabel?: string;
|
|
109
|
+
accountLabels?: string[];
|
|
110
|
+
organization?: string | null;
|
|
111
|
+
accountHome?: string;
|
|
112
|
+
}
|
|
113
|
+
export declare function claudeOAuthPollPath(root: string): string;
|
|
114
|
+
export declare function defaultClaudeCredentialsPath(home?: string): string;
|
|
115
|
+
/**
|
|
116
|
+
* Claude Code's keychain service for a config dir.
|
|
117
|
+
* The default home uses `Claude Code-credentials`. Any other resolved
|
|
118
|
+
* path uses that name plus the first 8 hex chars of SHA-256(utf8 path).
|
|
119
|
+
*/
|
|
120
|
+
export declare function claudeKeychainServiceName(configDir: string, defaultConfigDir?: string): string;
|
|
121
|
+
export declare function parseClaudeCliVersion(text: string): string | null;
|
|
122
|
+
/** Map a usage JSON body. Does not read credentials or call the network. */
|
|
123
|
+
export declare function mapClaudeOAuthUsage(body: unknown, options: MapOptions): {
|
|
124
|
+
readings: ReadingInput[];
|
|
125
|
+
failures: ParserFailureEvent[];
|
|
126
|
+
};
|
|
127
|
+
export declare function collectClaudeOAuth(options?: CollectClaudeOAuthOptions): Promise<ClaudeOAuthCollectResult>;
|
|
128
|
+
/**
|
|
129
|
+
* Ledger account id → hashed profile account id. Rows with no captured
|
|
130
|
+
* identity are omitted. The raw uuid is never returned.
|
|
131
|
+
*/
|
|
132
|
+
export declare function claudeIdentityKeys(root: string): ReadonlyMap<string, string>;
|
|
133
|
+
/**
|
|
134
|
+
* Claude names an individual plan's organization "<email>'s Organization".
|
|
135
|
+
* The ledger never stores an email, so such a name becomes the email's
|
|
136
|
+
* domain (for example "getindigo.ai"), or "Personal" for a consumer mail
|
|
137
|
+
* domain. Any other name containing "@" is dropped.
|
|
138
|
+
*/
|
|
139
|
+
export declare function displayOrganization(name: string): string | null;
|
|
140
|
+
export declare function claudeSeatAccountId(seat: string, orgId: string | null): string;
|
|
141
|
+
export declare function readInstalledClaudeVersion(readVersion?: () => string): string;
|
|
142
|
+
/** Async `claude --version` for the daemon. The sync reader stays for one-shot callers. */
|
|
143
|
+
export declare function readInstalledClaudeVersionAsync(): Promise<string>;
|
|
144
|
+
export {};
|
|
145
|
+
//# sourceMappingURL=claude-oauth.d.ts.map
|