@xynogen/pix-core 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +49 -40
- 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 +9 -9
- package/src/lib/data.ts +1 -1
- package/src/nudge/capability.test.ts +5 -8
- package/src/nudge/capability.ts +3 -1
- 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 +29 -18
- package/src/tool/ask/ask.ts +1004 -979
- package/src/tool/ask/single-select-layout.test.ts +21 -5
- package/src/tool/ask/single-select-layout.ts +48 -14
- package/src/tool/todo/todo.ts +24 -37
- package/src/tool/toolbox/toolbox.test.ts +2 -2
- package/src/tool/toolbox/toolbox.ts +0 -1
- package/src/ui/footer.ts +3 -4
- package/src/ui/welcome.test.ts +6 -6
package/package.json
CHANGED
|
@@ -1,42 +1,51 @@
|
|
|
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.1",
|
|
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
|
+
"typebox": "^1.1.38"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
49
|
+
"@earendil-works/pi-tui": "*"
|
|
50
|
+
}
|
|
42
51
|
}
|
|
@@ -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,20 +16,20 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
import
|
|
19
|
+
import registerClear from "./commands/clear/clear.ts";
|
|
20
|
+
import registerCopyAll from "./commands/copy-all/copy-all.ts";
|
|
21
|
+
import registerDiff from "./commands/diff/diff.ts";
|
|
22
|
+
import registerLg from "./commands/lg/lg.ts";
|
|
22
23
|
import registerModels from "./commands/models/models.ts";
|
|
23
24
|
import registerUpdate from "./commands/update/update.ts";
|
|
24
|
-
import registerLg from "./commands/lg/lg.ts";
|
|
25
25
|
import registerYeet from "./commands/yeet/yeet.ts";
|
|
26
|
-
import
|
|
27
|
-
import registerDiff from "./commands/diff/diff.ts";
|
|
28
|
-
import registerClear from "./commands/clear/clear.ts";
|
|
29
|
-
import registerTodo from "./tool/todo/todo.ts";
|
|
26
|
+
import registerNudges from "./nudge/index.ts";
|
|
30
27
|
import registerAsk from "./tool/ask/ask.ts";
|
|
28
|
+
import registerTodo from "./tool/todo/todo.ts";
|
|
31
29
|
import registerToolbox from "./tool/toolbox/toolbox.ts";
|
|
32
|
-
import
|
|
30
|
+
import registerDiagnostics from "./ui/diagnostics.ts";
|
|
31
|
+
import registerFooter from "./ui/footer.ts";
|
|
32
|
+
import registerWelcome from "./ui/welcome.ts";
|
|
33
33
|
|
|
34
34
|
export default function (pi: ExtensionAPI): void {
|
|
35
35
|
registerWelcome(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", () => {
|
|
@@ -176,18 +174,17 @@ describe("buildOrientation", () => {
|
|
|
176
174
|
test("lists invocable skill names, sorted, excluding user-only", () => {
|
|
177
175
|
const out = buildOrientation(
|
|
178
176
|
[],
|
|
179
|
-
[
|
|
180
|
-
skill("zebra", "z."),
|
|
181
|
-
skill("alpha", "a."),
|
|
182
|
-
skill("hidden", "h.", true),
|
|
183
|
-
],
|
|
177
|
+
[skill("zebra", "z."), skill("alpha", "a."), skill("hidden", "h.", true)],
|
|
184
178
|
);
|
|
185
179
|
expect(out).toContain("Skills: alpha, zebra.");
|
|
186
180
|
expect(out).not.toContain("hidden");
|
|
187
181
|
});
|
|
188
182
|
|
|
189
183
|
test("omits the skills line when no invocable skills", () => {
|
|
190
|
-
const out = buildOrientation(
|
|
184
|
+
const out = buildOrientation(
|
|
185
|
+
[tool("read", "builtin")],
|
|
186
|
+
[skill("x", ".", true)],
|
|
187
|
+
);
|
|
191
188
|
expect(out).not.toContain("Skills:");
|
|
192
189
|
});
|
|
193
190
|
|
package/src/nudge/capability.ts
CHANGED
|
@@ -32,7 +32,9 @@ export const CAPABILITY_REMINDER =
|
|
|
32
32
|
"All tools are always callable via function definitions.";
|
|
33
33
|
|
|
34
34
|
/** Count model-invocable skills (excludes user-only /skill:name entries). */
|
|
35
|
-
export function countInvocableSkills(
|
|
35
|
+
export function countInvocableSkills(
|
|
36
|
+
skills: LoadedSkill[] | undefined,
|
|
37
|
+
): number {
|
|
36
38
|
return (skills ?? []).filter((s) => !s.disableModelInvocation).length;
|
|
37
39
|
}
|
|
38
40
|
|
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);
|
package/src/nudge/tools.test.ts
CHANGED
|
@@ -114,15 +114,23 @@ describe("classifyCompound", () => {
|
|
|
114
114
|
});
|
|
115
115
|
|
|
116
116
|
describe("nudgeReason", () => {
|
|
117
|
-
|
|
118
|
-
const msg = nudgeReason(
|
|
117
|
+
test("active tool: point straight at it, no toolbox", () => {
|
|
118
|
+
const msg = nudgeReason(
|
|
119
|
+
"Searching file contents via bash grep/rg.",
|
|
120
|
+
"grep",
|
|
121
|
+
true,
|
|
122
|
+
);
|
|
119
123
|
expect(msg).toContain("Use `grep` instead");
|
|
120
124
|
expect(msg).toContain("function definitions");
|
|
121
125
|
expect(msg).not.toContain("toolbox");
|
|
122
126
|
});
|
|
123
127
|
|
|
124
|
-
|
|
125
|
-
const msg = nudgeReason(
|
|
128
|
+
test("gated tool: route through toolbox enable, not a direct call", () => {
|
|
129
|
+
const msg = nudgeReason(
|
|
130
|
+
"Listing a directory via bash ls/tree.",
|
|
131
|
+
"ls",
|
|
132
|
+
false,
|
|
133
|
+
);
|
|
126
134
|
expect(msg).toContain('toolbox(action:"enable", name:"ls")');
|
|
127
135
|
expect(msg).toContain("prompt-hidden");
|
|
128
136
|
expect(msg).toContain("function definitions");
|
|
@@ -131,13 +139,17 @@ describe("nudgeReason", () => {
|
|
|
131
139
|
});
|
|
132
140
|
|
|
133
141
|
test("gated find tool names itself in the enable hint", () => {
|
|
134
|
-
expect(
|
|
135
|
-
|
|
136
|
-
);
|
|
142
|
+
expect(
|
|
143
|
+
nudgeReason("Locating files via bash find/fd.", "find", false),
|
|
144
|
+
).toContain('toolbox(action:"enable", name:"find")');
|
|
137
145
|
});
|
|
138
146
|
|
|
139
|
-
|
|
140
|
-
const msg = nudgeReason(
|
|
147
|
+
test("is a single short line — no inventory dump, no newlines", () => {
|
|
148
|
+
const msg = nudgeReason(
|
|
149
|
+
"Listing a directory via bash ls/tree.",
|
|
150
|
+
"ls",
|
|
151
|
+
false,
|
|
152
|
+
);
|
|
141
153
|
expect(msg).not.toContain("\n");
|
|
142
154
|
expect(msg).not.toContain("Available tools");
|
|
143
155
|
expect(msg.length).toBeLessThan(400);
|
package/src/nudge/tools.ts
CHANGED
|
@@ -28,7 +28,6 @@ import {
|
|
|
28
28
|
isToolCallEventType,
|
|
29
29
|
} from "@earendil-works/pi-coding-agent";
|
|
30
30
|
|
|
31
|
-
|
|
32
31
|
/** Categories that map a raw shell command to a dedicated Pi tool. */
|
|
33
32
|
type Category = "read" | "ls" | "grep" | "find" | "edit";
|
|
34
33
|
|
|
@@ -116,7 +115,6 @@ export function splitSegments(command: string): Segment[] {
|
|
|
116
115
|
const re = /(\|\||&&|[|;&\n])/g;
|
|
117
116
|
let last = 0;
|
|
118
117
|
let m: RegExpExecArray | null;
|
|
119
|
-
// biome-ignore lint/suspicious/noAssignInExpressions: standard regex walk
|
|
120
118
|
while ((m = re.exec(command)) !== null) {
|
|
121
119
|
out.push({ text: command.slice(last, m.index), followedBy: m[1] });
|
|
122
120
|
last = m.index + m[1].length;
|