omniagent 0.1.8 → 0.1.11

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.
@@ -0,0 +1,388 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { c as cleanControlOutput, a as compactLines, p as parsePercentUsed, m as makeUsageLimit } from "./cli.js";
7
+ import { r as runPtyScenario, e as enterKey, a as escapeKey } from "./pty-CZBSAJzE.js";
8
+ const execFileAsync = promisify(execFile);
9
+ const CLAUDE_CODE_KEYCHAIN_SERVICE = "Claude Code-credentials";
10
+ const CLAUDE_CODE_CREDENTIALS_PATH = [".claude", ".credentials.json"];
11
+ const CLAUDE_USAGE_API_URL = "https://api.anthropic.com/v1/messages";
12
+ const CLAUDE_USAGE_API_TIMEOUT_MS = 1e4;
13
+ const CLAUDE_USAGE_API_HEADERS = {
14
+ "anthropic-version": "2023-06-01",
15
+ "anthropic-beta": "oauth-2025-04-20",
16
+ "content-type": "application/json",
17
+ "user-agent": "claude-code/2.1.5"
18
+ };
19
+ const CLAUDE_USAGE_API_BODY = {
20
+ model: "claude-haiku-4-5-20251001",
21
+ max_tokens: 1,
22
+ messages: [{ role: "user", content: "hi" }]
23
+ };
24
+ async function extractClaudeUsage(context) {
25
+ try {
26
+ return await extractClaudeUsageFromApi(context);
27
+ } catch (error) {
28
+ if (context.signal.aborted) {
29
+ throw error;
30
+ }
31
+ return extractClaudeUsageFromTui(context);
32
+ }
33
+ }
34
+ async function extractClaudeUsageFromTui(context) {
35
+ const command = context.command ?? context.launch?.command ?? "claude";
36
+ const model = context.launch?.cheapModel ?? "haiku";
37
+ validateClaudeModel(model);
38
+ const ptyResult = await runPtyScenario({
39
+ command,
40
+ args: context.launch?.args ?? ["--model", model],
41
+ cwd: context.repoRoot,
42
+ cols: 100,
43
+ rows: 40,
44
+ timeoutMs: context.launch?.timeoutMs ?? 6e4,
45
+ signal: context.signal,
46
+ debug: context.debug,
47
+ steps: [
48
+ { waitFor: /Claude|>|❯/u, waitForSource: "screen", waitForTimeoutMs: 4e3 },
49
+ { write: enterKey() },
50
+ { waitFor: /Claude|>|❯/u, waitForSource: "screen", waitForTimeoutMs: 8e3 },
51
+ { write: `/usage${enterKey()}` },
52
+ {
53
+ waitFor: hasClaudeUsageRows,
54
+ waitForTimeoutMs: 15e3,
55
+ capture: "usage",
56
+ captureWaitMs: 500
57
+ },
58
+ { write: escapeKey() },
59
+ { waitMs: 500 },
60
+ { write: `/exit${enterKey()}` }
61
+ ]
62
+ });
63
+ const usageSnapshot = ptyResult.snapshots.usage ?? ptyResult;
64
+ const cleanedOutput = cleanControlOutput(usageSnapshot.raw);
65
+ const parsed = parseClaudeUsage(usageSnapshot.screen, cleanedOutput);
66
+ const limits = buildClaudeUsageLimits(parsed, context);
67
+ if (limits.length === 0) {
68
+ throw new Error("Claude usage output did not include session or weekly usage rows.");
69
+ }
70
+ return {
71
+ targetId: context.targetId,
72
+ displayName: context.displayName,
73
+ command,
74
+ limits,
75
+ debug: ptyResult.debug.length > 0 ? ptyResult.debug : void 0
76
+ };
77
+ }
78
+ async function extractClaudeUsageFromApi(context) {
79
+ const command = context.command ?? context.launch?.command ?? "claude";
80
+ const token = await readClaudeAccessToken(context);
81
+ if (token == null) {
82
+ throw new Error("Claude Code OAuth token was not available.");
83
+ }
84
+ const response = await fetchClaudeUsageHeaders(token, context.signal);
85
+ if (response.status >= 400) {
86
+ throw new Error(`Claude usage API returned HTTP ${response.status}.`);
87
+ }
88
+ const result = buildClaudeApiUsageResult(response.headers, {
89
+ targetId: context.targetId,
90
+ displayName: context.displayName,
91
+ now: context.now,
92
+ command
93
+ });
94
+ if (result.limits.length === 0) {
95
+ throw new Error("Claude usage API response did not include usage headers.");
96
+ }
97
+ return result;
98
+ }
99
+ function buildClaudeApiUsageResult(headers, context) {
100
+ const sessionUsed = parseUsageHeaderFraction(
101
+ headers.get("anthropic-ratelimit-unified-5h-utilization")
102
+ );
103
+ const weekUsed = parseUsageHeaderFraction(
104
+ headers.get("anthropic-ratelimit-unified-7d-utilization")
105
+ );
106
+ if (sessionUsed == null || weekUsed == null) {
107
+ throw new Error("Claude usage API response did not include complete usage headers.");
108
+ }
109
+ return {
110
+ targetId: context.targetId,
111
+ displayName: context.displayName,
112
+ command: context.command,
113
+ limits: [
114
+ makeClaudeApiUsageLimit({
115
+ targetId: context.targetId,
116
+ scope: "current_session",
117
+ window: "session",
118
+ percentUsed: sessionUsed,
119
+ resetAt: parseEpochSecondsHeader(headers.get("anthropic-ratelimit-unified-5h-reset")),
120
+ now: context.now
121
+ }),
122
+ makeClaudeApiUsageLimit({
123
+ targetId: context.targetId,
124
+ scope: "current_week",
125
+ window: "weekly",
126
+ percentUsed: weekUsed,
127
+ resetAt: parseEpochSecondsHeader(headers.get("anthropic-ratelimit-unified-7d-reset")),
128
+ now: context.now
129
+ })
130
+ ]
131
+ };
132
+ }
133
+ function extractClaudeAccessToken(blob) {
134
+ const trimmed = blob.trim();
135
+ if (!trimmed) {
136
+ return null;
137
+ }
138
+ try {
139
+ const parsed = JSON.parse(trimmed);
140
+ const token = findAccessToken(parsed);
141
+ if (token != null) {
142
+ return token;
143
+ }
144
+ } catch {
145
+ }
146
+ const match = /"accessToken"\s*:\s*"([^"]+)"/.exec(trimmed);
147
+ if (match?.[1]) {
148
+ return match[1];
149
+ }
150
+ if (/^[A-Za-z0-9_\-.~+/=]{20,}$/.test(trimmed)) {
151
+ return trimmed;
152
+ }
153
+ return null;
154
+ }
155
+ async function readClaudeAccessToken(context) {
156
+ const tokenFromFile = await readClaudeAccessTokenFromFile(context.homeDir);
157
+ if (tokenFromFile != null) {
158
+ return tokenFromFile;
159
+ }
160
+ if (process.platform !== "darwin") {
161
+ return null;
162
+ }
163
+ return readClaudeAccessTokenFromKeychain(context.signal);
164
+ }
165
+ async function readClaudeAccessTokenFromFile(homeDir) {
166
+ try {
167
+ const raw = await readFile(path.join(homeDir, ...CLAUDE_CODE_CREDENTIALS_PATH), "utf8");
168
+ return extractClaudeAccessToken(raw);
169
+ } catch {
170
+ return null;
171
+ }
172
+ }
173
+ async function readClaudeAccessTokenFromKeychain(signal) {
174
+ const username = os.userInfo().username;
175
+ const keychainArgs = [
176
+ ["find-generic-password", "-s", CLAUDE_CODE_KEYCHAIN_SERVICE, "-a", username, "-w"],
177
+ ["find-generic-password", "-s", CLAUDE_CODE_KEYCHAIN_SERVICE, "-w"]
178
+ ];
179
+ for (const args of keychainArgs) {
180
+ try {
181
+ const { stdout } = await execFileAsync("security", args, {
182
+ timeout: 5e3,
183
+ signal,
184
+ maxBuffer: 1024 * 1024
185
+ });
186
+ const token = extractClaudeAccessToken(stdout);
187
+ if (token != null) {
188
+ return token;
189
+ }
190
+ } catch {
191
+ if (signal.aborted) {
192
+ throw signal.reason;
193
+ }
194
+ }
195
+ }
196
+ return null;
197
+ }
198
+ async function fetchClaudeUsageHeaders(token, parentSignal) {
199
+ const controller = new AbortController();
200
+ const timeout = setTimeout(() => {
201
+ controller.abort(new Error("Claude usage API request timed out."));
202
+ }, CLAUDE_USAGE_API_TIMEOUT_MS);
203
+ const abortFromParent = () => {
204
+ controller.abort(parentSignal.reason);
205
+ };
206
+ parentSignal.addEventListener("abort", abortFromParent, { once: true });
207
+ try {
208
+ const response = await fetch(CLAUDE_USAGE_API_URL, {
209
+ method: "POST",
210
+ headers: {
211
+ ...CLAUDE_USAGE_API_HEADERS,
212
+ authorization: `Bearer ${token}`
213
+ },
214
+ body: JSON.stringify(CLAUDE_USAGE_API_BODY),
215
+ signal: controller.signal
216
+ });
217
+ return {
218
+ status: response.status,
219
+ headers: response.headers
220
+ };
221
+ } finally {
222
+ clearTimeout(timeout);
223
+ parentSignal.removeEventListener("abort", abortFromParent);
224
+ }
225
+ }
226
+ function findAccessToken(value) {
227
+ if (value == null || typeof value !== "object") {
228
+ return null;
229
+ }
230
+ const record = value;
231
+ if (typeof record.accessToken === "string") {
232
+ return record.accessToken;
233
+ }
234
+ for (const child of Object.values(record)) {
235
+ const token = findAccessToken(child);
236
+ if (token != null) {
237
+ return token;
238
+ }
239
+ }
240
+ return null;
241
+ }
242
+ function makeClaudeApiUsageLimit(options) {
243
+ const percentUsed = clampPercent(options.percentUsed);
244
+ const resetText = options.resetAt == null ? null : `resets ${options.resetAt}`;
245
+ const raw = `${formatPercent(percentUsed)} used${resetText == null ? "" : ` (${resetText})`}`;
246
+ const limit = makeUsageLimit({
247
+ targetId: options.targetId,
248
+ scope: options.scope,
249
+ window: options.window,
250
+ percentUsed,
251
+ percentRemaining: 100 - percentUsed,
252
+ resetText,
253
+ raw,
254
+ now: options.now
255
+ });
256
+ return {
257
+ ...limit,
258
+ resetAt: options.resetAt
259
+ };
260
+ }
261
+ function parseUsageHeaderFraction(value) {
262
+ if (value == null || !value.trim()) {
263
+ return null;
264
+ }
265
+ const fraction = Number(value);
266
+ if (!Number.isFinite(fraction)) {
267
+ return null;
268
+ }
269
+ return fraction * 100;
270
+ }
271
+ function parseEpochSecondsHeader(value) {
272
+ if (value == null || !value.trim()) {
273
+ return null;
274
+ }
275
+ const seconds = Number(value);
276
+ if (!Number.isFinite(seconds) || seconds <= 0) {
277
+ return null;
278
+ }
279
+ return new Date(seconds * 1e3).toISOString();
280
+ }
281
+ function clampPercent(value) {
282
+ return Math.max(0, Math.min(100, value));
283
+ }
284
+ function formatPercent(value) {
285
+ return Number.isInteger(value) ? `${value}%` : `${Number(value.toFixed(2))}%`;
286
+ }
287
+ function hasClaudeUsageRows(snapshot) {
288
+ const parsed = parseClaudeUsage(snapshot.screen, cleanControlOutput(snapshot.raw));
289
+ return Boolean(parsed.currentSessionUsed || parsed.currentWeekUsed);
290
+ }
291
+ function buildClaudeUsageLimits(parsed, context) {
292
+ const sessionUsed = parsePercentUsed(parsed.currentSessionUsed);
293
+ const weekUsed = parsePercentUsed(parsed.currentWeekUsed);
294
+ const limits = [];
295
+ if (parsed.currentSessionUsed.trim()) {
296
+ limits.push(
297
+ makeUsageLimit({
298
+ targetId: context.targetId,
299
+ scope: "current_session",
300
+ window: "session",
301
+ percentUsed: sessionUsed,
302
+ percentRemaining: sessionUsed == null ? null : 100 - sessionUsed,
303
+ resetText: parsed.currentSessionResets,
304
+ raw: formatRaw(parsed.currentSessionUsed, parsed.currentSessionResets),
305
+ now: context.now
306
+ })
307
+ );
308
+ }
309
+ if (parsed.currentWeekUsed.trim()) {
310
+ limits.push(
311
+ makeUsageLimit({
312
+ targetId: context.targetId,
313
+ scope: "current_week",
314
+ window: "weekly",
315
+ percentUsed: weekUsed,
316
+ percentRemaining: weekUsed == null ? null : 100 - weekUsed,
317
+ resetText: parsed.currentWeekResets,
318
+ raw: formatRaw(parsed.currentWeekUsed, parsed.currentWeekResets),
319
+ now: context.now
320
+ })
321
+ );
322
+ }
323
+ return limits;
324
+ }
325
+ function parseClaudeUsage(screen, cleanedOutput = "") {
326
+ const fromScreen = parseClaudeLines(compactLines(screen));
327
+ if (fromScreen.currentSessionUsed || fromScreen.currentWeekUsed) {
328
+ return fromScreen;
329
+ }
330
+ return parseClaudeLines(compactLines(cleanedOutput));
331
+ }
332
+ function parseClaudeLines(lines) {
333
+ const values = {
334
+ currentSessionUsed: "",
335
+ currentSessionResets: "",
336
+ currentWeekUsed: "",
337
+ currentWeekResets: ""
338
+ };
339
+ let section = "";
340
+ for (const line of lines) {
341
+ if (line === "Current session") {
342
+ section = "currentSession";
343
+ continue;
344
+ }
345
+ if (line.startsWith("Current week")) {
346
+ section = "currentWeek";
347
+ continue;
348
+ }
349
+ if (!section) {
350
+ continue;
351
+ }
352
+ const usedMatch = /(\d+(?:\.\d+)?% used)/i.exec(line);
353
+ if (usedMatch != null) {
354
+ if (section === "currentSession") {
355
+ values.currentSessionUsed = usedMatch[1];
356
+ } else {
357
+ values.currentWeekUsed = usedMatch[1];
358
+ }
359
+ continue;
360
+ }
361
+ if (line.startsWith("Resets ")) {
362
+ if (section === "currentSession") {
363
+ values.currentSessionResets = line.slice("Resets ".length).trim();
364
+ } else {
365
+ values.currentWeekResets = line.slice("Resets ".length).trim();
366
+ }
367
+ }
368
+ }
369
+ return values;
370
+ }
371
+ function formatRaw(used, resets) {
372
+ if (!used) {
373
+ return "";
374
+ }
375
+ return resets ? `${used} (resets ${resets})` : used;
376
+ }
377
+ function validateClaudeModel(model) {
378
+ if (!/^[A-Za-z0-9._:-]+$/.test(model)) {
379
+ throw new Error(`Unsupported Claude usage model value: ${model}`);
380
+ }
381
+ }
382
+ export {
383
+ buildClaudeApiUsageResult,
384
+ buildClaudeUsageLimits,
385
+ extractClaudeAccessToken,
386
+ extractClaudeUsage,
387
+ parseClaudeUsage
388
+ };
package/dist/cli.js CHANGED
@@ -1322,7 +1322,7 @@ const claudeTarget = {
1322
1322
  timeoutMs: 6e4
1323
1323
  },
1324
1324
  extract: async (context) => {
1325
- const { extractClaudeUsage } = await import("./claude-Dmv_YFKX.js");
1325
+ const { extractClaudeUsage } = await import("./claude-C0SMAkM3.js");
1326
1326
  return extractClaudeUsage(context);
1327
1327
  }
1328
1328
  }
@@ -1402,7 +1402,7 @@ const codexTarget = {
1402
1402
  timeoutMs: 6e4
1403
1403
  },
1404
1404
  extract: async (context) => {
1405
- const { extractCodexUsage } = await import("./codex-Cl1dWwMk.js");
1405
+ const { extractCodexUsage } = await import("./codex-0b2YLh_8.js");
1406
1406
  return extractCodexUsage(context);
1407
1407
  }
1408
1408
  }
@@ -1606,7 +1606,7 @@ const geminiTarget = {
1606
1606
  timeoutMs: 7e4
1607
1607
  },
1608
1608
  extract: async (context) => {
1609
- const { extractGeminiUsage } = await import("./gemini-CskI3Qjp.js");
1609
+ const { extractGeminiUsage } = await import("./gemini-BVRg6OMO.js");
1610
1610
  return extractGeminiUsage(context);
1611
1611
  }
1612
1612
  }
@@ -8376,7 +8376,7 @@ function getTargetCliCommands(target) {
8376
8376
  add(target.cli?.modes?.oneShot?.command);
8377
8377
  return commands;
8378
8378
  }
8379
- async function checkCliOnPath(command) {
8379
+ async function checkCliOnPath(command, options = {}) {
8380
8380
  const normalized = normalizeCommand(command);
8381
8381
  if (!normalized) {
8382
8382
  return { command: command ?? "", result: "unavailable" };
@@ -8387,7 +8387,7 @@ async function checkCliOnPath(command) {
8387
8387
  if (hasPathSeparator(normalized)) {
8388
8388
  for (const candidate of candidates) {
8389
8389
  const check = await checkExecutable(candidate);
8390
- if (check.status === "available") {
8390
+ if (check.status === "available" && await isValidCandidate(check.resolvedPath, options.validateCandidate)) {
8391
8391
  return { command: normalized, result: "available", resolvedPath: check.resolvedPath };
8392
8392
  }
8393
8393
  if (check.status === "inconclusive") {
@@ -8415,7 +8415,7 @@ async function checkCliOnPath(command) {
8415
8415
  for (const candidate of candidates) {
8416
8416
  const fullPath = path.join(entry2, candidate);
8417
8417
  const check = await checkExecutable(fullPath);
8418
- if (check.status === "available") {
8418
+ if (check.status === "available" && await isValidCandidate(check.resolvedPath, options.validateCandidate)) {
8419
8419
  return { command: normalized, result: "available", resolvedPath: check.resolvedPath };
8420
8420
  }
8421
8421
  if (check.status === "inconclusive") {
@@ -8432,6 +8432,16 @@ async function checkCliOnPath(command) {
8432
8432
  }
8433
8433
  return { command: normalized, result: "unavailable" };
8434
8434
  }
8435
+ async function isValidCandidate(resolvedPath, validateCandidate) {
8436
+ if (!validateCandidate) {
8437
+ return true;
8438
+ }
8439
+ try {
8440
+ return await validateCandidate(resolvedPath);
8441
+ } catch {
8442
+ return false;
8443
+ }
8444
+ }
8435
8445
  async function checkTargetAvailability(target) {
8436
8446
  const commands = getTargetCliCommands(target);
8437
8447
  if (commands.length === 0) {
@@ -10666,7 +10676,9 @@ async function checkUsageCommandAvailability(target) {
10666
10676
  warnings: []
10667
10677
  };
10668
10678
  }
10669
- const check = await checkCliOnPath(command);
10679
+ const check = await checkCliOnPath(command, {
10680
+ validateCandidate: target.id === "codex" && command === "codex" ? validateCodexCommand : void 0
10681
+ });
10670
10682
  if (check.result === "available") {
10671
10683
  return {
10672
10684
  status: "available",
@@ -10688,6 +10700,29 @@ async function checkUsageCommandAvailability(target) {
10688
10700
  warnings: []
10689
10701
  };
10690
10702
  }
10703
+ function validateCodexCommand(candidate) {
10704
+ return new Promise((resolve) => {
10705
+ const child = spawn(candidate, ["--version"], {
10706
+ stdio: "ignore"
10707
+ });
10708
+ let settled = false;
10709
+ let timeout;
10710
+ const finish = (valid) => {
10711
+ if (settled) {
10712
+ return;
10713
+ }
10714
+ settled = true;
10715
+ clearTimeout(timeout);
10716
+ resolve(valid);
10717
+ };
10718
+ timeout = setTimeout(() => {
10719
+ child.kill();
10720
+ finish(false);
10721
+ }, 2e3);
10722
+ child.on("error", () => finish(false));
10723
+ child.on("exit", (code) => finish(code === 0));
10724
+ });
10725
+ }
10691
10726
  function buildContext(options) {
10692
10727
  const windows = uniqueNormalizedWindows(options.target.usage?.windows ?? []);
10693
10728
  const launch = {
@@ -10988,16 +11023,20 @@ function formatWindowLabel(window) {
10988
11023
  }
10989
11024
  return window;
10990
11025
  }
11026
+ function formatUsageAgentName(targetId, displayName) {
11027
+ return targetId === "codex" && displayName === "OpenAI Codex" ? "Codex CLI" : displayName;
11028
+ }
10991
11029
  function formatUsageTable(envelope, sortKey) {
10992
11030
  const useColor = shouldUseColor();
10993
11031
  const generatedAt = parseDate(envelope.generatedAt) ?? /* @__PURE__ */ new Date();
10994
11032
  const rows = [];
10995
11033
  for (const target of envelope.targets) {
10996
11034
  const limitLabels = formatLimitLabels(target.limits);
11035
+ const agentName = formatUsageAgentName(target.targetId, target.displayName);
10997
11036
  target.limits.forEach((limit, index) => {
10998
11037
  rows.push({
10999
11038
  status: "ok",
11000
- agent: sortKey == null && index > 0 ? "" : target.displayName,
11039
+ agent: sortKey == null && index > 0 ? "" : agentName,
11001
11040
  limitLabel: limitLabels[index] ?? formatLimitLabel(limit),
11002
11041
  reset: formatResetValue(limit, generatedAt),
11003
11042
  limit
@@ -11007,7 +11046,7 @@ function formatUsageTable(envelope, sortKey) {
11007
11046
  for (const error of envelope.errors) {
11008
11047
  rows.push({
11009
11048
  status: "error",
11010
- agent: error.displayName,
11049
+ agent: formatUsageAgentName(error.targetId, error.displayName),
11011
11050
  limitLabel: "error",
11012
11051
  message: error.message
11013
11052
  });
@@ -11262,16 +11301,7 @@ async function runUsageCommand(argv) {
11262
11301
  return null;
11263
11302
  }
11264
11303
  const startDir = process.cwd();
11265
- const repoRoot = await findRepoRoot(startDir);
11266
- if (!repoRoot) {
11267
- printError({
11268
- json: jsonOutput,
11269
- code: "repo_not_found",
11270
- message: `Repository root not found starting from ${startDir}. Looked for .git or package.json.`,
11271
- exitCode: 1
11272
- });
11273
- return null;
11274
- }
11304
+ const repoRoot = await findRepoRoot(startDir) ?? startDir;
11275
11305
  const agentsDirResolution = resolveAgentsDir(repoRoot, argv.agentsDir);
11276
11306
  if (agentsDirResolution.source === "override") {
11277
11307
  const validation2 = await validateAgentsDir(repoRoot, argv.agentsDir, { requireWrite: false });
@@ -11535,7 +11565,7 @@ const usageCommand = {
11535
11565
  describe: "Per-agent extraction timeout. Bare numbers are seconds; units include ms, s, and m."
11536
11566
  }).option("agentsDir", {
11537
11567
  type: "string",
11538
- describe: "Override the agents directory (relative paths resolve from the project root)",
11568
+ describe: "Override the agents directory (relative paths resolve from the project root, or the current directory outside a repo)",
11539
11569
  defaultDescription: DEFAULT_AGENTS_DIR,
11540
11570
  coerce: (value) => {
11541
11571
  if (typeof value !== "string") {