@herbertgao/pi-extensions 2026.9.4 → 2026.9.6
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 +3 -15
- package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
- package/node_modules/@dietrichgebert/ponytail/README.es.md +33 -2
- package/node_modules/@dietrichgebert/ponytail/README.ko.md +33 -2
- package/node_modules/@dietrichgebert/ponytail/README.md +49 -5
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +4 -0
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +4 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +0 -3
- package/node_modules/@dietrichgebert/ponytail/hooks/cursor-hooks.json +17 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +22 -3
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +27 -2
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +61 -2
- package/node_modules/@dietrichgebert/ponytail/package.json +2 -1
- package/node_modules/@dietrichgebert/ponytail/scripts/cursor-hooks.js +135 -0
- package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -4
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +33 -15
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +6 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +23 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +69 -14
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +1 -1
- package/node_modules/@herbertgao/sol-pi/package.json +1 -1
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +0 -5
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +4 -4
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +41 -1
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +3 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +5 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/events.ts +5 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/normalize-params.ts +56 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/option-list-view.ts +1 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/tab-bar.ts +1 -2
- package/node_modules/@pi-plugins/fast-mode/README.md +31 -44
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20914 -30
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/package.json +5 -7
- package/node_modules/@tifan/pi-copy-response/README.md +2 -2
- package/node_modules/@tifan/pi-copy-response/package.json +3 -2
- package/node_modules/@tifan/pi-inline-skills/README.md +2 -2
- package/node_modules/@tifan/pi-inline-skills/package.json +3 -2
- package/node_modules/@tifan/pi-mermaid-open/README.md +1 -1
- package/node_modules/@tifan/pi-mermaid-open/package.json +3 -2
- package/node_modules/@tifan/pi-preferred-thinking/package.json +3 -2
- package/node_modules/@tifan/pi-recap/package.json +3 -2
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +78 -0
- package/node_modules/pi-mcp-adapter/README.md +125 -19
- package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +69 -30
- package/node_modules/pi-mcp-adapter/agent-plugin-provenance.ts +36 -0
- package/node_modules/pi-mcp-adapter/claude-plugin-loader.ts +365 -0
- package/node_modules/pi-mcp-adapter/commands.ts +13 -15
- package/node_modules/pi-mcp-adapter/config.ts +198 -30
- package/node_modules/pi-mcp-adapter/consent-manager.ts +40 -9
- package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +250 -0
- package/node_modules/pi-mcp-adapter/direct-tools.ts +9 -228
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js +74 -32
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.d.ts +1 -0
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js +31 -0
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.d.ts +9 -0
- package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js +362 -0
- package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.d.ts +3 -1
- package/node_modules/pi-mcp-adapter/dist/config.js +189 -27
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +7 -16
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js +5 -17
- package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +30 -4
- package/node_modules/pi-mcp-adapter/dist/types.js +26 -0
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/utils.d.ts +6 -0
- package/node_modules/pi-mcp-adapter/dist/utils.js +65 -6
- package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
- package/node_modules/pi-mcp-adapter/host-html-template.ts +20 -19
- package/node_modules/pi-mcp-adapter/http-ca.ts +84 -0
- package/node_modules/pi-mcp-adapter/index.ts +827 -164
- package/node_modules/pi-mcp-adapter/init.ts +48 -10
- package/node_modules/pi-mcp-adapter/lazy-loader.ts +7 -0
- package/node_modules/pi-mcp-adapter/mcp-auth-fetch.ts +113 -0
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +256 -77
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +316 -54
- package/node_modules/pi-mcp-adapter/mcp-code.ts +45 -18
- package/node_modules/pi-mcp-adapter/mcp-install.ts +60 -0
- package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +70 -6
- package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +53 -42
- package/node_modules/pi-mcp-adapter/mcp-panel-theme.ts +104 -0
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +366 -332
- package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -14
- package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +3 -1
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +383 -341
- package/node_modules/pi-mcp-adapter/mcp-status.ts +5 -0
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +7 -16
- package/node_modules/pi-mcp-adapter/namespace-tools.ts +61 -3
- package/node_modules/pi-mcp-adapter/oauth.ts +6 -2
- package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +5 -17
- package/node_modules/pi-mcp-adapter/package.json +13 -3
- package/node_modules/pi-mcp-adapter/prompts.ts +35 -3
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -7
- package/node_modules/pi-mcp-adapter/runtime-owner.ts +16 -1
- package/node_modules/pi-mcp-adapter/sampling-handler.ts +9 -32
- package/node_modules/pi-mcp-adapter/sandbox-proxy-template.ts +20 -93
- package/node_modules/pi-mcp-adapter/server-manager.ts +227 -40
- package/node_modules/pi-mcp-adapter/session-approvals.ts +187 -0
- package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +9 -1
- package/node_modules/pi-mcp-adapter/state.ts +10 -1
- package/node_modules/pi-mcp-adapter/tool-approval.ts +10 -22
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +14 -0
- package/node_modules/pi-mcp-adapter/types.ts +57 -4
- package/node_modules/pi-mcp-adapter/ui-server.ts +36 -28
- package/node_modules/pi-mcp-adapter/utils.ts +64 -6
- package/package.json +13 -21
- package/examples/pi-footer.json +0 -275
- package/node_modules/@czottmann/pi-automode/CHANGELOG.md +0 -70
- package/node_modules/@czottmann/pi-automode/LICENSE.md +0 -21
- package/node_modules/@czottmann/pi-automode/README.md +0 -215
- package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +0 -39
- package/node_modules/@czottmann/pi-automode/docs/adr/ADR-001-permission-precedence-and-trust-boundaries.md +0 -46
- package/node_modules/@czottmann/pi-automode/docs/adr/ADR-002-global-config-in-extension-data-directory.md +0 -60
- package/node_modules/@czottmann/pi-automode/docs/adr/INDEX.md +0 -6
- package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +0 -428
- package/node_modules/@czottmann/pi-automode/docs/configuration.md +0 -171
- package/node_modules/@czottmann/pi-automode/docs/defaults.md +0 -178
- package/node_modules/@czottmann/pi-automode/docs/diagnostics.md +0 -90
- package/node_modules/@czottmann/pi-automode/docs/observability-logging.md +0 -160
- package/node_modules/@czottmann/pi-automode/examples/automode.local.json +0 -44
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/bash.ts +0 -692
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +0 -852
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +0 -904
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +0 -228
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +0 -954
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +0 -429
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/log.ts +0 -173
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/model-selector.ts +0 -113
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/model.ts +0 -13
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +0 -297
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +0 -623
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/state.ts +0 -102
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/transcript.ts +0 -236
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +0 -205
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/utils.ts +0 -54
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode.ts +0 -26
- package/node_modules/@czottmann/pi-automode/package.json +0 -61
- package/node_modules/@czottmann/pi-automode/skills/automode-diagnostics/SKILL.md +0 -63
|
@@ -3,7 +3,16 @@
|
|
|
3
3
|
// Inspects user input for /ponytail commands and writes mode to flag file
|
|
4
4
|
|
|
5
5
|
const { getDefaultMode, isDeactivationCommand, writeDefaultMode } = require('./ponytail-config');
|
|
6
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
clearMode,
|
|
8
|
+
cursorRuleNotice,
|
|
9
|
+
cursorRulePath,
|
|
10
|
+
isCursor,
|
|
11
|
+
isQoder,
|
|
12
|
+
readMode,
|
|
13
|
+
setMode,
|
|
14
|
+
writeHookOutput,
|
|
15
|
+
} = require('./ponytail-runtime');
|
|
7
16
|
const { getPonytailInstructions } = require('./ponytail-instructions');
|
|
8
17
|
|
|
9
18
|
let input = '';
|
|
@@ -17,6 +26,18 @@ function finish() {
|
|
|
17
26
|
const data = JSON.parse(input.replace(/^\uFEFF/, ''));
|
|
18
27
|
const prompt = (data.prompt || '').trim().toLowerCase();
|
|
19
28
|
|
|
29
|
+
// Cursor with the always-on rule in the workspace: no hook can change or
|
|
30
|
+
// switch off a rule, so answer the command with the notice instead of
|
|
31
|
+
// writing a mode the rule would contradict (#817). Ordinary prompts
|
|
32
|
+
// stay silent as usual.
|
|
33
|
+
if (isCursor && (/^[/@$]ponytail/.test(prompt) || isDeactivationCommand(prompt))) {
|
|
34
|
+
const rule = cursorRulePath();
|
|
35
|
+
if (rule) {
|
|
36
|
+
writeHookOutput('UserPromptSubmit', readMode() || 'off', cursorRuleNotice(rule));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
20
41
|
// Match /ponytail commands
|
|
21
42
|
let modeSwitched = false;
|
|
22
43
|
let deactivated = false;
|
|
@@ -68,10 +89,14 @@ function finish() {
|
|
|
68
89
|
// switch happens we fold the confirmation into the ruleset output
|
|
69
90
|
// below (one JSON on stdout) instead of emitting two separate writes.
|
|
70
91
|
if (!isQoder) {
|
|
92
|
+
// Cursor has no /ponytail command that would load the skill body
|
|
93
|
+
// for the new level, so the tracker delivers that level's ruleset
|
|
94
|
+
// along with the confirmation (#817).
|
|
95
|
+
const header = 'PONYTAIL MODE CHANGED — level: ' + mode;
|
|
71
96
|
writeHookOutput(
|
|
72
97
|
'UserPromptSubmit',
|
|
73
98
|
mode,
|
|
74
|
-
|
|
99
|
+
isCursor ? header + '\n\n' + getPonytailInstructions(mode) : header,
|
|
75
100
|
);
|
|
76
101
|
}
|
|
77
102
|
} else if (mode === 'off') {
|
|
@@ -4,14 +4,37 @@ const os = require('os');
|
|
|
4
4
|
const { getClaudeDir, getConfigDir } = require('./ponytail-config');
|
|
5
5
|
|
|
6
6
|
const STATE_FILE = '.ponytail-active';
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
// ponytail: VS Code Copilot never sets COPILOT_PLUGIN_DATA — it only injects
|
|
9
|
+
// CLAUDE_PLUGIN_ROOT, pointed at an install path under .vscode/agent-plugins/
|
|
10
|
+
// (#528). Without this fallback isCopilot was false, so ponytail assumed
|
|
11
|
+
// native Claude Code and emitted the statusline nudge, which VS Code Copilot
|
|
12
|
+
// doesn't read.
|
|
13
|
+
function isVsCodeCopilotRoot(pluginRoot) {
|
|
14
|
+
if (!pluginRoot) return false;
|
|
15
|
+
return pluginRoot.split(/[\\/]+/).includes('agent-plugins') &&
|
|
16
|
+
pluginRoot.toLowerCase().includes('.vscode');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const isCopilot = Boolean(process.env.COPILOT_PLUGIN_DATA) ||
|
|
20
|
+
isVsCodeCopilotRoot(process.env.CLAUDE_PLUGIN_ROOT);
|
|
8
21
|
const isCodex = !isCopilot && Boolean(process.env.PLUGIN_DATA);
|
|
9
22
|
const isQoder = !isCopilot && !isCodex && Boolean(process.env.QODER_SESSION_ID);
|
|
23
|
+
// Cursor (#817): CURSOR_VERSION is set only in the environment Cursor builds
|
|
24
|
+
// for hook processes (Cursor 3.20.17 assigns it in exactly one place, the hook
|
|
25
|
+
// env builder), so it never leaks into a Claude Code session running inside
|
|
26
|
+
// Cursor's terminal. Cursor also sets it when it runs a Claude-format plugin's
|
|
27
|
+
// hooks next to CLAUDE_PLUGIN_ROOT, and it needs Cursor-shaped JSON either
|
|
28
|
+
// way, so this check comes after the hosts with their own data dirs.
|
|
29
|
+
const isCursor = !isCopilot && !isCodex && !isQoder && Boolean(process.env.CURSOR_VERSION);
|
|
10
30
|
|
|
11
31
|
let stateDir = getClaudeDir();
|
|
12
32
|
if (isCodex) stateDir = process.env.PLUGIN_DATA;
|
|
13
|
-
|
|
33
|
+
// COPILOT_PLUGIN_DATA is unset under VS Code Copilot, so fall back to
|
|
34
|
+
// getClaudeDir() rather than building a path from undefined.
|
|
35
|
+
if (isCopilot) stateDir = process.env.COPILOT_PLUGIN_DATA || getClaudeDir();
|
|
14
36
|
if (isQoder) stateDir = path.join(os.homedir(), '.qoder');
|
|
37
|
+
if (isCursor) stateDir = path.join(os.homedir(), '.cursor');
|
|
15
38
|
|
|
16
39
|
const statePath = path.join(stateDir, STATE_FILE);
|
|
17
40
|
|
|
@@ -33,6 +56,27 @@ function readMode() {
|
|
|
33
56
|
}
|
|
34
57
|
}
|
|
35
58
|
|
|
59
|
+
// Cursor's always-on project rule (.cursor/rules/ponytail.mdc) already puts the
|
|
60
|
+
// ruleset in front of every prompt and no hook can switch a rule off, so while
|
|
61
|
+
// it is in the workspace the hooks step back instead of injecting a second,
|
|
62
|
+
// possibly contradicting, copy (#817). Cursor hands every hook the workspace
|
|
63
|
+
// root as CURSOR_PROJECT_DIR; project hooks also run from that directory.
|
|
64
|
+
// ponytail: first workspace root only, a rule in a secondary folder of a
|
|
65
|
+
// multi-root workspace goes undetected.
|
|
66
|
+
function cursorRulePath() {
|
|
67
|
+
const root = process.env.CURSOR_PROJECT_DIR || process.cwd();
|
|
68
|
+
const rule = path.join(root, '.cursor', 'rules', 'ponytail.mdc');
|
|
69
|
+
return fs.existsSync(rule) ? rule : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function cursorRuleNotice(rule) {
|
|
73
|
+
return 'PONYTAIL: the always-on Cursor rule ' + rule + ' is active in this workspace and ' +
|
|
74
|
+
'already carries the ponytail ruleset, so the ponytail hooks injected nothing further. ' +
|
|
75
|
+
'Mode switching (/ponytail lite|full|ultra|off, "stop ponytail") is unavailable while ' +
|
|
76
|
+
'that rule exists. When the user tries to switch or turn off ponytail, tell them to ' +
|
|
77
|
+
'delete that rule so hooks.json can manage the level.';
|
|
78
|
+
}
|
|
79
|
+
|
|
36
80
|
function writeHookOutput(event, mode, context = '') {
|
|
37
81
|
if (isCopilot) {
|
|
38
82
|
// Copilot reads additionalContext on SessionStart; ignores output elsewhere.
|
|
@@ -64,6 +108,18 @@ function writeHookOutput(event, mode, context = '') {
|
|
|
64
108
|
process.stdout.write(JSON.stringify(output));
|
|
65
109
|
return;
|
|
66
110
|
}
|
|
111
|
+
if (isCursor) {
|
|
112
|
+
// Cursor parses stdout as JSON and treats empty stdout as "nothing to
|
|
113
|
+
// say"; raw text would be logged as a parse error. sessionStart takes
|
|
114
|
+
// additional_context into the conversation's system context;
|
|
115
|
+
// beforeSubmitPrompt needs continue:true and, in Cursor 3.20.17, injects
|
|
116
|
+
// additional_context into that turn (docs/cursor-hooks.md).
|
|
117
|
+
if (!context) return;
|
|
118
|
+
const output = { additional_context: context };
|
|
119
|
+
if (event === 'UserPromptSubmit') output.continue = true;
|
|
120
|
+
process.stdout.write(JSON.stringify(output));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
67
123
|
// Native Claude: SessionStart accepts raw stdout, but SubagentStart needs the
|
|
68
124
|
// hookSpecificOutput JSON form or the context is dropped.
|
|
69
125
|
if (event === 'SubagentStart') {
|
|
@@ -76,8 +132,11 @@ function writeHookOutput(event, mode, context = '') {
|
|
|
76
132
|
|
|
77
133
|
module.exports = {
|
|
78
134
|
clearMode,
|
|
135
|
+
cursorRuleNotice,
|
|
136
|
+
cursorRulePath,
|
|
79
137
|
isCodex,
|
|
80
138
|
isCopilot,
|
|
139
|
+
isCursor,
|
|
81
140
|
isQoder,
|
|
82
141
|
readMode,
|
|
83
142
|
setMode,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dietrichgebert/ponytail",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.10.0",
|
|
4
4
|
"description": "Lazy senior dev mode for AI agents. The best code is the code you never wrote.",
|
|
5
5
|
"keywords": ["opencode-plugin", "opencode", "ponytail", "pi-package", "pi", "skills", "qoder"],
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
".qoder-plugin/",
|
|
31
31
|
"pi-extension/",
|
|
32
32
|
"scripts/uninstall.js",
|
|
33
|
+
"scripts/cursor-hooks.js",
|
|
33
34
|
"assets/",
|
|
34
35
|
"LICENSE"
|
|
35
36
|
],
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ponytail — install or remove the Cursor hooks (hooks/cursor-hooks.json) in
|
|
3
|
+
// ~/.cursor/hooks.json (default) or <cwd>/.cursor/hooks.json (--project),
|
|
4
|
+
// merging with whatever hooks are already there. Only entries that run one of
|
|
5
|
+
// ponytail's own hooks/ponytail-*.js scripts are added or removed; every other
|
|
6
|
+
// hook stays as it was.
|
|
7
|
+
//
|
|
8
|
+
// node scripts/cursor-hooks.js install [--project]
|
|
9
|
+
// node scripts/cursor-hooks.js uninstall [--project]
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { isShellSafe } = require('../hooks/ponytail-config');
|
|
15
|
+
|
|
16
|
+
const ROOT = path.join(__dirname, '..');
|
|
17
|
+
const TEMPLATE = path.join(ROOT, 'hooks', 'cursor-hooks.json');
|
|
18
|
+
const PONYTAIL_HOOK = /ponytail-[\w-]+\.js/;
|
|
19
|
+
|
|
20
|
+
function isPonytailHook(entry) {
|
|
21
|
+
return Boolean(entry && typeof entry.command === 'string' && PONYTAIL_HOOK.test(entry.command));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function hooksPath(scope) {
|
|
25
|
+
return scope === 'project'
|
|
26
|
+
? path.join(process.cwd(), '.cursor', 'hooks.json')
|
|
27
|
+
: path.join(os.homedir(), '.cursor', 'hooks.json');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Missing file → empty config. Malformed JSON throws a SyntaxError so the
|
|
31
|
+
// caller can refuse to touch the file rather than overwrite it.
|
|
32
|
+
function readConfig(file) {
|
|
33
|
+
let config = {};
|
|
34
|
+
try {
|
|
35
|
+
config = JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
|
|
36
|
+
} catch (e) {
|
|
37
|
+
if (e.code !== 'ENOENT') throw e;
|
|
38
|
+
}
|
|
39
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) config = {};
|
|
40
|
+
if (!config.hooks || typeof config.hooks !== 'object' || Array.isArray(config.hooks)) config.hooks = {};
|
|
41
|
+
return config;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function writeConfig(file, config) {
|
|
45
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
46
|
+
fs.writeFileSync(file, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The template's PONYTAIL_DIR placeholder becomes this checkout's absolute
|
|
50
|
+
// path. Forward slashes run unchanged under cmd, PowerShell and bash, and the
|
|
51
|
+
// same allowlist that guards the statusline snippet keeps shell metacharacters
|
|
52
|
+
// out of the command string.
|
|
53
|
+
function ponytailEntries() {
|
|
54
|
+
const root = ROOT.replace(/\\/g, '/');
|
|
55
|
+
if (!isShellSafe(root)) {
|
|
56
|
+
throw new Error('ponytail is checked out at a path with shell metacharacters (' + ROOT +
|
|
57
|
+
'); move it, or copy hooks/cursor-hooks.json by hand and quote the path for your shell');
|
|
58
|
+
}
|
|
59
|
+
const template = JSON.parse(fs.readFileSync(TEMPLATE, 'utf8'));
|
|
60
|
+
const hooks = {};
|
|
61
|
+
for (const [event, entries] of Object.entries(template.hooks)) {
|
|
62
|
+
hooks[event] = entries.map((entry) => ({ ...entry, command: entry.command.replace(/PONYTAIL_DIR/g, root) }));
|
|
63
|
+
}
|
|
64
|
+
return hooks;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function stripPonytail(config) {
|
|
68
|
+
for (const [event, entries] of Object.entries(config.hooks)) {
|
|
69
|
+
if (!Array.isArray(entries)) continue;
|
|
70
|
+
const kept = entries.filter((entry) => !isPonytailHook(entry));
|
|
71
|
+
if (kept.length) config.hooks[event] = kept;
|
|
72
|
+
else delete config.hooks[event];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function install(scope) {
|
|
77
|
+
const file = hooksPath(scope);
|
|
78
|
+
const config = readConfig(file);
|
|
79
|
+
if (config.version === undefined) config.version = 1;
|
|
80
|
+
// Re-running replaces stale ponytail entries instead of duplicating them.
|
|
81
|
+
stripPonytail(config);
|
|
82
|
+
for (const [event, entries] of Object.entries(ponytailEntries())) {
|
|
83
|
+
config.hooks[event] = [...(config.hooks[event] || []), ...entries];
|
|
84
|
+
}
|
|
85
|
+
writeConfig(file, config);
|
|
86
|
+
return file;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Returns the file it changed, or null when there was nothing of ponytail's in it.
|
|
90
|
+
function uninstall(scope) {
|
|
91
|
+
const file = hooksPath(scope);
|
|
92
|
+
if (!fs.existsSync(file)) return null;
|
|
93
|
+
const config = readConfig(file);
|
|
94
|
+
const before = JSON.stringify(config);
|
|
95
|
+
stripPonytail(config);
|
|
96
|
+
if (JSON.stringify(config) === before) return null;
|
|
97
|
+
const otherKeys = Object.keys(config).filter((k) => k !== 'version' && k !== 'hooks');
|
|
98
|
+
if (Object.keys(config.hooks).length === 0 && otherKeys.length === 0) {
|
|
99
|
+
// Only ponytail lived here: drop the file rather than leave an empty husk.
|
|
100
|
+
fs.unlinkSync(file);
|
|
101
|
+
} else {
|
|
102
|
+
writeConfig(file, config);
|
|
103
|
+
}
|
|
104
|
+
return file;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (require.main === module) {
|
|
108
|
+
const args = process.argv.slice(2);
|
|
109
|
+
const action = args[0];
|
|
110
|
+
const scope = args.includes('--project') ? 'project' : 'user';
|
|
111
|
+
try {
|
|
112
|
+
if (action === 'install') {
|
|
113
|
+
const file = install(scope);
|
|
114
|
+
console.log('Installed ponytail hooks in ' + file);
|
|
115
|
+
console.log('Cursor reloads hooks.json on save; start a new chat to activate.');
|
|
116
|
+
} else if (action === 'uninstall') {
|
|
117
|
+
const file = uninstall(scope);
|
|
118
|
+
console.log(file
|
|
119
|
+
? 'Removed ponytail hooks from ' + file
|
|
120
|
+
: 'No ponytail hooks in ' + hooksPath(scope));
|
|
121
|
+
} else {
|
|
122
|
+
console.error('usage: node scripts/cursor-hooks.js install|uninstall [--project]');
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
} catch (e) {
|
|
126
|
+
if (e instanceof SyntaxError) {
|
|
127
|
+
console.error(hooksPath(scope) + ' is not valid JSON; nothing was changed, fix it by hand (' + e.message + ')');
|
|
128
|
+
} else {
|
|
129
|
+
console.error(e.message);
|
|
130
|
+
}
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = { hooksPath, install, isPonytailHook, uninstall };
|
|
@@ -1,13 +1,15 @@
|
|
|
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,
|
|
4
|
-
// settings.json
|
|
5
|
-
// uninstall command (see README);
|
|
6
|
-
// can't see.
|
|
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.
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
9
10
|
const path = require('path');
|
|
10
11
|
const { getConfigPath, getClaudeDir } = require('../hooks/ponytail-config');
|
|
12
|
+
const cursorHooks = require('./cursor-hooks');
|
|
11
13
|
|
|
12
14
|
const STATUSLINE_SCRIPT = 'ponytail-statusline';
|
|
13
15
|
|
|
@@ -21,8 +23,23 @@ function removeIfExists(filePath, label) {
|
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
removeIfExists(path.join(getClaudeDir(), '.ponytail-active'), 'mode flag');
|
|
26
|
+
removeIfExists(path.join(os.homedir(), '.cursor', '.ponytail-active'), 'Cursor mode flag');
|
|
24
27
|
removeIfExists(getConfigPath(), 'config file');
|
|
25
28
|
|
|
29
|
+
// Cursor hooks (#817): drop only ponytail's entries from ~/.cursor/hooks.json,
|
|
30
|
+
// keep every other hook the user configured there.
|
|
31
|
+
try {
|
|
32
|
+
const hooksFile = cursorHooks.uninstall('user');
|
|
33
|
+
if (hooksFile) console.log(`Removed ponytail hooks from ${hooksFile}`);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
if (e instanceof SyntaxError) {
|
|
36
|
+
// ponytail: malformed hooks.json — can't safely edit it; leave intact, warn
|
|
37
|
+
console.warn(`~/.cursor/hooks.json is malformed — could not remove the ponytail hook entries. Remove them manually from: ${cursorHooks.hooksPath('user')} (${e.message})`);
|
|
38
|
+
} else {
|
|
39
|
+
throw e;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
26
43
|
const settingsPath = path.join(getClaudeDir(), 'settings.json');
|
|
27
44
|
try {
|
|
28
45
|
const raw = fs.readFileSync(settingsPath, 'utf8').replace(/^\uFEFF/, '');
|
|
@@ -334,6 +334,7 @@ export function createAutocompleteProvider(
|
|
|
334
334
|
current: AutocompleteProvider,
|
|
335
335
|
getReferences: () => Promise<SessionReference[]>,
|
|
336
336
|
currentCwd: string,
|
|
337
|
+
isCurrent: () => boolean = () => true,
|
|
337
338
|
): AutocompleteProvider {
|
|
338
339
|
return {
|
|
339
340
|
triggerCharacters: ["@"],
|
|
@@ -346,14 +347,15 @@ export function createAutocompleteProvider(
|
|
|
346
347
|
const currentLine = lines[cursorLine] ?? "";
|
|
347
348
|
const query = extractMentionQuery(currentLine.slice(0, cursorCol));
|
|
348
349
|
if (query === undefined) {
|
|
349
|
-
|
|
350
|
+
const suggestions = await current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
351
|
+
return options.signal.aborted || !isCurrent() ? null : suggestions;
|
|
350
352
|
}
|
|
351
353
|
|
|
352
354
|
const [baseSuggestions, references] = await Promise.all([
|
|
353
355
|
current.getSuggestions(lines, cursorLine, cursorCol, options),
|
|
354
356
|
getReferences(),
|
|
355
357
|
]);
|
|
356
|
-
if (options.signal.aborted) return null;
|
|
358
|
+
if (options.signal.aborted || !isCurrent()) return null;
|
|
357
359
|
|
|
358
360
|
const sessionItems = filterSessions(references, query, currentCwd);
|
|
359
361
|
const fileItems = baseSuggestions?.prefix === `@${query}` ? baseSuggestions.items : [];
|
|
@@ -416,6 +418,7 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
|
|
|
416
418
|
subagentIds.clear();
|
|
417
419
|
clearLiveSubagentRecords();
|
|
418
420
|
let loadErrorShown = false;
|
|
421
|
+
const currentCwd = ctx.cwd;
|
|
419
422
|
const currentSessionId = ctx.sessionManager.getSessionId();
|
|
420
423
|
const currentSessionFile = ctx.sessionManager.getSessionFile();
|
|
421
424
|
let sessionsPromise: Promise<SessionInfo[]> | undefined;
|
|
@@ -429,7 +432,7 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
|
|
|
429
432
|
),
|
|
430
433
|
)
|
|
431
434
|
.catch((error: unknown) => {
|
|
432
|
-
if (!loadErrorShown) {
|
|
435
|
+
if (!loadErrorShown && generation === sessionGeneration) {
|
|
433
436
|
loadErrorShown = true;
|
|
434
437
|
const reason = error instanceof Error ? error.message : String(error);
|
|
435
438
|
ctx.ui.notify(`session-reference: failed to load sessions: ${reason}`, "error");
|
|
@@ -445,6 +448,8 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
|
|
|
445
448
|
| undefined;
|
|
446
449
|
const getReferences = async (): Promise<SessionReference[]> => {
|
|
447
450
|
const sessions = await getSessions();
|
|
451
|
+
// Session replacement can invalidate this closure while listAll() is pending.
|
|
452
|
+
if (generation !== sessionGeneration) return [];
|
|
448
453
|
const subagentKey = [...subagentIds].join("\0");
|
|
449
454
|
if (
|
|
450
455
|
referencesCache &&
|
|
@@ -455,21 +460,27 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
|
|
|
455
460
|
}
|
|
456
461
|
const ordered = orderSessionReferences(
|
|
457
462
|
mergeReferences(sessions, liveSubagentReferences(subagentIds, currentSessionId)),
|
|
458
|
-
|
|
463
|
+
currentCwd,
|
|
459
464
|
);
|
|
460
465
|
referencesCache = { sessions, subagentKey, ordered };
|
|
461
466
|
return ordered;
|
|
462
467
|
};
|
|
463
468
|
|
|
464
469
|
getAvailableReferences = getReferences;
|
|
465
|
-
|
|
470
|
+
const isTui = ctx.mode === "tui";
|
|
471
|
+
if (isTui) {
|
|
466
472
|
void getReferences();
|
|
467
473
|
// Register after other session_start handlers. pi-fff claims every @
|
|
468
474
|
// prefix, so a provider installed before it would never see session mentions.
|
|
469
475
|
setTimeout(() => {
|
|
470
476
|
if (generation !== sessionGeneration) return;
|
|
471
477
|
ctx.ui.addAutocompleteProvider((current) =>
|
|
472
|
-
createAutocompleteProvider(
|
|
478
|
+
createAutocompleteProvider(
|
|
479
|
+
current,
|
|
480
|
+
getReferences,
|
|
481
|
+
currentCwd,
|
|
482
|
+
() => generation === sessionGeneration,
|
|
483
|
+
),
|
|
473
484
|
);
|
|
474
485
|
}, 0);
|
|
475
486
|
}
|
|
@@ -479,14 +490,23 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
|
|
|
479
490
|
const referenceIds = extractSessionReferenceIds(event.prompt);
|
|
480
491
|
if (referenceIds.length === 0) return;
|
|
481
492
|
|
|
493
|
+
const generation = sessionGeneration;
|
|
482
494
|
const currentSessionId = ctx.sessionManager.getSessionId();
|
|
483
495
|
const references = await (getAvailableReferences?.() ??
|
|
484
|
-
SessionManager.listAll()
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
496
|
+
SessionManager.listAll()
|
|
497
|
+
.then((sessions) =>
|
|
498
|
+
mergeReferences(
|
|
499
|
+
sessions.filter((session) => session.id !== currentSessionId),
|
|
500
|
+
liveSubagentReferences(subagentIds, currentSessionId),
|
|
501
|
+
),
|
|
502
|
+
)
|
|
503
|
+
.catch((error: unknown) => {
|
|
504
|
+
if (generation !== sessionGeneration) return [];
|
|
505
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
506
|
+
ctx.ui.notify(`session-reference: failed to load sessions: ${reason}`, "error");
|
|
507
|
+
return [];
|
|
508
|
+
}));
|
|
509
|
+
if (generation !== sessionGeneration) return;
|
|
490
510
|
const referencesById = new Map<string, SessionReference>();
|
|
491
511
|
const referencesByName = new Map<string, SessionReference>();
|
|
492
512
|
const ambiguousNames = new Set<string>();
|
|
@@ -565,9 +585,7 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
|
|
|
565
585
|
});
|
|
566
586
|
|
|
567
587
|
pi.on("session_before_switch", () => {
|
|
568
|
-
|
|
569
|
-
clearLiveSubagentRecords();
|
|
570
|
-
getAvailableReferences = undefined;
|
|
588
|
+
// This event can cancel; preserve current-session state until shutdown.
|
|
571
589
|
});
|
|
572
590
|
|
|
573
591
|
pi.on("session_shutdown", () => {
|
|
@@ -42,11 +42,13 @@ import {
|
|
|
42
42
|
isScrollBottomInput,
|
|
43
43
|
renderScrollButton,
|
|
44
44
|
resetScrollButtonState,
|
|
45
|
+
restoreOfficialScrollToEnd,
|
|
45
46
|
scheduleScrollButtonSync,
|
|
46
47
|
setScrollButtonHovered,
|
|
47
48
|
setScrollButtonVisible,
|
|
48
49
|
setScrollButtonWidget,
|
|
49
50
|
setToolMouseTui,
|
|
51
|
+
syncOfficialScrollToEnd,
|
|
50
52
|
getScrollButtonVisible,
|
|
51
53
|
getScrollButtonWidget,
|
|
52
54
|
getToolMouseTui,
|
|
@@ -879,6 +881,7 @@ export function teardownToolMouseInteraction(
|
|
|
879
881
|
}
|
|
880
882
|
restoreToolMouseRenderPatch();
|
|
881
883
|
restoreFullscreenViewportInput(getToolMouseTui());
|
|
884
|
+
restoreOfficialScrollToEnd(getToolMouseTui());
|
|
882
885
|
resetScrollButtonState();
|
|
883
886
|
setToolMouseTui(null);
|
|
884
887
|
toolMouseUi = null;
|
|
@@ -894,6 +897,7 @@ export function resetToolHoverState(): void {
|
|
|
894
897
|
setHoveredCompactAssistant(null);
|
|
895
898
|
setScrollButtonVisible(false);
|
|
896
899
|
setScrollButtonHovered(false);
|
|
900
|
+
restoreOfficialScrollToEnd(getToolMouseTui());
|
|
897
901
|
releaseFullscreenToolMouseMotion(getToolMouseTui());
|
|
898
902
|
}
|
|
899
903
|
|
|
@@ -916,12 +920,12 @@ export function installToolMouseInteraction(
|
|
|
916
920
|
setToolMouseTui(tui);
|
|
917
921
|
setToolTuiFullscreen(fullscreenLazyTui(tui));
|
|
918
922
|
if (isLazyProxyTui(tui)) {
|
|
919
|
-
|
|
923
|
+
syncOfficialScrollToEnd(tui);
|
|
920
924
|
patchFullscreenViewportInput(tui);
|
|
921
925
|
ensureFullscreenToolMouseMotion(tui);
|
|
922
926
|
setScrollButtonWidget({
|
|
923
927
|
render: (width: number) => {
|
|
924
|
-
|
|
928
|
+
syncOfficialScrollToEnd(tui);
|
|
925
929
|
patchFullscreenViewportInput(tui);
|
|
926
930
|
ensureFullscreenToolMouseMotion(tui);
|
|
927
931
|
return renderScrollButton(width, theme);
|
|
@@ -83,9 +83,31 @@ export function fullscreenLazyTui(tui: any): boolean {
|
|
|
83
83
|
return isLazyProxyTui(tui) && tui.mode === "fullscreen";
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
const OFFICIAL_SCROLL_TO_END_KEY = Symbol.for("pi.ccstyle.official-scroll-to-end");
|
|
87
|
+
|
|
86
88
|
/** 关掉 pi 0.85 Jump to latest overlay,避免和本仓库 dock 按钮叠两层。 */
|
|
87
89
|
export function disableOfficialScrollToEnd(tui: any): void {
|
|
88
|
-
if (
|
|
90
|
+
if (!tui) return;
|
|
91
|
+
const current = tui.scrollToEndIndicator;
|
|
92
|
+
if (typeof current !== "function") return;
|
|
93
|
+
if (!tui[OFFICIAL_SCROLL_TO_END_KEY]) {
|
|
94
|
+
tui[OFFICIAL_SCROLL_TO_END_KEY] = { original: current };
|
|
95
|
+
}
|
|
96
|
+
tui.scrollToEndIndicator = undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** /ccstyle off 或 teardown 时还回官方 overlay。 */
|
|
100
|
+
export function restoreOfficialScrollToEnd(tui: any): void {
|
|
101
|
+
if (!tui) return;
|
|
102
|
+
const original = tui[OFFICIAL_SCROLL_TO_END_KEY]?.original;
|
|
103
|
+
if (typeof original === "function") tui.scrollToEndIndicator = original;
|
|
104
|
+
tui[OFFICIAL_SCROLL_TO_END_KEY] = undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** on/compact 用 dock 按钮;off 还回 pi 原生 Jump to latest。 */
|
|
108
|
+
export function syncOfficialScrollToEnd(tui: any): void {
|
|
109
|
+
if (toolMouseInteractionActive()) disableOfficialScrollToEnd(tui);
|
|
110
|
+
else restoreOfficialScrollToEnd(tui);
|
|
89
111
|
}
|
|
90
112
|
|
|
91
113
|
/** 官方 fullscreen:是否已跟随 transcript 底部(按钮隐藏条件)。 */
|