@apilift/cli 1.4.35 → 1.4.37

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.
@@ -140,10 +140,51 @@ var init_state = __esm({
140
140
  });
141
141
 
142
142
  // src/usage/ownership.ts
143
- import { mkdirSync as mkdirSync2, chmodSync as chmodSync2, existsSync, readFileSync, writeFileSync, renameSync, unlinkSync } from "node:fs";
144
- import { randomUUID } from "node:crypto";
145
- import { join as join2 } from "node:path";
143
+ import { mkdirSync as mkdirSync2, chmodSync as chmodSync2, existsSync, readFileSync, writeFileSync, renameSync, unlinkSync, statSync } from "node:fs";
144
+ import { createHash, randomUUID } from "node:crypto";
145
+ import { join as join2, dirname, isAbsolute as isAbsolute2 } from "node:path";
146
146
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
147
+ function requestCollectorTarget(root2, entry, env) {
148
+ const owner = readOwner(root2);
149
+ if (!owner) return;
150
+ const pending = join2(root2, "usage", `.collector-target-${randomUUID()}`);
151
+ try {
152
+ const target = { owner: owner.id, entry, interpreter: process.execPath, fingerprint: sourceFingerprint(entry), env: Object.fromEntries(runtimeKeys.map((key) => [key, env[key] ?? null])) };
153
+ writeFileSync(pending, JSON.stringify(target), { mode: 384 });
154
+ renameSync(pending, targetFile(root2));
155
+ } finally {
156
+ try {
157
+ unlinkSync(pending);
158
+ } catch {
159
+ }
160
+ }
161
+ }
162
+ function collectorTarget(root2, owner, loadedFingerprint) {
163
+ try {
164
+ const target = JSON.parse(readFileSync(targetFile(root2), "utf8"));
165
+ if (target.owner !== owner || target.fingerprint === loadedFingerprint || !isAbsolute2(target.entry) || !isAbsolute2(target.interpreter) || !target.entry.endsWith("apilift-collector.js")) return null;
166
+ const pkg = JSON.parse(readFileSync(join2(dirname(target.entry), "..", "package.json"), "utf8"));
167
+ if (pkg.name !== "@apilift/cli" || sourceFingerprint(target.entry) !== target.fingerprint) return null;
168
+ if (!target.env || Object.entries(target.env).some(([key, value]) => !runtimeKeys.includes(key) || value !== null && typeof value !== "string")) return null;
169
+ return target;
170
+ } catch {
171
+ return null;
172
+ }
173
+ }
174
+ function watchCollectorSource(entry) {
175
+ const loaded = sourceFingerprint(entry);
176
+ let last = statSync(entry);
177
+ return { fingerprint: loaded, changed: () => {
178
+ try {
179
+ const current = statSync(entry);
180
+ if (current.ino === last.ino && current.size === last.size && current.mtimeMs === last.mtimeMs) return false;
181
+ last = current;
182
+ return sourceFingerprint(entry) !== loaded;
183
+ } catch {
184
+ return false;
185
+ }
186
+ } };
187
+ }
147
188
  function readOwner(root2) {
148
189
  try {
149
190
  const value = JSON.parse(readFileSync(ownerFile(root2), "utf8"));
@@ -180,7 +221,7 @@ function collectorLock(root2) {
180
221
  }
181
222
  };
182
223
  refresh();
183
- return { exec: (sql) => db.exec(sql), refresh, close: () => {
224
+ return { id, exec: (sql) => db.exec(sql), refresh, close: () => {
184
225
  if (closed) return;
185
226
  closed = true;
186
227
  try {
@@ -193,11 +234,38 @@ function collectorLock(root2) {
193
234
  }
194
235
  } };
195
236
  }
196
- var ownerLifetimeMs, ownerFile;
237
+ function collectorActive(root2) {
238
+ const owner = readOwner(root2), now = Date.now();
239
+ if (owner && owner.expiresAt > now && owner.expiresAt <= now + ownerLifetimeMs) {
240
+ try {
241
+ process.kill(owner.pid, 0);
242
+ return true;
243
+ } catch {
244
+ }
245
+ }
246
+ const file2 = join2(root2, "usage", "collector-lock.sqlite");
247
+ if (!existsSync(file2)) return false;
248
+ try {
249
+ const db = new DatabaseSync2(file2, { readOnly: true });
250
+ try {
251
+ db.exec("PRAGMA busy_timeout=0");
252
+ db.prepare("PRAGMA schema_version").get();
253
+ return false;
254
+ } finally {
255
+ db.close();
256
+ }
257
+ } catch {
258
+ return true;
259
+ }
260
+ }
261
+ var ownerLifetimeMs, targetFile, sourceFingerprint, runtimeKeys, ownerFile;
197
262
  var init_ownership = __esm({
198
263
  "src/usage/ownership.ts"() {
199
264
  "use strict";
200
265
  ownerLifetimeMs = 6e4;
266
+ targetFile = (root2) => join2(root2, "usage", "collector-target.json");
267
+ sourceFingerprint = (entry) => createHash("sha256").update(readFileSync(entry)).digest("hex");
268
+ runtimeKeys = ["APILIFT_URL", "CODEX_HOME", "CLAUDE_CONFIG_DIR", "APILIFT_BIN", "APILIFT_DEV_PLUGIN_ROOT"];
201
269
  ownerFile = (root2) => join2(root2, "usage", "collector-owner.json");
202
270
  }
203
271
  });
@@ -28441,8 +28509,8 @@ var init_baselines = __esm({
28441
28509
  });
28442
28510
 
28443
28511
  // src/usage/transcript.ts
28444
- import { createHash } from "node:crypto";
28445
- import { closeSync, openSync, readSync, readdirSync, statSync } from "node:fs";
28512
+ import { createHash as createHash2 } from "node:crypto";
28513
+ import { closeSync, openSync, readSync, readdirSync, statSync as statSync2 } from "node:fs";
28446
28514
  import { homedir as homedir2, platform as platform2 } from "node:os";
28447
28515
  import { basename, join as join5 } from "node:path";
28448
28516
  import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
@@ -28528,7 +28596,7 @@ function commandIdentity(command) {
28528
28596
  let argv = words.slice(index + 1);
28529
28597
  if (argv[0] === "--session") argv = argv.slice(2);
28530
28598
  if (!argv.length) return null;
28531
- return { key: argv.slice(0, 3).join(" "), argumentHash: createHash("sha256").update(JSON.stringify(argv)).digest("hex") };
28599
+ return { key: argv.slice(0, 3).join(" "), argumentHash: createHash2("sha256").update(JSON.stringify(argv)).digest("hex") };
28532
28600
  }
28533
28601
  function commandKey(command) {
28534
28602
  return commandIdentity(command)?.key ?? null;
@@ -28591,7 +28659,7 @@ function transcriptPaths(agent2, nativeId, env, limit = 64) {
28591
28659
  for (const root2 of roots) walk(root2, token, limit, found);
28592
28660
  return [...new Set(found)].slice(0, limit).filter((path2) => {
28593
28661
  try {
28594
- return statSync(path2).isFile();
28662
+ return statSync2(path2).isFile();
28595
28663
  } catch {
28596
28664
  return false;
28597
28665
  }
@@ -28627,7 +28695,7 @@ var init_transcript = __esm({
28627
28695
  provider = null;
28628
28696
  thread = null;
28629
28697
  lastAt = null;
28630
- hash = createHash("sha256");
28698
+ hash = createHash2("sha256");
28631
28699
  head = Buffer.alloc(0);
28632
28700
  tail = Buffer.alloc(0);
28633
28701
  bytes(bytes) {
@@ -28791,7 +28859,7 @@ var init_transcript = __esm({
28791
28859
  const command = Array.isArray(item.command) ? item.command.map(text) : [];
28792
28860
  const args = command.some((arg) => /^-[a-z]*c$/u.test(arg)) ? command.at(-1) : command.length ? command.join(" ") : text(item.command);
28793
28861
  const start = num(payload.started_at_ms) ?? at, stop = num(payload.completed_at_ms) ?? at;
28794
- const parents = [...this.tools.values()].filter((t) => !t.structured && t.name === "exec" && t.returnedAt === null && (t.calledAt ?? Infinity) <= (start ?? -Infinity));
28862
+ const parents = [...this.tools.values()].filter((t) => !t.structured && t.name === "exec" && (t.calledAt ?? Infinity) <= (start ?? -Infinity) && (t.returnedAt === null || start !== null && start <= t.returnedAt));
28795
28863
  const parent = parents.length === 1 ? parents[0] : null;
28796
28864
  const req = parent ? this.facts.requests[parent.request] : this.current ?? this.request(start);
28797
28865
  const tool = this.call(id, "exec_command", args, start, req, true, parent?.id ?? null);
@@ -28860,8 +28928,8 @@ var init_transcript = __esm({
28860
28928
  }
28861
28929
  this.facts.records.sort((a, b2) => (a.startedAt ?? Infinity) - (b2.startedAt ?? Infinity));
28862
28930
  this.facts.sourceHash = this.hash.digest("hex");
28863
- this.facts.headHash = createHash("sha256").update(this.head).digest("hex");
28864
- this.facts.tailHash = createHash("sha256").update(this.tail).digest("hex");
28931
+ this.facts.headHash = createHash2("sha256").update(this.head).digest("hex");
28932
+ this.facts.tailHash = createHash2("sha256").update(this.tail).digest("hex");
28865
28933
  return this.facts;
28866
28934
  }
28867
28935
  };
@@ -163473,7 +163541,7 @@ var init_pricing = __esm({
163473
163541
  });
163474
163542
 
163475
163543
  // src/usage/measurement.ts
163476
- import { createHash as createHash2 } from "node:crypto";
163544
+ import { createHash as createHash3 } from "node:crypto";
163477
163545
  import { readFileSync as readFileSync3 } from "node:fs";
163478
163546
  function largestRemainder(total, weights) {
163479
163547
  const positive = weights.map((v2) => Math.max(0, Math.floor(v2))), sum = positive.reduce((a, b2) => a + b2, 0);
@@ -163539,11 +163607,13 @@ function matchesCommand(row, tool) {
163539
163607
  return false;
163540
163608
  }
163541
163609
  const expected = argv[0] === "--session" ? argv.slice(2) : argv;
163542
- return tool.category === "apilift" && tool.argumentHash === createHash2("sha256").update(JSON.stringify(expected)).digest("hex");
163610
+ return tool.category === "apilift" && tool.argumentHash === createHash3("sha256").update(JSON.stringify(expected)).digest("hex");
163543
163611
  }
163544
163612
  function invocationMatches(row, facts) {
163545
163613
  if (row.agent !== facts.runtime || row.session_id && !row.session_id.includes(":label:") && facts.sessionId && row.session_id !== `${facts.runtime}:${facts.sessionId}`) return [];
163546
- return facts.requests.flatMap((request) => request.tools.filter((t) => t.accounting && t.receipts.includes(row.id) && (!facts.sessionId || !t.thread || t.thread === facts.sessionId)).map((tool) => ({ request, tool })));
163614
+ const matches = facts.requests.flatMap((request) => request.tools.filter((t) => t.accounting && t.receipts.includes(row.id) && (!facts.sessionId || !t.thread || t.thread === facts.sessionId)).map((tool) => ({ request, tool })));
163615
+ const originating = matches.filter(({ tool }) => tool.structured && matchesCommand(row, tool));
163616
+ return originating.length ? originating : matches;
163547
163617
  }
163548
163618
  function sessionMatches(sessionId, facts, rows) {
163549
163619
  if (facts.sessionId && sessionId === `${facts.runtime}:${facts.sessionId}`) return true;
@@ -163750,13 +163820,33 @@ __export(collector_exports, {
163750
163820
  collectorLock: () => collectorLock,
163751
163821
  countPending: () => countPending,
163752
163822
  deliverPending: () => deliverPending,
163823
+ hasPendingUsage: () => hasPendingUsage,
163753
163824
  largestRemainder: () => largestRemainder,
163754
163825
  reconcile: () => reconcile,
163755
163826
  reconcileBaselines: () => reconcileBaselines,
163756
- reconcilePending: () => reconcilePending
163827
+ reconcilePending: () => reconcilePending,
163828
+ upgradeMeasurementAdapter: () => upgradeMeasurementAdapter
163757
163829
  });
163758
- import { rmSync as rmSync2, statSync as statSync2 } from "node:fs";
163759
- import { createHash as createHash3 } from "node:crypto";
163830
+ import { rmSync as rmSync2, statSync as statSync3 } from "node:fs";
163831
+ import { createHash as createHash4 } from "node:crypto";
163832
+ function upgradeMeasurementAdapter(ledger) {
163833
+ const previous = ledger.db.prepare("SELECT value FROM usage_meta WHERE key='measurement_adapter_revision'").get();
163834
+ if (previous && Number(previous.value) >= ADAPTER_REVISION) return;
163835
+ ledger.db.exec("BEGIN IMMEDIATE");
163836
+ try {
163837
+ ledger.db.prepare("UPDATE invocation SET revision=revision+1,measurement_json=NULL,status='pending',attempts=0,next_attempt_at=0 WHERE execution_id IS NOT NULL AND finished_at>=? AND measurement_json IS NOT NULL AND (session_id LIKE '%:label:%' OR json_extract(measurement_json,'$.coverage')='unavailable' OR json_extract(measurement_json,'$.timeCoverage')='unavailable' OR json_extract(measurement_json,'$.provider') IS NULL OR json_extract(measurement_json,'$.model') IS NULL)").run(new Date(Date.now() - 7 * 864e5).toISOString());
163838
+ ledger.db.prepare("INSERT INTO usage_meta(key,value) VALUES ('measurement_adapter_revision',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(String(ADAPTER_REVISION));
163839
+ ledger.db.exec("COMMIT");
163840
+ } catch (error62) {
163841
+ ledger.db.exec("ROLLBACK");
163842
+ throw error62;
163843
+ }
163844
+ }
163845
+ function hasPendingUsage(ledger) {
163846
+ if (ledger.db.prepare("SELECT count(*) AS n FROM invocation WHERE output_path IS NOT NULL OR (execution_id IS NOT NULL AND status='pending')").get().n) return true;
163847
+ if (!ledger.db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='baseline_candidate'").get()) return false;
163848
+ return new BaselineStore(ledger).rows().some((row) => row.status === "pending" && baselineEligible(row));
163849
+ }
163760
163850
  function countPending(ledger) {
163761
163851
  for (const row of ledger.pendingOutput()) {
163762
163852
  if (!row.output_path) continue;
@@ -163793,7 +163883,8 @@ function reconcilePending(ledger, env, now = Date.now(), scan = new TranscriptSc
163793
163883
  if (!facts && (scan.deferred || now - Date.parse(row.finished_at) < THIRTY_MINUTES)) continue;
163794
163884
  const actual = reconcile(row, facts);
163795
163885
  const purpose = row.command_key === "run" && row.session_id ? "productive" : row.purpose;
163796
- const payload = { id: row.id, executionId: row.execution_id, revision: row.revision, purpose, session: ledger.pseudonym(row.agent, row.session_id), worker: ledger.pseudonym(row.agent, row.session_id), durationMs: actual.durationMs, tokens: actual.input !== null && actual.output !== null ? { input: actual.input, output: actual.output } : null, coverage: actual.coverage, timeCoverage: actual.timeCoverage, provider: actual.provider, model: actual.model };
163886
+ const identity = actual.linked && facts?.sessionId ? `${row.agent}:${facts.sessionId}` : row.session_id;
163887
+ const payload = { id: row.id, executionId: row.execution_id, revision: row.revision, purpose, session: ledger.pseudonym(row.agent, identity), worker: ledger.pseudonym(row.agent, identity), durationMs: actual.durationMs, tokens: actual.input !== null && actual.output !== null ? { input: actual.input, output: actual.output } : null, coverage: actual.coverage, timeCoverage: actual.timeCoverage, provider: actual.provider, model: actual.model };
163797
163888
  const checked = executionMeasurementSchema.safeParse(payload);
163798
163889
  if (checked.success) ledger.db.prepare("UPDATE invocation SET measurement_json=?,status='pending' WHERE id=?").run(JSON.stringify(checked.data), row.id);
163799
163890
  else ledger.db.prepare("UPDATE invocation SET status='rejected' WHERE id=?").run(row.id);
@@ -163878,6 +163969,7 @@ async function collectOnce(dataRoot2, env, fetcher = fetch) {
163878
163969
  if (!installation) return;
163879
163970
  await flushExecutionReports(createCliApi(new URL(env.APILIFT_URL || "https://apilift.dev").origin, fetcher, installation.token), state);
163880
163971
  ledger.clearForAccountChange(installation.accountId);
163972
+ upgradeMeasurementAdapter(ledger);
163881
163973
  countPending(ledger);
163882
163974
  const scan = new TranscriptScan(ledger);
163883
163975
  reconcilePending(ledger, env, Date.now(), scan);
@@ -163889,7 +163981,7 @@ async function collectOnce(dataRoot2, env, fetcher = fetch) {
163889
163981
  state.close();
163890
163982
  }
163891
163983
  }
163892
- var THIRTY_MINUTES, scans, TranscriptScan;
163984
+ var THIRTY_MINUTES, ADAPTER_REVISION, scans, TranscriptScan;
163893
163985
  var init_collector = __esm({
163894
163986
  "src/usage/collector.ts"() {
163895
163987
  "use strict";
@@ -163904,6 +163996,7 @@ var init_collector = __esm({
163904
163996
  init_measurement();
163905
163997
  init_ownership();
163906
163998
  THIRTY_MINUTES = 30 * 6e4;
163999
+ ADAPTER_REVISION = 2;
163907
164000
  scans = /* @__PURE__ */ new Map();
163908
164001
  TranscriptScan = class {
163909
164002
  constructor(ledger) {
@@ -163916,7 +164009,7 @@ var init_collector = __esm({
163916
164009
  if (this.cache.has(path2)) return this.cache.get(path2);
163917
164010
  let stat;
163918
164011
  try {
163919
- stat = statSync2(path2);
164012
+ stat = statSync3(path2);
163920
164013
  } catch {
163921
164014
  return null;
163922
164015
  }
@@ -163953,7 +164046,7 @@ var init_collector = __esm({
163953
164046
  entry.mtime = stat.mtimeMs;
163954
164047
  }
163955
164048
  const facts = entry.facts;
163956
- const pathHash = createHash3("sha256").update(path2).digest("hex");
164049
+ const pathHash = createHash4("sha256").update(path2).digest("hex");
163957
164050
  const previous = this.ledger.db.prepare("SELECT source_hash,head_hash,bytes,gap FROM source_observation WHERE path_hash=?").get(pathHash);
163958
164051
  if (previous) {
163959
164052
  const replaced = facts.bytes < previous.bytes || facts.bytes === previous.bytes && facts.sourceHash !== previous.source_hash || previous.head_hash !== null && previous.bytes >= 4096 && facts.headHash !== previous.head_hash;
@@ -163973,9 +164066,51 @@ var init_collector = __esm({
163973
164066
  // src/bin/collector.ts
163974
164067
  init_state();
163975
164068
  init_ownership();
163976
- init_ledger();
163977
164069
  import { enableCompileCache } from "node:module";
163978
164070
 
164071
+ // src/usage/process.ts
164072
+ init_ownership();
164073
+ init_state();
164074
+
164075
+ // src/process.ts
164076
+ import { spawn } from "node:child_process";
164077
+ function backgroundOptions(env) {
164078
+ return { detached: true, windowsHide: true, stdio: "ignore", env };
164079
+ }
164080
+ function startBackground(script, env, onError) {
164081
+ const child = spawn(process.execPath, [script], backgroundOptions(env));
164082
+ child.once("error", onError);
164083
+ child.unref();
164084
+ return child;
164085
+ }
164086
+
164087
+ // src/usage/process.ts
164088
+ import { existsSync as existsSync2 } from "node:fs";
164089
+ import { fileURLToPath } from "node:url";
164090
+ function startCollector(env) {
164091
+ if (process.env.VITEST || env.VITEST || env.APILIFT_COLLECTOR_DISABLED === "1") return;
164092
+ const entry = fileURLToPath(new URL("./apilift-collector.js", import.meta.url));
164093
+ if (!existsSync2(entry)) return;
164094
+ const root2 = dataRoot(env);
164095
+ if (collectorActive(root2)) {
164096
+ try {
164097
+ requestCollectorTarget(root2, entry, env);
164098
+ } catch {
164099
+ }
164100
+ return;
164101
+ }
164102
+ try {
164103
+ startBackground(entry, env, () => {
164104
+ });
164105
+ } catch {
164106
+ }
164107
+ }
164108
+
164109
+ // src/bin/collector.ts
164110
+ import { spawn as spawn2 } from "node:child_process";
164111
+ init_ledger();
164112
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
164113
+
163979
164114
  // src/analytics.ts
163980
164115
  import { randomUUID as randomUUID3 } from "node:crypto";
163981
164116
 
@@ -164157,10 +164292,22 @@ enableCompileCache();
164157
164292
  var root = dataRoot(process.env);
164158
164293
  var held = collectorLock(root);
164159
164294
  if (held) {
164295
+ let restart = false;
164296
+ let target = null;
164160
164297
  let lastActivity = Date.now();
164161
164298
  try {
164162
- const { collectOnce: collectOnce2 } = await Promise.resolve().then(() => (init_collector(), collector_exports));
164299
+ const source = watchCollectorSource(fileURLToPath2(import.meta.url));
164300
+ const { collectOnce: collectOnce2, hasPendingUsage: hasPendingUsage2 } = await Promise.resolve().then(() => (init_collector(), collector_exports));
164163
164301
  for (; ; ) {
164302
+ target = collectorTarget(root, held.id, source.fingerprint);
164303
+ if (target) {
164304
+ restart = true;
164305
+ break;
164306
+ }
164307
+ if (source.changed()) {
164308
+ restart = true;
164309
+ break;
164310
+ }
164164
164311
  held.refresh();
164165
164312
  await collectOnce2(root, process.env);
164166
164313
  const state = new LocalState(root), ledger = new UsageLedger(root);
@@ -164173,8 +164320,7 @@ if (held) {
164173
164320
  }
164174
164321
  if (refresh === "unauthorized") break;
164175
164322
  await analytics.flush();
164176
- const waiting = ledger.db.prepare("SELECT count(*) AS n FROM invocation WHERE output_path IS NOT NULL OR (execution_id IS NOT NULL AND measurement_json IS NULL) OR status='pending'").get().n;
164177
- if (waiting) lastActivity = Date.now();
164323
+ if (hasPendingUsage2(ledger)) lastActivity = Date.now();
164178
164324
  } finally {
164179
164325
  ledger.close();
164180
164326
  state.close();
@@ -164187,4 +164333,18 @@ if (held) {
164187
164333
  held.exec("ROLLBACK");
164188
164334
  held.close();
164189
164335
  }
164336
+ if (restart && target) {
164337
+ const env = { ...process.env, APILIFT_HOME: root };
164338
+ for (const [key, value] of Object.entries(target.env)) {
164339
+ if (value === null) delete env[key];
164340
+ else env[key] = value;
164341
+ }
164342
+ try {
164343
+ const next = spawn2(target.interpreter, [target.entry], backgroundOptions(env));
164344
+ next.once("error", () => {
164345
+ });
164346
+ next.unref();
164347
+ } catch {
164348
+ }
164349
+ } else if (restart) startCollector(process.env);
164190
164350
  }
@@ -134731,7 +134731,7 @@ var TranscriptParser = class {
134731
134731
  const command = Array.isArray(item.command) ? item.command.map(text) : [];
134732
134732
  const args = command.some((arg) => /^-[a-z]*c$/u.test(arg)) ? command.at(-1) : command.length ? command.join(" ") : text(item.command);
134733
134733
  const start = num(payload.started_at_ms) ?? at, stop = num(payload.completed_at_ms) ?? at;
134734
- const parents = [...this.tools.values()].filter((t) => !t.structured && t.name === "exec" && t.returnedAt === null && (t.calledAt ?? Infinity) <= (start ?? -Infinity));
134734
+ const parents = [...this.tools.values()].filter((t) => !t.structured && t.name === "exec" && (t.calledAt ?? Infinity) <= (start ?? -Infinity) && (t.returnedAt === null || start !== null && start <= t.returnedAt));
134735
134735
  const parent = parents.length === 1 ? parents[0] : null;
134736
134736
  const req = parent ? this.facts.requests[parent.request] : this.current ?? this.request(start);
134737
134737
  const tool = this.call(id, "exec_command", args, start, req, true, parent?.id ?? null);
@@ -134978,7 +134978,9 @@ function matchesCommand(row, tool) {
134978
134978
  }
134979
134979
  function invocationMatches(row, facts) {
134980
134980
  if (row.agent !== facts.runtime || row.session_id && !row.session_id.includes(":label:") && facts.sessionId && row.session_id !== `${facts.runtime}:${facts.sessionId}`) return [];
134981
- return facts.requests.flatMap((request) => request.tools.filter((t) => t.accounting && t.receipts.includes(row.id) && (!facts.sessionId || !t.thread || t.thread === facts.sessionId)).map((tool) => ({ request, tool })));
134981
+ const matches = facts.requests.flatMap((request) => request.tools.filter((t) => t.accounting && t.receipts.includes(row.id) && (!facts.sessionId || !t.thread || t.thread === facts.sessionId)).map((tool) => ({ request, tool })));
134982
+ const originating = matches.filter(({ tool }) => tool.structured && matchesCommand(row, tool));
134983
+ return originating.length ? originating : matches;
134982
134984
  }
134983
134985
  function sessionMatches(sessionId, facts, rows) {
134984
134986
  if (facts.sessionId && sessionId === `${facts.runtime}:${facts.sessionId}`) return true;
package/dist/apilift.js CHANGED
@@ -35949,9 +35949,25 @@ var init_analytics2 = __esm({
35949
35949
  });
35950
35950
 
35951
35951
  // src/usage/ownership.ts
35952
- import { mkdirSync as mkdirSync11, chmodSync as chmodSync7, existsSync as existsSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync10, renameSync as renameSync4, unlinkSync as unlinkSync4 } from "node:fs";
35953
- import { join as join15 } from "node:path";
35952
+ import { mkdirSync as mkdirSync11, chmodSync as chmodSync7, existsSync as existsSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync10, renameSync as renameSync4, unlinkSync as unlinkSync4, statSync as statSync3 } from "node:fs";
35953
+ import { createHash as createHash6, randomUUID as randomUUID12 } from "node:crypto";
35954
+ import { join as join15, dirname as dirname9, isAbsolute as isAbsolute2 } from "node:path";
35954
35955
  import { DatabaseSync as DatabaseSync6 } from "node:sqlite";
35956
+ function requestCollectorTarget(root, entry2, env) {
35957
+ const owner = readOwner(root);
35958
+ if (!owner) return;
35959
+ const pending = join15(root, "usage", `.collector-target-${randomUUID12()}`);
35960
+ try {
35961
+ const target = { owner: owner.id, entry: entry2, interpreter: process.execPath, fingerprint: sourceFingerprint(entry2), env: Object.fromEntries(runtimeKeys.map((key) => [key, env[key] ?? null])) };
35962
+ writeFileSync10(pending, JSON.stringify(target), { mode: 384 });
35963
+ renameSync4(pending, targetFile(root));
35964
+ } finally {
35965
+ try {
35966
+ unlinkSync4(pending);
35967
+ } catch {
35968
+ }
35969
+ }
35970
+ }
35955
35971
  function readOwner(root) {
35956
35972
  try {
35957
35973
  const value = JSON.parse(readFileSync10(ownerFile(root), "utf8"));
@@ -35984,11 +36000,14 @@ function collectorActive(root) {
35984
36000
  return true;
35985
36001
  }
35986
36002
  }
35987
- var ownerLifetimeMs, ownerFile;
36003
+ var ownerLifetimeMs, targetFile, sourceFingerprint, runtimeKeys, ownerFile;
35988
36004
  var init_ownership = __esm({
35989
36005
  "src/usage/ownership.ts"() {
35990
36006
  "use strict";
35991
36007
  ownerLifetimeMs = 6e4;
36008
+ targetFile = (root) => join15(root, "usage", "collector-target.json");
36009
+ sourceFingerprint = (entry2) => createHash6("sha256").update(readFileSync10(entry2)).digest("hex");
36010
+ runtimeKeys = ["APILIFT_URL", "CODEX_HOME", "CLAUDE_CONFIG_DIR", "APILIFT_BIN", "APILIFT_DEV_PLUGIN_ROOT"];
35992
36011
  ownerFile = (root) => join15(root, "usage", "collector-owner.json");
35993
36012
  }
35994
36013
  });
@@ -35999,7 +36018,15 @@ import { fileURLToPath as fileURLToPath3 } from "node:url";
35999
36018
  function startCollector(env) {
36000
36019
  if (process.env.VITEST || env.VITEST || env.APILIFT_COLLECTOR_DISABLED === "1") return;
36001
36020
  const entry2 = fileURLToPath3(new URL("./apilift-collector.js", import.meta.url));
36002
- if (!existsSync7(entry2) || collectorActive(dataRoot(env))) return;
36021
+ if (!existsSync7(entry2)) return;
36022
+ const root = dataRoot(env);
36023
+ if (collectorActive(root)) {
36024
+ try {
36025
+ requestCollectorTarget(root, entry2, env);
36026
+ } catch {
36027
+ }
36028
+ return;
36029
+ }
36003
36030
  try {
36004
36031
  startBackground(entry2, env, () => {
36005
36032
  });
@@ -38050,11 +38077,11 @@ var init_compat2 = __esm({
38050
38077
  });
38051
38078
 
38052
38079
  // src/native-host/registration.ts
38053
- import { createHash as createHash6, randomUUID as randomUUID12 } from "node:crypto";
38080
+ import { createHash as createHash7, randomUUID as randomUUID13 } from "node:crypto";
38054
38081
  import { execFileSync as execFileSync2 } from "node:child_process";
38055
38082
  import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync11 } from "node:fs";
38056
38083
  import { homedir as homedir3, platform as platform2 } from "node:os";
38057
- import { dirname as dirname9, isAbsolute as isAbsolute2, join as join16, resolve as resolve8 } from "node:path";
38084
+ import { dirname as dirname10, isAbsolute as isAbsolute3, join as join16, resolve as resolve8 } from "node:path";
38058
38085
  import { chmod as chmod2, mkdir, readFile, rename as rename2, writeFile, copyFile, rm, stat } from "node:fs/promises";
38059
38086
  function batchLiteral(value) {
38060
38087
  if (/["\r\n\0]/u.test(value)) throw new Error("Native host launcher values contain unsupported control characters");
@@ -38129,7 +38156,7 @@ async function registerNativeHost(options2) {
38129
38156
  const hostName = options2.hostName ?? "com.apilift.browser";
38130
38157
  if (!hostNames.has(hostName)) throw new Error("Unknown Apilift native host name");
38131
38158
  if (!validId.test(options2.extensionId)) throw new Error("Extension id must be 32 a\u2013p characters");
38132
- if (![options2.root, options2.hostScript, options2.nodePath ?? process.execPath].every(isAbsolute2)) throw new Error("Native host paths must be absolute");
38159
+ if (![options2.root, options2.hostScript, options2.nodePath ?? process.execPath].every(isAbsolute3)) throw new Error("Native host paths must be absolute");
38133
38160
  const siteUrl2 = options2.siteUrl ? new URL(options2.siteUrl) : null;
38134
38161
  if (siteUrl2 && (!["http:", "https:"].includes(siteUrl2.protocol) || siteUrl2.origin !== options2.siteUrl)) throw new Error("Native host site URL must be an http(s) origin");
38135
38162
  const root = resolve8(options2.root), hostScript = resolve8(options2.hostScript), nodePath = resolve8(options2.nodePath ?? process.execPath);
@@ -38140,7 +38167,7 @@ async function registerNativeHost(options2) {
38140
38167
  const manifests = [manifest];
38141
38168
  if (platform2() === "linux" && !env.APILIFT_NATIVE_MESSAGING_DIR) {
38142
38169
  for (const browser of ["google-chrome-beta", "google-chrome-unstable", "chromium"]) {
38143
- const profile = join16(dirname9(dirname9(manifestRoot)), browser);
38170
+ const profile = join16(dirname10(dirname10(manifestRoot)), browser);
38144
38171
  if (await stat(profile).then((value) => value.isDirectory()).catch(() => false)) manifests.push(join16(profile, "NativeMessagingHosts", `${hostName}.json`));
38145
38172
  }
38146
38173
  }
@@ -38174,7 +38201,7 @@ async function registerNativeHost(options2) {
38174
38201
  const launcherText = nativeLauncher({ root, hostName, nodePath, hostScript, manifestRoot, site: siteUrl2?.origin }, platform2() === "win32");
38175
38202
  const manifestText = JSON.stringify({ name: hostName, description: "Apilift browser bridge", path: launcher, type: "stdio", allowed_origins: [`chrome-extension://${options2.extensionId}/`] }, null, 2) + "\n";
38176
38203
  const atomicWrite = async (path2, bytes, mode) => {
38177
- const temp = join16(dirname9(path2), `.${hostName}-${randomUUID12()}.tmp`);
38204
+ const temp = join16(dirname10(path2), `.${hostName}-${randomUUID13()}.tmp`);
38178
38205
  try {
38179
38206
  await writeFile(temp, bytes, { mode, flag: "wx" });
38180
38207
  await chmod2(temp, mode);
@@ -38185,7 +38212,7 @@ async function registerNativeHost(options2) {
38185
38212
  };
38186
38213
  await atomicWrite(launcher, launcherText, platform2() === "win32" ? 384 : 448);
38187
38214
  for (const path2 of manifests) {
38188
- await mkdir(dirname9(path2), { recursive: true, mode: 448 });
38215
+ await mkdir(dirname10(path2), { recursive: true, mode: 448 });
38189
38216
  await atomicWrite(path2, manifestText, 384);
38190
38217
  }
38191
38218
  const receipt = { ...previous, [launcher]: fingerprint(launcherText), ...Object.fromEntries(manifests.map((path2) => [path2, fingerprint(manifestText)])) };
@@ -38197,17 +38224,17 @@ var fingerprint, hostNames, validId, posixQuote, absoluteEnv;
38197
38224
  var init_registration = __esm({
38198
38225
  "src/native-host/registration.ts"() {
38199
38226
  "use strict";
38200
- fingerprint = (bytes) => createHash6("sha256").update(bytes).digest("hex");
38227
+ fingerprint = (bytes) => createHash7("sha256").update(bytes).digest("hex");
38201
38228
  hostNames = /* @__PURE__ */ new Set(["com.apilift.browser", "com.apilift.browser.dev"]);
38202
38229
  validId = /^[a-p]{32}$/;
38203
38230
  posixQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
38204
- absoluteEnv = (value) => value && isAbsolute2(value) ? value : void 0;
38231
+ absoluteEnv = (value) => value && isAbsolute3(value) ? value : void 0;
38205
38232
  }
38206
38233
  });
38207
38234
 
38208
38235
  // src/update.ts
38209
38236
  import { fileURLToPath as fileURLToPath4 } from "node:url";
38210
- import { dirname as dirname10, join as join17, resolve as resolve9, sep, isAbsolute as isAbsolute3 } from "node:path";
38237
+ import { dirname as dirname11, join as join17, resolve as resolve9, sep, isAbsolute as isAbsolute4 } from "node:path";
38211
38238
  import { mkdirSync as mkdirSync13, realpathSync as realpathSync2, writeFileSync as writeFileSync12, readFileSync as readFileSync11 } from "node:fs";
38212
38239
  import { DatabaseSync as DatabaseSync7 } from "node:sqlite";
38213
38240
  function globalPackage(script2, prefix) {
@@ -38230,7 +38257,7 @@ async function pnpmInstallations(runner, env) {
38230
38257
  if (!Array.isArray(groups)) return [];
38231
38258
  return groups.flatMap((group) => {
38232
38259
  const pkg = group?.dependencies?.["@apilift/cli"];
38233
- return pkg && typeof pkg.path === "string" && isAbsolute3(pkg.path) && typeof pkg.version === "string" ? [{ entrypoint: join17(pkg.path, "dist", "apilift.js"), version: pkg.version }] : [];
38260
+ return pkg && typeof pkg.path === "string" && isAbsolute4(pkg.path) && typeof pkg.version === "string" ? [{ entrypoint: join17(pkg.path, "dist", "apilift.js"), version: pkg.version }] : [];
38234
38261
  });
38235
38262
  } catch {
38236
38263
  return [];
@@ -38324,10 +38351,10 @@ async function updateCli(options2, onInstalled) {
38324
38351
  const candidates = (await pnpmInstallations(runner, options2.env)).filter((candidate2) => candidate2.version === latest);
38325
38352
  if (candidates.length !== 1) throw new Error("Updated pnpm installation is ambiguous or unavailable");
38326
38353
  entrypoint = candidates[0].entrypoint;
38327
- const pkg = JSON.parse(readFileSync11(join17(dirname10(entrypoint), "..", "package.json"), "utf8"));
38354
+ const pkg = JSON.parse(readFileSync11(join17(dirname11(entrypoint), "..", "package.json"), "utf8"));
38328
38355
  if (pkg.name !== "@apilift/cli" || pkg.version !== latest) throw new Error("Updated pnpm package could not be verified");
38329
38356
  }
38330
- await registerNativeHost({ root: options2.root, hostScript: join17(dirname10(entrypoint), "apilift-native-host.js"), extensionId: options2.env.APILIFT_DEV_EXTENSION_ID || STORE_EXTENSION_ID, hostName: options2.env.APILIFT_NATIVE_HOST_NAME, siteUrl: options2.site, env: options2.env });
38357
+ await registerNativeHost({ root: options2.root, hostScript: join17(dirname11(entrypoint), "apilift-native-host.js"), extensionId: options2.env.APILIFT_DEV_EXTENSION_ID || STORE_EXTENSION_ID, hostName: options2.env.APILIFT_NATIVE_HOST_NAME, siteUrl: options2.site, env: options2.env });
38331
38358
  for (const agent2 of linkedAgents) {
38332
38359
  if (!ensureAgentPlugin(agent2, options2.env).ready) throw new Error("Updated agent plugin activation is unverified");
38333
38360
  }
@@ -38368,9 +38395,9 @@ var init_update = __esm({
38368
38395
  });
38369
38396
 
38370
38397
  // src/setup-check.ts
38371
- import { createHash as createHash7, randomUUID as randomUUID13 } from "node:crypto";
38398
+ import { createHash as createHash8, randomUUID as randomUUID14 } from "node:crypto";
38372
38399
  import { existsSync as existsSync8, readFileSync as readFileSync12, realpathSync as realpathSync3, writeFileSync as writeFileSync13 } from "node:fs";
38373
- import { dirname as dirname11, join as join18 } from "node:path";
38400
+ import { dirname as dirname12, join as join18 } from "node:path";
38374
38401
  async function setupCheckBody(client, scopeId, value) {
38375
38402
  if (typeof value.body === "string") return Buffer.byteLength(value.body, "utf8") <= maxCheckBodyBytes ? value.body : null;
38376
38403
  if (value.body != null) return null;
@@ -38378,7 +38405,7 @@ async function setupCheckBody(client, scopeId, value) {
38378
38405
  if (!artifact || typeof artifact.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(artifact.sha256) || !Number.isInteger(artifact.bytes) || Number(artifact.bytes) < 1 || Number(artifact.bytes) > maxCheckBodyBytes || artifact.encoding !== "utf-8") return null;
38379
38406
  try {
38380
38407
  const bytes = await client.blob(scopeId, artifact.sha256);
38381
- if (!bytes || bytes.length !== artifact.bytes || createHash7("sha256").update(bytes).digest("hex") !== artifact.sha256) return null;
38408
+ if (!bytes || bytes.length !== artifact.bytes || createHash8("sha256").update(bytes).digest("hex") !== artifact.sha256) return null;
38382
38409
  return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
38383
38410
  } catch {
38384
38411
  return null;
@@ -38386,7 +38413,7 @@ async function setupCheckBody(client, scopeId, value) {
38386
38413
  }
38387
38414
  async function registerSetupHost(root, site, env) {
38388
38415
  const script2 = realpathSync3(process.argv[1]);
38389
- const hostScript = join18(dirname11(script2), "apilift-native-host.js");
38416
+ const hostScript = join18(dirname12(script2), "apilift-native-host.js");
38390
38417
  if (!existsSync8(hostScript)) throw new Error("The native host binary is missing beside the Apilift CLI");
38391
38418
  await registerNativeHost({ root, hostScript, extensionId: env.APILIFT_DEV_EXTENSION_ID || storeExtensionId, hostName: env.APILIFT_NATIVE_HOST_NAME, siteUrl: site, env });
38392
38419
  }
@@ -38400,13 +38427,13 @@ function setupChallenge(root, agent2, site) {
38400
38427
  if (new URL(saved.url).origin === site && Number.isFinite(saved.createdAt) && Date.now() - saved.createdAt < 10 * 6e4) return saved;
38401
38428
  } catch {
38402
38429
  }
38403
- const challenge = { url: `${site}/setup-check?challenge=${randomUUID13()}`, createdAt: Date.now() };
38430
+ const challenge = { url: `${site}/setup-check?challenge=${randomUUID14()}`, createdAt: Date.now() };
38404
38431
  writeFileSync13(file2, JSON.stringify(challenge), { mode: 384 });
38405
38432
  return challenge;
38406
38433
  }
38407
38434
  async function setupBridge(root) {
38408
38435
  const startedAt = Date.now();
38409
- const script2 = join18(dirname11(realpathSync3(process.argv[1])), "apilift-capture-server.js");
38436
+ const script2 = join18(dirname12(realpathSync3(process.argv[1])), "apilift-capture-server.js");
38410
38437
  const client = await ensureCaptureServer({ root, script: script2, version: cliVersion });
38411
38438
  if (!client.legacyBridge) await waitForBridge(root, cliVersion, Math.max(0, 9e4 - (Date.now() - startedAt)), () => client.bridgeDemand(), client.receipt.version);
38412
38439
  return client;
@@ -38446,7 +38473,7 @@ async function verifySetupCheck(root, challenge) {
38446
38473
  }
38447
38474
  if (candidates.length !== 1) return { ok: false, reason: candidates.length ? "More than one exact check tab matched" : "The extension did not report the exact check tab within 10 seconds" };
38448
38475
  const execute = await client.openScope({ kind: "execute", tabSelector: candidates[0].selector, ...scopeInput });
38449
- const id2 = randomUUID13();
38476
+ const id2 = randomUUID14();
38450
38477
  try {
38451
38478
  await client.enqueueJob(execute.id, { job: { id: id2, kind: "replay", effect: "read", method: "GET", url: challenge.url, headers: {}, pageOrigin, allowedRequestOrigin: pageOrigin, timeoutMs: 2e4 } });
38452
38479
  const deadline = Date.now() + 3e4;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apilift",
3
- "version": "1.4.35",
3
+ "version": "1.4.37",
4
4
  "description": "Use logged-in apps and discover missing actions with Apilift.",
5
5
  "author": {
6
6
  "name": "Apilift"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apilift",
3
- "version": "1.4.35",
3
+ "version": "1.4.37",
4
4
  "description": "Use logged-in apps and discover missing actions with Apilift.",
5
5
  "author": {
6
6
  "name": "Apilift"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apilift/cli",
3
- "version": "1.4.35",
3
+ "version": "1.4.37",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,7 +36,7 @@
36
36
  "engines": {
37
37
  "node": ">=24.16.0 <27"
38
38
  },
39
- "gitHead": "6cc5ff47bf97a681f4b379d006679203bb88d293",
39
+ "gitHead": "e08008c496f41cb177195b7f8afca5e6f0373c82",
40
40
  "repository": {
41
41
  "type": "git",
42
42
  "url": "git+https://github.com/egor-sergeev/apilift.git",