@frod.io/bridge 0.7.4 → 0.7.5

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.
Files changed (2) hide show
  1. package/dist/cli.js +170 -21
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // @frod.io/bridge 0.7.4. One file; do not edit: the source is apps/bridge/src in the platform repository.
2
+ // @frod.io/bridge 0.7.5. One file; do not edit: the source is apps/bridge/src in the platform repository.
3
3
  import { createRequire as __createRequire } from 'node:module';
4
4
  const require = __createRequire(import.meta.url);
5
5
  var __create = Object.create;
@@ -4090,6 +4090,25 @@ var LineBuffer = class {
4090
4090
  // src/session-store.ts
4091
4091
  import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, chmodSync as chmodSync3 } from "node:fs";
4092
4092
  import { join as join4 } from "node:path";
4093
+
4094
+ // src/adapters/usage-delta.ts
4095
+ var KEYS = ["inputTokens", "outputTokens", "cacheCreationTokens", "cacheReadTokens"];
4096
+ function usageDelta(current, previous) {
4097
+ if (!previous || KEYS.some((key) => current[key] < previous[key])) return { ...current };
4098
+ return {
4099
+ inputTokens: current.inputTokens - previous.inputTokens,
4100
+ outputTokens: current.outputTokens - previous.outputTokens,
4101
+ cacheCreationTokens: current.cacheCreationTokens - previous.cacheCreationTokens,
4102
+ cacheReadTokens: current.cacheReadTokens - previous.cacheReadTokens
4103
+ };
4104
+ }
4105
+ function isUsageCounters(value) {
4106
+ if (!value || typeof value !== "object") return false;
4107
+ const row = value;
4108
+ return KEYS.every((key) => typeof row[key] === "number" && Number.isFinite(row[key]) && row[key] >= 0);
4109
+ }
4110
+
4111
+ // src/session-store.ts
4093
4112
  var PATH = join4(FROD_DIR, "sessions.json");
4094
4113
  function read() {
4095
4114
  if (!existsSync3(PATH)) return {};
@@ -4107,6 +4126,9 @@ function write(store) {
4107
4126
  function getStored(platformSessionId) {
4108
4127
  return read()[platformSessionId] ?? null;
4109
4128
  }
4129
+ function getStoredByHostSessionId(hostSessionId) {
4130
+ return Object.values(read()).filter((session) => session.hostSessionId === hostSessionId).sort((a, b) => Date.parse(b.lastPromptAt ?? b.startedAt) - Date.parse(a.lastPromptAt ?? a.startedAt))[0] ?? null;
4131
+ }
4110
4132
  function putStored(platformSessionId, s) {
4111
4133
  const store = read();
4112
4134
  store[platformSessionId] = s;
@@ -4121,6 +4143,14 @@ function touchStored(platformSessionId) {
4121
4143
  write(store);
4122
4144
  }
4123
4145
  }
4146
+ function putStoredUsage(platformSessionId, adapter, counters) {
4147
+ const store = read();
4148
+ const session = store[platformSessionId];
4149
+ if (!session || !isUsageCounters(counters)) return;
4150
+ session.cumulativeUsage = { adapter, counters: { ...counters } };
4151
+ session.lastPromptAt = (/* @__PURE__ */ new Date()).toISOString();
4152
+ write(store);
4153
+ }
4124
4154
 
4125
4155
  // src/log.ts
4126
4156
  var REDACT = [
@@ -4860,6 +4890,8 @@ var CodexTranslator = class {
4860
4890
  }
4861
4891
  /** The newest `thread/tokenUsage/updated` of this turn, or null if none arrived. */
4862
4892
  usage = null;
4893
+ /** The running thread counters immediately before this turn's first model call. */
4894
+ billingBase = null;
4863
4895
  /** Item ids whose text was already streamed as deltas — not repeated on completion. */
4864
4896
  streamed = /* @__PURE__ */ new Set();
4865
4897
  /** Tool items announced with `item/started`, so a completion without a start still opens a card. */
@@ -4949,19 +4981,42 @@ var CodexTranslator = class {
4949
4981
  }
4950
4982
  onTokenUsage(p) {
4951
4983
  const u = p.tokenUsage;
4952
- if (u && typeof u === "object" && u.last && u.total) this.usage = u;
4984
+ if (u && typeof u === "object" && u.last && u.total) {
4985
+ if (!this.billingBase) {
4986
+ const total = countersOf(u.total);
4987
+ const last = countersOf(u.last);
4988
+ if (total && last && countersCover(total, last)) this.billingBase = usageDelta(total, last);
4989
+ }
4990
+ this.usage = u;
4991
+ }
4953
4992
  return true;
4954
4993
  }
4955
4994
  };
4956
- function toUsage(usage, model) {
4995
+ function countersOf(breakdown) {
4996
+ const inputTokens = num(breakdown.inputTokens);
4997
+ const outputTokens = num(breakdown.outputTokens);
4998
+ if (inputTokens === null || outputTokens === null) return null;
4999
+ return {
5000
+ inputTokens,
5001
+ outputTokens,
5002
+ cacheCreationTokens: num(breakdown.cacheWriteInputTokens) ?? 0,
5003
+ cacheReadTokens: num(breakdown.cachedInputTokens) ?? 0
5004
+ };
5005
+ }
5006
+ function countersCover(total, part) {
5007
+ return total.inputTokens >= part.inputTokens && total.outputTokens >= part.outputTokens && total.cacheCreationTokens >= part.cacheCreationTokens && total.cacheReadTokens >= part.cacheReadTokens;
5008
+ }
5009
+ function toUsage(usage, model, billingBase = null) {
4957
5010
  if (!usage || !model) return null;
4958
5011
  const { last, total } = usage;
4959
- if (num(total.inputTokens) === null || num(total.outputTokens) === null) return null;
5012
+ const cumulative = countersOf(total);
5013
+ if (!cumulative) return null;
5014
+ const turn = usageDelta(cumulative, billingBase);
4960
5015
  return {
4961
- input_tokens: Math.max(0, total.inputTokens - (num(total.cachedInputTokens) ?? 0)),
4962
- output_tokens: total.outputTokens,
4963
- cache_creation_tokens: num(total.cacheWriteInputTokens) ?? 0,
4964
- cache_read_tokens: num(total.cachedInputTokens) ?? 0,
5016
+ input_tokens: Math.max(0, turn.inputTokens - turn.cacheReadTokens),
5017
+ output_tokens: turn.outputTokens,
5018
+ cache_creation_tokens: turn.cacheCreationTokens,
5019
+ cache_read_tokens: turn.cacheReadTokens,
4965
5020
  model,
4966
5021
  context_tokens: num(last.inputTokens),
4967
5022
  context_window: num(usage.modelContextWindow)
@@ -5232,7 +5287,8 @@ var CodexAdapter = class _CodexAdapter {
5232
5287
  const status2 = typeof turn.status === "string" ? turn.status : "completed";
5233
5288
  const err = turn.error;
5234
5289
  const stopReason = status2 === "interrupted" ? "cancelled" : status2 === "failed" ? "error" : "end_turn";
5235
- const usage = toUsage(live.turn?.translator.usage ?? null, live.model);
5290
+ const translator = live.turn?.translator;
5291
+ const usage = toUsage(translator?.usage ?? null, live.model, translator?.billingBase ?? null);
5236
5292
  live.turnId = null;
5237
5293
  this.settleTurn(live, {
5238
5294
  stopReason,
@@ -5571,19 +5627,29 @@ function stopReasonFor(acp) {
5571
5627
  }
5572
5628
  }
5573
5629
  var num2 = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
5574
- function toUsage2(meta, fallbackModel, contextWindow) {
5630
+ function cumulativeUsageOf(meta) {
5575
5631
  const total = meta?.usage;
5632
+ if (!total) return null;
5633
+ const inputTokens = num2(total.inputTokens);
5634
+ const outputTokens = num2(total.outputTokens);
5635
+ if (inputTokens === null || outputTokens === null) return null;
5636
+ return {
5637
+ inputTokens,
5638
+ outputTokens,
5639
+ cacheCreationTokens: num2(total.cacheCreationTokens) ?? 0,
5640
+ cacheReadTokens: num2(total.cachedReadTokens) ?? 0
5641
+ };
5642
+ }
5643
+ function toUsage2(meta, fallbackModel, contextWindow, previous = null) {
5576
5644
  const model = str2(meta?.modelId) ?? fallbackModel;
5577
- if (!total || !model) return null;
5578
- const input = num2(total.inputTokens);
5579
- const output = num2(total.outputTokens);
5580
- if (input === null || output === null) return null;
5581
- const cacheRead = num2(total.cachedReadTokens) ?? 0;
5645
+ const cumulative = cumulativeUsageOf(meta);
5646
+ if (!cumulative || !model) return null;
5647
+ const turn = usageDelta(cumulative, previous);
5582
5648
  return {
5583
- input_tokens: Math.max(0, input - cacheRead),
5584
- output_tokens: output,
5585
- cache_creation_tokens: num2(total.cacheCreationTokens) ?? 0,
5586
- cache_read_tokens: cacheRead,
5649
+ input_tokens: Math.max(0, turn.inputTokens - turn.cacheReadTokens),
5650
+ output_tokens: turn.outputTokens,
5651
+ cache_creation_tokens: turn.cacheCreationTokens,
5652
+ cache_read_tokens: turn.cacheReadTokens,
5587
5653
  model,
5588
5654
  context_tokens: num2(meta?.inputTokens),
5589
5655
  context_window: contextWindow
@@ -5809,6 +5875,8 @@ ${res.stderr ?? ""}`;
5809
5875
  turn: null,
5810
5876
  model: spec.model?.name ?? null,
5811
5877
  contextWindow: null,
5878
+ cumulativeUsage: null,
5879
+ needsUsageBaseline: false,
5812
5880
  onExit: o.onExit,
5813
5881
  askPermission: o.askPermission,
5814
5882
  reportAutoAllowed: o.reportAutoAllowed,
@@ -5839,6 +5907,10 @@ ${res.stderr ?? ""}`;
5839
5907
  const mcpServers = mcpServersFor(spec);
5840
5908
  const params = { cwd: o.sessionDir, mcpServers, _meta: sessionMeta(spec) };
5841
5909
  const resumeId = spec.resume_session_id ?? null;
5910
+ const resumedStore = resumeId ? getStoredByHostSessionId(resumeId) : null;
5911
+ if (resumedStore?.cumulativeUsage?.adapter === "grok" && isUsageCounters(resumedStore.cumulativeUsage.counters)) {
5912
+ live.cumulativeUsage = { ...resumedStore.cumulativeUsage.counters };
5913
+ }
5842
5914
  let resumed = false;
5843
5915
  let started;
5844
5916
  if (resumeId) {
@@ -5850,12 +5922,25 @@ ${res.stderr ?? ""}`;
5850
5922
  }
5851
5923
  }
5852
5924
  if (!started) started = await this.request(live, "session/new", params);
5925
+ if (!resumed) live.cumulativeUsage = null;
5926
+ live.needsUsageBaseline = resumed && !live.cumulativeUsage;
5853
5927
  const sessionId = typeof started?.sessionId === "string" ? started.sessionId : null;
5854
5928
  if (!sessionId) throw new Error("Grok Build started but did not name the session \u2014 it cannot be addressed.");
5855
5929
  live.acpSessionId = sessionId;
5856
5930
  const models = started?.models ?? init?._meta?.modelState ?? null;
5857
5931
  live.model = live.model ?? (typeof models?.currentModelId === "string" ? models.currentModelId : null);
5858
5932
  live.contextWindow = contextWindowOf(models, live.model);
5933
+ try {
5934
+ putStored(spec.session_id, {
5935
+ hostSessionId: sessionId,
5936
+ cwd: o.sessionDir,
5937
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5938
+ lastPromptAt: null,
5939
+ ...resumed && live.cumulativeUsage ? { cumulativeUsage: { adapter: "grok", counters: { ...live.cumulativeUsage } } } : {}
5940
+ });
5941
+ } catch (err) {
5942
+ log.warn(`[${spec.session_id.slice(0, 8)}] grok: could not open the usage-counter record (${err instanceof Error ? err.message : String(err)})`);
5943
+ }
5859
5944
  log.event("grok.started", {
5860
5945
  session: spec.session_id,
5861
5946
  acp_session: sessionId,
@@ -5880,7 +5965,21 @@ ${res.stderr ?? ""}`;
5880
5965
  const meta = r._meta ?? null;
5881
5966
  if (typeof meta?.modelId === "string" && meta.modelId) live.model = meta.modelId;
5882
5967
  const { stopReason, error } = stopReasonFor(r.stopReason);
5883
- this.settleTurn(live, { stopReason, usage: toUsage2(meta, live.model, live.contextWindow), ...error ? { error } : {} });
5968
+ const cumulative = cumulativeUsageOf(meta);
5969
+ const usage = live.needsUsageBaseline ? null : toUsage2(meta, live.model, live.contextWindow, live.cumulativeUsage);
5970
+ if (cumulative) {
5971
+ if (live.needsUsageBaseline) {
5972
+ log.warn(`[${live.spec.session_id.slice(0, 8)}] grok: resumed without an earlier usage snapshot \u2014 this turn is unpriced and seeds the next increment`);
5973
+ live.needsUsageBaseline = false;
5974
+ }
5975
+ live.cumulativeUsage = cumulative;
5976
+ try {
5977
+ putStoredUsage(live.spec.session_id, "grok", cumulative);
5978
+ } catch (err) {
5979
+ log.warn(`[${live.spec.session_id.slice(0, 8)}] grok: could not remember usage counters (${err instanceof Error ? err.message : String(err)})`);
5980
+ }
5981
+ }
5982
+ this.settleTurn(live, { stopReason, usage, ...error ? { error } : {} });
5884
5983
  }).catch((err) => {
5885
5984
  this.settleTurn(live, { stopReason: "error", usage: null, error: err.message });
5886
5985
  });
@@ -7790,6 +7889,8 @@ function askChild(input) {
7790
7889
  } catch {
7791
7890
  }
7792
7891
  };
7892
+ child.stdin?.on("error", () => {
7893
+ });
7793
7894
  child.on("error", (err) => finish({ problem: `could not start: ${err.message}` }));
7794
7895
  child.on("exit", (code) => {
7795
7896
  if (!done) finish({ problem: `ended (exit ${code ?? "signal"}) before answering` });
@@ -7849,8 +7950,56 @@ function samplesInWords(samples) {
7849
7950
  return samples.map((s) => `${label[s.provider]} ${s.usedPct === null ? `(${s.words ?? "no number"})` : `${Math.round(s.usedPct)}% of ${win[s.window]}`}`).join(" \xB7 ") || "nothing to report";
7850
7951
  }
7851
7952
 
7953
+ // package.json
7954
+ var package_default = {
7955
+ name: "@frod.io/bridge",
7956
+ version: "0.7.5",
7957
+ type: "module",
7958
+ description: "Your computer is a desk: hosts a Frod.io agent's sessions with your own Claude Code, Codex or Grok Build. Installs and updates itself.",
7959
+ license: "UNLICENSED",
7960
+ homepage: "https://frod.io",
7961
+ repository: {
7962
+ type: "git",
7963
+ url: "https://github.com/awafeek/agentic-engineering.git",
7964
+ directory: "apps/bridge"
7965
+ },
7966
+ bin: {
7967
+ "frod-bridge": "dist/cli.js"
7968
+ },
7969
+ files: [
7970
+ "dist",
7971
+ "README.md"
7972
+ ],
7973
+ engines: {
7974
+ node: ">=20"
7975
+ },
7976
+ publishConfig: {
7977
+ access: "public"
7978
+ },
7979
+ scripts: {
7980
+ build: "node scripts/bundle.mjs && tsc --noEmit",
7981
+ bundle: "node scripts/bundle.mjs",
7982
+ lint: "eslint src",
7983
+ test: "vitest run --passWithNoTests",
7984
+ "type-check": "tsc --noEmit",
7985
+ "pack:check": "npm pack --dry-run"
7986
+ },
7987
+ devDependencies: {
7988
+ "@eslint/js": "^9.39.3",
7989
+ "@types/node": "^22.0.0",
7990
+ "@types/ws": "^8.18.1",
7991
+ esbuild: "^0.27.3",
7992
+ eslint: "^9.0.0",
7993
+ tsx: "^4.19.0",
7994
+ typescript: "^5.7.0",
7995
+ "typescript-eslint": "^8.56.1",
7996
+ vitest: "^4.0.18",
7997
+ ws: "^8.21.3"
7998
+ }
7999
+ };
8000
+
7852
8001
  // src/cli.ts
7853
- var VERSION = "0.7.4";
8002
+ var VERSION = package_default.version;
7854
8003
  function arg(flag, argv) {
7855
8004
  const i = argv.indexOf(flag);
7856
8005
  return i >= 0 ? argv[i + 1] : void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frod.io/bridge",
3
- "version": "0.7.4",
3
+ "version": "0.7.5",
4
4
  "type": "module",
5
5
  "description": "Your computer is a desk: hosts a Frod.io agent's sessions with your own Claude Code, Codex or Grok Build. Installs and updates itself.",
6
6
  "license": "UNLICENSED",