@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.
- package/README.md +7 -7
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/index.js +4 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail-frontmatter.cjs +43 -3
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail.mjs +142 -59
- package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
- package/node_modules/@dietrichgebert/ponytail/README.es.md +87 -204
- package/node_modules/@dietrichgebert/ponytail/README.ja.md +212 -0
- package/node_modules/@dietrichgebert/ponytail/README.ko.md +92 -226
- package/node_modules/@dietrichgebert/ponytail/README.md +58 -244
- package/node_modules/@dietrichgebert/ponytail/README.zh-CN.md +212 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +3 -3
- package/node_modules/@dietrichgebert/ponytail/hooks/copilot-hooks.json +4 -4
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +63 -16
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-config.js +8 -7
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +46 -23
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +39 -7
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-subagent.js +20 -3
- package/node_modules/@dietrichgebert/ponytail/hooks/qoder-hooks.json +3 -3
- package/node_modules/@dietrichgebert/ponytail/package.json +2 -2
- package/node_modules/@dietrichgebert/ponytail/pi-extension/index.js +47 -17
- package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -7
- package/node_modules/@dietrichgebert/ponytail/skills/plugin.json +5 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +7 -2
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +16 -14
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +3 -3
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +12 -7
- package/node_modules/@narumitw/pi-caffeinate/package.json +4 -4
- package/node_modules/pi-typesafe/README.md +1 -1
- package/node_modules/pi-typesafe/dist/backends.d.ts +15 -5
- package/node_modules/pi-typesafe/dist/backends.js +18 -0
- package/node_modules/pi-typesafe/dist/client.d.ts +4 -3
- package/node_modules/pi-typesafe/dist/client.js +22 -7
- package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
- package/node_modules/pi-typesafe/dist/index.js +1 -1
- package/node_modules/pi-typesafe/dist/usage.d.ts +14 -4
- package/node_modules/pi-typesafe/dist/usage.js +147 -42
- package/node_modules/pi-typesafe/package.json +1 -1
- package/node_modules/pi-web-access/CHANGELOG.md +48 -0
- package/node_modules/pi-web-access/README.md +57 -11
- package/node_modules/pi-web-access/brave-rate-limit.ts +8 -2
- package/node_modules/pi-web-access/curator-page.ts +5 -1
- package/node_modules/pi-web-access/curator-server.ts +2 -0
- package/node_modules/pi-web-access/declared-web-links.ts +2 -2
- package/node_modules/pi-web-access/degoog.ts +323 -0
- package/node_modules/pi-web-access/dist/index.js +3483 -2745
- package/node_modules/pi-web-access/dist/mcp-cli.js +27443 -0
- package/node_modules/pi-web-access/exa.ts +5 -1
- package/node_modules/pi-web-access/extract.ts +41 -3
- package/node_modules/pi-web-access/firecrawl.ts +35 -4
- package/node_modules/pi-web-access/gemini-search.ts +34 -11
- package/node_modules/pi-web-access/gemini-web.ts +14 -9
- package/node_modules/pi-web-access/index.ts +153 -1202
- package/node_modules/pi-web-access/keenable.ts +221 -0
- package/node_modules/pi-web-access/mcp-cli.ts +40 -0
- package/node_modules/pi-web-access/mcp-server.ts +81 -0
- package/node_modules/pi-web-access/mcp-tool-definitions.ts +117 -0
- package/node_modules/pi-web-access/openai-search.ts +34 -1
- package/node_modules/pi-web-access/package.json +23 -5
- package/node_modules/pi-web-access/perplexity.ts +64 -56
- package/node_modules/pi-web-access/tool-arguments.ts +20 -0
- package/node_modules/pi-web-access/video-extract.ts +1 -1
- package/node_modules/pi-web-access/web-tool-contract.ts +86 -0
- package/node_modules/pi-web-access/web-tool-core.ts +1502 -0
- package/node_modules/pi-web-access/youtube-extract.ts +2 -5
- package/package.json +6 -6
- package/node_modules/@dietrichgebert/ponytail/assets/benchmark-3model.svg +0 -21
- package/node_modules/@dietrichgebert/ponytail/assets/benchmark-agentic.svg +0 -62
- package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.svg +0 -115
- package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt-dark.svg +0 -27
- package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt.svg +0 -27
- package/node_modules/@dietrichgebert/ponytail/assets/logo.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +0 -4
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +0 -4
- package/node_modules/@dietrichgebert/ponytail/assets/social-preview.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-es.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-ko.png +0 -0
- 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
|
|
4
|
-
// settings.json, and its entries in
|
|
5
|
-
// themselves are removed by each host's
|
|
6
|
-
// this only cleans up what those
|
|
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' &&
|
|
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) => !
|
|
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');
|
|
@@ -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 '(
|
|
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
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
|
28
|
+
ponytail gain benchmark average · 12 tasks · Haiku 4.5
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
✅ `
|
|
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.
|
|
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.
|
|
39
|
-
"@types/node": "26.6.
|
|
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.
|
|
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
|
|
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,
|
|
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):
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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
|
-
|
|
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;
|