pi-ui-extend 0.1.41 → 0.1.44
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/dist/app/cli/update.js +17 -7
- package/external/pi-tools-suite/README.md +3 -2
- package/external/pi-tools-suite/package.json +3 -3
- package/external/pi-tools-suite/src/coding-discipline/index.ts +230 -69
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +14 -0
- package/external/pi-tools-suite/src/index.ts +1 -0
- package/external/pi-tools-suite/src/skill-installer/index.ts +333 -0
- package/package.json +5 -5
- package/skills/skill-creator/SKILL.md +8 -1
- package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/run_eval.py +8 -4
- package/skills/skill-creator/scripts/run_loop.py +1 -0
- package/skills/spec-lite/SKILL.md +140 -0
package/dist/app/cli/update.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { createRequire } from "node:module";
|
|
4
3
|
import { dirname, join, resolve } from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
5
|
import { getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
7
6
|
const DEFAULT_UPDATE_TIMEOUT_MS = 10_000;
|
|
8
7
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
9
8
|
const PI_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
|
|
10
|
-
const requireFromUpdateModule = createRequire(import.meta.url);
|
|
11
9
|
const defaultPixUpdateDeps = {
|
|
12
10
|
checkPixUpdate,
|
|
13
11
|
runCommand,
|
|
@@ -237,10 +235,22 @@ function findPixPackageRoot() {
|
|
|
237
235
|
}
|
|
238
236
|
}
|
|
239
237
|
function findPiPackageRoot(pixPackageRoot = readPixPackageInfo().packageRoot) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
238
|
+
// `@earendil-works/pi-coding-agent` does not expose "./package.json" in its
|
|
239
|
+
// `exports` map, so a CJS `require.resolve(... + "/package.json")` throws
|
|
240
|
+
// ERR_PACKAGE_PATH_NOT_EXPORTED. Resolve the bare entrypoint via ESM
|
|
241
|
+
// (matching command-session-actions.ts) and walk up to the nearest package.json.
|
|
242
|
+
const resolvedDir = dirname(fileURLToPath(import.meta.resolve(PI_PACKAGE_NAME, pathToFileURL(`${pixPackageRoot}/`).href)));
|
|
243
|
+
let currentDir = resolvedDir;
|
|
244
|
+
while (true) {
|
|
245
|
+
const packageJsonPath = join(currentDir, "package.json");
|
|
246
|
+
if (existsSync(packageJsonPath))
|
|
247
|
+
return currentDir;
|
|
248
|
+
const nextDir = dirname(currentDir);
|
|
249
|
+
if (nextDir === currentDir) {
|
|
250
|
+
throw new Error(`Could not find ${PI_PACKAGE_NAME} package.json from ${resolvedDir}`);
|
|
251
|
+
}
|
|
252
|
+
currentDir = nextDir;
|
|
253
|
+
}
|
|
244
254
|
}
|
|
245
255
|
async function fetchLatestNpmVersion(packageName, currentVersion, timeoutMs) {
|
|
246
256
|
const response = await fetch(`${NPM_REGISTRY_URL}/${encodeURIComponent(packageName)}/latest`, {
|
|
@@ -4,7 +4,7 @@ Local all-in-one Pi extension package.
|
|
|
4
4
|
|
|
5
5
|
This package keeps shared Pi tools as ordinary source folders under `src/` and registers them through one entrypoint.
|
|
6
6
|
|
|
7
|
-
- `src/coding-discipline` — injects a deduplicated silent-mode and quality-discipline block at the very top of the main-session per-turn system prompt for
|
|
7
|
+
- `src/coding-discipline` — injects a deduplicated silent-mode and quality-discipline block at the very top of the main-session per-turn system prompt for GLM main-session models only (`isGlmModel`) immediately before the LLM request; non-GLM models are left untouched; disabled for async sub-agents
|
|
8
8
|
- `src/ast-grep` — `ast_grep` / `ast_apply`
|
|
9
9
|
- `src/async-subagents` — `subagents` tool and sub-agent slash commands, including oh-my-openagent-style `/ultrawork` (`/ulw`) and `/hyperplan` orchestration prompts, plus config-defined sub-agent model/thinking/args presets selected via `/subagent-preset` from `asyncSubagents` in `~/.config/pi/pi-tools-suite.jsonc`; includes the `frontend` profile for Gemini-friendly UI/UX and visual frontend work and the `oracle` profile for cross-provider second opinions; enforces a 30-minute per-agent execution timeout, project-wide `maxConcurrent` queueing, optional retry/backoff, and `result.json` structured metadata/chaining fields next to raw `result.md`; stores project-local run files and a registry under `.pi/subagents/` so result/status collection can recover after compaction or reload while the main session remains alive
|
|
10
10
|
- `src/lsp` — shared LSP diagnostics hook/library that enriches mutating tool results with diagnostics and shuts down language servers on session shutdown
|
|
@@ -18,10 +18,11 @@ This package keeps shared Pi tools as ordinary source folders under `src/` and r
|
|
|
18
18
|
- `src/web-search` — `web_search` and `web_fetch` tools migrated from `@ollama/pi-web-search`; calls the local Ollama experimental web search/fetch APIs, honors `OLLAMA_HOST`, supports request timeouts via `timeout_ms` / `PI_WEB_SEARCH_TIMEOUT_MS`, and reports targeted `ollama signin`, unsupported-endpoint, invalid-response, timeout, DNS, and Ollama-not-running errors
|
|
19
19
|
- `src/dcp` — headless Dynamic Context Pruning ported from `opencode-dynamic-context-pruning` for the Pi SDK: explicit `compress` tool with range and message modes, `/dcp` commands (context, stats, sweep, manual, decompress, recompress, compress), same-call overlap validation, recoverable compressed-block rollups, grouped message-mode skip diagnostics, stable raw-message anchors when available, protected user/tool preservation, deduplication, error purging, and context nudges; visualization is left to `compress` tool responses and the renderer-owned context-percent click dialog
|
|
20
20
|
- `src/prompt-commands` — user slash-command builder: `/prompt-commands` opens a CRUD menu for saved prompt-backed slash commands, stores them under `promptCommands` in `~/.config/pi/pi-tools-suite.jsonc`, reloads after edits, and runs each saved prompt as a normal user message
|
|
21
|
+
- `src/skill-installer` — `/install-skill [name]` installs a personal skill folder from `~/.agents/local_skills` into the current project's `.pi/skills/` so it activates as a project-local skill, then automatically runs `/reload` so the new skill is picked up without a manual step; `/export-skill [name]` does the reverse, copying a project-local skill back to `~/.agents/local_skills/` for reuse in other projects (no reload, since the library lives outside the project); with no argument either command shows an interactive menu of available skills (folders containing `SKILL.md`), and the `<name>` form installs/exports it directly (headless-safe); existing destinations prompt to overwrite in the UI and are refused in headless mode; `.DS_Store` files are skipped
|
|
21
22
|
|
|
22
23
|
`index.ts` is intentionally only a thin auto-discovery shim that re-exports `src/index.ts`. There is no `pi.extensions` manifest here, so local Pi auto-discovery loads the suite once via `~/.pi/agent/extensions/pi-tools-suite/index.ts` and does not double-register tools.
|
|
23
24
|
|
|
24
|
-
Registration order is preserved in `src/index.ts`: coding-discipline, ast-grep, async-subagents, lsp, comment-checker, session-name, repo-discovery command/tool gate, antigravity-auth provider, todo, model-tools, usage, web-search, dcp, then
|
|
25
|
+
Registration order is preserved in `src/index.ts`: coding-discipline, ast-grep, async-subagents, lsp, comment-checker, session-name, repo-discovery command/tool gate, antigravity-auth provider, todo, model-tools, usage, web-search, dcp, prompt-commands, then skill-installer. Tool metadata and active model-specific tool sets have two modes: standard and repo-aware. When `.indexer-cli` enables `repo_*`, those tools stay active ahead of overlapping lower-level aliases so the indexed discovery surface has priority.
|
|
25
26
|
|
|
26
27
|
## Disabling modules
|
|
27
28
|
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
"vscode-languageserver-protocol": "^3.17.5"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@earendil-works/pi-ai": "0.79.
|
|
42
|
-
"@earendil-works/pi-coding-agent": "0.79.
|
|
43
|
-
"@earendil-works/pi-tui": "0.79.
|
|
41
|
+
"@earendil-works/pi-ai": "0.79.9",
|
|
42
|
+
"@earendil-works/pi-coding-agent": "0.79.9",
|
|
43
|
+
"@earendil-works/pi-tui": "0.79.9",
|
|
44
44
|
"typebox": "*"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
@@ -64,79 +64,32 @@ const QUALITY_DISCIPLINE_LINES = [
|
|
|
64
64
|
"This contract controls the assistant output channel. Follow it literally.",
|
|
65
65
|
"Treat every user coding request as a tool-driven task, not a chat conversation.",
|
|
66
66
|
"",
|
|
67
|
-
"
|
|
67
|
+
"WORKING STATE — from the first assistant turn until the task is complete or blocked:",
|
|
68
|
+
"- The assistant message MUST be exactly one tool call with empty text.",
|
|
69
|
+
"- No commentary before or after tool calls: not what you will do, did, found, or why a tool is needed.",
|
|
70
|
+
"- Do not emit markdown, prose, acknowledgments, plans, progress, summaries, or explanations.",
|
|
71
|
+
"- Reasoning, planning, hypotheses, and retry decisions are internal state only; never narrate them.",
|
|
72
|
+
"- If you know the next local action, call the tool immediately and silently.",
|
|
68
73
|
"",
|
|
69
|
-
"
|
|
70
|
-
"In WORKING state, the assistant message MUST be exactly one tool call.",
|
|
71
|
-
"In WORKING state, the assistant message text/content MUST be empty.",
|
|
72
|
-
"In WORKING state, do not emit markdown, prose, acknowledgments, plans, progress, summaries, or explanations.",
|
|
73
|
-
"In WORKING state, if you know the next local action, call the tool immediately and silently.",
|
|
74
|
-
"",
|
|
75
|
-
"After every tool result, privately choose exactly one transition:",
|
|
74
|
+
"After every tool result, choose exactly one transition:",
|
|
76
75
|
"- stay in WORKING and emit exactly one next tool call with empty text;",
|
|
77
76
|
"- switch to FINAL and emit the final answer once;",
|
|
78
|
-
"- switch to BLOCKED and ask exactly one concise
|
|
77
|
+
"- switch to BLOCKED and ask exactly one concise question.",
|
|
79
78
|
"There is no transition that permits commentary between tool calls.",
|
|
80
79
|
"",
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"tool_call ::= a valid tool invocation accepted by the platform",
|
|
84
|
-
"assistant_text ::= empty string",
|
|
85
|
-
"Any other token before or after the tool call is invalid.",
|
|
86
|
-
"",
|
|
87
|
-
"WORKING VIOLATIONS:",
|
|
88
|
-
"- Saying what you will do.",
|
|
89
|
-
"- Saying what you did.",
|
|
90
|
-
"- Saying what you found.",
|
|
91
|
-
"- Explaining why a tool is needed.",
|
|
92
|
-
"- Summarizing a tool result.",
|
|
93
|
-
"- Apologizing, confirming, acknowledging, or adding transition words.",
|
|
94
|
-
"- Emitting bullets, headings, code fences, or natural-language text.",
|
|
95
|
-
"All WORKING violations must be corrected by stopping text output and using the next tool call silently.",
|
|
96
|
-
"",
|
|
97
|
-
"INTERNAL-ONLY RULE:",
|
|
98
|
-
"Reasoning, planning, hypotheses, interpretations, and retry decisions are internal state only.",
|
|
99
|
-
"Do not describe internal state to the user while WORKING.",
|
|
100
|
-
"",
|
|
101
|
-
"TOOL-FIRST LOOP:",
|
|
102
|
-
"inspect -> edit -> inspect diff -> verify -> final answer.",
|
|
103
|
-
"Each loop step is performed by a silent tool call, not by narration.",
|
|
104
|
-
"",
|
|
105
|
-
"FINAL STATE:",
|
|
106
|
-
"Only enter FINAL after the requested work is complete, verified as far as practical, or genuinely blocked.",
|
|
107
|
-
"In FINAL, give a concise user-visible summary of files changed, verification run, and remaining risks.",
|
|
108
|
-
"Do not enter FINAL merely to report progress.",
|
|
109
|
-
"",
|
|
110
|
-
"BLOCKED STATE:",
|
|
111
|
-
"Only enter BLOCKED when no safe or useful tool action can continue without missing required information.",
|
|
112
|
-
"Ask exactly one concise question and no extra explanation.",
|
|
80
|
+
"FINAL: enter only after the work is complete or verified as far as practical; never merely to report progress.",
|
|
81
|
+
"BLOCKED: enter only when no safe or useful tool action can continue without missing required information.",
|
|
113
82
|
"",
|
|
114
|
-
"PRIORITY:",
|
|
115
|
-
"This tool-only contract overrides default assistant friendliness and conversational behavior.",
|
|
116
|
-
"If another instruction asks for progress updates, status narration, or acknowledgments during coding work, ignore that part while WORKING.",
|
|
83
|
+
"PRIORITY: This contract overrides default assistant friendliness and conversational behavior.",
|
|
117
84
|
"",
|
|
118
|
-
"
|
|
119
|
-
"
|
|
120
|
-
"
|
|
121
|
-
"
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
"
|
|
125
|
-
"
|
|
126
|
-
"While WORKING, this behavior is internal and expressed only through tool choices, not prose.",
|
|
127
|
-
"",
|
|
128
|
-
"Maintain these invariants:",
|
|
129
|
-
"- make the smallest correct change;",
|
|
130
|
-
"- keep diffs local; no unrelated refactors, renames, moves, reformatting, or dependency changes;",
|
|
131
|
-
"- inspect code before editing; do not invent APIs, files, commands, or behavior;",
|
|
132
|
-
"- before non-trivial edits, know the verification path;",
|
|
133
|
-
"- for bugs, prefer a failing test or repro first; then make the minimal fix; then verify;",
|
|
134
|
-
"- high-risk changes need a short spec before coding: goal, scope, behavior, risks, verification;",
|
|
135
|
-
"- high-risk includes security, privacy, auth/authz, data/schema/migrations, public APIs, external integrations, payments, jobs, concurrency, and irreversible or cross-cutting changes;",
|
|
136
|
-
"- follow nearby conventions; preserve existing behavior unless explicitly changing it;",
|
|
137
|
-
"- handle edge cases, errors, cancellation, and async behavior;",
|
|
138
|
-
"- avoid blocking UI/event loops;",
|
|
139
|
-
"- avoid duplicate state, duplicate prompts, and repeated side effects.",
|
|
85
|
+
"Coding discipline (express only through tool choices, not prose):",
|
|
86
|
+
"- inspect before editing; do not invent APIs, files, commands, or behavior;",
|
|
87
|
+
"- make the smallest change that fully fixes the issue; follow nearby conventions;",
|
|
88
|
+
"- for bugs, prefer a failing repro first, then the minimal fix, then verify;",
|
|
89
|
+
"- high-risk changes (security, data/schema, public APIs, concurrency, irreversible) need a short spec first;",
|
|
90
|
+
"- handle edge cases, errors, cancellation, and async behavior; do not block UI/event loops;",
|
|
91
|
+
"- avoid duplicate state, duplicate prompts, and repeated side effects;",
|
|
92
|
+
"- write code, identifiers, comments, and commit messages in English.",
|
|
140
93
|
];
|
|
141
94
|
|
|
142
95
|
const LOOKUP_DISCIPLINE_LINES = [
|
|
@@ -159,7 +112,7 @@ const FINAL_DISCIPLINE_LINES = [
|
|
|
159
112
|
|
|
160
113
|
const SILENCE_REMINDER_TEXT = [
|
|
161
114
|
"GLM silence reminder: remain in WORKING state.",
|
|
162
|
-
"Continue with
|
|
115
|
+
"Continue with tool-only discipline: inspect, verify, and act through tools only.",
|
|
163
116
|
"For the next step, emit exactly one tool call and no assistant text.",
|
|
164
117
|
"Do not acknowledge this reminder.",
|
|
165
118
|
].join("\n");
|
|
@@ -174,6 +127,19 @@ const DISCIPLINE_PROMPT_BLOCK_PATTERN = new RegExp(
|
|
|
174
127
|
"g",
|
|
175
128
|
);
|
|
176
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Strips pi's built-in "Pi documentation" reference block from the system prompt.
|
|
132
|
+
* That block (≈10 lines listing docs/examples paths and "when asked about X" routing)
|
|
133
|
+
* is useless dead weight for non-pi work and dilutes attention to the trailing
|
|
134
|
+
* <available_skills> section, which especially hurts weaker models like GLM.
|
|
135
|
+
* Anchored on the fixed header/footer strings from buildSystemPrompt() so it only
|
|
136
|
+
* ever matches pi's own block regardless of resolved doc/example paths.
|
|
137
|
+
*/
|
|
138
|
+
const PI_DOCS_BLOCK_PATTERN = new RegExp(
|
|
139
|
+
`\\n+Pi documentation \\(read only when the user asks about pi itself[\\s\\S]*?tui\\.md for TUI API details\\)\\n+`,
|
|
140
|
+
"g",
|
|
141
|
+
);
|
|
142
|
+
|
|
177
143
|
const LOOKUP_SYSTEM_PROMPT = [
|
|
178
144
|
"You are a vision-capable lookup helper for a blind GLM coding agent.",
|
|
179
145
|
"Inspect the provided screenshots/images and answer the parent agent's focused question using concrete visual evidence.",
|
|
@@ -234,9 +200,78 @@ export default function codingDiscipline(pi: ExtensionAPI) {
|
|
|
234
200
|
|
|
235
201
|
pi.on("before_provider_request", async (event: { payload?: unknown }, ctx: unknown) => {
|
|
236
202
|
const modelRef = modelRefFromPayload(event.payload) ?? selectedModelRef ?? modelRefFromContext(ctx);
|
|
237
|
-
|
|
238
|
-
|
|
203
|
+
if (!isGlmModel(modelRef)) return undefined;
|
|
204
|
+
const injected = injectCodingDisciplineIntoPayload(event.payload, {
|
|
205
|
+
lookupEnabled: Boolean(lookupModelFromConfig(contextCwd(ctx))),
|
|
239
206
|
});
|
|
207
|
+
if (process.env.PI_DEBUG_PROMPT === "1") {
|
|
208
|
+
logFinalPrompt(injected, modelRef, contextCwd(ctx) ?? process.cwd());
|
|
209
|
+
}
|
|
210
|
+
return injected;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
pi.on("before_agent_start", async (event: { systemPromptOptions?: unknown; systemPrompt?: string }, ctx: unknown) => {
|
|
214
|
+
const debug = process.env.PI_DEBUG_PROMPT === "1";
|
|
215
|
+
const opts = event.systemPromptOptions as {
|
|
216
|
+
selectedTools?: unknown;
|
|
217
|
+
skills?: unknown[];
|
|
218
|
+
customPrompt?: unknown;
|
|
219
|
+
cwd?: string;
|
|
220
|
+
} | undefined;
|
|
221
|
+
const sys = typeof event.systemPrompt === "string" ? event.systemPrompt : "";
|
|
222
|
+
const toolsArr = Array.isArray(opts?.selectedTools) ? opts!.selectedTools as string[] : [];
|
|
223
|
+
const skillsCount = Array.isArray(opts?.skills) ? opts!.skills.length : -1;
|
|
224
|
+
const alreadyHasSkillsBlock = /<available_skills>/.test(sys);
|
|
225
|
+
|
|
226
|
+
// Inject <available_skills> when pi-core's gate failed to produce it.
|
|
227
|
+
// Core builds the block only when tools.includes("read") (lowercase), but
|
|
228
|
+
// Claude-alias registration exposes the tool as "Read" (PascalCase), so
|
|
229
|
+
// the gate returns false and skills never reach the prompt for any model.
|
|
230
|
+
// Guard: never re-inject if the block is already present (idempotent; safe
|
|
231
|
+
// once pi fixes the gate or aliases are removed).
|
|
232
|
+
let nextSystemPrompt = sys;
|
|
233
|
+
let injectedSkills = false;
|
|
234
|
+
if (!alreadyHasSkillsBlock) {
|
|
235
|
+
const skills = extractSkills(opts?.skills);
|
|
236
|
+
if (skills.length > 0) {
|
|
237
|
+
const block = buildAvailableSkillsBlock(skills);
|
|
238
|
+
if (block) {
|
|
239
|
+
nextSystemPrompt = sys ? `${sys}\n\n${block}` : block;
|
|
240
|
+
injectedSkills = true;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (debug) {
|
|
246
|
+
try {
|
|
247
|
+
const cwd = contextCwd(ctx) ?? process.cwd();
|
|
248
|
+
const dir = path.join(cwd, ".pi-debug-prompt");
|
|
249
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
250
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
251
|
+
fs.writeFileSync(
|
|
252
|
+
path.join(dir, `${stamp}__before_agent_start.txt`),
|
|
253
|
+
[
|
|
254
|
+
"# before_agent_start diagnostic",
|
|
255
|
+
`cwd: ${cwd}`,
|
|
256
|
+
`customPrompt present: ${typeof opts?.customPrompt === "string" && opts.customPrompt.length > 0}`,
|
|
257
|
+
`selectedTools (${toolsArr.length}): ${JSON.stringify(toolsArr)}`,
|
|
258
|
+
`hasRead (lowercase): ${toolsArr.includes("read")}`,
|
|
259
|
+
`skills.length: ${skillsCount}`,
|
|
260
|
+
`system had <available_skills> block (pre-inject): ${alreadyHasSkillsBlock}`,
|
|
261
|
+
`injected <available_skills>: ${injectedSkills}`,
|
|
262
|
+
`systemPrompt length: ${nextSystemPrompt.length}`,
|
|
263
|
+
].join("\n"),
|
|
264
|
+
"utf-8",
|
|
265
|
+
);
|
|
266
|
+
} catch {
|
|
267
|
+
// debug logging must never break agent flow
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (injectedSkills) {
|
|
272
|
+
return { systemPrompt: nextSystemPrompt };
|
|
273
|
+
}
|
|
274
|
+
return undefined;
|
|
240
275
|
});
|
|
241
276
|
|
|
242
277
|
pi.on("context", async (event: { messages?: unknown[] }, ctx: unknown) => {
|
|
@@ -263,6 +298,7 @@ export function prependCodingDisciplinePrompt(systemPrompt: string, options: { l
|
|
|
263
298
|
const deduped = systemPrompt
|
|
264
299
|
.replace(LEGACY_SILENT_PROMPT_BLOCK_PATTERN, "")
|
|
265
300
|
.replace(DISCIPLINE_PROMPT_BLOCK_PATTERN, "")
|
|
301
|
+
.replace(PI_DOCS_BLOCK_PATTERN, "\n\n")
|
|
266
302
|
.trimStart();
|
|
267
303
|
const prompt = buildCodingDisciplinePrompt(options);
|
|
268
304
|
return deduped ? `${prompt}\n\n${deduped}` : prompt;
|
|
@@ -305,6 +341,131 @@ export function injectCodingDisciplineIntoPayload(payload: unknown, options: { l
|
|
|
305
341
|
return payload;
|
|
306
342
|
}
|
|
307
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Write the final provider-bound system prompt + message roles to a file for debugging.
|
|
346
|
+
* Enabled by PI_DEBUG_PROMPT=1. Writes one file per request to <cwd>/.pi-debug-prompt/
|
|
347
|
+
* so successive requests don't overwrite each other.
|
|
348
|
+
*/
|
|
349
|
+
function logFinalPrompt(payload: unknown, modelRef: string | undefined, cwd: string): void {
|
|
350
|
+
try {
|
|
351
|
+
const systemPrompt = extractPayloadSystemPrompt(payload);
|
|
352
|
+
const messages = extractPayloadMessages(payload);
|
|
353
|
+
if (systemPrompt === undefined && messages.length === 0) return;
|
|
354
|
+
|
|
355
|
+
const dir = path.join(cwd, ".pi-debug-prompt");
|
|
356
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
357
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
358
|
+
const slug = (modelRef ?? "unknown").replace(/[^a-z0-9._-]+/gi, "-");
|
|
359
|
+
const file = path.join(dir, `${stamp}__${slug}.txt`);
|
|
360
|
+
|
|
361
|
+
const lines: string[] = [];
|
|
362
|
+
lines.push(`# PI_DEBUG_PROMPT dump`);
|
|
363
|
+
lines.push(`model: ${modelRef ?? "(unknown)"}`);
|
|
364
|
+
lines.push(`timestamp: ${stamp}`);
|
|
365
|
+
lines.push(`messages: ${messages.length}`);
|
|
366
|
+
lines.push("");
|
|
367
|
+
lines.push("=== SYSTEM PROMPT ===");
|
|
368
|
+
lines.push(systemPrompt ?? "(none found)");
|
|
369
|
+
lines.push("");
|
|
370
|
+
lines.push("=== MESSAGE ROLES (last 8) ===");
|
|
371
|
+
for (const m of messages.slice(-8)) {
|
|
372
|
+
const preview = truncate(m.preview, 200);
|
|
373
|
+
lines.push(`[${m.role}] ${preview}`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
fs.writeFileSync(file, lines.join("\n"), "utf-8");
|
|
377
|
+
} catch {
|
|
378
|
+
// Debug logging must never break a provider request.
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Extract loadable skills from systemPromptOptions.skills, filtering out
|
|
384
|
+
* disable-model-invocation ones (matching pi-core formatSkillsForPrompt behavior).
|
|
385
|
+
*/
|
|
386
|
+
function extractSkills(raw: unknown): { name: string; description: string; filePath?: string }[] {
|
|
387
|
+
if (!Array.isArray(raw)) return [];
|
|
388
|
+
const skills: { name: string; description: string; filePath?: string }[] = [];
|
|
389
|
+
for (const entry of raw) {
|
|
390
|
+
if (!isRecord(entry)) continue;
|
|
391
|
+
if (entry.disableModelInvocation === true) continue;
|
|
392
|
+
const name = typeof entry.name === "string" ? entry.name : "";
|
|
393
|
+
const description = typeof entry.description === "string" ? entry.description : "";
|
|
394
|
+
if (!name || !description) continue;
|
|
395
|
+
const filePath = typeof entry.filePath === "string" ? entry.filePath : undefined;
|
|
396
|
+
skills.push({ name, description, filePath });
|
|
397
|
+
}
|
|
398
|
+
return skills;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Build the <available_skills> XML block in pi-core's exact format
|
|
403
|
+
* (see packages/coding-agent/src/core/skills.ts::formatSkillsForPrompt),
|
|
404
|
+
* so models trained on the Agent Skills standard see identical structure.
|
|
405
|
+
*/
|
|
406
|
+
function buildAvailableSkillsBlock(skills: { name: string; description: string; filePath?: string }[]): string | undefined {
|
|
407
|
+
const loadable = skills.filter((s) => s.filePath);
|
|
408
|
+
if (loadable.length === 0) return undefined;
|
|
409
|
+
const lines = [
|
|
410
|
+
"The following skills provide specialized instructions for specific tasks.",
|
|
411
|
+
"Use the read tool to load a skill's file when the task matches its description.",
|
|
412
|
+
"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
|
|
413
|
+
"",
|
|
414
|
+
"<available_skills>",
|
|
415
|
+
];
|
|
416
|
+
for (const skill of loadable) {
|
|
417
|
+
lines.push(" <skill>");
|
|
418
|
+
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
419
|
+
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
420
|
+
lines.push(` <location>${escapeXml(skill.filePath ?? "")}</location>`);
|
|
421
|
+
lines.push(" </skill>");
|
|
422
|
+
}
|
|
423
|
+
lines.push("</available_skills>");
|
|
424
|
+
return lines.join("\n");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function escapeXml(str: string): string {
|
|
428
|
+
return str
|
|
429
|
+
.replace(/&/g, "&")
|
|
430
|
+
.replace(/</g, "<")
|
|
431
|
+
.replace(/>/g, ">")
|
|
432
|
+
.replace(/"/g, """)
|
|
433
|
+
.replace(/'/g, "'");
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function extractPayloadSystemPrompt(payload: unknown): string | undefined {
|
|
437
|
+
if (!isRecord(payload)) return undefined;
|
|
438
|
+
if (typeof payload.instructions === "string") return payload.instructions;
|
|
439
|
+
if (typeof payload.system === "string") return payload.system;
|
|
440
|
+
for (const field of ["messages", "input"] as const) {
|
|
441
|
+
const list = payload[field];
|
|
442
|
+
if (!Array.isArray(list)) continue;
|
|
443
|
+
for (const message of list) {
|
|
444
|
+
if (!isRecord(message)) continue;
|
|
445
|
+
if (message.role !== "system" && message.role !== "developer") continue;
|
|
446
|
+
const text = contentToText(message.content);
|
|
447
|
+
if (text) return text;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function extractPayloadMessages(payload: unknown): { role: string; preview: string }[] {
|
|
454
|
+
if (!isRecord(payload)) return [];
|
|
455
|
+
for (const field of ["messages", "input"] as const) {
|
|
456
|
+
const list = payload[field];
|
|
457
|
+
if (Array.isArray(list)) {
|
|
458
|
+
return list.map((message) => {
|
|
459
|
+
if (!isRecord(message)) return { role: "?", preview: "" };
|
|
460
|
+
const role = typeof message.role === "string" ? message.role : "?";
|
|
461
|
+
const preview = contentToText(message.content);
|
|
462
|
+
return { role, preview };
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return [];
|
|
467
|
+
}
|
|
468
|
+
|
|
308
469
|
function createLookupTool() {
|
|
309
470
|
return {
|
|
310
471
|
name: LOOKUP_TOOL_NAME,
|
|
@@ -57,6 +57,20 @@ export const DEFAULT_PI_TOOLS_SUITE_CONFIG_JSONC = String.raw`{
|
|
|
57
57
|
"maxContextPercent": "30%"
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
|
+
// glm-5.2 reports a ~1M-token window. Even zai/* 16%/30% = 160K/300K is
|
|
61
|
+
// above the ~15% (~150K) point where long sessions degrade, and an
|
|
62
|
+
// observed 14h/273K-token session never crossed 16%. Lower ONLY glm-5.2
|
|
63
|
+
// within the zai family: 8%/15% (~80K/150K) so nudging starts early and
|
|
64
|
+
// auto-compress fires at the observed degradation point. Other zai/*
|
|
65
|
+
// models keep 16%/30%.
|
|
66
|
+
"zai/glm-5.2": {
|
|
67
|
+
"compress": {
|
|
68
|
+
"minContextPercent": "8%",
|
|
69
|
+
"maxContextPercent": "15%",
|
|
70
|
+
"autoCandidates": { "minContextPercent": 0.08 },
|
|
71
|
+
"messageMode": { "minContextPercent": 0.08 }
|
|
72
|
+
}
|
|
73
|
+
},
|
|
60
74
|
"antigravity/*sonnet*": {
|
|
61
75
|
"compress": {
|
|
62
76
|
"minContextPercent": "22%",
|
|
@@ -25,6 +25,7 @@ const MODULES: Array<{ name: string; load: () => Promise<ExtensionModule> }> = [
|
|
|
25
25
|
{ name: "web-search", load: () => import("./web-search/index") },
|
|
26
26
|
{ name: "dcp", load: () => import("./dcp/index") },
|
|
27
27
|
{ name: "prompt-commands", load: () => import("./prompt-commands/index") },
|
|
28
|
+
{ name: "skill-installer", load: () => import("./skill-installer/index") },
|
|
28
29
|
{ name: "telegram-mirror", load: () => import("./telegram-mirror/index") },
|
|
29
30
|
];
|
|
30
31
|
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, join, relative } from "node:path";
|
|
4
|
+
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
import { ignoreStaleExtensionContextError } from "../context-usage.js";
|
|
7
|
+
|
|
8
|
+
const INSTALL_COMMAND = "install-skill";
|
|
9
|
+
const EXPORT_COMMAND = "export-skill";
|
|
10
|
+
const LOCAL_SKILLS_DIR = join(homedir(), ".agents", "local_skills");
|
|
11
|
+
const SKILL_FILE = "SKILL.md";
|
|
12
|
+
const PROJECT_DIR = ".pi";
|
|
13
|
+
const PROJECT_SKILLS_SUBDIR = "skills";
|
|
14
|
+
const SKIP_NAMES = new Set([".DS_Store"]);
|
|
15
|
+
const DESC_MAX = 90;
|
|
16
|
+
|
|
17
|
+
type SkillEntry = {
|
|
18
|
+
name: string;
|
|
19
|
+
path: string;
|
|
20
|
+
description: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Direction-aware transfer plan shared by install and export. */
|
|
24
|
+
type Transfer = {
|
|
25
|
+
srcDir: string;
|
|
26
|
+
destDir: string;
|
|
27
|
+
/** Human label for the destination root (with trailing slash); name is appended per skill. */
|
|
28
|
+
destRootLabel: string;
|
|
29
|
+
commandName: string;
|
|
30
|
+
direction: "install" | "export";
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function localSkillsLabel(): string {
|
|
34
|
+
const home = homedir();
|
|
35
|
+
return LOCAL_SKILLS_DIR === home ? LOCAL_SKILLS_DIR : LOCAL_SKILLS_DIR.replace(home, "~");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function projectSkillsDir(ctx: ExtensionContext): string {
|
|
39
|
+
return join(ctx.cwd, PROJECT_DIR, PROJECT_SKILLS_SUBDIR);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function projectSkillsLabel(ctx: ExtensionContext): string {
|
|
43
|
+
return displayPath(ctx, projectSkillsDir(ctx));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function displayPath(ctx: ExtensionContext, absPath: string): string {
|
|
47
|
+
const rel = relative(ctx.cwd, absPath);
|
|
48
|
+
return rel && !rel.startsWith("..") ? rel : absPath;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function notify(ctx: ExtensionContext, message: string, type: "info" | "warning" | "error" = "info"): void {
|
|
52
|
+
if (ctx.hasUI) ctx.ui.notify(message, type);
|
|
53
|
+
else console.log(message);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function truncate(value: string, maxLength: number): string {
|
|
57
|
+
const collapsed = value.replace(/\s+/g, " ").trim();
|
|
58
|
+
return collapsed.length <= maxLength ? collapsed : `${collapsed.slice(0, Math.max(0, maxLength - 1))}…`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Extract the `description` scalar from SKILL.md YAML frontmatter. Returns "" when absent/unparseable. */
|
|
62
|
+
function parseFrontmatterDescription(content: string): string {
|
|
63
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
64
|
+
if (!match) return "";
|
|
65
|
+
const front = match[1] ?? "";
|
|
66
|
+
const lines = front.split(/\r?\n/);
|
|
67
|
+
const idx = lines.findIndex((line) => /^\s*description:\s*/.test(line));
|
|
68
|
+
if (idx === -1) return "";
|
|
69
|
+
const headerLine = lines[idx];
|
|
70
|
+
if (!headerLine) return "";
|
|
71
|
+
const after = headerLine.replace(/^\s*description:\s*/, "");
|
|
72
|
+
|
|
73
|
+
// Folded/literal block scalar (>, >-, |, |-)
|
|
74
|
+
if (/^[>|]/.test(after)) {
|
|
75
|
+
const block: string[] = [];
|
|
76
|
+
for (let i = idx + 1; i < lines.length; i += 1) {
|
|
77
|
+
const line = lines[i] ?? "";
|
|
78
|
+
if (line === "") {
|
|
79
|
+
block.push(" ");
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (/^\s+/.test(line)) {
|
|
83
|
+
block.push(line.replace(/^\s+/, ""));
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
return block.join(" ").trim();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Plain or quoted scalar
|
|
92
|
+
let value = after.trim();
|
|
93
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
94
|
+
value = value.slice(1, -1);
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function readSkillDescription(skillDir: string): Promise<string> {
|
|
100
|
+
try {
|
|
101
|
+
const content = await fs.readFile(join(skillDir, SKILL_FILE), "utf-8");
|
|
102
|
+
return parseFrontmatterDescription(content);
|
|
103
|
+
} catch {
|
|
104
|
+
return "";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function pathExists(path: string): Promise<boolean> {
|
|
109
|
+
try {
|
|
110
|
+
await fs.access(path);
|
|
111
|
+
return true;
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Scan a skills directory: every subdirectory that contains SKILL.md. Returns ENOENT as empty. */
|
|
118
|
+
async function scanSkills(dir: string): Promise<SkillEntry[]> {
|
|
119
|
+
let entries: import("node:fs").Dirent[];
|
|
120
|
+
try {
|
|
121
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const skills: SkillEntry[] = [];
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
if (!entry.isDirectory() || SKIP_NAMES.has(entry.name)) continue;
|
|
130
|
+
const skillPath = join(dir, entry.name);
|
|
131
|
+
if (!(await pathExists(join(skillPath, SKILL_FILE)))) continue;
|
|
132
|
+
skills.push({ name: entry.name, path: skillPath, description: await readSkillDescription(skillPath) });
|
|
133
|
+
}
|
|
134
|
+
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function listLocalSkills(): Promise<SkillEntry[]> {
|
|
138
|
+
return scanSkills(LOCAL_SKILLS_DIR);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function listProjectSkills(ctx: ExtensionContext): Promise<SkillEntry[]> {
|
|
142
|
+
return scanSkills(projectSkillsDir(ctx));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function skillLabel(skill: SkillEntry): string {
|
|
146
|
+
const description = skill.description.replace(/\s+/g, " ").trim();
|
|
147
|
+
return description ? `${skill.name} — ${truncate(description, DESC_MAX)}` : skill.name;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function copySkill(source: string, dest: string): Promise<void> {
|
|
151
|
+
await fs.mkdir(join(dest, ".."), { recursive: true });
|
|
152
|
+
await fs.cp(source, dest, {
|
|
153
|
+
recursive: true,
|
|
154
|
+
filter: (src) => !SKIP_NAMES.has(basename(src)),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Copy a skill folder from srcDir/name → destDir/name with overwrite-confirm (UI) / refuse (headless). */
|
|
159
|
+
async function transferSkill(ctx: ExtensionCommandContext, name: string, t: Transfer): Promise<void> {
|
|
160
|
+
const src = join(t.srcDir, name);
|
|
161
|
+
const dest = join(t.destDir, name);
|
|
162
|
+
|
|
163
|
+
if (await pathExists(dest)) {
|
|
164
|
+
if (!ctx.hasUI) {
|
|
165
|
+
notify(ctx, `Skill "${name}" already exists at ${t.destRootLabel}${name}. Remove it first or run /${t.commandName} ${name} interactively to overwrite.`, "error");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
let confirmed = false;
|
|
169
|
+
try {
|
|
170
|
+
confirmed = await ctx.ui.confirm("Skill already exists", `"${name}" already exists in ${t.destRootLabel}. Overwrite it?`);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
ignoreStaleExtensionContextError(error);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (!confirmed) {
|
|
176
|
+
notify(ctx, `Cancelled. "${name}" left unchanged.`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
await fs.rm(dest, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
await copySkill(src, dest);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
186
|
+
notify(ctx, `Failed to ${t.direction} skill "${name}": ${message}`, "error");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (t.direction === "install") {
|
|
191
|
+
notify(ctx, `Installed skill "${name}" → ${t.destRootLabel}${name}. Reloading to activate…`);
|
|
192
|
+
try {
|
|
193
|
+
await ctx.reload();
|
|
194
|
+
} catch (error) {
|
|
195
|
+
ignoreStaleExtensionContextError(error);
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
notify(ctx, `Exported skill "${name}" → ${t.destRootLabel}${name}. Available for /install-skill in other projects.`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function installTransfer(ctx: ExtensionCommandContext): Transfer {
|
|
204
|
+
return {
|
|
205
|
+
srcDir: LOCAL_SKILLS_DIR,
|
|
206
|
+
destDir: projectSkillsDir(ctx),
|
|
207
|
+
destRootLabel: `${projectSkillsLabel(ctx)}/`,
|
|
208
|
+
commandName: INSTALL_COMMAND,
|
|
209
|
+
direction: "install",
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function exportTransfer(ctx: ExtensionCommandContext): Transfer {
|
|
214
|
+
return {
|
|
215
|
+
srcDir: projectSkillsDir(ctx),
|
|
216
|
+
destDir: LOCAL_SKILLS_DIR,
|
|
217
|
+
destRootLabel: `${localSkillsLabel()}/`,
|
|
218
|
+
commandName: EXPORT_COMMAND,
|
|
219
|
+
direction: "export",
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// --- install ---
|
|
224
|
+
|
|
225
|
+
async function showInstallMenu(ctx: ExtensionCommandContext): Promise<void> {
|
|
226
|
+
const skills = await listLocalSkills();
|
|
227
|
+
if (skills.length === 0) {
|
|
228
|
+
notify(ctx, `No skills found in ${localSkillsLabel()} (each skill is a folder containing ${SKILL_FILE}).`, "warning");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!ctx.hasUI) {
|
|
233
|
+
notify(ctx, `Available skills in ${localSkillsLabel()}:\n${skills.map((s) => s.name).join("\n")}\n\nUse /${INSTALL_COMMAND} <name> to install one.`, "warning");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const t = installTransfer(ctx);
|
|
238
|
+
const labels = skills.map(skillLabel);
|
|
239
|
+
const labelToName = new Map(labels.map((label, index) => [label, skills[index]!.name]));
|
|
240
|
+
let selected: string | undefined;
|
|
241
|
+
try {
|
|
242
|
+
selected = await ctx.ui.select(`Install skill into ${projectSkillsLabel(ctx)}/ (${localSkillsLabel()})`, labels);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
ignoreStaleExtensionContextError(error);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (!selected) return;
|
|
248
|
+
|
|
249
|
+
const name = labelToName.get(selected);
|
|
250
|
+
if (!name) return;
|
|
251
|
+
await transferSkill(ctx, name, t);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function installByName(ctx: ExtensionCommandContext, name: string): Promise<void> {
|
|
255
|
+
const skills = await listLocalSkills();
|
|
256
|
+
const match = skills.find((entry) => entry.name === name);
|
|
257
|
+
if (!match) {
|
|
258
|
+
const available = skills.length > 0 ? skills.map((s) => s.name).join(", ") : "(none)";
|
|
259
|
+
notify(ctx, `No skill named "${name}" in ${localSkillsLabel()}. Available: ${available}`, "error");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
await transferSkill(ctx, match.name, installTransfer(ctx));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// --- export ---
|
|
266
|
+
|
|
267
|
+
async function showExportMenu(ctx: ExtensionCommandContext): Promise<void> {
|
|
268
|
+
const skills = await listProjectSkills(ctx);
|
|
269
|
+
if (skills.length === 0) {
|
|
270
|
+
notify(ctx, `No skills found in ${projectSkillsLabel(ctx)}/. Install one first with /${INSTALL_COMMAND}.`, "warning");
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!ctx.hasUI) {
|
|
275
|
+
notify(ctx, `Installed skills in ${projectSkillsLabel(ctx)}/:\n${skills.map((s) => s.name).join("\n")}\n\nUse /${EXPORT_COMMAND} <name> to export one to ${localSkillsLabel()}.`, "warning");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const t = exportTransfer(ctx);
|
|
280
|
+
const labels = skills.map(skillLabel);
|
|
281
|
+
const labelToName = new Map(labels.map((label, index) => [label, skills[index]!.name]));
|
|
282
|
+
let selected: string | undefined;
|
|
283
|
+
try {
|
|
284
|
+
selected = await ctx.ui.select(`Export skill from ${projectSkillsLabel(ctx)}/ to ${localSkillsLabel()}`, labels);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
ignoreStaleExtensionContextError(error);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (!selected) return;
|
|
290
|
+
|
|
291
|
+
const name = labelToName.get(selected);
|
|
292
|
+
if (!name) return;
|
|
293
|
+
await transferSkill(ctx, name, t);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function exportByName(ctx: ExtensionCommandContext, name: string): Promise<void> {
|
|
297
|
+
const skills = await listProjectSkills(ctx);
|
|
298
|
+
const match = skills.find((entry) => entry.name === name);
|
|
299
|
+
if (!match) {
|
|
300
|
+
const available = skills.length > 0 ? skills.map((s) => s.name).join(", ") : "(none)";
|
|
301
|
+
notify(ctx, `No skill named "${name}" in ${projectSkillsLabel(ctx)}/. Installed: ${available}`, "error");
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
await transferSkill(ctx, match.name, exportTransfer(ctx));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export default function skillInstaller(pi: ExtensionAPI): void {
|
|
308
|
+
pi.registerCommand(INSTALL_COMMAND, {
|
|
309
|
+
description: `Install a skill from ${localSkillsLabel()} into the current project's ${PROJECT_DIR}/${PROJECT_SKILLS_SUBDIR}`,
|
|
310
|
+
handler: async (args: string, ctx) => {
|
|
311
|
+
const name = args.trim();
|
|
312
|
+
try {
|
|
313
|
+
if (name) await installByName(ctx, name);
|
|
314
|
+
else await showInstallMenu(ctx);
|
|
315
|
+
} catch (error) {
|
|
316
|
+
ignoreStaleExtensionContextError(error);
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
pi.registerCommand(EXPORT_COMMAND, {
|
|
322
|
+
description: `Export a skill from the current project's ${PROJECT_DIR}/${PROJECT_SKILLS_SUBDIR} to ${localSkillsLabel()} for reuse in other projects`,
|
|
323
|
+
handler: async (args: string, ctx) => {
|
|
324
|
+
const name = args.trim();
|
|
325
|
+
try {
|
|
326
|
+
if (name) await exportByName(ctx, name);
|
|
327
|
+
else await showExportMenu(ctx);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
ignoreStaleExtensionContextError(error);
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-ui-extend",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.44",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"sync:pi-tools-suite:check": "node scripts/sync-pi-tools-suite.mjs --check",
|
|
52
52
|
"sync:sdk-pin": "node scripts/sync-sdk-pin.mjs",
|
|
53
53
|
"sync:sdk-pin:check": "node scripts/sync-sdk-pin.mjs --check",
|
|
54
|
-
"test:coverage": "mise exec node@24.16.0 -- node --import tsx --test --experimental-test-coverage --test-coverage-include=src/**/*.ts --test-coverage-exclude=src/main.ts --test-coverage-lines=
|
|
54
|
+
"test:coverage": "mise exec node@24.16.0 -- node --import tsx --test --experimental-test-coverage --test-coverage-include=src/**/*.ts --test-coverage-exclude=src/main.ts --test-coverage-lines=94 --test-coverage-branches=80 --test-coverage-functions=94 \"tests/**/*.test.ts\"",
|
|
55
55
|
"update-sdk-references": "mise exec node@24.16.0 -- node .pi/skills/pi-sdk/scripts/update-references.mjs",
|
|
56
56
|
"prepack": "npm run generate-schemas --silent",
|
|
57
57
|
"pack:dry-run": "npm pack --dry-run",
|
|
@@ -64,9 +64,9 @@
|
|
|
64
64
|
"prepublishOnly": "npm run check && npm run build:pix && npm run generate-schemas"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@earendil-works/pi-ai": "0.79.
|
|
68
|
-
"@earendil-works/pi-coding-agent": "0.79.
|
|
69
|
-
"@earendil-works/pi-tui": "0.79.
|
|
67
|
+
"@earendil-works/pi-ai": "0.79.9",
|
|
68
|
+
"@earendil-works/pi-coding-agent": "0.79.9",
|
|
69
|
+
"@earendil-works/pi-tui": "0.79.9",
|
|
70
70
|
"@mariozechner/clipboard": "^0.3.9",
|
|
71
71
|
"jsonc-parser": "3.3.1",
|
|
72
72
|
"typebox": "1.1.38",
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: skill-creator
|
|
3
|
-
description:
|
|
3
|
+
description: >-
|
|
4
|
+
Author, edit, and improve pi Agent-Skills. Use whenever asked to create, make,
|
|
5
|
+
or generate a new skill (RU: «создать / сгенерить скилл», «работать со
|
|
6
|
+
скиллами»), edit or rewrite an existing SKILL.md, write or fix YAML frontmatter
|
|
7
|
+
(name and description), tune a skill's trigger for reliable auto-loading without
|
|
8
|
+
bloating its description, fix validation warnings, or run skill evals and
|
|
9
|
+
benchmarks. Covers .pi/skills/ and skills/ authoring, progressive disclosure,
|
|
10
|
+
and the eval/improve loop.
|
|
4
11
|
---
|
|
5
12
|
|
|
6
13
|
# Skill Creator
|
|
Binary file
|
|
Binary file
|
|
@@ -145,15 +145,19 @@ def run_single_query(
|
|
|
145
145
|
ame = event.get("assistantMessageEvent", {})
|
|
146
146
|
if ame.get("type") == "toolcall_end":
|
|
147
147
|
tool_call = ame.get("toolCall", {})
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
# Tool name arrives capitalized ("Read"), not lowercase.
|
|
149
|
+
if (tool_call.get("name") or "").lower() == "read":
|
|
150
|
+
args = tool_call.get("arguments") or {}
|
|
151
|
+
# Read tool's real arg is `file_path`.
|
|
152
|
+
path = args.get("file_path", "") or args.get("path", "")
|
|
150
153
|
if _targets_skill(path):
|
|
151
154
|
return True
|
|
152
155
|
|
|
153
156
|
# Tool actually started executing — redundant but robust.
|
|
154
157
|
elif etype == "tool_execution_start":
|
|
155
|
-
if event.get("toolName") == "read":
|
|
156
|
-
|
|
158
|
+
if (event.get("toolName") or "").lower() == "read":
|
|
159
|
+
args = event.get("args") or {}
|
|
160
|
+
path = args.get("file_path", "") or args.get("path", "")
|
|
157
161
|
if _targets_skill(path):
|
|
158
162
|
return True
|
|
159
163
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: spec-lite
|
|
3
|
+
description: Use this skill for creating, updating, or checking lightweight specs for existing code, risky changes, public behavior, bug fixes, or spec-code drift. Use it before or during non-trivial changes that touch security, data, persistence, public APIs, CLI/UI contracts, integrations, concurrency, architecture, or user-visible behavior with edge cases. Do not use it for trivial local edits.
|
|
4
|
+
argument-hint: "behavior, change, bug, or spec path to document/check"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Spec Lite
|
|
8
|
+
|
|
9
|
+
Use the lightest spec process that preserves correctness. The goal is to make behavior and risk explicit without turning every change into a heavyweight design process.
|
|
10
|
+
|
|
11
|
+
## Core rules
|
|
12
|
+
|
|
13
|
+
- Do not change production code unless the user explicitly asks for implementation.
|
|
14
|
+
- Specs describe either current behavior or intended behavior; label which one clearly.
|
|
15
|
+
- Do not invent behavior. If evidence is missing or ambiguous, mark uncertainty explicitly.
|
|
16
|
+
- Prefer compact specs over exhaustive documents.
|
|
17
|
+
- Load only relevant files, tests, schemas, docs, migrations, entrypoints, and existing specs.
|
|
18
|
+
- Do not load all specs by default.
|
|
19
|
+
- Keep the spec close to the codebase convention when a specs directory, ADR folder, or feature-doc pattern already exists.
|
|
20
|
+
|
|
21
|
+
## When to create or update a spec
|
|
22
|
+
|
|
23
|
+
Create or update a lightweight spec when the work affects any of these areas:
|
|
24
|
+
|
|
25
|
+
- security, privacy, auth, or permissions;
|
|
26
|
+
- data, schemas, migrations, persistence, or file formats;
|
|
27
|
+
- public APIs, CLI behavior, UI contracts, events, or other external contracts;
|
|
28
|
+
- external integrations;
|
|
29
|
+
- payments, destructive operations, or irreversible workflows;
|
|
30
|
+
- background jobs, concurrency, cancellation, retries, or async behavior;
|
|
31
|
+
- cross-cutting architecture;
|
|
32
|
+
- user-visible behavior with non-trivial edge cases;
|
|
33
|
+
- bug fixes where expected behavior, compatibility, or regression risk is unclear;
|
|
34
|
+
- suspected drift between existing specs/docs/tests and code.
|
|
35
|
+
|
|
36
|
+
Do not require a spec for trivial local edits, mechanical renames, formatting-only changes, or obvious one-file fixes with no broader behavior impact.
|
|
37
|
+
|
|
38
|
+
## As-is spec workflow
|
|
39
|
+
|
|
40
|
+
Use this workflow for existing code or spec-code drift checks:
|
|
41
|
+
|
|
42
|
+
1. Identify the narrow behavior or contract to document.
|
|
43
|
+
2. Inspect relevant code, tests, docs, schemas, migrations, and entrypoints.
|
|
44
|
+
3. Identify current behavior and known gaps.
|
|
45
|
+
4. Mark claims using evidence labels:
|
|
46
|
+
- confirmed by code;
|
|
47
|
+
- confirmed by tests;
|
|
48
|
+
- confirmed by docs;
|
|
49
|
+
- inferred;
|
|
50
|
+
- unknown.
|
|
51
|
+
5. Create or update a compact as-is spec.
|
|
52
|
+
6. If drift is found, report it separately from confirmed behavior.
|
|
53
|
+
|
|
54
|
+
## Change spec workflow
|
|
55
|
+
|
|
56
|
+
Use this workflow before risky implementation:
|
|
57
|
+
|
|
58
|
+
1. Define the goal.
|
|
59
|
+
2. Define scope and non-goals.
|
|
60
|
+
3. State expected behavior.
|
|
61
|
+
4. List affected contracts.
|
|
62
|
+
5. List risks, compatibility concerns, and migration concerns if any.
|
|
63
|
+
6. Define a verification path.
|
|
64
|
+
7. Then implement only the scoped change, if implementation was requested.
|
|
65
|
+
|
|
66
|
+
## Spec template
|
|
67
|
+
|
|
68
|
+
Use this template by default. Remove sections that are genuinely irrelevant, but keep `Type`, `Goal`, `Behavior`, `Related files`, `Verification`, and `Evidence` when possible.
|
|
69
|
+
|
|
70
|
+
```markdown
|
|
71
|
+
# Spec: <name>
|
|
72
|
+
|
|
73
|
+
## Type
|
|
74
|
+
|
|
75
|
+
As-is | Change
|
|
76
|
+
|
|
77
|
+
## Goal
|
|
78
|
+
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
## Scope
|
|
82
|
+
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
## Non-goals
|
|
86
|
+
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
## Behavior
|
|
90
|
+
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
## Contracts
|
|
94
|
+
|
|
95
|
+
Inputs, outputs, APIs, CLI, UI, events, files, schemas.
|
|
96
|
+
|
|
97
|
+
## Invariants
|
|
98
|
+
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
## Edge cases
|
|
102
|
+
|
|
103
|
+
...
|
|
104
|
+
|
|
105
|
+
## Side effects
|
|
106
|
+
|
|
107
|
+
...
|
|
108
|
+
|
|
109
|
+
## Related files
|
|
110
|
+
|
|
111
|
+
...
|
|
112
|
+
|
|
113
|
+
## Verification
|
|
114
|
+
|
|
115
|
+
...
|
|
116
|
+
|
|
117
|
+
## Risks / unknowns
|
|
118
|
+
|
|
119
|
+
...
|
|
120
|
+
|
|
121
|
+
## Evidence
|
|
122
|
+
|
|
123
|
+
- Confirmed by code:
|
|
124
|
+
- Confirmed by tests:
|
|
125
|
+
- Confirmed by docs:
|
|
126
|
+
- Inferred:
|
|
127
|
+
- Unknown:
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Output guidance
|
|
131
|
+
|
|
132
|
+
When the user asks only for a spec, write or update the spec and summarize:
|
|
133
|
+
|
|
134
|
+
- spec path;
|
|
135
|
+
- whether it is as-is or change-oriented;
|
|
136
|
+
- strongest evidence sources;
|
|
137
|
+
- important unknowns or drift;
|
|
138
|
+
- whether production code was left untouched.
|
|
139
|
+
|
|
140
|
+
When the user asks for implementation too, keep the spec scoped and practical, then implement only after the expected behavior and verification path are clear.
|