@herbertgao/pi-extensions 2026.10.0 → 2026.10.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 (86) hide show
  1. package/README.md +7 -7
  2. package/THIRD_PARTY_NOTICES.md +2 -2
  3. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +1 -1
  4. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +1 -1
  5. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +1 -1
  6. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +1 -1
  7. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +1 -1
  8. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/index.js +4 -0
  9. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail-frontmatter.cjs +43 -3
  10. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail.mjs +142 -59
  11. package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
  12. package/node_modules/@dietrichgebert/ponytail/README.es.md +87 -204
  13. package/node_modules/@dietrichgebert/ponytail/README.ja.md +212 -0
  14. package/node_modules/@dietrichgebert/ponytail/README.ko.md +92 -226
  15. package/node_modules/@dietrichgebert/ponytail/README.md +58 -244
  16. package/node_modules/@dietrichgebert/ponytail/README.zh-CN.md +212 -0
  17. package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +3 -3
  18. package/node_modules/@dietrichgebert/ponytail/hooks/copilot-hooks.json +4 -4
  19. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +63 -16
  20. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-config.js +8 -7
  21. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +46 -23
  22. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +39 -7
  23. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-subagent.js +20 -3
  24. package/node_modules/@dietrichgebert/ponytail/hooks/qoder-hooks.json +3 -3
  25. package/node_modules/@dietrichgebert/ponytail/package.json +2 -2
  26. package/node_modules/@dietrichgebert/ponytail/pi-extension/index.js +47 -17
  27. package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -7
  28. package/node_modules/@dietrichgebert/ponytail/skills/plugin.json +5 -0
  29. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +7 -2
  30. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +1 -1
  31. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +16 -14
  32. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +3 -3
  33. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +12 -7
  34. package/node_modules/@narumitw/pi-caffeinate/package.json +4 -4
  35. package/node_modules/pi-typesafe/README.md +1 -1
  36. package/node_modules/pi-typesafe/dist/backends.d.ts +15 -5
  37. package/node_modules/pi-typesafe/dist/backends.js +18 -0
  38. package/node_modules/pi-typesafe/dist/client.d.ts +4 -3
  39. package/node_modules/pi-typesafe/dist/client.js +22 -7
  40. package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
  41. package/node_modules/pi-typesafe/dist/index.js +1 -1
  42. package/node_modules/pi-typesafe/dist/usage.d.ts +14 -4
  43. package/node_modules/pi-typesafe/dist/usage.js +147 -42
  44. package/node_modules/pi-typesafe/package.json +1 -1
  45. package/node_modules/pi-web-access/CHANGELOG.md +48 -0
  46. package/node_modules/pi-web-access/README.md +57 -11
  47. package/node_modules/pi-web-access/brave-rate-limit.ts +8 -2
  48. package/node_modules/pi-web-access/curator-page.ts +5 -1
  49. package/node_modules/pi-web-access/curator-server.ts +2 -0
  50. package/node_modules/pi-web-access/declared-web-links.ts +2 -2
  51. package/node_modules/pi-web-access/degoog.ts +323 -0
  52. package/node_modules/pi-web-access/dist/index.js +3483 -2745
  53. package/node_modules/pi-web-access/dist/mcp-cli.js +27443 -0
  54. package/node_modules/pi-web-access/exa.ts +5 -1
  55. package/node_modules/pi-web-access/extract.ts +41 -3
  56. package/node_modules/pi-web-access/firecrawl.ts +35 -4
  57. package/node_modules/pi-web-access/gemini-search.ts +34 -11
  58. package/node_modules/pi-web-access/gemini-web.ts +14 -9
  59. package/node_modules/pi-web-access/index.ts +153 -1202
  60. package/node_modules/pi-web-access/keenable.ts +221 -0
  61. package/node_modules/pi-web-access/mcp-cli.ts +40 -0
  62. package/node_modules/pi-web-access/mcp-server.ts +81 -0
  63. package/node_modules/pi-web-access/mcp-tool-definitions.ts +117 -0
  64. package/node_modules/pi-web-access/openai-search.ts +34 -1
  65. package/node_modules/pi-web-access/package.json +23 -5
  66. package/node_modules/pi-web-access/perplexity.ts +64 -56
  67. package/node_modules/pi-web-access/tool-arguments.ts +20 -0
  68. package/node_modules/pi-web-access/video-extract.ts +1 -1
  69. package/node_modules/pi-web-access/web-tool-contract.ts +86 -0
  70. package/node_modules/pi-web-access/web-tool-core.ts +1502 -0
  71. package/node_modules/pi-web-access/youtube-extract.ts +2 -5
  72. package/package.json +6 -6
  73. package/node_modules/@dietrichgebert/ponytail/assets/benchmark-3model.svg +0 -21
  74. package/node_modules/@dietrichgebert/ponytail/assets/benchmark-agentic.svg +0 -62
  75. package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.png +0 -0
  76. package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.svg +0 -115
  77. package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt-dark.svg +0 -27
  78. package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt.svg +0 -27
  79. package/node_modules/@dietrichgebert/ponytail/assets/logo.png +0 -0
  80. package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
  81. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +0 -4
  82. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +0 -4
  83. package/node_modules/@dietrichgebert/ponytail/assets/social-preview.png +0 -0
  84. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-es.png +0 -0
  85. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-ko.png +0 -0
  86. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner.png +0 -0
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  // ponytail — removes state ponytail wrote outside the plugin's own files:
3
- // the mode flag, the config file, the statusLine entry it added to
4
- // settings.json, and its entries in ~/.cursor/hooks.json. Plugin files
5
- // themselves are removed by each host's own uninstall command (see README);
6
- // this only cleans up what those commands can't see.
3
+ // the mode flag, the config file, the statusline script copy, the
4
+ // statusLine entry it added to settings.json, and its entries in
5
+ // ~/.cursor/hooks.json. Plugin files themselves are removed by each host's
6
+ // own uninstall command (see README); this only cleans up what those
7
+ // commands can't see.
7
8
 
8
9
  const fs = require('fs');
9
10
  const os = require('os');
@@ -11,7 +12,7 @@ const path = require('path');
11
12
  const { getConfigPath, getClaudeDir } = require('../hooks/ponytail-config');
12
13
  const cursorHooks = require('./cursor-hooks');
13
14
 
14
- const STATUSLINE_SCRIPT = 'ponytail-statusline';
15
+ const STATUSLINE_SCRIPT = /(?:^|[\s"'\\/])ponytail-statusline\.(?:sh|ps1)(?=$|[\s"'])/;
15
16
 
16
17
  function removeIfExists(filePath, label) {
17
18
  try {
@@ -23,8 +24,21 @@ function removeIfExists(filePath, label) {
23
24
  }
24
25
 
25
26
  removeIfExists(path.join(getClaudeDir(), '.ponytail-active'), 'mode flag');
27
+ removeIfExists(path.join(getClaudeDir(), '.ponytail-statusline-nudged'), 'statusline nudge flag');
26
28
  removeIfExists(path.join(os.homedir(), '.cursor', '.ponytail-active'), 'Cursor mode flag');
29
+ removeIfExists(path.join(os.homedir(), '.qoder', '.ponytail-active'), 'Qoder mode flag');
30
+ for (const dir of [path.join(getClaudeDir(), 'ponytail-modes'), path.join(os.homedir(), '.cursor', 'ponytail-modes'), path.join(process.env.CODEBUDDY_CONFIG_DIR || path.join(os.homedir(), '.codebuddy'), 'ponytail-modes')]) {
31
+ if (fs.existsSync(dir)) {
32
+ fs.rmSync(dir, { recursive: true });
33
+ console.log(`Removed per-project mode flags: ${dir}`);
34
+ }
35
+ }
36
+ removeIfExists(path.join(process.env.CODEBUDDY_CONFIG_DIR || path.join(os.homedir(), '.codebuddy'), '.ponytail-active'), 'CodeBuddy mode flag');
27
37
  removeIfExists(getConfigPath(), 'config file');
38
+ // Statusline script copies the activate hook keeps in the config dir (#1032).
39
+ for (const name of ['ponytail-statusline.sh', 'ponytail-statusline.ps1']) {
40
+ removeIfExists(path.join(getClaudeDir(), name), 'statusline script');
41
+ }
28
42
 
29
43
  // Cursor hooks (#817): drop only ponytail's entries from ~/.cursor/hooks.json,
30
44
  // keep every other hook the user configured there.
@@ -49,12 +63,12 @@ try {
49
63
  // (e.g. caveman && ponytail), keep the other plugin's command intact.
50
64
  // ponytail: splits on && / ; to detect other segments — good enough; a user
51
65
  // piping statuslines together is on their own.
52
- if (typeof cmd === 'string' && cmd.includes(STATUSLINE_SCRIPT)) {
66
+ if (typeof cmd === 'string' && STATUSLINE_SCRIPT.test(cmd)) {
53
67
  const parts = cmd
54
68
  .split(/&&|;/)
55
69
  .map((s) => s.trim())
56
70
  .filter(Boolean);
57
- const others = parts.filter((s) => !s.includes(STATUSLINE_SCRIPT));
71
+ const others = parts.filter((s) => !STATUSLINE_SCRIPT.test(s));
58
72
  if (others.length === 0) {
59
73
  delete settings.statusLine;
60
74
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "ponytail",
3
+ "description": "Lazy senior dev mode. Forces the simplest solution that works: YAGNI, stdlib first, one line over fifty.",
4
+ "skills": "."
5
+ }
@@ -19,6 +19,7 @@ Same as ponytail-review:
19
19
  - `delete:` dead code, unused flexibility, speculative feature. Replacement: nothing.
20
20
  - `stdlib:` hand-rolled thing the standard library ships. Name the function.
21
21
  - `native:` dependency or code doing what the platform already does. Name the feature.
22
+ - `reuse:` equivalent helper, util, or pattern already in this repo. Name the path.
22
23
  - `yagni:` abstraction with one implementation, config nobody sets, layer with one caller.
23
24
  - `shrink:` same logic, fewer lines. Show the shorter form.
24
25
 
@@ -26,11 +27,15 @@ Same as ponytail-review:
26
27
 
27
28
  Deps the stdlib or platform already ships, single-implementation interfaces,
28
29
  factories with one product, wrappers that only delegate, files exporting one
29
- thing, dead flags and config, hand-rolled stdlib.
30
+ thing, dead flags and config, hand-rolled stdlib, helpers duplicating an
31
+ equivalent that already lives in this repo.
32
+ Before emitting `delete:`, grep the whole tree for the symbol, including tests,
33
+ fixtures and string or dynamic references.
30
34
 
31
35
  ## Output
32
36
 
33
- One line per finding, ranked: `<tag> <what to cut>. <replacement>. [path]`.
37
+ One line per finding, numbered and ranked: `<N>. <tag> <what to cut>. <replacement>. [path]`,
38
+ so the user can say "fix 2 and 5".
34
39
  End with `net: -<N> lines, -<M> deps possible.` Nothing to cut: `Lean already. Ship.`
35
40
 
36
41
  ## Boundaries
@@ -17,7 +17,7 @@ can't quietly become permanent.
17
17
  Grep the repo for comment markers, skipping `node_modules`, `.git`, and build
18
18
  output:
19
19
 
20
- `grep -rnE '(#|//) ?ponytail:' .` (add other comment prefixes if your stack uses them)
20
+ `grep -rnE --exclude-dir=.git --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=build '(#|//|/[*]) ?ponytail:' .` (add other comment prefixes if your stack uses them)
21
21
 
22
22
  Each hit is one ledger row. The comment prefix keeps prose that merely mentions
23
23
  the convention out of the ledger.
@@ -2,7 +2,7 @@
2
2
  name: ponytail-gain
3
3
  description: >
4
4
  Show ponytail's measured impact as a compact scoreboard: less code, less
5
- cost, more speed, from the benchmark medians. One-shot display, not a
5
+ cost, more speed, from the agentic benchmark averages. One-shot display, not a
6
6
  persistent mode, and not a per-repo number. Trigger: /ponytail-gain,
7
7
  "ponytail gain", "what does ponytail save", "show ponytail impact",
8
8
  "ponytail scoreboard".
@@ -13,24 +13,26 @@ description: >
13
13
  Display this scoreboard when invoked. One-shot: do NOT change mode, write flag
14
14
  files, or persist anything.
15
15
 
16
- The figures are the published benchmark medians (5 everyday tasks: email
17
- validator, debounce, CSV sum, countdown timer, rate limiter; three models:
18
- Haiku, Sonnet, Opus). They are measured, not computed from the current repo.
19
- Source: `benchmarks/` and the README.
16
+ The figures are the published agentic benchmark averages: a headless Claude
17
+ Code session (Haiku 4.5) doing 12 feature tickets on a real FastAPI + React
18
+ repo, each task averaged over 4 runs, against the same agent without the
19
+ skill, plus 6 safety tasks. They are measured, not computed from the current
20
+ repo. Source: `benchmarks/results/2026-06-18-agentic.md` and the README.
20
21
 
21
22
  ## Scoreboard
22
23
 
23
- Render plain ASCII bars. The bar length shows the measured range; the label
24
- carries the exact figure:
24
+ Render plain ASCII bars. The bar length shows ponytail as a share of the
25
+ no-skill baseline; the label carries the exact figure:
25
26
 
26
27
  ```
27
- ponytail gain benchmark median · 5 tasks · 3 models
28
+ ponytail gain benchmark average · 12 tasks · Haiku 4.5
28
29
 
29
- Lines of code no-skill ████████████████████ 100%
30
- ponytail ██▌················· 6–20% ▼ 80–94%
31
- Cost no-skill ████████████████████ 100%
32
- ponytail █████▌·············· 23–53% ▼ 47–77%
33
- Speed ponytail ▸ 3–6× faster
30
+ no-skill ████████████████████ 100%
31
+ Lines of code █████████··········· 46% ▼ 54%
32
+ Tokens ████████████████···· 78% ▼ 22%
33
+ Cost ████████████████···· 80% ▼ 20%
34
+ Time ███████████████····· 73% ▼ 27%
35
+ Safety kept 100% (validation, error handling, security)
34
36
 
35
37
  This repo: /ponytail-debt (shortcuts you deferred)
36
38
  /ponytail-audit (what's still cuttable)
@@ -38,7 +40,7 @@ carries the exact figure:
38
40
 
39
41
  ## Honesty boundary
40
42
 
41
- These are benchmark medians, not this repo. NEVER print a per-repo savings
43
+ These are benchmark averages, not this repo. NEVER print a per-repo savings
42
44
  number ("you saved X lines/tokens here"): the unbuilt version was never
43
45
  written, so there is no real baseline to subtract from in a live repo. The
44
46
  only real per-repo figures come from `/ponytail-debt` (a counted ledger), and
@@ -32,9 +32,9 @@ Level sticks until changed or session end.
32
32
  | **ponytail-gain** | `/ponytail-gain` | Measured-impact scoreboard: less code, less cost, more speed. |
33
33
  | **ponytail-help** | `/ponytail-help` | This card. |
34
34
 
35
- Codex uses `@ponytail`, `@ponytail-review`, and `@ponytail-help`; Claude Code
36
- and OpenCode use the slash-command forms above (OpenCode ships all six as
37
- slash commands).
35
+ In Codex CLI and the IDE extension, invoke skills with `$ponytail:ponytail`,
36
+ `$ponytail:ponytail-review`, or `$ponytail:ponytail-help`. Claude Code and OpenCode use the
37
+ slash-command forms above (OpenCode ships all six as slash commands).
38
38
 
39
39
  ## Deactivate
40
40
 
@@ -15,31 +15,36 @@ to cut, what replaces it. The diff's best outcome is getting shorter.
15
15
 
16
16
  ## Format
17
17
 
18
- `L<line>: <tag> <what>. <replacement>.`, or `<file>:L<line>: ...` for
19
- multi-file diffs.
18
+ `<N>. L<line>: <tag> <what>. <replacement>.`, or `<N>. <file>:L<line>: ...` for
19
+ multi-file diffs. Number findings `1.`, `2.`, ... across the whole report, so the
20
+ user can say "fix 2 and 5".
20
21
 
21
22
  Tags:
22
23
 
23
24
  - `delete:` dead code, unused flexibility, speculative feature. Replacement: nothing.
24
25
  - `stdlib:` hand-rolled thing the standard library ships. Name the function.
25
26
  - `native:` dependency or code doing what the platform already does. Name the feature.
27
+ - `reuse:` equivalent helper, util, or pattern already in this repo. Name the path.
26
28
  - `yagni:` abstraction with one implementation, config nobody sets, layer with one caller.
27
29
  - `shrink:` same logic, fewer lines. Show the shorter form.
28
30
 
31
+
29
32
  ## Examples
30
33
 
31
34
  ❌ "This EmailValidator class might be more complex than necessary, have you
32
35
  considered whether all these validation rules are needed at this stage?"
33
36
 
34
- ✅ `L12-38: stdlib: 27-line validator class. "@" in email, 1 line, real validation is the confirmation mail.`
37
+ ✅ `1. L12-38: stdlib: 27-line validator class. "@" in email, 1 line, real validation is the confirmation mail.`
38
+
39
+ ✅ `2. L4: native: moment.js imported for one format call. Intl.DateTimeFormat, 0 deps.`
35
40
 
36
- ✅ `L4: native: moment.js imported for one format call. Intl.DateTimeFormat, 0 deps.`
41
+ ✅ `3. L18-29: reuse: slugify helper duplicates src/lib/slug.ts slugify. Delete it, import the existing one.`
37
42
 
38
- ✅ `repo.py:L88: yagni: AbstractRepository with one implementation. Inline it until a second one exists.`
43
+ ✅ `4. repo.py:L88: yagni: AbstractRepository with one implementation. Inline it until a second one exists.`
39
44
 
40
- ✅ `L52-71: delete: retry wrapper around an idempotent local call. Nothing replaces it.`
45
+ ✅ `5. L52-71: delete: retry wrapper around an idempotent local call. Nothing replaces it.`
41
46
 
42
- ✅ `L30-44: shrink: manual loop builds dict. dict(zip(keys, values)), 1 line.`
47
+ ✅ `6. L30-44: shrink: manual loop builds dict. dict(zip(keys, values)), 1 line.`
43
48
 
44
49
  ## Scoring
45
50
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-caffeinate",
3
- "version": "0.49.8",
3
+ "version": "0.49.9",
4
4
  "description": "Pi extension that keeps the computer awake while the agent is running.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -35,8 +35,8 @@
35
35
  "@earendil-works/pi-coding-agent": "*"
36
36
  },
37
37
  "devDependencies": {
38
- "@earendil-works/pi-coding-agent": "0.86.0",
39
- "@types/node": "26.6.2",
38
+ "@earendil-works/pi-coding-agent": "1.0.0",
39
+ "@types/node": "26.6.4",
40
40
  "esbuild": "0.28.2",
41
41
  "typescript": "7.0.2"
42
42
  },
@@ -47,6 +47,6 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@narumitw/pi-tui-kit": "^0.59.0",
50
- "dbus-native": "^0.15.2"
50
+ "dbus-native": "^0.16.0"
51
51
  }
52
52
  }
@@ -142,7 +142,7 @@ if (!answer.ok) return { skipped: answer.errorCode === "budget" }; // never thr
142
142
 
143
143
  Your extension owns its own user consent and budget; `/typesafe enable` applies only to this package's tool. Check `authState()` rather than your own consent flag before you report that judgments are on.
144
144
 
145
- Judgments can also go through OpenRouter: `createTypeSafe({ backend: "openrouter" })` sends them to `openrouter.ai` with the key from `OPENROUTER_API_KEY`. Command Code serves the same protocol: `createTypeSafe({ backend: "commandcode" })` sends them to `api.commandcode.ai` with the key from `COMMANDCODE_API_KEY` and the model `typesafe/jev`. Or pass a `BackendEndpoint` object as `backend` to name any host that serves the protocol: it brings its own key variable, never reads `TYPESAFE_API_KEY`, and `backendHost()` reports its destination host for your consent text. These backends have no login store, so `/typesafe login` does not apply to them. Pass the same `backend` to `authState`, `keySituation`, and `ensureApiKey`, or the status you report describes the TypeSafe key while the requests use another one. The `/typesafe` commands and the `typesafe_evaluate` tool always use the TypeSafe backend. Every export — the client, `ask`, batching, the usage ledger, auth state, and the `pi-typesafe/calibrate` and `pi-typesafe/ui` entry points — is in [docs/api.md](docs/api.md).
145
+ Judgments can also go through OpenRouter: `createTypeSafe({ backend: "openrouter" })` sends them to `openrouter.ai` with the key from `OPENROUTER_API_KEY`. Command Code serves the same protocol: `createTypeSafe({ backend: "commandcode" })` sends them to `api.commandcode.ai` with the key from `COMMANDCODE_API_KEY` and the model `typesafe/jev`. So does Liquid AI: `createTypeSafe({ backend: "liquid" })` sends them to `api.liquid.ai` with the key from `LIQUID_API_KEY` and the model `d1:free`, which is free; pass `model: "d1"` for the paid model, priced at $0.040 per million input tokens. Liquid counts the state once per question, where TypeSafe counts it once per request, so a request with N questions costs about N times the state on Liquid. Speed, measured on 2026-10-03 with one request at a time: `d1:free` took a median of about 14 seconds and sometimes failed with HTTP 504 after about 46 seconds, while `d1` took about 0.5 seconds. With `d1:free`, raise `timeoutMs` above its 15-second default, or pass `model: "d1"`. Or pass a `BackendEndpoint` object as `backend` to name any host that serves the protocol: it brings its own key variable, never reads `TYPESAFE_API_KEY`, and `backendHost()` reports its destination host for your consent text. These backends have no login store, so `/typesafe login` does not apply to them. Pass the same `backend` to `authState`, `keySituation`, and `ensureApiKey`, or the status you report describes the TypeSafe key while the requests use another one. The `/typesafe` commands and the `typesafe_evaluate` tool always use the TypeSafe backend. Every export — the client, `ask`, batching, the usage ledger, auth state, and the `pi-typesafe/calibrate` and `pi-typesafe/ui` entry points — is in [docs/api.md](docs/api.md).
146
146
 
147
147
  ## Development
148
148
 
@@ -1,6 +1,6 @@
1
- export type TypeSafeBackend = "typesafe" | "openrouter" | "commandcode";
1
+ export type TypeSafeBackend = "typesafe" | "openrouter" | "commandcode" | "liquid";
2
2
  export interface BackendConfig {
3
- /** Human name for status lines: "TypeSafe", "OpenRouter", "Command Code". */
3
+ /** Human name for status lines: "TypeSafe", "OpenRouter", "Command Code", "Liquid AI". */
4
4
  label: string;
5
5
  host: string;
6
6
  /** The environment variable that carries this backend's key. Absent means the TypeSafe key resolution applies. */
@@ -16,6 +16,11 @@ export interface BackendConfig {
16
16
  /** Whether the model list checks the key. A public list accepts any key, so it proves nothing. Absent means it does. */
17
17
  modelsVerifyKey?: boolean;
18
18
  }
19
+ /** A registry entry: a backend config that can also name a price per model. Caller-supplied endpoints carry none. */
20
+ export interface RegistryBackendConfig extends BackendConfig {
21
+ /** USD per million input tokens, by the model id as it is sent. A model with no entry is priced at DEFAULT_USD_PER_MTOK. */
22
+ prices?: Readonly<Record<string, number>>;
23
+ }
19
24
  /** A caller-supplied endpoint that serves the Jev decisions protocol. Passed per call; never added to the registry. */
20
25
  export interface BackendEndpoint extends BackendConfig {
21
26
  /** Required. The environment variable with this endpoint's key. Must not be TYPESAFE_API_KEY. */
@@ -26,7 +31,7 @@ export interface BackendEndpoint extends BackendConfig {
26
31
  /** A registry name or a caller-supplied endpoint. */
27
32
  export type BackendSpec = TypeSafeBackend | BackendEndpoint;
28
33
  /** A backend resolved and validated: what the client will actually use. */
29
- export interface ResolvedBackend extends BackendConfig {
34
+ export interface ResolvedBackend extends RegistryBackendConfig {
30
35
  /** Registry name, absent for a caller-supplied endpoint. */
31
36
  readonly name?: TypeSafeBackend;
32
37
  /** Origin only: scheme, host, and port. */
@@ -42,9 +47,9 @@ export declare const DEFAULT_BACKEND: TypeSafeBackend;
42
47
  /** The environment variable and login store that the default backend reads. */
43
48
  export declare const TYPESAFE_KEY_ENV = "TYPESAFE_API_KEY";
44
49
  /** Registry of known judgment backends. Callers pass a `BackendEndpoint` for a host this registry does not name. */
45
- export declare const DECISIONS_BACKENDS: Record<TypeSafeBackend, BackendConfig>;
50
+ export declare const DECISIONS_BACKENDS: Record<TypeSafeBackend, RegistryBackendConfig>;
46
51
  /** The registry entry for a backend name; a `configuration` error for a name the registry does not know. */
47
- export declare function backendConfig(name: TypeSafeBackend): BackendConfig;
52
+ export declare function backendConfig(name: TypeSafeBackend): RegistryBackendConfig;
48
53
  /**
49
54
  * The model id to send for a caller's `model` on this backend. OpenRouter routes a bare Jev id under the `typesafe`
50
55
  * author: `jev-latest` becomes its alias form `~typesafe/jev-latest`, and a bare `jev-<major>.<minor>` — with or
@@ -55,6 +60,11 @@ export declare function backendConfig(name: TypeSafeBackend): BackendConfig;
55
60
  export declare function backendModelId(backend: TypeSafeBackend, model: string): string;
56
61
  /** The model a client sends when the caller names none: the backend's own default, in the form that backend accepts. */
57
62
  export declare function defaultModelId(backend: TypeSafeBackend): string;
63
+ /**
64
+ * The registry's price for a model, in USD per million input tokens, or undefined when the backend names none.
65
+ * `model` is the id as sent. A caller-supplied endpoint has no prices.
66
+ */
67
+ export declare function backendPrice(backend: ResolvedBackend, model: string): number | undefined;
58
68
  /**
59
69
  * Whether a backend's key comes from the TypeSafe resolution (`TYPESAFE_API_KEY`, then the login store) or only from
60
70
  * its own environment variable. Only the TypeSafe backend has a login store; every other backend is environment-only.
@@ -26,6 +26,15 @@ export const DECISIONS_BACKENDS = {
26
26
  modelsIdField: "id",
27
27
  modelsVerifyKey: false,
28
28
  },
29
+ liquid: {
30
+ label: "Liquid AI",
31
+ host: "https://api.liquid.ai",
32
+ keyEnv: "LIQUID_API_KEY",
33
+ path: "/decisions/v1/systemone",
34
+ modelsPath: "/decisions/v1/models",
35
+ modelsVerifyKey: true,
36
+ prices: { d1: 0.04, "d1:free": 0 },
37
+ },
29
38
  };
30
39
  /** The registry entry for a backend name; a `configuration` error for a name the registry does not know. */
31
40
  export function backendConfig(name) {
@@ -39,6 +48,7 @@ const DEFAULT_MODEL = {
39
48
  typesafe: "jev-latest",
40
49
  openrouter: "typesafe/jev-1.13",
41
50
  commandcode: "typesafe/jev",
51
+ liquid: "d1:free",
42
52
  };
43
53
  /**
44
54
  * The model id to send for a caller's `model` on this backend. OpenRouter routes a bare Jev id under the `typesafe`
@@ -61,6 +71,14 @@ export function backendModelId(backend, model) {
61
71
  export function defaultModelId(backend) {
62
72
  return backendModelId(backend, DEFAULT_MODEL[backend]);
63
73
  }
74
+ /**
75
+ * The registry's price for a model, in USD per million input tokens, or undefined when the backend names none.
76
+ * `model` is the id as sent. A caller-supplied endpoint has no prices.
77
+ */
78
+ export function backendPrice(backend, model) {
79
+ const prices = backend.prices;
80
+ return prices !== undefined && Object.hasOwn(prices, model) ? prices[model] : undefined;
81
+ }
64
82
  /**
65
83
  * Whether a backend's key comes from the TypeSafe resolution (`TYPESAFE_API_KEY`, then the login store) or only from
66
84
  * its own environment variable. Only the TypeSafe backend has a login store; every other backend is environment-only.
@@ -2,8 +2,8 @@ import type { Fetch, Questions, SystemOneRequest, SystemOneResult } from "@types
2
2
  import type { BackendSpec } from "./backends.js";
3
3
  import type { BatchEvaluation, BatchOptions } from "./batch.js";
4
4
  import type { BlockedCap, SpendCaps, UsageLedger, UsageReport } from "./usage.js";
5
- export { backendHost, resolveBackend, DECISIONS_BACKENDS, DEFAULT_BACKEND } from "./backends.js";
6
- export type { BackendConfig, BackendEndpoint, BackendSpec, ResolvedBackend, TypeSafeBackend } from "./backends.js";
5
+ export { backendHost, backendPrice, resolveBackend, DECISIONS_BACKENDS, DEFAULT_BACKEND } from "./backends.js";
6
+ export type { BackendConfig, BackendEndpoint, BackendSpec, RegistryBackendConfig, ResolvedBackend, TypeSafeBackend } from "./backends.js";
7
7
  export interface TypeSafeOptions {
8
8
  /** Defaults to TYPESAFE_API_KEY, then the key saved by `/typesafe login`; never returned. */
9
9
  apiKey?: string;
@@ -23,7 +23,7 @@ export interface TypeSafeOptions {
23
23
  maxInputTokensPerDay?: number;
24
24
  /** Estimated spend per local day, in US dollars. Unlimited by default. */
25
25
  maxUsdPerDay?: number;
26
- /** Price used for the cost estimate and the USD cap. Default: DEFAULT_USD_PER_MTOK. */
26
+ /** Price in USD per million input tokens, used for every request's cost and the USD cap; 0 means free. Default: the backend's price for the model sent, else DEFAULT_USD_PER_MTOK. */
27
27
  usdPerMTok?: number;
28
28
  /** The usage ledger; defaults to the store next to the key. Injected by tests. */
29
29
  ledger?: UsageLedger;
@@ -50,6 +50,7 @@ export interface SpendReport {
50
50
  readonly session: UsageSnapshot;
51
51
  readonly today: UsageReport;
52
52
  readonly caps: SpendCaps;
53
+ /** The price of this client's default model. A request that names another model may be priced differently. */
53
54
  readonly usdPerMTok: number;
54
55
  readonly blocked?: BlockedCap;
55
56
  }
@@ -1,12 +1,12 @@
1
1
  import { TypeSafeClient } from "@typesafe-ai/sdk";
2
2
  import { recordAuthFailure, recordAuthVerified } from "./auth.js";
3
- import { TYPESAFE_KEY_ENV, backendModelId, resolveBackend, usesTypesafeKey } from "./backends.js";
3
+ import { TYPESAFE_KEY_ENV, backendModelId, backendPrice, resolveBackend, usesTypesafeKey } from "./backends.js";
4
4
  import { evaluateAll, evaluateMany } from "./batch.js";
5
5
  import { keySituation } from "./credentials.js";
6
6
  import { TypeSafeIntegrationError, safeError } from "./errors.js";
7
7
  import { DEFAULT_MAX_INPUT_BYTES, assertWithinByteLimit, prepareEvaluationRequest } from "./schema.js";
8
8
  import { DEFAULT_USD_PER_MTOK, capsFromEnvironment, estimateUsd, mergeCaps, openUsageLedger } from "./usage.js";
9
- export { backendHost, resolveBackend, DECISIONS_BACKENDS, DEFAULT_BACKEND } from "./backends.js";
9
+ export { backendHost, backendPrice, resolveBackend, DECISIONS_BACKENDS, DEFAULT_BACKEND } from "./backends.js";
10
10
  /** The paths the SDK appends to whatever base URL it is given. */
11
11
  const SDK_PATH = "/v1/systemone";
12
12
  const SDK_MODELS_PATH = "/v1/models";
@@ -63,6 +63,12 @@ function positiveInteger(value, label) {
63
63
  }
64
64
  return value;
65
65
  }
66
+ function nonNegativeNumber(value, label) {
67
+ if (!Number.isFinite(value) || value < 0) {
68
+ throw new TypeSafeIntegrationError("configuration", `${label} must be a non-negative number.`);
69
+ }
70
+ return value;
71
+ }
66
72
  function positiveNumber(value, label) {
67
73
  if (!Number.isFinite(value) || value <= 0) {
68
74
  throw new TypeSafeIntegrationError("configuration", `${label} must be a positive number.`);
@@ -138,7 +144,7 @@ export function createTypeSafe(options = {}) {
138
144
  const timeout = positiveInteger(options.timeoutMs ?? 15_000, "timeoutMs");
139
145
  const maxInputBytes = positiveInteger(options.maxInputBytes ?? DEFAULT_MAX_INPUT_BYTES, "maxInputBytes");
140
146
  const maxRequests = positiveInteger(options.maxRequests ?? DEFAULT_MAX_REQUESTS, "maxRequests");
141
- const usdPerMTok = positiveNumber(options.usdPerMTok ?? DEFAULT_USD_PER_MTOK, "usdPerMTok");
147
+ const callerRate = options.usdPerMTok === undefined ? undefined : nonNegativeNumber(options.usdPerMTok, "usdPerMTok");
142
148
  const caps = mergeCaps({
143
149
  maxRequests,
144
150
  ...(options.maxRequestsPerDay === undefined ? {} : { maxRequestsPerDay: positiveInteger(options.maxRequestsPerDay, "maxRequestsPerDay") }),
@@ -165,12 +171,17 @@ export function createTypeSafe(options = {}) {
165
171
  logLevel: "off",
166
172
  ...(transport ? { fetch: transport } : {}),
167
173
  });
168
- const ledger = options.ledger ?? openUsageLedger({ usdPerMTok });
174
+ // The caller's price wins; else the backend's price for the model sent; else the default. Each request is priced alone.
175
+ const rateFor = (sent) => callerRate ?? backendPrice(backend, sent) ?? DEFAULT_USD_PER_MTOK;
176
+ const usdPerMTok = rateFor(model);
177
+ // The ledger's own rate only estimates input tokens that carry no recorded cost, so it follows the caller's price alone.
178
+ const ledger = options.ledger ?? openUsageLedger(callerRate === undefined ? {} : { usdPerMTok: callerRate });
179
+ let sessionUsd = 0;
169
180
  const usage = { requestsStarted: 0, requestsSucceeded: 0, requestsFailed: 0, inputTokens: 0, outputTokens: 0 };
170
181
  // Auth state writes are deduplicated: one verification record per process, one record per distinct failure.
171
182
  let verificationRecorded = false;
172
183
  let lastFailureRecorded;
173
- const snapshot = () => ({ ...usage, estimatedUsd: estimateUsd(usage.inputTokens, usdPerMTok) });
184
+ const snapshot = () => ({ ...usage, estimatedUsd: Math.round(sessionUsd * 1e6) / 1e6 });
174
185
  const blocked = () => (usage.requestsStarted >= maxRequests ? undefined : ledger.blocked(caps));
175
186
  const typesafe = {
176
187
  getUsage: snapshot,
@@ -203,7 +214,8 @@ export function createTypeSafe(options = {}) {
203
214
  async evaluate(input, callOptions = {}) {
204
215
  const validated = prepareEvaluationRequest(input, { maxInputBytes });
205
216
  // A per-request model meets the same mapping as the client default; the schema already limited the caller's own id.
206
- const body = JSON.stringify({ ...validated, model: validated.model === undefined ? model : mapModel(validated.model) });
217
+ const sentModel = validated.model === undefined ? model : mapModel(validated.model);
218
+ const body = JSON.stringify({ ...validated, model: sentModel });
207
219
  assertWithinByteLimit(body, maxInputBytes);
208
220
  if (callOptions.signal?.aborted)
209
221
  throw new TypeSafeIntegrationError("aborted", "TypeSafe request cancelled before submission.");
@@ -227,7 +239,10 @@ export function createTypeSafe(options = {}) {
227
239
  usage.requestsSucceeded += 1;
228
240
  usage.inputTokens += result.usage.input_tokens;
229
241
  usage.outputTokens += result.usage.output_tokens;
230
- ledger.recordSuccess(result.usage.input_tokens, result.usage.output_tokens);
242
+ // Unrounded: a small request must still count toward the cap.
243
+ const cost = (result.usage.input_tokens * rateFor(sentModel)) / 1e6;
244
+ sessionUsd += cost;
245
+ ledger.recordSuccess(result.usage.input_tokens, result.usage.output_tokens, cost);
231
246
  if (!verificationRecorded) {
232
247
  verificationRecorded = true;
233
248
  recordAuthVerified();
@@ -1,5 +1,5 @@
1
- export { createTypeSafe, backendHost, resolveBackend, DEFAULT_MAX_REQUESTS, DECISIONS_BACKENDS, DEFAULT_BACKEND } from "./client.js";
2
- export type { TypeSafe, TypeSafeOptions, EvaluationOptions, Evaluation, UsageSnapshot, SpendReport, TypeSafeBackend, BackendConfig, BackendEndpoint, BackendSpec, ResolvedBackend, } from "./client.js";
1
+ export { createTypeSafe, backendHost, backendPrice, resolveBackend, DEFAULT_MAX_REQUESTS, DECISIONS_BACKENDS, DEFAULT_BACKEND } from "./client.js";
2
+ export type { TypeSafe, TypeSafeOptions, EvaluationOptions, Evaluation, UsageSnapshot, SpendReport, TypeSafeBackend, BackendConfig, BackendEndpoint, BackendSpec, RegistryBackendConfig, ResolvedBackend, } from "./client.js";
3
3
  export { ask, DEFAULT_ASK_TIMEOUT_MS } from "./ask.js";
4
4
  export type { AskAnswer, AskOptions, Judge } from "./ask.js";
5
5
  export { fanOut, DEFAULT_CONCURRENCY, evaluateMany, evaluateAll, chunkEvaluationRequest } from "./batch.js";
@@ -1,4 +1,4 @@
1
- export { createTypeSafe, backendHost, resolveBackend, DEFAULT_MAX_REQUESTS, DECISIONS_BACKENDS, DEFAULT_BACKEND } from "./client.js";
1
+ export { createTypeSafe, backendHost, backendPrice, resolveBackend, DEFAULT_MAX_REQUESTS, DECISIONS_BACKENDS, DEFAULT_BACKEND } from "./client.js";
2
2
  export { ask, DEFAULT_ASK_TIMEOUT_MS } from "./ask.js";
3
3
  export { fanOut, DEFAULT_CONCURRENCY, evaluateMany, evaluateAll, chunkEvaluationRequest } from "./batch.js";
4
4
  export { authState, authStatePath, clearAuthState, describeAuth, recordAuthFailure, recordAuthVerified } from "./auth.js";
@@ -40,6 +40,10 @@ export declare function usagePath(): string;
40
40
  /** Input-token cost, rounded to a micro-dollar so the number stays readable. */
41
41
  export declare function estimateUsd(inputTokens: number, usdPerMTok: number): number;
42
42
  export declare function emptyTotals(): UsageTotals;
43
+ /** Longest an update waits for the ledger lock. A request that cannot get it keeps its delta and adds it with the next update. */
44
+ export declare const LOCK_WAIT_MS = 200;
45
+ /** A lock this old belongs to a process that died while holding it; the next update takes it over. */
46
+ export declare const LOCK_STALE_MS = 5000;
43
47
  /** The caps a headless run may set without code: `PI_TYPESAFE_MAX_USD_PER_DAY` and its siblings. */
44
48
  export declare function capsFromEnvironment(env?: NodeJS.ProcessEnv): SpendCaps;
45
49
  /** Environment first for day caps, then the explicit option; a cap is never raised by the environment. */
@@ -49,12 +53,15 @@ export interface UsageLedgerOptions {
49
53
  path?: string;
50
54
  /** Clock for the local day and for rollover; injectable for tests. */
51
55
  now?: () => Date;
56
+ /** Rate for input tokens that have no recorded cost: requests recorded without one, and days from an older ledger file. Default: DEFAULT_USD_PER_MTOK. */
52
57
  usdPerMTok?: number;
53
58
  }
54
59
  /**
55
- * One day of persisted request, token, and cost totals, plus the caps that stop the next request. The in-memory copy is
56
- * authoritative for this process; the file is the cross-process, across-restart record. Reads are defensive, writes are
57
- * atomic and best-effort, and a day rolls over on the local date, so a long eval cannot accumulate forever unnoticed.
60
+ * One day of persisted request, token, and cost totals, plus the caps that stop the next request. The file is the
61
+ * record for every process: each update adds its delta to the file's current content under a lock, and `today()`,
62
+ * `blocked()`, and `describe()` read the file, so a cap counts every process that writes it. A delta that cannot be
63
+ * written in time (the lock is held, or the file is unwritable) stays in this process and is added with its next
64
+ * update. Reads are defensive, writes are atomic and best-effort, and a day rolls over on the local date.
58
65
  */
59
66
  export interface UsageLedger {
60
67
  readonly path: string;
@@ -63,11 +70,14 @@ export interface UsageLedger {
63
70
  today(): UsageReport;
64
71
  /** Count the attempt before it is submitted; a request that never returns still counts. */
65
72
  recordStart(): void;
66
- recordSuccess(inputTokens: number, outputTokens: number): void;
73
+ /** `usd` is this request's cost at its own price; without it the request is estimated at the ledger's rate. */
74
+ recordSuccess(inputTokens: number, outputTokens: number, usd?: number): void;
67
75
  recordFailure(): void;
68
76
  /** The reached day cap that blocks the next request, or undefined. The caller owns the caps. */
69
77
  blocked(caps: SpendCaps): BlockedCap | undefined;
70
78
  /** One line for status output: today's requests, tokens, and cost, with the caps that apply. */
71
79
  describe(caps?: SpendCaps): string;
80
+ /** Add to the file any delta this process still holds. Updates do this themselves; call it before the process exits. */
81
+ flush?(): void;
72
82
  }
73
83
  export declare function openUsageLedger(options?: UsageLedgerOptions): UsageLedger;