@xynogen/pix-core 0.1.0 → 0.1.2
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/package.json +50 -40
- package/src/commands/agent-sop/agent-sop.ts +58 -0
- package/src/commands/clear/clear.ts +1 -1
- package/src/commands/copy-all/copy-all.ts +21 -6
- package/src/commands/diff/diff.ts +45 -13
- package/src/commands/models/models.test.ts +2 -2
- package/src/commands/models/models.ts +1 -1
- package/src/commands/update/update.test.ts +8 -8
- package/src/commands/update/update.ts +4 -4
- package/src/index.ts +11 -9
- package/src/lib/data.ts +1 -1
- package/src/nudge/capability.test.ts +19 -13
- package/src/nudge/capability.ts +17 -9
- package/src/nudge/index.ts +1 -1
- package/src/nudge/tools.test.ts +21 -9
- package/src/nudge/tools.ts +0 -2
- package/src/tool/ask/ask.test.ts +31 -20
- package/src/tool/ask/components.ts +55 -0
- package/src/tool/ask/helpers.ts +77 -0
- package/src/tool/ask/index.ts +130 -0
- package/src/tool/ask/questionnaire.ts +693 -0
- package/src/tool/ask/rpc.ts +84 -0
- package/src/tool/ask/schema.ts +69 -0
- package/src/tool/ask/single-select-layout.test.ts +21 -5
- package/src/tool/ask/single-select-layout.ts +48 -14
- package/src/tool/ask/types.ts +17 -0
- package/src/tool/todo/todo.ts +24 -37
- package/src/tool/toolbox/toolbox.test.ts +2 -2
- package/src/tool/toolbox/toolbox.ts +9 -2
- package/src/ui/diagnostics.ts +3 -6
- package/src/ui/footer.ts +3 -4
- package/src/ui/welcome.test.ts +6 -6
- package/src/ui/welcome.ts +5 -2
- package/src/tool/ask/ask.ts +0 -1081
package/package.json
CHANGED
|
@@ -1,42 +1,52 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
2
|
+
"name": "@xynogen/pix-core",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Pi extension — core UI/UX bundle (welcome banner, footer, model picker, self-update)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "bun test"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"skills",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"pi": {
|
|
17
|
+
"extensions": [
|
|
18
|
+
"src/index.ts"
|
|
19
|
+
],
|
|
20
|
+
"skills": [
|
|
21
|
+
"./skills"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"pi",
|
|
26
|
+
"pi-package",
|
|
27
|
+
"pi-extension",
|
|
28
|
+
"pix"
|
|
29
|
+
],
|
|
30
|
+
"author": "xynogen",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/xynogen/pix-mono.git",
|
|
35
|
+
"directory": "packages/pix-core"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/xynogen/pix-mono/tree/main/packages/pix-core#readme",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/xynogen/pix-mono/issues"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@xynogen/pix-skills": "^0.1.1",
|
|
46
|
+
"typebox": "^1.1.38"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
50
|
+
"@earendil-works/pi-tui": "*"
|
|
51
|
+
}
|
|
42
52
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-sop — inject AGENT.md (from pix-skills) into system prompt
|
|
3
|
+
*
|
|
4
|
+
* Reads AGENT.md from the @xynogen/pix-skills package and appends it to the
|
|
5
|
+
* system prompt on every agent start via `before_agent_start`.
|
|
6
|
+
*
|
|
7
|
+
* This is the "register skill" mechanism for the agent operating spec — no
|
|
8
|
+
* static SKILL.md file needed. The content becomes part of the model's
|
|
9
|
+
* standing instructions.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
import { resolve } from "node:path";
|
|
15
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
|
|
17
|
+
/** Resolve the absolute path to AGENT.md inside @xynogen/pix-skills. */
|
|
18
|
+
function resolveAgentMdPath(): string | null {
|
|
19
|
+
try {
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const pkgJson = require.resolve("@xynogen/pix-skills/package.json");
|
|
22
|
+
return resolve(pkgJson, "..", "AGENT.md");
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Read and return AGENT.md content, or null if unavailable. */
|
|
29
|
+
function loadAgentMd(): string | null {
|
|
30
|
+
const p = resolveAgentMdPath();
|
|
31
|
+
if (!p || !existsSync(p)) return null;
|
|
32
|
+
try {
|
|
33
|
+
return readFileSync(p, "utf-8");
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default function registerAgentSop(pi: ExtensionAPI): void {
|
|
40
|
+
// Load once at startup (content is static per session).
|
|
41
|
+
const agentMdContent = loadAgentMd();
|
|
42
|
+
|
|
43
|
+
if (!agentMdContent) {
|
|
44
|
+
// Silent skip — pix-skills might not be installed.
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pi.on("before_agent_start", async (event) => {
|
|
49
|
+
// Skip if already injected (idempotent check via a simple marker).
|
|
50
|
+
if (event.systemPrompt.includes("pix-agent-sop")) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
systemPrompt: `${event.systemPrompt}\n\n<pix-agent-sop>\n${agentMdContent}\n</pix-agent-sop>`,
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -12,7 +12,7 @@ async function clearCache(pi: ExtensionAPI, ctx: ExtensionCommandContext) {
|
|
|
12
12
|
.filter(Boolean)
|
|
13
13
|
.join("\n")
|
|
14
14
|
.trim();
|
|
15
|
-
if ((result.
|
|
15
|
+
if ((result.code ?? 0) !== 0) {
|
|
16
16
|
ctx.ui.notify(`Cache clear failed. ${output || "No output."}`, "error");
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
@@ -16,7 +16,12 @@ function textFromContent(content: unknown): string {
|
|
|
16
16
|
return content
|
|
17
17
|
.map((block) => {
|
|
18
18
|
if (!block || typeof block !== "object" || !("type" in block)) return "";
|
|
19
|
-
if (
|
|
19
|
+
if (
|
|
20
|
+
block.type === "text" &&
|
|
21
|
+
"text" in block &&
|
|
22
|
+
typeof block.text === "string"
|
|
23
|
+
)
|
|
24
|
+
return block.text;
|
|
20
25
|
if (block.type === "image") return "[image]";
|
|
21
26
|
return "";
|
|
22
27
|
})
|
|
@@ -43,11 +48,14 @@ function copyToClipboard(text: string): Promise<void> {
|
|
|
43
48
|
}
|
|
44
49
|
const child = spawn(c.cmd, c.args);
|
|
45
50
|
let stderr = "";
|
|
46
|
-
child.stderr.on("data", (chunk) => {
|
|
51
|
+
child.stderr.on("data", (chunk) => {
|
|
52
|
+
stderr += String(chunk);
|
|
53
|
+
});
|
|
47
54
|
child.on("error", reject);
|
|
48
55
|
child.on("close", (code) => {
|
|
49
56
|
if (code === 0) resolve();
|
|
50
|
-
else
|
|
57
|
+
else
|
|
58
|
+
reject(new Error(stderr.trim() || `${c.cmd} exited with code ${code}`));
|
|
51
59
|
});
|
|
52
60
|
child.stdin.end(text);
|
|
53
61
|
});
|
|
@@ -55,7 +63,8 @@ function copyToClipboard(text: string): Promise<void> {
|
|
|
55
63
|
|
|
56
64
|
export default function (pi: ExtensionAPI) {
|
|
57
65
|
pi.registerCommand("copy-all", {
|
|
58
|
-
description:
|
|
66
|
+
description:
|
|
67
|
+
"Copy all user/assistant messages in this thread to the clipboard",
|
|
59
68
|
handler: async (_args, ctx) => {
|
|
60
69
|
await ctx.waitForIdle();
|
|
61
70
|
|
|
@@ -80,9 +89,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
80
89
|
|
|
81
90
|
try {
|
|
82
91
|
await copyToClipboard(text);
|
|
83
|
-
ctx.ui.notify(
|
|
92
|
+
ctx.ui.notify(
|
|
93
|
+
`📋 Copied ${messages.length} messages to clipboard`,
|
|
94
|
+
"info",
|
|
95
|
+
);
|
|
84
96
|
} catch (err) {
|
|
85
|
-
ctx.ui.notify(
|
|
97
|
+
ctx.ui.notify(
|
|
98
|
+
`Clipboard copy failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
99
|
+
"error",
|
|
100
|
+
);
|
|
86
101
|
}
|
|
87
102
|
},
|
|
88
103
|
});
|
|
@@ -18,7 +18,8 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
18
18
|
const COMMAND = "diff";
|
|
19
19
|
|
|
20
20
|
function getStringPath(input: unknown): string | undefined {
|
|
21
|
-
if (!input || typeof input !== "object" || !("path" in input))
|
|
21
|
+
if (!input || typeof input !== "object" || !("path" in input))
|
|
22
|
+
return undefined;
|
|
22
23
|
const p = (input as { path?: unknown }).path;
|
|
23
24
|
return typeof p === "string" ? p : undefined;
|
|
24
25
|
}
|
|
@@ -45,8 +46,15 @@ function parseGitStatus(output: string, cwd: string): Set<string> {
|
|
|
45
46
|
return files;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
async function getGitChanged(
|
|
49
|
-
|
|
49
|
+
async function getGitChanged(
|
|
50
|
+
pi: ExtensionAPI,
|
|
51
|
+
cwd: string,
|
|
52
|
+
): Promise<Set<string>> {
|
|
53
|
+
const r = await pi.exec(
|
|
54
|
+
"git",
|
|
55
|
+
["status", "--porcelain", "--untracked-files=all"],
|
|
56
|
+
{ cwd, timeout: 5000 },
|
|
57
|
+
);
|
|
50
58
|
if (r.code !== 0) return new Set();
|
|
51
59
|
return parseGitStatus(r.stdout, cwd);
|
|
52
60
|
}
|
|
@@ -56,7 +64,8 @@ function diff(current: Set<string>, baseline: Set<string>): Set<string> {
|
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
function pickEditor(): { cmd: string; args: (file: string) => string[] } {
|
|
59
|
-
const env =
|
|
67
|
+
const env =
|
|
68
|
+
process.env.PI_DIFF_EDITOR || process.env.VISUAL || process.env.EDITOR;
|
|
60
69
|
if (env) {
|
|
61
70
|
const parts = env.split(/\s+/);
|
|
62
71
|
const cmd = parts[0];
|
|
@@ -88,12 +97,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
88
97
|
const now = await getGitChanged(pi, ctx.cwd);
|
|
89
98
|
changed = new Set([...diff(now, baseline), ...touched]);
|
|
90
99
|
if (changed.size > 0) {
|
|
91
|
-
ctx.ui.notify(
|
|
100
|
+
ctx.ui.notify(
|
|
101
|
+
`📝 ${changed.size} changed file(s). Run /${COMMAND} to view/open.`,
|
|
102
|
+
"info",
|
|
103
|
+
);
|
|
92
104
|
}
|
|
93
105
|
});
|
|
94
106
|
|
|
95
107
|
pi.registerCommand(COMMAND, {
|
|
96
|
-
description:
|
|
108
|
+
description:
|
|
109
|
+
"Show files changed by the last agent run and open one in your editor",
|
|
97
110
|
handler: async (args, ctx) => {
|
|
98
111
|
await ctx.waitForIdle();
|
|
99
112
|
const arg = (args ?? "").trim();
|
|
@@ -106,19 +119,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
106
119
|
return;
|
|
107
120
|
}
|
|
108
121
|
|
|
109
|
-
const files = [...changed].sort((a, b) =>
|
|
122
|
+
const files = [...changed].sort((a, b) =>
|
|
123
|
+
toRel(ctx.cwd, a).localeCompare(toRel(ctx.cwd, b)),
|
|
124
|
+
);
|
|
110
125
|
if (files.length === 0) {
|
|
111
|
-
ctx.ui.notify(
|
|
126
|
+
ctx.ui.notify(
|
|
127
|
+
"No changed files tracked from the last agent run",
|
|
128
|
+
"info",
|
|
129
|
+
);
|
|
112
130
|
return;
|
|
113
131
|
}
|
|
114
132
|
|
|
115
133
|
if (arg === "list") {
|
|
116
|
-
ctx.ui.notify(
|
|
134
|
+
ctx.ui.notify(
|
|
135
|
+
`Changed files:\n${files.map((f) => `- ${toRel(ctx.cwd, f)}`).join("\n")}`,
|
|
136
|
+
"info",
|
|
137
|
+
);
|
|
117
138
|
return;
|
|
118
139
|
}
|
|
119
140
|
|
|
120
141
|
if (arg) {
|
|
121
|
-
ctx.ui.notify(
|
|
142
|
+
ctx.ui.notify(
|
|
143
|
+
`Unknown /${COMMAND} argument: ${arg}. Try /${COMMAND}, /${COMMAND} list, /${COMMAND} clear.`,
|
|
144
|
+
"warning",
|
|
145
|
+
);
|
|
122
146
|
return;
|
|
123
147
|
}
|
|
124
148
|
|
|
@@ -130,9 +154,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
130
154
|
if (!file) return;
|
|
131
155
|
|
|
132
156
|
const ed = pickEditor();
|
|
133
|
-
const r = await pi.exec(ed.cmd, ed.args(file), {
|
|
134
|
-
|
|
135
|
-
|
|
157
|
+
const r = await pi.exec(ed.cmd, ed.args(file), {
|
|
158
|
+
cwd: ctx.cwd,
|
|
159
|
+
timeout: 5000,
|
|
160
|
+
});
|
|
161
|
+
if (r.code === 0)
|
|
162
|
+
ctx.ui.notify(`Opened ${selected} in ${ed.cmd}`, "info");
|
|
163
|
+
else
|
|
164
|
+
ctx.ui.notify(
|
|
165
|
+
r.stderr.trim() || `Failed to open ${selected} in ${ed.cmd}`,
|
|
166
|
+
"error",
|
|
167
|
+
);
|
|
136
168
|
},
|
|
137
169
|
});
|
|
138
170
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { describe,
|
|
2
|
-
import {
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { benchStars, fmtCost, fmtCtx, sortModels } from "./models.ts";
|
|
3
3
|
|
|
4
4
|
describe("fmtCtx", () => {
|
|
5
5
|
it("formats 0 as 0", () => expect(fmtCtx(0)).toBe("0"));
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
Text,
|
|
24
24
|
visibleWidth,
|
|
25
25
|
} from "@earendil-works/pi-tui";
|
|
26
|
-
import {
|
|
26
|
+
import { lookupBenchmark, lookupModelsDev } from "../../lib/data";
|
|
27
27
|
|
|
28
28
|
// ─── Pure logic (exported for tests) ─────────────────────────────────────────
|
|
29
29
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { describe,
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import {
|
|
3
|
-
isTransient,
|
|
4
3
|
commandFor,
|
|
5
4
|
formatUpdateSummary,
|
|
6
|
-
PACKAGE_NAME,
|
|
7
5
|
type InstallMethod,
|
|
6
|
+
isTransient,
|
|
7
|
+
PACKAGE_NAME,
|
|
8
8
|
} from "./update.ts";
|
|
9
9
|
|
|
10
10
|
describe("isTransient", () => {
|
|
@@ -48,28 +48,28 @@ describe("commandFor", () => {
|
|
|
48
48
|
for (const m of methods) {
|
|
49
49
|
const spec = commandFor(m);
|
|
50
50
|
expect(spec).toBeDefined();
|
|
51
|
-
expect(spec
|
|
52
|
-
expect(spec
|
|
51
|
+
expect(spec?.command).toBeTruthy();
|
|
52
|
+
expect(spec?.label).toBeTruthy();
|
|
53
53
|
}
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
it("vp uses vp add -g", () => {
|
|
57
57
|
const spec = commandFor("vp")!;
|
|
58
58
|
expect(spec.command).toBe("vp");
|
|
59
|
-
expect(spec.args).toContain(PACKAGE_NAME
|
|
59
|
+
expect(spec.args).toContain(`${PACKAGE_NAME}@latest`);
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
it("bun uses bun add -g", () => {
|
|
63
63
|
const spec = commandFor("bun")!;
|
|
64
64
|
expect(spec.command).toBe("bun");
|
|
65
|
-
expect(spec.args).toContain(PACKAGE_NAME
|
|
65
|
+
expect(spec.args).toContain(`${PACKAGE_NAME}@latest`);
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
it("npm uses npm install -g", () => {
|
|
69
69
|
const spec = commandFor("npm")!;
|
|
70
70
|
expect(spec.command).toBe("npm");
|
|
71
71
|
expect(spec.args).toContain("-g");
|
|
72
|
-
expect(spec.args).toContain(PACKAGE_NAME
|
|
72
|
+
expect(spec.args).toContain(`${PACKAGE_NAME}@latest`);
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
it("brew uses sh -lc", () => {
|
|
@@ -127,7 +127,7 @@ async function detectInstallMethod(pi: ExtensionAPI): Promise<InstallMethod> {
|
|
|
127
127
|
],
|
|
128
128
|
{ timeout: 10_000 },
|
|
129
129
|
);
|
|
130
|
-
if ((hasGlobalNpm.
|
|
130
|
+
if ((hasGlobalNpm.code ?? 1) === 0) return "npm";
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
if (await resolveCommand("vp", pi)) return "vp";
|
|
@@ -145,7 +145,7 @@ async function runWithRetry(pi: ExtensionAPI, spec: CommandSpec) {
|
|
|
145
145
|
.filter(Boolean)
|
|
146
146
|
.join("\n")
|
|
147
147
|
.trim();
|
|
148
|
-
if ((result.
|
|
148
|
+
if ((result.code ?? 0) === 0)
|
|
149
149
|
return { ok: true, output: lastOutput, attempts: attempt };
|
|
150
150
|
if (attempt === 3 || !isTransient(lastOutput))
|
|
151
151
|
return { ok: false, output: lastOutput, attempts: attempt };
|
|
@@ -200,7 +200,7 @@ async function updateExtensions(
|
|
|
200
200
|
.filter(Boolean)
|
|
201
201
|
.join("\n")
|
|
202
202
|
.trim();
|
|
203
|
-
if ((result.
|
|
203
|
+
if ((result.code ?? 0) !== 0) {
|
|
204
204
|
ctx.ui.notify(
|
|
205
205
|
`Pi extensions update failed. ${output || "No output."}`,
|
|
206
206
|
"error",
|
|
@@ -222,7 +222,7 @@ async function updatePackages(pi: ExtensionAPI, ctx: ExtensionCommandContext) {
|
|
|
222
222
|
.filter(Boolean)
|
|
223
223
|
.join("\n")
|
|
224
224
|
.trim();
|
|
225
|
-
if ((result.
|
|
225
|
+
if ((result.code ?? 0) !== 0) {
|
|
226
226
|
ctx.ui.notify(
|
|
227
227
|
`Pi package update failed. ${output || "No output."}`,
|
|
228
228
|
"error",
|
package/src/index.ts
CHANGED
|
@@ -16,22 +16,24 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
import
|
|
19
|
+
import registerAgentSop from "./commands/agent-sop/agent-sop.ts";
|
|
20
|
+
import registerClear from "./commands/clear/clear.ts";
|
|
21
|
+
import registerCopyAll from "./commands/copy-all/copy-all.ts";
|
|
22
|
+
import registerDiff from "./commands/diff/diff.ts";
|
|
23
|
+
import registerLg from "./commands/lg/lg.ts";
|
|
22
24
|
import registerModels from "./commands/models/models.ts";
|
|
23
25
|
import registerUpdate from "./commands/update/update.ts";
|
|
24
|
-
import registerLg from "./commands/lg/lg.ts";
|
|
25
26
|
import registerYeet from "./commands/yeet/yeet.ts";
|
|
26
|
-
import
|
|
27
|
-
import
|
|
28
|
-
import registerClear from "./commands/clear/clear.ts";
|
|
27
|
+
import registerNudges from "./nudge/index.ts";
|
|
28
|
+
import registerAsk from "./tool/ask/index.ts";
|
|
29
29
|
import registerTodo from "./tool/todo/todo.ts";
|
|
30
|
-
import registerAsk from "./tool/ask/ask.ts";
|
|
31
30
|
import registerToolbox from "./tool/toolbox/toolbox.ts";
|
|
32
|
-
import
|
|
31
|
+
import registerDiagnostics from "./ui/diagnostics.ts";
|
|
32
|
+
import registerFooter from "./ui/footer.ts";
|
|
33
|
+
import registerWelcome from "./ui/welcome.ts";
|
|
33
34
|
|
|
34
35
|
export default function (pi: ExtensionAPI): void {
|
|
36
|
+
registerAgentSop(pi);
|
|
35
37
|
registerWelcome(pi);
|
|
36
38
|
registerFooter(pi);
|
|
37
39
|
registerDiagnostics(pi);
|
package/src/lib/data.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* models.ts — lookupModelsDev, lookupBenchmark
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
13
12
|
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { dirname, join } from "node:path";
|
|
16
16
|
|
|
@@ -18,7 +18,6 @@ const skill = (
|
|
|
18
18
|
disableModelInvocation,
|
|
19
19
|
filePath: "",
|
|
20
20
|
baseDir: "",
|
|
21
|
-
// biome-ignore lint: test fixture
|
|
22
21
|
sourceInfo: {} as never,
|
|
23
22
|
}) as never;
|
|
24
23
|
|
|
@@ -29,7 +28,6 @@ const tool = (name: string, source: string) =>
|
|
|
29
28
|
description: `${name} desc.`,
|
|
30
29
|
parameters: {},
|
|
31
30
|
sourceInfo: { source, path: "", scope: "user", origin: "package" },
|
|
32
|
-
// biome-ignore lint: test fixture
|
|
33
31
|
}) as never;
|
|
34
32
|
|
|
35
33
|
describe("CAPABILITY_REMINDER", () => {
|
|
@@ -47,8 +45,14 @@ describe("CAPABILITY_REMINDER", () => {
|
|
|
47
45
|
expect(CAPABILITY_REMINDER.toLowerCase()).toContain("improvis");
|
|
48
46
|
});
|
|
49
47
|
|
|
50
|
-
test("
|
|
51
|
-
expect(CAPABILITY_REMINDER).toContain("
|
|
48
|
+
test("nudges model to call skill() when a skill matches", () => {
|
|
49
|
+
expect(CAPABILITY_REMINDER).toContain("skill()");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("points at /toolbox slash command for discovery (not a function call)", () => {
|
|
53
|
+
expect(CAPABILITY_REMINDER).toContain("/toolbox");
|
|
54
|
+
// must NOT imply toolbox is a callable function
|
|
55
|
+
expect(CAPABILITY_REMINDER).not.toContain("toolbox(");
|
|
52
56
|
expect(CAPABILITY_REMINDER).toContain("function definitions");
|
|
53
57
|
});
|
|
54
58
|
});
|
|
@@ -125,10 +129,13 @@ describe("buildOrientation", () => {
|
|
|
125
129
|
expect(out).toContain("2 skills");
|
|
126
130
|
});
|
|
127
131
|
|
|
128
|
-
test("explains how to
|
|
132
|
+
test("explains how to use skill() and /toolbox for discovery", () => {
|
|
129
133
|
const out = buildOrientation([tool("read", "builtin")], []);
|
|
130
|
-
expect(out).toContain("
|
|
131
|
-
expect(out
|
|
134
|
+
expect(out).toContain("skill()");
|
|
135
|
+
expect(out).toContain("/toolbox");
|
|
136
|
+
// toolbox must NOT appear as a function call
|
|
137
|
+
expect(out).not.toContain("toolbox(");
|
|
138
|
+
expect(out.toLowerCase()).toMatch(/discover|enable/);
|
|
132
139
|
});
|
|
133
140
|
|
|
134
141
|
test("calls out gated tools and points at toolbox to enable them", () => {
|
|
@@ -176,18 +183,17 @@ describe("buildOrientation", () => {
|
|
|
176
183
|
test("lists invocable skill names, sorted, excluding user-only", () => {
|
|
177
184
|
const out = buildOrientation(
|
|
178
185
|
[],
|
|
179
|
-
[
|
|
180
|
-
skill("zebra", "z."),
|
|
181
|
-
skill("alpha", "a."),
|
|
182
|
-
skill("hidden", "h.", true),
|
|
183
|
-
],
|
|
186
|
+
[skill("zebra", "z."), skill("alpha", "a."), skill("hidden", "h.", true)],
|
|
184
187
|
);
|
|
185
188
|
expect(out).toContain("Skills: alpha, zebra.");
|
|
186
189
|
expect(out).not.toContain("hidden");
|
|
187
190
|
});
|
|
188
191
|
|
|
189
192
|
test("omits the skills line when no invocable skills", () => {
|
|
190
|
-
const out = buildOrientation(
|
|
193
|
+
const out = buildOrientation(
|
|
194
|
+
[tool("read", "builtin")],
|
|
195
|
+
[skill("x", ".", true)],
|
|
196
|
+
);
|
|
191
197
|
expect(out).not.toContain("Skills:");
|
|
192
198
|
});
|
|
193
199
|
|
package/src/nudge/capability.ts
CHANGED
|
@@ -9,11 +9,15 @@
|
|
|
9
9
|
* Two modes:
|
|
10
10
|
* 1. FIRST prompt of the session — an orientation block: a high-level
|
|
11
11
|
* description of WHAT is available (counts of tools / MCP tools / skills)
|
|
12
|
-
* and HOW to explore it
|
|
13
|
-
*
|
|
14
|
-
* (
|
|
12
|
+
* and HOW to explore it. We deliberately do NOT dump the whole inventory
|
|
13
|
+
* every turn — the model should call skill() for skills and use /toolbox
|
|
14
|
+
* (slash command, user-facing) to discover/enable gated tools.
|
|
15
15
|
* 2. EVERY subsequent prompt — the terse one-line CAPABILITY_REMINDER, a
|
|
16
|
-
* cheap (~40 tok) reinforcement that
|
|
16
|
+
* cheap (~40 tok) reinforcement that steers toward skill() and /toolbox.
|
|
17
|
+
*
|
|
18
|
+
* NOTE: `toolbox` is a slash command only (/toolbox) — NOT a model-callable
|
|
19
|
+
* function tool. The model cannot call toolbox() in function definitions.
|
|
20
|
+
* The `skill` tool IS model-callable: skill() lists/loads bundled skills.
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
23
|
import type {
|
|
@@ -26,13 +30,16 @@ type LoadedSkill = NonNullable<BuildSystemPromptOptions["skills"]>[number];
|
|
|
26
30
|
|
|
27
31
|
/** The standing per-turn reminder. Kept terse — it ships on every turn. */
|
|
28
32
|
export const CAPABILITY_REMINDER =
|
|
29
|
-
"Reminder —
|
|
33
|
+
"Reminder — check knowledge resources " +
|
|
30
34
|
"(skills/tools/MCP/web/user) before improvising. " +
|
|
31
|
-
"
|
|
32
|
-
"
|
|
35
|
+
"Matching skill? Call skill() first. " +
|
|
36
|
+
"Use /toolbox to discover/enable gated tools. " +
|
|
37
|
+
"All tools callable via function definitions.";
|
|
33
38
|
|
|
34
39
|
/** Count model-invocable skills (excludes user-only /skill:name entries). */
|
|
35
|
-
export function countInvocableSkills(
|
|
40
|
+
export function countInvocableSkills(
|
|
41
|
+
skills: LoadedSkill[] | undefined,
|
|
42
|
+
): number {
|
|
36
43
|
return (skills ?? []).filter((s) => !s.disableModelInvocation).length;
|
|
37
44
|
}
|
|
38
45
|
|
|
@@ -105,7 +112,8 @@ export function buildOrientation(
|
|
|
105
112
|
if (gateLine) lines.push(gateLine);
|
|
106
113
|
lines.push(
|
|
107
114
|
"Don't improvise what a capability covers — ask the user, search the web, or pull docs first.",
|
|
108
|
-
"
|
|
115
|
+
"`skill()` lists/loads bundled skills — call it when a skill matches your task. " +
|
|
116
|
+
"/toolbox (slash command) discovers and enables gated tools.",
|
|
109
117
|
);
|
|
110
118
|
if (skillNames.length) {
|
|
111
119
|
lines.push(`Skills: ${skillNames.join(", ")}.`);
|
package/src/nudge/index.ts
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import registerToolsNudge from "./tools.ts";
|
|
12
11
|
import registerCapabilityNudge from "./capability.ts";
|
|
12
|
+
import registerToolsNudge from "./tools.ts";
|
|
13
13
|
|
|
14
14
|
export default function registerNudges(pi: ExtensionAPI): void {
|
|
15
15
|
registerToolsNudge(pi);
|