@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 CHANGED
@@ -1,42 +1,52 @@
1
1
  {
2
- "name": "@xynogen/pix-core",
3
- "version": "0.1.0",
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": ["src/index.ts"],
18
- "skills": ["./skills"]
19
- },
20
- "keywords": ["pi", "pi-package", "pi-extension", "pix"],
21
- "author": "xynogen",
22
- "license": "MIT",
23
- "repository": {
24
- "type": "git",
25
- "url": "git+https://github.com/xynogen/pix-mono.git",
26
- "directory": "packages/pix-core"
27
- },
28
- "homepage": "https://github.com/xynogen/pix-mono/tree/main/packages/pix-core#readme",
29
- "bugs": {
30
- "url": "https://github.com/xynogen/pix-mono/issues"
31
- },
32
- "publishConfig": {
33
- "access": "public"
34
- },
35
- "dependencies": {
36
- "typebox": "^1.1.38"
37
- },
38
- "peerDependencies": {
39
- "@earendil-works/pi-coding-agent": "*",
40
- "@earendil-works/pi-tui": "*"
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.exitCode ?? 0) !== 0) {
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 (block.type === "text" && "text" in block && typeof block.text === "string") return block.text;
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) => { stderr += String(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 reject(new Error(stderr.trim() || `${c.cmd} exited with code ${code}`));
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: "Copy all user/assistant messages in this thread to the clipboard",
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(`📋 Copied ${messages.length} messages to clipboard`, "info");
92
+ ctx.ui.notify(
93
+ `📋 Copied ${messages.length} messages to clipboard`,
94
+ "info",
95
+ );
84
96
  } catch (err) {
85
- ctx.ui.notify(`Clipboard copy failed: ${err instanceof Error ? err.message : String(err)}`, "error");
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)) return undefined;
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(pi: ExtensionAPI, cwd: string): Promise<Set<string>> {
49
- const r = await pi.exec("git", ["status", "--porcelain", "--untracked-files=all"], { cwd, timeout: 5000 });
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 = process.env.PI_DIFF_EDITOR || process.env.VISUAL || process.env.EDITOR;
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(`📝 ${changed.size} changed file(s). Run /${COMMAND} to view/open.`, "info");
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: "Show files changed by the last agent run and open one in your editor",
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) => toRel(ctx.cwd, a).localeCompare(toRel(ctx.cwd, 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("No changed files tracked from the last agent run", "info");
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(`Changed files:\n${files.map((f) => `- ${toRel(ctx.cwd, f)}`).join("\n")}`, "info");
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(`Unknown /${COMMAND} argument: ${arg}. Try /${COMMAND}, /${COMMAND} list, /${COMMAND} clear.`, "warning");
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), { cwd: ctx.cwd, timeout: 5000 });
134
- if (r.code === 0) ctx.ui.notify(`Opened ${selected} in ${ed.cmd}`, "info");
135
- else ctx.ui.notify(r.stderr.trim() || `Failed to open ${selected} in ${ed.cmd}`, "error");
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, it, expect } from "bun:test";
2
- import { fmtCtx, fmtCost, benchStars, sortModels } from "./models.ts";
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 { lookupModelsDev, lookupBenchmark } from "../../lib/data";
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, it, expect } from "bun:test";
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!.command).toBeTruthy();
52
- expect(spec!.label).toBeTruthy();
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 + "@latest");
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 + "@latest");
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 + "@latest");
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.exitCode ?? 1) === 0) return "npm";
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.exitCode ?? 0) === 0)
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.exitCode ?? 0) !== 0) {
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.exitCode ?? 0) !== 0) {
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 registerWelcome from "./ui/welcome.ts";
20
- import registerFooter from "./ui/footer.ts";
21
- import registerDiagnostics from "./ui/diagnostics.ts";
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 registerCopyAll from "./commands/copy-all/copy-all.ts";
27
- import registerDiff from "./commands/diff/diff.ts";
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 registerNudges from "./nudge/index.ts";
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("points at the toolbox tool for discovery", () => {
51
- expect(CAPABILITY_REMINDER).toContain("toolbox");
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 explore via the toolbox tool", () => {
132
+ test("explains how to use skill() and /toolbox for discovery", () => {
129
133
  const out = buildOrientation([tool("read", "builtin")], []);
130
- expect(out).toContain("toolbox(query");
131
- expect(out.toLowerCase()).toMatch(/fuzzy|search|discover/);
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([tool("read", "builtin")], [skill("x", ".", true)]);
193
+ const out = buildOrientation(
194
+ [tool("read", "builtin")],
195
+ [skill("x", ".", true)],
196
+ );
191
197
  expect(out).not.toContain("Skills:");
192
198
  });
193
199
 
@@ -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 on demand via the `toolbox` tool. We deliberately
13
- * do NOT dump the whole inventory every turn that is what `toolbox`
14
- * (fuzzy search over names + descriptions) is for.
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 points back at toolbox.
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 — always check your knowledge resources " +
33
+ "Reminder — check knowledge resources " +
30
34
  "(skills/tools/MCP/web/user) before improvising. " +
31
- "`toolbox(query)` finds the right one; enable gated tools via toolbox. " +
32
- "All tools are always callable via function definitions.";
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(skills: LoadedSkill[] | undefined): number {
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
- "toolbox is the gateway: `toolbox(query)` searches tools/MCP/skills/commands; `toolbox(action:'enable'|'disable', name)` toggles a gated tool prompt-visible or gated. Empty query lists all.",
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(", ")}.`);
@@ -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);