ai-spend-agent 0.9.9 → 0.9.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.
package/dist/index.js CHANGED
@@ -1098,14 +1098,17 @@ async function workspaceCommand(args, runtime) {
1098
1098
  : fail("Pairing outcome is unknown. Do not replay this response bundle; inspect and revoke the enrollment in Settings before starting another.");
1099
1099
  }
1100
1100
  if (action === "disconnect") {
1101
- const result = await disconnectWorkspace({ home: runtime.homeDirectory, transport: runtime.workspaceDisconnectTransport,
1101
+ const result = await disconnectWorkspace({ home: runtime.homeDirectory, retry: args.workspaceDisconnectRetry, transport: runtime.workspaceDisconnectTransport,
1102
1102
  confirm: action => consent(action === "abandon_unexchanged"
1103
1103
  ? "This native request has never attempted exchange. Destroy its unused private pairing key so it can no longer complete enrollment? Cancel any browser challenge in Settings as well. [y/N] "
1104
- : "Revoke this machine's Workspace grant and remove its local pairing after the accepted receipt? [y/N] ") });
1105
- return result.state === "revoked" ? ok("Workspace revoked this machine grant. Local pairing was removed.")
1106
- : result.state === "abandoned" ? ok("Unused native pairing key removed. No remote revocation was claimed. Run npx aibill workspace connect to start a new request.")
1107
- : result.state === "cancelled" ? fail("Pairing kept. Disconnect was not confirmed; no request or key was removed.") : result.state === "not_paired" ? ok("No completed local pairing exists.")
1108
- : fail(`Disconnect outcome is unknown. Local keys remain; no retry was sent. Reconcile this machine at ${WORKSPACE_ORIGIN}/settings/machines.`);
1104
+ : action === "retry_revoke"
1105
+ ? "Repeat the identical retained disconnect request once? If already revoked, Tilden returns that outcome; otherwise this revokes the same grant. No session facts are sent. Local pairing is removed only after a matching receipt. [y/N] "
1106
+ : "Revoke this machine's Workspace grant and remove its local pairing after the accepted receipt? [y/N] ") });
1107
+ return result.state === "retry_not_pending" ? fail("No uncertain disconnect is retained. Run workspace disconnect without --retry to start a new disconnect.")
1108
+ : result.state === "revoked" ? ok("Workspace revoked this machine grant. Local pairing was removed.")
1109
+ : result.state === "abandoned" ? ok("Unused native pairing key removed. No remote revocation was claimed. Run npx aibill workspace connect to start a new request.")
1110
+ : result.state === "cancelled" ? fail("Pairing kept. Disconnect was not confirmed; no request or key was removed.") : result.state === "not_paired" ? ok("No completed local pairing exists.")
1111
+ : fail(`Disconnect outcome is unknown. Local keys remain and uploads are blocked. Run npx aibill@latest workspace disconnect --retry to review and explicitly repeat the identical retained request once, or inspect this machine at ${WORKSPACE_ORIGIN}/settings/machines.`);
1109
1112
  }
1110
1113
  const status = await workspaceStatus(runtime.homeDirectory);
1111
1114
  if (status.state !== "connected")
@@ -1116,12 +1119,27 @@ async function workspaceCommand(args, runtime) {
1116
1119
  return fail("A retained batch was refused. No new envelope or nonce was created; resolve the refusal before pushing again.");
1117
1120
  const now = runtime.workspaceNow ?? workspaceClock.now();
1118
1121
  const loaded = status.pending ? undefined : runtime.workspaceLoadCalls ? await runtime.workspaceLoadCalls()
1119
- : await loadLocalAgentFinancialUsage({ workspaceDailyFacts: true, sinceIso: workspaceClock.daysBefore(now, 30) });
1120
- if (loaded?.diagnostics.some(item => item.code !== "directory_missing"))
1121
- return fail("Local source reading is incomplete. No facts were replaced or sent; resolve the reported source coverage before pushing.");
1122
+ : await loadLocalAgentFinancialUsage({ workspaceDailyFacts: true, sinceIso: workspaceClock.daysBefore(now, 30),
1123
+ untilIso: `${now.slice(0, 10)}T00:00:00.000Z` });
1124
+ const representedUnknowns = loaded?.diagnostics.filter(item => item.code === "unsupported_token_shape"
1125
+ && item.workspaceFactCoverage === "unknown_tokens").reduce((count, item) => count + item.count, 0) ?? 0;
1126
+ const blockingDiagnostics = loaded?.diagnostics.filter(item => item.code !== "directory_missing"
1127
+ && !(item.code === "unsupported_token_shape" && item.workspaceFactCoverage === "unknown_tokens")) ?? [];
1128
+ if (blockingDiagnostics.length) {
1129
+ const counts = new Map();
1130
+ for (const diagnostic of blockingDiagnostics) {
1131
+ const label = `${localAgentFormatLabel(diagnostic.agent)}: ${diagnostic.code}${diagnostic.workspaceReason ? ` (${diagnostic.workspaceReason})` : ""}`;
1132
+ counts.set(label, (counts.get(label) ?? 0) + diagnostic.count);
1133
+ }
1134
+ return fail(["Local source reading is incomplete. No facts were replaced or sent.",
1135
+ "Local scan diagnostics (counts, without paths or transcript contents):",
1136
+ ...[...counts].sort(([left], [right]) => left.localeCompare(right)).map(([label, count]) => `${label}: ${count}`),
1137
+ "Share these diagnostic counts for help before retrying. Do not delete logs or reconnect this machine."].join("\n"));
1138
+ }
1122
1139
  const prepared = await prepareWorkspacePush({ home: runtime.homeDirectory, calls: loaded?.calls ?? [], generatedAt: now,
1123
1140
  confirm: (payload, coverage) => consent(["Exact outgoing local facts (no prompts, paths, session IDs, or amounts):", payload,
1124
1141
  `Excluded calls: ${coverage.excludedCalls}. Missing token components: ${coverage.incompleteComponents}.`,
1142
+ ...(representedUnknowns ? [`${representedUnknowns} incomplete local usage records are represented by unknown token values, never zero.`] : []),
1125
1143
  status.pending?.state === "uncertain" ? "This retries only the identical retained signed batch with its original nonce. Send this exact batch again? [y/N] "
1126
1144
  : "Machine tokens remain separate from provider-reported tokens and billed costs. Send this batch? [y/N] "].join("\n")) });
1127
1145
  if (prepared.state === "unchanged")
@@ -6741,8 +6759,12 @@ function parseArgs(argv) {
6741
6759
  parsed.workspaceRestart = true;
6742
6760
  rest.shift();
6743
6761
  }
6762
+ if (parsed.workspaceAction === "disconnect" && rest.length === 1 && rest[0] === "--retry") {
6763
+ parsed.workspaceDisconnectRetry = true;
6764
+ rest.shift();
6765
+ }
6744
6766
  if (rest.length)
6745
- parsed.parseErrors.push("workspace accepts only connect [response-bundle], connect --restart, push, status, or disconnect");
6767
+ parsed.parseErrors.push("workspace accepts only connect [response-bundle], connect --restart, push, status, or disconnect [--retry]");
6746
6768
  }
6747
6769
  if (command === "statusline" && rest[0] && !rest[0].startsWith("--")) {
6748
6770
  parsed.statuslineAction = rest.shift();
@@ -7414,6 +7436,7 @@ function helpText(telemetryDisclosure) {
7414
7436
  " npx aibill workspace push Preview and send local session facts",
7415
7437
  " npx aibill workspace status Read local pairing/pending status only",
7416
7438
  " npx aibill workspace disconnect Revoke the grant or abandon an unused pairing key",
7439
+ " npx aibill workspace disconnect --retry Confirm one identical uncertain disconnect attempt",
7417
7440
  "",
7418
7441
  "Add official provider-reported cost (ADMIN/owner-gated):",
7419
7442
  " npx aibill connect openai Requires an org-owner Admin credential reference",
@@ -227,13 +227,14 @@ export declare function finishWorkspaceEnrollment(options: {
227
227
  }): Promise<{
228
228
  state: "connected" | "cancelled" | "unconfirmed";
229
229
  }>;
230
- /** Revocation is one native attempt. Unknown outcomes retain keys and block further writes. */
230
+ /** One native attempt per confirmation. Explicit recovery repeats the retained proof; unknown outcomes keep writes blocked. */
231
231
  export declare function disconnectWorkspace(options: {
232
232
  home?: string;
233
- confirm: (action: "revoke" | "abandon_unexchanged") => Promise<boolean>;
233
+ retry?: boolean;
234
+ confirm: (action: "revoke" | "retry_revoke" | "abandon_unexchanged") => Promise<boolean>;
234
235
  transport?: WorkspaceDisconnectTransport;
235
236
  }): Promise<{
236
- state: "revoked" | "abandoned" | "cancelled" | "unconfirmed" | "not_paired";
237
+ state: "revoked" | "abandoned" | "cancelled" | "unconfirmed" | "not_paired" | "retry_not_pending";
237
238
  }>;
238
239
  export {};
239
240
  //# sourceMappingURL=workspaceConnect.d.ts.map
@@ -668,9 +668,11 @@ export async function finishWorkspaceEnrollment(options) {
668
668
  }
669
669
  });
670
670
  }
671
- /** Revocation is one native attempt. Unknown outcomes retain keys and block further writes. */
671
+ /** One native attempt per confirmation. Explicit recovery repeats the retained proof; unknown outcomes keep writes blocked. */
672
672
  export async function disconnectWorkspace(options) {
673
673
  return withState(options.home ?? homedir(), async (state, save, path) => {
674
+ if (options.retry && state?.disconnect?.state !== "uncertain")
675
+ return { state: "retry_not_pending" };
674
676
  if (!state) {
675
677
  const enrollment = await readEnrollment(path);
676
678
  if (!enrollment)
@@ -692,14 +694,14 @@ export async function disconnectWorkspace(options) {
692
694
  }
693
695
  return { state: "abandoned" };
694
696
  }
695
- if (state.disconnect?.state === "uncertain")
697
+ if (state.disconnect?.state === "uncertain" && !options.retry)
696
698
  return { state: "unconfirmed" };
697
- if (!await options.confirm("revoke"))
699
+ if (!await options.confirm(state.disconnect?.state === "uncertain" ? "retry_revoke" : "revoke"))
698
700
  return { state: "cancelled" };
699
- if (!state.disconnect) {
701
+ if (state.disconnect?.state !== "revoked") {
700
702
  const proof = { purpose: "machine_disconnect_v1", grantId: state.device.grantId,
701
703
  grantRevision: state.device.grantRevision, connectionEpoch: state.device.connectionEpoch,
702
- requestId: `oref_${randomBytes(32).toString("base64url")}` };
704
+ requestId: state.disconnect?.requestId ?? `oref_${randomBytes(32).toString("base64url")}` };
703
705
  const body = { ...proof, publicKey: createPublicKey(state.device.privateKeyPem).export({ format: "jwk" }).x,
704
706
  signature: sign(null, Buffer.from(workspaceCanonicalJson(proof)), state.device.privateKeyPem).toString("base64url") };
705
707
  const uncertain = { ...state, disconnect: { state: "uncertain", requestId: proof.requestId, revokedAt: null } };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-spend-agent",
3
- "version": "0.9.9",
3
+ "version": "0.9.11",
4
4
  "funding": "https://asktilden.com",
5
5
  "description": "Local-first financial accountability CLI: Claude Code/Codex attribution, provenance, and next actions, plus experimental Gemini CLI cost evidence.",
6
6
  "type": "module",
@@ -55,8 +55,8 @@
55
55
  "prepack": "npm run build"
56
56
  },
57
57
  "dependencies": {
58
- "@agent-finops/core": "0.9.9",
59
- "@agent-finops/report": "0.9.9",
58
+ "@agent-finops/core": "0.9.11",
59
+ "@agent-finops/report": "0.9.11",
60
60
  "yocto-spinner": "^1.2.0"
61
61
  }
62
62
  }