@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
package/README.md CHANGED
@@ -21,10 +21,10 @@ When migrating from `pi-mcp-adapter`, remove any standalone adapter install as w
21
21
  The package bundles 4 active `@herbertgao/*` child packages—`pi-bark`, `pi-cc-extensions`, `pi-subagents`, and `sol-pi`—plus the following upstream packages under their original names:
22
22
 
23
23
  - `pi-antigravity@0.9.0`
24
- - `@dietrichgebert/ponytail@4.10.0`
24
+ - `@dietrichgebert/ponytail@4.13.0`
25
25
  - `@juicesharp/rpiv-ask-user-question@2.12.0`
26
26
  - `@narumitw/pi-btw@0.61.1`
27
- - `@narumitw/pi-caffeinate@0.49.8`
27
+ - `@narumitw/pi-caffeinate@0.49.9`
28
28
  - `@pi-plugins/fast-mode@0.1.13`
29
29
  - `@tifan/pi-copy-response@0.2.7`
30
30
  - `@tifan/pi-handoff@2.2.2`
@@ -33,12 +33,12 @@ The package bundles 4 active `@herbertgao/*` child packages—`pi-bark`, `pi-cc-
33
33
  - `@tifan/pi-preferred-thinking@1.0.2`
34
34
  - `@tifan/pi-recap@0.4.7`
35
35
  - `@tifan/pi-rename@0.6.0`
36
- - `pi-typesafe@0.8.1`
36
+ - `pi-typesafe@0.9.1`
37
37
  - `pi-multi-account@1.24.0`
38
38
  - `pi-next-cue@1.0.7`
39
39
  - `pi-jev-auto-mode@0.5.0`
40
40
  - `pi-lens@4.3.0`
41
- - `pi-web-access@0.35.0`
41
+ - `pi-web-access@0.37.0`
42
42
  - `remote-pi@0.7.0`
43
43
  - `resume-from@0.4.1`
44
44
 
@@ -52,11 +52,11 @@ The locally customized CC and SoL-Pi packages now follow complete upstream basel
52
52
 
53
53
  `@herbertgao/pi-bark` sends localized Bark pushes when a user-facing Pi run fully settles or `@juicesharp/rpiv-ask-user-question` needs an answer. Notifications include emoji-labeled machine/path metadata and support the bundled official Pi badge through Bark's `icon` parameter. Configure it in `$PI_CODING_AGENT_DIR/bark.json` (normally `~/.pi/agent/bark.json`); see the [package README](../pi-bark/README.md). Without a valid endpoint it stays inactive.
54
54
 
55
- `@narumitw/pi-caffeinate@0.49.8` uses the host platform's sleep inhibitor during each Pi agent run. On macOS, `/caffeinate sleep` keeps the system awake while allowing the display to sleep; `/caffeinate display` also keeps the display awake. It releases the inhibitor when the run or session ends.
55
+ `@narumitw/pi-caffeinate@0.49.9` uses the host platform's sleep inhibitor during each Pi agent run. On macOS, `/caffeinate sleep` keeps the system awake while allowing the display to sleep; `/caffeinate display` also keeps the display awake. It releases the inhibitor when the run or session ends. Its D-Bus dependency remains on the normal socket transport under Node; Bun on Linux/macOS can use FFI and an in-process reader thread for Unix file-descriptor transport, falling back when unavailable. No daemon or build step is added.
56
56
 
57
- `pi-jev-auto-mode@0.5.0` adds fail-closed Jev safety gates for shell commands and file changes. `pi-typesafe@0.8.1` adds the `/typesafe` command and `typesafe_evaluate` tool for explicit structured decisions; both use the TypeSafe API after login.
57
+ `pi-jev-auto-mode@0.5.0` adds fail-closed Jev safety gates for shell commands and file changes. `pi-typesafe@0.9.1` provides `/typesafe` and `typesafe_evaluate` for explicit structured decisions; `/typesafe enable` or `PI_TYPESAFE_ENABLED=1` is still required for agent tool calls. The Pi extension continues to use the TypeSafe API, not Liquid. The package's programmatic client additionally supports Liquid with its own API key and free or paid models; existing usage accounting now merges writes under a short-lived file lock (up to 200ms synchronous wait under contention).
58
58
 
59
- `resume-from@0.4.1` continues sessions across Pi, Claude Code, and Codex; it keeps a Claude Code session matched to its start directory when the active transcript later moves into a nested cwd. `@herbertgao/sol-pi` adds opt-in Action Fusion, ObservationPack, Evidence-Preserving Reducer, and Online Context Compact; see its [configuration guide](../sol-pi/docs/configuration.md). `pi-lens@4.3.0` expands language routing and bounds retained diagnostic facts across multi-root sessions. `pi-web-access@0.35.0` provides configurable web search and fetch tools; its `toolActivation` defaults to `auto`, choosing eager or dynamic tool activation per model. `pi-antigravity@0.9.0` reads Pi 0.86+ transcript system messages, so Antigravity requests carry Pi's system prompt and tools; it also registers `google_search` and `generate_image` tools that use the signed-in Antigravity account. Preferred Thinking 1.0.2 preserves an explicit subagent `--thinking` choice. Deprecated `@tifan/pi-titlebar-spinner` is no longer bundled; Rename remains the single owner of Herdr tab naming.
59
+ `resume-from@0.4.1` continues sessions across Pi, Claude Code, and Codex; it keeps a Claude Code session matched to its start directory when the active transcript later moves into a nested cwd. `@herbertgao/sol-pi` adds opt-in Action Fusion, ObservationPack, Evidence-Preserving Reducer, and Online Context Compact; see its [configuration guide](../sol-pi/docs/configuration.md). `pi-lens@4.3.0` expands language routing and bounds retained diagnostic facts across multi-root sessions. `pi-web-access@0.37.0` provides configurable web search and fetch tools; its `toolActivation` defaults to `auto`, choosing eager or dynamic tool activation per model. It adds structured search/fetch data for codemode and an optional, manually started `pi-web-access-mcp` stdio CLI; installing the aggregate does not start that server. Degoog and Keenable are explicit-only providers, excluded from automatic and `all` selection. Perplexity search now uses the Search API's ranked pages and snippets rather than a Sonar-generated answer; requests requiring prose still use Sonar. `pi-antigravity@0.9.0` reads Pi 0.86+ transcript system messages, so Antigravity requests carry Pi's system prompt and tools; it also registers `google_search` and `generate_image` tools that use the signed-in Antigravity account. Preferred Thinking 1.0.2 preserves an explicit subagent `--thinking` choice. Deprecated `@tifan/pi-titlebar-spinner` is no longer bundled; Rename remains the single owner of Herdr tab naming.
60
60
 
61
61
  `pi-stash` is no longer bundled: `/btw` already preserves the main editor draft while handling side questions outside the main conversation. Prior `@herbertgao/pi-stash` releases remain available but are no longer maintained here.
62
62
 
@@ -62,7 +62,7 @@ SOFTWARE.
62
62
 
63
63
  ## `@narumitw/pi-caffeinate`
64
64
 
65
- The aggregate bundles [`@narumitw/pi-caffeinate`](https://github.com/narumiruna/pi-extensions/tree/main/packages/pi-caffeinate) version 0.49.7 under its original MIT license:
65
+ The aggregate bundles [`@narumitw/pi-caffeinate`](https://github.com/narumiruna/pi-extensions/tree/main/packages/pi-caffeinate) version 0.49.9 under its original MIT license:
66
66
 
67
67
  MIT License
68
68
 
@@ -137,7 +137,7 @@ THE SOFTWARE.
137
137
 
138
138
  ## `pi-typesafe`
139
139
 
140
- The aggregate bundles [`pi-typesafe`](https://github.com/DevMortimer/pi-typesafe) version 0.5.0 under its original MIT license:
140
+ The aggregate bundles [`pi-typesafe`](https://github.com/DevMortimer/pi-typesafe) version 0.9.1 under its original MIT license:
141
141
 
142
142
  MIT License
143
143
 
@@ -2,4 +2,4 @@
2
2
  description: Audit the whole repo for over-engineering, what can be deleted
3
3
  ---
4
4
 
5
- Audit the entire repository for over-engineering only, not correctness. Scan the whole tree, not a diff. One line per finding, ranked biggest cut first: <tag> <what to cut>. <replacement>. [path]. Tags: delete (dead code/speculative feature), stdlib (reinvented standard library), native (dependency doing what the platform does), yagni (abstraction with one implementation), shrink (same logic, fewer lines). End with the net lines and dependencies removable. If nothing to cut: 'Lean already. Ship.'
5
+ Audit the entire repository for over-engineering only, not correctness. Scan the whole tree, not a diff. Before emitting delete, grep the whole tree for the symbol, including tests, fixtures and string or dynamic references. One numbered line per finding, ranked biggest cut first: N. <tag> <what to cut>. <replacement>. [path]. Tags: delete (dead code/speculative feature), stdlib (reinvented standard library), native (dependency doing what the platform does), yagni (abstraction with one implementation), shrink (same logic, fewer lines), reuse (duplicates a helper already in this repo). End with the net lines and dependencies removable. If nothing to cut: 'Lean already. Ship.'
@@ -2,4 +2,4 @@
2
2
  description: "Harvest ponytail: comments into a tracked debt ledger"
3
3
  ---
4
4
 
5
- Harvest every `ponytail:` comment in this repository into a debt ledger so deferrals do not rot into 'later means never'. Grep the whole tree for comment markers (grep -rnE '(#|//) ?ponytail:' ., skipping node_modules/.git/build output). One row per marker, grouped by file: <file>:<line>, <what was simplified>. ceiling: <the limit named in the comment>. upgrade: <the trigger to revisit>. Tag any marker that names no upgrade path or trigger as no-trigger, those rot silently. End with the count of markers and how many lack a trigger. If none: 'No ponytail: debt. Clean ledger.' Report only, change nothing.
5
+ Harvest every `ponytail:` comment in this repository into a debt ledger so deferrals do not rot into 'later means never'. Grep the whole tree for comment markers (grep -rnE --exclude-dir=.git --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=build '(#|//|/[*]) ?ponytail:' ., skipping node_modules/.git/build output). One row per marker, grouped by file: <file>:<line>, <what was simplified>. ceiling: <the limit named in the comment>. upgrade: <the trigger to revisit>. Tag any marker that names no upgrade path or trigger as no-trigger, those rot silently. End with the count of markers and how many lack a trigger. If none: 'No ponytail: debt. Clean ledger.' Report only, change nothing.
@@ -2,4 +2,4 @@
2
2
  description: Show ponytail's measured impact scoreboard (less code, cost, time)
3
3
  ---
4
4
 
5
- Show the ponytail gain scoreboard. One shot, change nothing: do not switch mode, write flag files, or persist anything. Render the published benchmark medians (5 everyday tasks; models Haiku, Sonnet, Opus; source benchmarks/ and the README) as plain ASCII bars: Lines of code, no-skill 100% vs ponytail 6-20% (down 80-94%); Cost, no-skill 100% vs ponytail 23-53% (down 47-77%); Speed, ponytail 3-6x faster. The bar length shows the measured range, the label carries the exact figure. These are benchmark medians, not this repo. NEVER print a per-repo savings number: the unbuilt version was never written, so there is no real baseline to subtract from in a live repo. For real per-repo figures, point to /ponytail-debt (the counted shortcut ledger) and /ponytail-audit (what is still cuttable). Report only.
5
+ Show the ponytail gain scoreboard. One shot, change nothing: do not switch mode, write flag files, or persist anything. Render the published agentic benchmark averages (12 feature tasks on a real FastAPI + React repo, Haiku 4.5, each task averaged over 4 runs, against the same agent without the skill; source benchmarks/results/2026-06-18-agentic.md and the README) as plain ASCII bars, ponytail as a share of the no-skill baseline (100%): Lines of code 46% (down 54%); Tokens 78% (down 22%); Cost 80% (down 20%); Time 73% (down 27%); Safety kept 100%. The bar length shows the share, the label carries the exact figure. These are benchmark averages, not this repo. NEVER print a per-repo savings number: the unbuilt version was never written, so there is no real baseline to subtract from in a live repo. For real per-repo figures, point to /ponytail-debt (the counted shortcut ledger) and /ponytail-audit (what is still cuttable). Report only.
@@ -2,4 +2,4 @@
2
2
  description: Review changes for over-engineering, what can be deleted
3
3
  ---
4
4
 
5
- Review the current code changes for over-engineering only, not correctness. One line per finding: L<line>: <tag> <what to cut>. <replacement>. Tags: delete (dead code/speculative feature), stdlib (reinvented standard library), native (dependency doing what the platform does), yagni (abstraction with one implementation), shrink (same logic, fewer lines). End with the net lines removable. If nothing to cut: 'Lean already. Ship.'
5
+ Review the current code changes for over-engineering only, not correctness. One numbered line per finding: N. L<line>: <tag> <what to cut>. <replacement>. Tags: delete (dead code/speculative feature), stdlib (reinvented standard library), native (dependency doing what the platform does), yagni (abstraction with one implementation), shrink (same logic, fewer lines), reuse (duplicates a helper already in this repo). End with the net lines removable. If nothing to cut: 'Lean already. Ship.'
@@ -2,4 +2,4 @@
2
2
  description: Switch ponytail intensity level (lite/full/ultra/off)
3
3
  ---
4
4
 
5
- Switch to ponytail $ARGUMENTS mode. If no level specified, use full. Lazy senior dev mode, before any code: does it need to exist at all (YAGNI)? Does the standard library do it? A native platform feature? Can it be one line? Build the minimum that works. No unrequested abstractions, no avoidable dependencies, no boilerplate. Mark deliberate simplifications that cut a real corner with a known ceiling using a ponytail: comment that names the ceiling and upgrade path.
5
+ Switch to ponytail $ARGUMENTS mode. With no level, keep the current level, or switch on at the default level if ponytail was off. Lazy senior dev mode, before any code: does it need to exist at all (YAGNI)? Does the standard library do it? A native platform feature? Can it be one line? Build the minimum that works. No unrequested abstractions, no avoidable dependencies, no boilerplate. Mark deliberate simplifications that cut a real corner with a known ceiling using a ponytail: comment that names the ceiling and upgrade path.
@@ -0,0 +1,4 @@
1
+ // OpenCode 2 loads this file automatically from .opencode/plugins/ and does not
2
+ // read ponytail.mjs next to it, so keep the one-line entry here. npm consumers
3
+ // never see it: package.json main/exports still point at ponytail.mjs.
4
+ export { default } from './ponytail.mjs'
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
- // ponytail command-file frontmatter parser.
3
+ // ponytail command-file and SKILL.md frontmatter parser.
4
4
  //
5
5
  // Pulled out of ponytail.mjs so the plugin module's only top-level export is
6
- // the plugin function itself. OpenCode's legacy plugin loader (the one that
6
+ // the plugin definition itself. OpenCode's legacy plugin loader (the one that
7
7
  // runs before v1 plugins are detected) treats every function exported from a
8
8
  // plugin module as a plugin; calling the frontmatter parser as one threw
9
9
  // "path must be a string or a file descriptor" because it got the plugin
@@ -20,4 +20,44 @@ function parseCommandFile(filePath) {
20
20
  return { description, template: match[2].trim() };
21
21
  }
22
22
 
23
- module.exports = { parseCommandFile };
23
+ // ponytail: flat frontmatter only — the shape every shipped SKILL.md uses. A
24
+ // nested key or an anchor is not interpreted, and a real YAML parser is the
25
+ // upgrade path if a skill ever needs one.
26
+ function frontmatterField(frontmatter, key) {
27
+ const lines = frontmatter.split(/\r?\n/);
28
+ const at = lines.findIndex((line) => line.startsWith(key + ':'));
29
+ if (at === -1) return undefined;
30
+ const value = lines[at].slice(key.length + 1).trim();
31
+ if (value[0] !== '>' && value[0] !== '|') return value;
32
+
33
+ // Block scalar: `>` folds the lines into one, `|` keeps them. Indented lines
34
+ // continue the block, the next unindented key ends it (skills/ponytail has
35
+ // `argument-hint` after its description), and a trailing `-` drops the
36
+ // newline YAML would otherwise close the scalar with.
37
+ const block = [];
38
+ for (const line of lines.slice(at + 1)) {
39
+ if (line.trim() && !/^\s/.test(line)) break;
40
+ block.push(line.trim());
41
+ }
42
+ while (block.length && !block[block.length - 1]) block.pop();
43
+ const joined = block.join(value[0] === '|' ? '\n' : ' ');
44
+ return value.endsWith('-') ? joined : joined + '\n';
45
+ }
46
+
47
+ // OpenCode's V2 skill transform takes the resolved fields, not the file: the
48
+ // skill id is the directory name, and `content` is the body with its
49
+ // frontmatter stripped. Matches what OpenCode itself derives from a discovered
50
+ // .opencode/skills/<id>/SKILL.md.
51
+ function parseSkillFile(filePath) {
52
+ const fs = require('fs');
53
+ const content = fs.readFileSync(filePath, 'utf8');
54
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---[^\S\n]*\r?\n?([\s\S]*)$/);
55
+ if (!match) return null;
56
+ return {
57
+ name: frontmatterField(match[1], 'name'),
58
+ description: frontmatterField(match[1], 'description'),
59
+ body: match[2],
60
+ };
61
+ }
62
+
63
+ module.exports = { parseCommandFile, parseSkillFile };
@@ -1,19 +1,22 @@
1
1
  // ponytail — OpenCode plugin.
2
2
  //
3
3
  // Injects the ponytail ruleset into every chat's system prompt at the active
4
- // intensity, persists /ponytail mode switches, and registers slash commands so
5
- // they work when the package is installed from npm. Reuses the shared
6
- // instruction builder so Claude Code, Codex, pi, and OpenCode all read one
7
- // source of truth.
4
+ // intensity, persists /ponytail mode switches, and registers the /ponytail
5
+ // commands and skills so they work when the package is installed from npm.
6
+ // Reuses the shared instruction builder so Claude Code, Codex, pi, and OpenCode
7
+ // all read one source of truth.
8
8
  //
9
9
  // OpenCode loads this as a server plugin — add it to your opencode.json:
10
- // { "plugin": ["@dietrichgebert/ponytail"] }
10
+ // { "plugins": ["@dietrichgebert/ponytail"] }
11
+ //
12
+ // One default export serves both plugin APIs: V2 reads `id` + `setup`, V1 calls
13
+ // `server()`.
11
14
 
12
- import { createRequire } from 'module';
13
- import fs from 'fs';
14
- import os from 'os';
15
- import path from 'path';
16
- import { fileURLToPath } from 'url';
15
+ import { createRequire } from 'node:module';
16
+ import fs from 'node:fs';
17
+ import os from 'node:os';
18
+ import path from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
17
20
 
18
21
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
22
 
@@ -21,9 +24,10 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
24
  const require = createRequire(import.meta.url);
22
25
  const { getPonytailInstructions } = require('../../hooks/ponytail-instructions');
23
26
  const { getDefaultMode, normalizePersistedMode } = require('../../hooks/ponytail-config');
24
- const { parseCommandFile } = require('./ponytail-frontmatter.cjs');
27
+ const { parseCommandFile, parseSkillFile } = require('./ponytail-frontmatter.cjs');
25
28
 
26
29
  // OpenCode has no flag-file convention of its own; keep mode beside its config.
30
+ // Shared with the V1 path, so a level set under either API is read by both.
27
31
  const statePath = path.join(
28
32
  process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'),
29
33
  'opencode',
@@ -43,57 +47,136 @@ function writeMode(mode) {
43
47
  fs.writeFileSync(statePath, mode);
44
48
  }
45
49
 
46
- export default async ({ client } = {}) => {
47
- const log = (level, message) => {
48
- try { client && client.app && client.app.log({ body: { service: 'ponytail', level, message } }); } catch (e) {}
49
- };
50
-
51
- const ponytailSkillsDir = path.resolve(__dirname, '../../skills');
52
-
53
- return {
54
- // Register slash commands + skills directory.
55
- config: async (config) => {
56
- if (!config.command) config.command = {};
57
- const commandDir = path.join(__dirname, '..', 'command');
58
- try {
59
- for (const file of fs.readdirSync(commandDir).filter((f) => f.endsWith('.md'))) {
60
- const name = path.basename(file, '.md');
61
- const parsed = parseCommandFile(path.join(commandDir, file));
62
- if (parsed) config.command[name] = parsed;
63
- }
64
- } catch (e) {}
50
+ // `off` is persisted like any mode; the injection reads it and stays silent.
51
+ // An unrecognized level leaves the current one alone. The write lands before
52
+ // the next turn's injection, not the current one — good enough; switch to a
53
+ // synchronous store if same-turn switching ever matters.
54
+ function persistMode(args) {
55
+ const wanted = String(args == null ? '' : args).trim();
56
+ // Bare /ponytail switches ponytail on, or keeps the level when it already is (#639).
57
+ if (!wanted && readMode() !== 'off') return;
58
+ const mode = wanted ? normalizePersistedMode(wanted) : (getDefaultMode() === 'off' ? 'full' : getDefaultMode());
59
+ if (!mode) return;
60
+ writeMode(mode);
61
+ console.log('ponytail ' + mode);
62
+ }
63
+
64
+ // V2 has no `config` hook: the domains that own these definitions own them now,
65
+ // so the command and skill files are read once here and handed to the
66
+ // transforms that register them.
67
+ function readCommands() {
68
+ const dir = path.join(__dirname, '..', 'command');
69
+ try {
70
+ return fs.readdirSync(dir)
71
+ .filter((file) => file.endsWith('.md'))
72
+ .map((file) => {
73
+ const parsed = parseCommandFile(path.join(dir, file));
74
+ return parsed && { name: path.basename(file, '.md'), ...parsed };
75
+ })
76
+ .filter(Boolean);
77
+ } catch (e) {
78
+ return [];
79
+ }
80
+ }
81
+
82
+ function readSkills() {
83
+ const dir = path.resolve(__dirname, '../../skills');
84
+ try {
85
+ return fs.readdirSync(dir, { withFileTypes: true })
86
+ .filter((entry) => entry.isDirectory())
87
+ .map((entry) => {
88
+ const file = path.join(dir, entry.name, 'SKILL.md');
89
+ const parsed = parseSkillFile(file);
90
+ return parsed && {
91
+ id: entry.name,
92
+ name: parsed.name || entry.name,
93
+ description: parsed.description,
94
+ path: file,
95
+ content: parsed.body,
96
+ };
97
+ })
98
+ .filter(Boolean);
99
+ } catch (e) {
100
+ return [];
101
+ }
102
+ }
65
103
 
66
- config.skills = config.skills || {};
67
- config.skills.paths = config.skills.paths || [];
68
- if (!config.skills.paths.includes(ponytailSkillsDir)) {
69
- config.skills.paths.push(ponytailSkillsDir);
104
+ export default {
105
+ id: 'ponytail',
106
+
107
+ async setup(ctx) {
108
+ const commands = readCommands();
109
+ const skills = readSkills();
110
+
111
+ await ctx.skill.transform((editor) => {
112
+ for (const skill of skills) editor.add(skill);
113
+ });
114
+
115
+ await ctx.command.transform((editor) => {
116
+ for (const command of commands) {
117
+ editor.add({
118
+ name: command.name,
119
+ description: command.description,
120
+ execute: async ({ sessionID, prompt, delivery }) => {
121
+ if (command.name === 'ponytail') persistMode(prompt.text);
122
+ await ctx.session.prompt({
123
+ ...prompt,
124
+ sessionID,
125
+ text: command.template.replaceAll('$ARGUMENTS', prompt.text || ''),
126
+ delivery,
127
+ });
128
+ },
129
+ });
70
130
  }
71
- },
131
+ });
72
132
 
73
- // Append the ruleset to the system prompt every turn.
74
- 'experimental.chat.system.transform': async (_input, output) => {
133
+ // Append the ruleset to the system prompt every turn. V2 hands over owned
134
+ // system parts, so push one instead of rewriting the tail of another.
135
+ await ctx.session.hook('context', (event) => {
75
136
  const mode = readMode();
76
137
  if (mode === 'off') return;
77
- const instructions = getPonytailInstructions(mode);
78
- if (output.system.length > 0) {
79
- output.system[output.system.length - 1] += '\n\n' + instructions;
80
- } else {
81
- output.system.push(instructions);
82
- }
83
- },
84
-
85
- // Persist `/ponytail <level>` so the next turn's injection follows it.
86
- // ponytail: mode applies from the next message, not the current one — the
87
- // transform reads the flag the command writes. Good enough; switch to a
88
- // synchronous store if same-turn switching ever matters.
89
- 'command.execute.before': async (input) => {
90
- if (!input || input.command !== 'ponytail') return;
91
- // `off` is persisted like any mode; the transform reads it and stays silent.
92
- const args = String(input.arguments || '').trim();
93
- const mode = args ? normalizePersistedMode(args) : getDefaultMode();
94
- if (!mode) return;
95
- writeMode(mode);
96
- log('info', 'ponytail ' + mode);
97
- },
98
- };
138
+ event.system.push({ type: 'text', text: getPonytailInstructions(mode) });
139
+ });
140
+ },
141
+
142
+ // OpenCode V1: same three behaviors, as hooks on the V1 hook names.
143
+ async server({ client } = {}) {
144
+ const log = (level, message) => {
145
+ try { client && client.app && client.app.log({ body: { service: 'ponytail', level, message } }); } catch (e) {}
146
+ };
147
+
148
+ return {
149
+ // Register slash commands + skills directory.
150
+ config: async (config) => {
151
+ if (!config.command) config.command = {};
152
+ for (const command of readCommands()) {
153
+ config.command[command.name] = { description: command.description, template: command.template };
154
+ }
155
+
156
+ config.skills = config.skills || {};
157
+ config.skills.paths = config.skills.paths || [];
158
+ const ponytailSkillsDir = path.resolve(__dirname, '../../skills');
159
+ if (!config.skills.paths.includes(ponytailSkillsDir)) {
160
+ config.skills.paths.push(ponytailSkillsDir);
161
+ }
162
+ },
163
+
164
+ 'experimental.chat.system.transform': async (_input, output) => {
165
+ const mode = readMode();
166
+ if (mode === 'off') return;
167
+ const instructions = getPonytailInstructions(mode);
168
+ if (output.system.length > 0) {
169
+ output.system[output.system.length - 1] += '\n\n' + instructions;
170
+ } else {
171
+ output.system.push(instructions);
172
+ }
173
+ },
174
+
175
+ 'command.execute.before': async (input) => {
176
+ if (!input || input.command !== 'ponytail') return;
177
+ persistMode(input.arguments);
178
+ log('info', 'ponytail ' + readMode());
179
+ },
180
+ };
181
+ },
99
182
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ponytail",
3
- "version": "4.10.0",
3
+ "version": "4.13.0",
4
4
  "description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
5
5
  "author": {
6
6
  "name": "Dietrich Gebert",