@vellumai/assistant 0.11.10 → 0.11.11-staging.1

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 (199) hide show
  1. package/AGENTS.md +1 -1
  2. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  3. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
  6. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
  8. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +2 -1
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
  12. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  13. package/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
  14. package/openapi.yaml +152 -55
  15. package/package.json +1 -1
  16. package/src/__tests__/anthropic-provider.test.ts +60 -0
  17. package/src/__tests__/config-get-vision-flag.test.ts +36 -0
  18. package/src/__tests__/conversation-error.test.ts +38 -0
  19. package/src/__tests__/copy-composer-tc-templates.test.ts +2 -2
  20. package/src/__tests__/guardian-action-service-scope.test.ts +53 -0
  21. package/src/__tests__/headless-browser-mode.test.ts +63 -13
  22. package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
  23. package/src/__tests__/heartbeat-service.test.ts +118 -27
  24. package/src/__tests__/host-app-control-proxy.test.ts +2 -2
  25. package/src/__tests__/host-app-control-routes.test.ts +41 -0
  26. package/src/__tests__/host-bash-routes.test.ts +30 -1
  27. package/src/__tests__/host-browser-routes.test.ts +34 -0
  28. package/src/__tests__/host-cu-routes-targeted.test.ts +29 -0
  29. package/src/__tests__/host-file-routes-targeted.test.ts +29 -0
  30. package/src/__tests__/host-proxy-base.test.ts +1 -0
  31. package/src/__tests__/host-transfer-routes-targeted.test.ts +86 -0
  32. package/src/__tests__/llm-schema.test.ts +32 -0
  33. package/src/__tests__/mcp-config-secret-boundary.test.ts +2 -0
  34. package/src/__tests__/memory-config-file-parity.test.ts +18 -0
  35. package/src/__tests__/memory-retrieval-hook.test.ts +15 -0
  36. package/src/__tests__/messaging-send-tool.test.ts +159 -13
  37. package/src/__tests__/messaging-shared.test.ts +19 -0
  38. package/src/__tests__/notification-events-source-context-probe.test.ts +211 -0
  39. package/src/__tests__/openai-responses-prompt-cache.test.ts +40 -6
  40. package/src/__tests__/outlook-messaging-provider.test.ts +9 -0
  41. package/src/__tests__/provider-catalog-visibility.test.ts +2 -2
  42. package/src/__tests__/schedule-routes.test.ts +30 -1
  43. package/src/__tests__/scheduler-result-notification.test.ts +209 -0
  44. package/src/__tests__/skills-install-extract.test.ts +26 -0
  45. package/src/__tests__/skills-install-staging.test.ts +11 -2
  46. package/src/api/constants/personality-sliders.test.ts +21 -0
  47. package/src/api/constants/personality-sliders.ts +64 -0
  48. package/src/api/index.ts +8 -0
  49. package/src/archive/ustar-size.test.ts +32 -0
  50. package/src/archive/ustar-size.ts +23 -0
  51. package/src/background-wake/next-wake.test.ts +13 -0
  52. package/src/background-wake/next-wake.ts +4 -2
  53. package/src/calls/voice-session-bridge.ts +26 -0
  54. package/src/cli/__tests__/catalog-search-help.test.ts +1 -0
  55. package/src/cli/commands/__tests__/plugins-search.test.ts +81 -0
  56. package/src/cli/commands/config.help.ts +3 -1
  57. package/src/cli/commands/memory/worker.ts +1 -1
  58. package/src/cli/commands/platform/__tests__/credits-format.test.ts +94 -5
  59. package/src/cli/commands/platform/__tests__/credits.test.ts +10 -0
  60. package/src/cli/commands/platform/credits-format.ts +57 -0
  61. package/src/cli/commands/platform/index.help.ts +19 -3
  62. package/src/cli/commands/plugins.help.ts +4 -1
  63. package/src/cli/commands/plugins.ts +2 -1
  64. package/src/cli/lib/bundled-marketplace.json +1 -1
  65. package/src/cli/lib/install-from-platform.ts +8 -10
  66. package/src/config/bundled-skills/messaging/SKILL.md +2 -0
  67. package/src/config/bundled-skills/messaging/TOOLS.json +4 -4
  68. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +85 -1
  69. package/src/config/bundled-skills/messaging/tools/shared.ts +6 -0
  70. package/src/config/bundled-skills/schedule/SKILL.md +9 -1
  71. package/src/config/bundled-skills/screen-annotation/SKILL.md +87 -20
  72. package/src/config/bundled-skills/screen-annotation/TOOLS.json +21 -5
  73. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +67 -0
  74. package/src/config/feature-flag-registry.json +17 -1
  75. package/src/config/input-modalities.test.ts +71 -0
  76. package/src/config/input-modalities.ts +95 -0
  77. package/src/config/schemas/heartbeat.ts +3 -3
  78. package/src/config/schemas/llm.ts +13 -3
  79. package/src/config/schemas/memory-storage.test.ts +39 -0
  80. package/src/config/schemas/memory-storage.ts +21 -0
  81. package/src/context/token-estimator.ts +2 -2
  82. package/src/daemon/__tests__/embedding-reconcile.test.ts +77 -0
  83. package/src/daemon/__tests__/lifecycle-embedding-reconcile.test.ts +3 -0
  84. package/src/daemon/conversation-error.ts +20 -7
  85. package/src/daemon/embedding-reconcile.ts +64 -1
  86. package/src/daemon/handlers/config-embeddings.ts +31 -0
  87. package/src/daemon/host-app-control-proxy.ts +2 -0
  88. package/src/daemon/host-bash-proxy.ts +1 -0
  89. package/src/daemon/host-cu-proxy.ts +6 -6
  90. package/src/daemon/host-file-proxy.ts +6 -6
  91. package/src/daemon/host-proxy-base.ts +10 -9
  92. package/src/daemon/host-transfer-proxy.ts +21 -24
  93. package/src/daemon/mcp-reload-service.ts +6 -0
  94. package/src/daemon/providers-setup.ts +5 -26
  95. package/src/heartbeat/heartbeat-service.ts +72 -13
  96. package/src/live-voice/__tests__/live-voice-photo.test.ts +72 -0
  97. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +81 -0
  98. package/src/live-voice/live-voice-photo.ts +68 -4
  99. package/src/live-voice/live-voice-session.ts +36 -2
  100. package/src/live-voice/protocol.ts +98 -1
  101. package/src/mcp/__tests__/reload-signal-emission.test.ts +54 -0
  102. package/src/mcp/__tests__/reload-signal.test.ts +57 -0
  103. package/src/mcp/__tests__/startup.test.ts +150 -0
  104. package/src/mcp/reload-signal.ts +50 -0
  105. package/src/mcp/startup.ts +101 -0
  106. package/src/messaging/providers/outlook/adapter.ts +2 -13
  107. package/src/messaging/providers/outlook/client.ts +13 -1
  108. package/src/messaging/providers/outlook/types.ts +3 -0
  109. package/src/notifications/__tests__/decision-engine.test.ts +79 -6
  110. package/src/notifications/__tests__/home-feed-side-effect.test.ts +27 -0
  111. package/src/notifications/__tests__/schedule-result-producer.test.ts +397 -0
  112. package/src/notifications/copy-composer.ts +33 -11
  113. package/src/notifications/decision-engine.ts +48 -11
  114. package/src/notifications/events-store.ts +68 -2
  115. package/src/notifications/home-feed-side-effect.ts +15 -3
  116. package/src/notifications/schedule-result-producer.ts +275 -0
  117. package/src/notifications/signal.ts +5 -0
  118. package/src/oauth/seed-providers.ts +3 -2
  119. package/src/persistence/conversation-crud.ts +38 -0
  120. package/src/persistence/embeddings/embedding-backend.test.ts +162 -0
  121. package/src/persistence/embeddings/embedding-backend.ts +148 -11
  122. package/src/persistence/embeddings/embedding-openai.test.ts +113 -0
  123. package/src/persistence/embeddings/embedding-openai.ts +55 -4
  124. package/src/persistence/embeddings/embedding-types.test.ts +24 -0
  125. package/src/persistence/embeddings/embedding-types.ts +25 -1
  126. package/src/persistence/job-utils.ts +6 -2
  127. package/src/plugin-api/index.ts +2 -1
  128. package/src/plugin-api/vision-support.test.ts +81 -0
  129. package/src/plugin-api/vision-support.ts +57 -10
  130. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +8 -0
  131. package/src/plugins/defaults/memory/jobs/__tests__/embed-concept-page.test.ts +1 -0
  132. package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +11 -3
  133. package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +20 -0
  134. package/src/plugins/defaults/memory/src/memory-worker-routes.ts +3 -1
  135. package/src/plugins/defaults/memory/substrate/__tests__/reembed-job.test.ts +1 -0
  136. package/src/plugins/defaults/memory/v2/__tests__/backfill-jobs.test.ts +1 -0
  137. package/src/plugins/defaults/memory/v3/__tests__/section-dense-store.test.ts +82 -0
  138. package/src/plugins/defaults/memory/v3/section-dense-store.ts +9 -27
  139. package/src/providers/__tests__/provider-secret-catalog.test.ts +4 -0
  140. package/src/providers/__tests__/xml-tool-call-salvage.test.ts +172 -0
  141. package/src/providers/anthropic/client.ts +9 -4
  142. package/src/providers/inline-audio-support.test.ts +34 -0
  143. package/src/providers/inline-audio-support.ts +27 -0
  144. package/src/providers/model-catalog.ts +22 -2
  145. package/src/providers/openai/__tests__/api-error-normalization.test.ts +50 -0
  146. package/src/providers/openai/__tests__/chat-completions-extra-body.test.ts +51 -0
  147. package/src/providers/openai/__tests__/chat-completions-provider-xml-salvage.test.ts +291 -0
  148. package/src/providers/openai/api-error-normalization.ts +18 -3
  149. package/src/providers/openai/chat-completions-provider.ts +113 -13
  150. package/src/providers/openai/responses-provider.ts +10 -8
  151. package/src/providers/provider-secret-catalog.ts +11 -1
  152. package/src/providers/vellum/client.ts +14 -1
  153. package/src/providers/vellum/personality-directions.test.ts +163 -0
  154. package/src/providers/vellum/personality-directions.ts +120 -0
  155. package/src/providers/xml-tool-call-salvage.ts +212 -0
  156. package/src/runtime/__tests__/background-job-runner.test.ts +42 -4
  157. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -6
  158. package/src/runtime/auth/__tests__/route-policy.test.ts +65 -0
  159. package/src/runtime/auth/same-actor.ts +29 -5
  160. package/src/runtime/background-job-runner.ts +38 -29
  161. package/src/runtime/guardian-action-service.ts +20 -1
  162. package/src/runtime/migrations/vbundle-validator.ts +3 -3
  163. package/src/runtime/pending-interactions.ts +4 -5
  164. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +24 -0
  165. package/src/runtime/routes/__tests__/platform-credits-routes.test.ts +99 -5
  166. package/src/runtime/routes/conversation-query-routes.ts +53 -11
  167. package/src/runtime/routes/document-comments-routes.ts +8 -2
  168. package/src/runtime/routes/guardian-action-routes.ts +7 -0
  169. package/src/runtime/routes/heartbeat-routes.ts +41 -53
  170. package/src/runtime/routes/host-app-control-routes.ts +10 -36
  171. package/src/runtime/routes/host-bash-routes.ts +11 -45
  172. package/src/runtime/routes/host-browser-routes.ts +24 -0
  173. package/src/runtime/routes/host-cu-routes.ts +11 -45
  174. package/src/runtime/routes/host-file-routes.ts +11 -44
  175. package/src/runtime/routes/host-proxy-result-binding.ts +57 -0
  176. package/src/runtime/routes/host-transfer-routes.ts +33 -25
  177. package/src/runtime/routes/index.ts +0 -2
  178. package/src/runtime/routes/integrations/a2a.ts +12 -3
  179. package/src/runtime/routes/integrations/vercel.ts +13 -3
  180. package/src/runtime/routes/platform-routes.ts +121 -24
  181. package/src/runtime/routes/schedule-routes.ts +15 -1
  182. package/src/schedule/__tests__/schedule-timezone.test.ts +21 -3
  183. package/src/schedule/__tests__/worker-mcp-tools.test.ts +88 -0
  184. package/src/schedule/schedule-timezone.ts +25 -0
  185. package/src/schedule/scheduler.ts +19 -0
  186. package/src/schedule/worker.ts +119 -2
  187. package/src/skills/catalog-install.ts +40 -5
  188. package/src/tools/browser/__tests__/browser-status.test.ts +3 -0
  189. package/src/tools/browser/browser-execution.ts +117 -53
  190. package/src/tools/browser/browser-status-constants.ts +6 -6
  191. package/src/tools/capability-offer.test.ts +87 -0
  192. package/src/tools/capability-offer.ts +93 -0
  193. package/src/tools/terminal/__tests__/safe-env.test.ts +17 -0
  194. package/src/tools/terminal/safe-env.ts +3 -2
  195. package/src/util/provider-error-patterns.ts +34 -0
  196. package/src/watcher/__tests__/engine.test.ts +5 -2
  197. package/src/watcher/engine.ts +7 -4
  198. package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +0 -487
  199. package/src/runtime/routes/sight-frame-routes.ts +0 -136
package/AGENTS.md CHANGED
@@ -16,7 +16,7 @@ When you introduce a new env var that the assistant process needs to read at run
16
16
 
17
17
  **Default to including it.** If the var doesn't contain secrets (e.g. a URL, a feature flag, a path, a mode string), add it. Only omit it if it carries credential material (tokens, passwords, private keys) — those must stay isolated to CES.
18
18
 
19
- `CES_LOCAL_SOCKET` is intentionally included despite the socket exposing credential RPCs — assistant subprocesses are expected to reach CES. Credential protection is rules-based access control inside CES, not socket-path secrecy (see root `AGENTS.md`).
19
+ `CES_LOCAL_SOCKET` is intentionally included despite the socket exposing credential RPCs: assistant subprocesses are expected to reach CES. Credential protection is rules-based access control inside CES, not socket-path secrecy (see root `AGENTS.md`). Do not forward `CES_SERVICE_TOKEN` or `CES_CREDENTIAL_URL`: the HTTP bearer is a vault secret and must stay in the assistant process.
20
20
 
21
21
  ## Daemon startup philosophy
22
22
 
@@ -26,7 +26,8 @@
26
26
  "./error": "./src/error.ts",
27
27
  "./reactions": "./src/reactions.ts",
28
28
  "./guardian-requests": "./src/guardian-requests.ts",
29
- "./platform-credential": "./src/platform-credential.ts"
29
+ "./platform-credential": "./src/platform-credential.ts",
30
+ "./plan-credit": "./src/plan-credit.ts"
30
31
  },
31
32
  "scripts": {
32
33
  "typecheck": "bunx tsc --noEmit",
@@ -0,0 +1,51 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ extraCreditUsd,
5
+ planCreditUsedFraction,
6
+ usageGrantRatio,
7
+ } from "../plan-credit.js";
8
+
9
+ describe("usageGrantRatio", () => {
10
+ test("reads the used share and clamps to 0..1", () => {
11
+ expect(usageGrantRatio(20, 9.1)).toBeCloseTo(0.545);
12
+ expect(usageGrantRatio(20, 0)).toBe(1);
13
+ expect(usageGrantRatio(20, 25)).toBe(0);
14
+ });
15
+
16
+ test("has no reading without both figures or with nothing granted", () => {
17
+ expect(usageGrantRatio(null, 5)).toBeNull();
18
+ expect(usageGrantRatio(20, null)).toBeNull();
19
+ expect(usageGrantRatio(0, 0)).toBeNull();
20
+ });
21
+ });
22
+
23
+ describe("planCreditUsedFraction", () => {
24
+ test("prefers the ratio whatever the plan", () => {
25
+ expect(planCreditUsedFraction(20, 9.1, "base")).toBeCloseTo(0.545);
26
+ expect(planCreditUsedFraction(20, 9.1, null)).toBeCloseTo(0.545);
27
+ });
28
+
29
+ test("treats a Pro plan with nothing granted as fully spent", () => {
30
+ expect(planCreditUsedFraction(0, 0, "pro")).toBe(1);
31
+ expect(planCreditUsedFraction(0, null, "pro")).toBe(1);
32
+ });
33
+
34
+ test("gives a base or unknown plan with nothing granted no reading", () => {
35
+ expect(planCreditUsedFraction(0, 0, "base")).toBeNull();
36
+ expect(planCreditUsedFraction(0, 0, null)).toBeNull();
37
+ expect(planCreditUsedFraction(null, null, "pro")).toBeNull();
38
+ });
39
+ });
40
+
41
+ describe("extraCreditUsd", () => {
42
+ test("nets unused grant credit out of the balance to the cent", () => {
43
+ expect(extraCreditUsd(42.17, 9.1)).toBe(33.07);
44
+ expect(extraCreditUsd(34.65, 9.1)).toBe(25.55);
45
+ });
46
+
47
+ test("never goes below zero", () => {
48
+ expect(extraCreditUsd(5, 9.1)).toBe(0);
49
+ expect(extraCreditUsd(-2.5, 0)).toBe(0);
50
+ });
51
+ });
@@ -28,6 +28,7 @@ export * from "./rpc.js";
28
28
  export * from "./trust-rules.js";
29
29
  export * from "./ingress.js";
30
30
  export * from "./no-response.js";
31
+ export * from "./plan-credit.js";
31
32
  export * from "./platform-credential.js";
32
33
  export * from "./remote-web-pairing.js";
33
34
  export * from "./twilio-ingress.js";
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Plan-credit derivations shared by the assistant runtime's `platform credits`
3
+ * route and the web client's usage meter, so both read the billing summary's
4
+ * grant figures the same way and can never quote different readings.
5
+ */
6
+
7
+ export type BillingPlanId = "base" | "pro";
8
+
9
+ function clamp01(value: number): number {
10
+ if (!Number.isFinite(value) || value < 0) {
11
+ return 0;
12
+ }
13
+ return value > 1 ? 1 : value;
14
+ }
15
+
16
+ /**
17
+ * How much of the usage credit an account was granted it has already used:
18
+ * the granted total less what is still unused, over that total. The initial
19
+ * $5 grant burns 0 to 100% as it is spent, and a further grant grows the total
20
+ * so the bar drops back. Null when nothing was ever granted, or when the
21
+ * platform reports neither figure, which has no honest reading rather than a
22
+ * full or empty bar.
23
+ */
24
+ export function usageGrantRatio(
25
+ totalUsd: number | null,
26
+ availableUsd: number | null,
27
+ ): number | null {
28
+ if (totalUsd == null || availableUsd == null || totalUsd <= 0) {
29
+ return null;
30
+ }
31
+ return clamp01((totalUsd - availableUsd) / totalUsd);
32
+ }
33
+
34
+ /**
35
+ * The plan-credit reading with the plan's fallback applied. The grant figures
36
+ * count only unexpired grants, so a Pro subscription whose grants total
37
+ * nothing has used or expired everything it was granted (a full bar, with
38
+ * nothing scheduled to refill it), while a base plan in that position was
39
+ * never granted anything (no reading). An unknown plan gets the ratio when
40
+ * the figures allow one and no fallback otherwise.
41
+ */
42
+ export function planCreditUsedFraction(
43
+ totalUsd: number | null,
44
+ availableUsd: number | null,
45
+ planId: BillingPlanId | null,
46
+ ): number | null {
47
+ const ratio = usageGrantRatio(totalUsd, availableUsd);
48
+ if (ratio != null) {
49
+ return ratio;
50
+ }
51
+ if (planId === "pro" && totalUsd != null && totalUsd <= 0) {
52
+ return 1;
53
+ }
54
+ return null;
55
+ }
56
+
57
+ /**
58
+ * Credit bought or earned on top of the plan-included grants: the balance less
59
+ * whatever is still unused on those grants, never below zero, to the cent.
60
+ */
61
+ export function extraCreditUsd(
62
+ balanceUsd: number,
63
+ availableUsd: number,
64
+ ): number {
65
+ return Math.round(Math.max(0, balanceUsd - availableUsd) * 100) / 100;
66
+ }
@@ -26,7 +26,8 @@
26
26
  "./error": "./src/error.ts",
27
27
  "./reactions": "./src/reactions.ts",
28
28
  "./guardian-requests": "./src/guardian-requests.ts",
29
- "./platform-credential": "./src/platform-credential.ts"
29
+ "./platform-credential": "./src/platform-credential.ts",
30
+ "./plan-credit": "./src/plan-credit.ts"
30
31
  },
31
32
  "scripts": {
32
33
  "typecheck": "bunx tsc --noEmit",
@@ -0,0 +1,51 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ extraCreditUsd,
5
+ planCreditUsedFraction,
6
+ usageGrantRatio,
7
+ } from "../plan-credit.js";
8
+
9
+ describe("usageGrantRatio", () => {
10
+ test("reads the used share and clamps to 0..1", () => {
11
+ expect(usageGrantRatio(20, 9.1)).toBeCloseTo(0.545);
12
+ expect(usageGrantRatio(20, 0)).toBe(1);
13
+ expect(usageGrantRatio(20, 25)).toBe(0);
14
+ });
15
+
16
+ test("has no reading without both figures or with nothing granted", () => {
17
+ expect(usageGrantRatio(null, 5)).toBeNull();
18
+ expect(usageGrantRatio(20, null)).toBeNull();
19
+ expect(usageGrantRatio(0, 0)).toBeNull();
20
+ });
21
+ });
22
+
23
+ describe("planCreditUsedFraction", () => {
24
+ test("prefers the ratio whatever the plan", () => {
25
+ expect(planCreditUsedFraction(20, 9.1, "base")).toBeCloseTo(0.545);
26
+ expect(planCreditUsedFraction(20, 9.1, null)).toBeCloseTo(0.545);
27
+ });
28
+
29
+ test("treats a Pro plan with nothing granted as fully spent", () => {
30
+ expect(planCreditUsedFraction(0, 0, "pro")).toBe(1);
31
+ expect(planCreditUsedFraction(0, null, "pro")).toBe(1);
32
+ });
33
+
34
+ test("gives a base or unknown plan with nothing granted no reading", () => {
35
+ expect(planCreditUsedFraction(0, 0, "base")).toBeNull();
36
+ expect(planCreditUsedFraction(0, 0, null)).toBeNull();
37
+ expect(planCreditUsedFraction(null, null, "pro")).toBeNull();
38
+ });
39
+ });
40
+
41
+ describe("extraCreditUsd", () => {
42
+ test("nets unused grant credit out of the balance to the cent", () => {
43
+ expect(extraCreditUsd(42.17, 9.1)).toBe(33.07);
44
+ expect(extraCreditUsd(34.65, 9.1)).toBe(25.55);
45
+ });
46
+
47
+ test("never goes below zero", () => {
48
+ expect(extraCreditUsd(5, 9.1)).toBe(0);
49
+ expect(extraCreditUsd(-2.5, 0)).toBe(0);
50
+ });
51
+ });
@@ -28,6 +28,7 @@ export * from "./rpc.js";
28
28
  export * from "./trust-rules.js";
29
29
  export * from "./ingress.js";
30
30
  export * from "./no-response.js";
31
+ export * from "./plan-credit.js";
31
32
  export * from "./platform-credential.js";
32
33
  export * from "./remote-web-pairing.js";
33
34
  export * from "./twilio-ingress.js";
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Plan-credit derivations shared by the assistant runtime's `platform credits`
3
+ * route and the web client's usage meter, so both read the billing summary's
4
+ * grant figures the same way and can never quote different readings.
5
+ */
6
+
7
+ export type BillingPlanId = "base" | "pro";
8
+
9
+ function clamp01(value: number): number {
10
+ if (!Number.isFinite(value) || value < 0) {
11
+ return 0;
12
+ }
13
+ return value > 1 ? 1 : value;
14
+ }
15
+
16
+ /**
17
+ * How much of the usage credit an account was granted it has already used:
18
+ * the granted total less what is still unused, over that total. The initial
19
+ * $5 grant burns 0 to 100% as it is spent, and a further grant grows the total
20
+ * so the bar drops back. Null when nothing was ever granted, or when the
21
+ * platform reports neither figure, which has no honest reading rather than a
22
+ * full or empty bar.
23
+ */
24
+ export function usageGrantRatio(
25
+ totalUsd: number | null,
26
+ availableUsd: number | null,
27
+ ): number | null {
28
+ if (totalUsd == null || availableUsd == null || totalUsd <= 0) {
29
+ return null;
30
+ }
31
+ return clamp01((totalUsd - availableUsd) / totalUsd);
32
+ }
33
+
34
+ /**
35
+ * The plan-credit reading with the plan's fallback applied. The grant figures
36
+ * count only unexpired grants, so a Pro subscription whose grants total
37
+ * nothing has used or expired everything it was granted (a full bar, with
38
+ * nothing scheduled to refill it), while a base plan in that position was
39
+ * never granted anything (no reading). An unknown plan gets the ratio when
40
+ * the figures allow one and no fallback otherwise.
41
+ */
42
+ export function planCreditUsedFraction(
43
+ totalUsd: number | null,
44
+ availableUsd: number | null,
45
+ planId: BillingPlanId | null,
46
+ ): number | null {
47
+ const ratio = usageGrantRatio(totalUsd, availableUsd);
48
+ if (ratio != null) {
49
+ return ratio;
50
+ }
51
+ if (planId === "pro" && totalUsd != null && totalUsd <= 0) {
52
+ return 1;
53
+ }
54
+ return null;
55
+ }
56
+
57
+ /**
58
+ * Credit bought or earned on top of the plan-included grants: the balance less
59
+ * whatever is still unused on those grants, never below zero, to the cent.
60
+ */
61
+ export function extraCreditUsd(
62
+ balanceUsd: number,
63
+ availableUsd: number,
64
+ ): number {
65
+ return Math.round(Math.max(0, balanceUsd - availableUsd) * 100) / 100;
66
+ }
@@ -26,7 +26,8 @@
26
26
  "./error": "./src/error.ts",
27
27
  "./reactions": "./src/reactions.ts",
28
28
  "./guardian-requests": "./src/guardian-requests.ts",
29
- "./platform-credential": "./src/platform-credential.ts"
29
+ "./platform-credential": "./src/platform-credential.ts",
30
+ "./plan-credit": "./src/plan-credit.ts"
30
31
  },
31
32
  "scripts": {
32
33
  "typecheck": "bunx tsc --noEmit",
@@ -0,0 +1,51 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ extraCreditUsd,
5
+ planCreditUsedFraction,
6
+ usageGrantRatio,
7
+ } from "../plan-credit.js";
8
+
9
+ describe("usageGrantRatio", () => {
10
+ test("reads the used share and clamps to 0..1", () => {
11
+ expect(usageGrantRatio(20, 9.1)).toBeCloseTo(0.545);
12
+ expect(usageGrantRatio(20, 0)).toBe(1);
13
+ expect(usageGrantRatio(20, 25)).toBe(0);
14
+ });
15
+
16
+ test("has no reading without both figures or with nothing granted", () => {
17
+ expect(usageGrantRatio(null, 5)).toBeNull();
18
+ expect(usageGrantRatio(20, null)).toBeNull();
19
+ expect(usageGrantRatio(0, 0)).toBeNull();
20
+ });
21
+ });
22
+
23
+ describe("planCreditUsedFraction", () => {
24
+ test("prefers the ratio whatever the plan", () => {
25
+ expect(planCreditUsedFraction(20, 9.1, "base")).toBeCloseTo(0.545);
26
+ expect(planCreditUsedFraction(20, 9.1, null)).toBeCloseTo(0.545);
27
+ });
28
+
29
+ test("treats a Pro plan with nothing granted as fully spent", () => {
30
+ expect(planCreditUsedFraction(0, 0, "pro")).toBe(1);
31
+ expect(planCreditUsedFraction(0, null, "pro")).toBe(1);
32
+ });
33
+
34
+ test("gives a base or unknown plan with nothing granted no reading", () => {
35
+ expect(planCreditUsedFraction(0, 0, "base")).toBeNull();
36
+ expect(planCreditUsedFraction(0, 0, null)).toBeNull();
37
+ expect(planCreditUsedFraction(null, null, "pro")).toBeNull();
38
+ });
39
+ });
40
+
41
+ describe("extraCreditUsd", () => {
42
+ test("nets unused grant credit out of the balance to the cent", () => {
43
+ expect(extraCreditUsd(42.17, 9.1)).toBe(33.07);
44
+ expect(extraCreditUsd(34.65, 9.1)).toBe(25.55);
45
+ });
46
+
47
+ test("never goes below zero", () => {
48
+ expect(extraCreditUsd(5, 9.1)).toBe(0);
49
+ expect(extraCreditUsd(-2.5, 0)).toBe(0);
50
+ });
51
+ });
@@ -28,6 +28,7 @@ export * from "./rpc.js";
28
28
  export * from "./trust-rules.js";
29
29
  export * from "./ingress.js";
30
30
  export * from "./no-response.js";
31
+ export * from "./plan-credit.js";
31
32
  export * from "./platform-credential.js";
32
33
  export * from "./remote-web-pairing.js";
33
34
  export * from "./twilio-ingress.js";
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Plan-credit derivations shared by the assistant runtime's `platform credits`
3
+ * route and the web client's usage meter, so both read the billing summary's
4
+ * grant figures the same way and can never quote different readings.
5
+ */
6
+
7
+ export type BillingPlanId = "base" | "pro";
8
+
9
+ function clamp01(value: number): number {
10
+ if (!Number.isFinite(value) || value < 0) {
11
+ return 0;
12
+ }
13
+ return value > 1 ? 1 : value;
14
+ }
15
+
16
+ /**
17
+ * How much of the usage credit an account was granted it has already used:
18
+ * the granted total less what is still unused, over that total. The initial
19
+ * $5 grant burns 0 to 100% as it is spent, and a further grant grows the total
20
+ * so the bar drops back. Null when nothing was ever granted, or when the
21
+ * platform reports neither figure, which has no honest reading rather than a
22
+ * full or empty bar.
23
+ */
24
+ export function usageGrantRatio(
25
+ totalUsd: number | null,
26
+ availableUsd: number | null,
27
+ ): number | null {
28
+ if (totalUsd == null || availableUsd == null || totalUsd <= 0) {
29
+ return null;
30
+ }
31
+ return clamp01((totalUsd - availableUsd) / totalUsd);
32
+ }
33
+
34
+ /**
35
+ * The plan-credit reading with the plan's fallback applied. The grant figures
36
+ * count only unexpired grants, so a Pro subscription whose grants total
37
+ * nothing has used or expired everything it was granted (a full bar, with
38
+ * nothing scheduled to refill it), while a base plan in that position was
39
+ * never granted anything (no reading). An unknown plan gets the ratio when
40
+ * the figures allow one and no fallback otherwise.
41
+ */
42
+ export function planCreditUsedFraction(
43
+ totalUsd: number | null,
44
+ availableUsd: number | null,
45
+ planId: BillingPlanId | null,
46
+ ): number | null {
47
+ const ratio = usageGrantRatio(totalUsd, availableUsd);
48
+ if (ratio != null) {
49
+ return ratio;
50
+ }
51
+ if (planId === "pro" && totalUsd != null && totalUsd <= 0) {
52
+ return 1;
53
+ }
54
+ return null;
55
+ }
56
+
57
+ /**
58
+ * Credit bought or earned on top of the plan-included grants: the balance less
59
+ * whatever is still unused on those grants, never below zero, to the cent.
60
+ */
61
+ export function extraCreditUsd(
62
+ balanceUsd: number,
63
+ availableUsd: number,
64
+ ): number {
65
+ return Math.round(Math.max(0, balanceUsd - availableUsd) * 100) / 100;
66
+ }