@preapexis/piexis 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -2
- package/apps/server/src/index.ts +97 -0
- package/apps/server/src/session-manager.ts +95 -0
- package/apps/server/src/workspace.ts +39 -0
- package/{tsconfig.json → apps/server/tsconfig.json} +3 -4
- package/apps/web/dist/assets/index-D4n3_lAw.js +10 -0
- package/apps/web/dist/assets/index-igiK0FPY.css +1 -0
- package/apps/web/dist/index.html +3 -0
- package/bin/piexis-web.mjs +24 -0
- package/extensions/webui.ts +45 -0
- package/package.json +38 -3
- package/packages/pi-integration/src/rpc-client.ts +99 -0
- package/packages/shared/src/index.ts +27 -0
- package/themes/midnight.json +81 -0
- package/.github/workflows/publish.yml +0 -35
- package/AGENTS.md +0 -38
- package/scripts/release.mjs +0 -32
- package/skills-lock.json +0 -365
- package/tests/agents.test.mjs +0 -605
- package/tests/ask-question-pty.test.mjs +0 -16
- package/tests/ask-question.test.mjs +0 -269
- package/tests/fixtures/agents-provider.ts +0 -55
- package/tests/fixtures/ask-question-pty.py +0 -91
- package/tests/fixtures/ask-question-terminal.mjs +0 -69
- package/tests/fixtures/recommended-tools-terminal.mjs +0 -103
- package/tests/init-workflow.test.mjs +0 -31
- package/tests/recommended-tools-pty.test.mjs +0 -16
- package/tests/recommended-tools.test.mjs +0 -423
- package/tests/resources.test.mjs +0 -66
- package/tests/skills-routing.test.mjs +0 -54
- package/tests/ui-tip-reload.test.mjs +0 -70
- package/tests/ui.test.mjs +0 -450
|
@@ -1,423 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtemp, mkdir, readFile, readdir, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import test from "node:test";
|
|
6
|
-
import recommendedToolsExtension from "../extensions/recommended-tools.ts";
|
|
7
|
-
import { agentConfigSchemaUrl } from "../lib/agent-config.ts";
|
|
8
|
-
import { hasToolOverrides, readSetupSettings, saveSetupChoice, supportsAdditiveTools, toolsToOffer } from "../lib/recommended-tools.ts";
|
|
9
|
-
|
|
10
|
-
const base = ["read", "bash", "edit", "write", "AskQuestion"];
|
|
11
|
-
const recommended = ["grep", "find", "ls"];
|
|
12
|
-
|
|
13
|
-
async function fixture(work) {
|
|
14
|
-
const root = await mkdtemp(join(tmpdir(), "piexis-setup-"));
|
|
15
|
-
const cwd = join(root, "project");
|
|
16
|
-
const agentDir = join(root, "agent");
|
|
17
|
-
await mkdir(cwd);
|
|
18
|
-
await mkdir(agentDir);
|
|
19
|
-
const projectPath = join(cwd, ".pi", "settings.json");
|
|
20
|
-
const globalPath = join(agentDir, "settings.json");
|
|
21
|
-
async function project(value) {
|
|
22
|
-
await mkdir(join(cwd, ".pi"), { recursive: true });
|
|
23
|
-
await writeFile(projectPath, typeof value === "string" ? value : JSON.stringify(value), { mode: 0o640 });
|
|
24
|
-
}
|
|
25
|
-
async function global(value) { await writeFile(globalPath, JSON.stringify(value)); }
|
|
26
|
-
try { await work({ cwd, agentDir, projectPath, globalPath, project, global }); }
|
|
27
|
-
finally { await rm(root, { recursive: true, force: true }); }
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function policy(project = {}, global = {}) {
|
|
31
|
-
return { project: { settings: project }, global: { settings: global } };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
test("offers only available inactive recommendations, not active tools or prior choices", () => {
|
|
35
|
-
assert.deepEqual(toolsToOffer(policy(), recommended, base, true), recommended);
|
|
36
|
-
assert.deepEqual(toolsToOffer(policy(), ["grep", "find"], [...base, "grep"], true), ["find"]);
|
|
37
|
-
assert.deepEqual(toolsToOffer(policy(), recommended, [...base, ...recommended], true), []);
|
|
38
|
-
for (const choice of ["enabled", "disabled"]) {
|
|
39
|
-
assert.deepEqual(toolsToOffer(policy({ piexis: { recommendedTools: choice } }), recommended, base, true), []);
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("preserves global/project allowlists, empty lists and ordered exclusions", () => {
|
|
44
|
-
for (const list of [[], ["read"], ["read", "+find"], ["grep", "find", "ls"]]) {
|
|
45
|
-
assert.deepEqual(toolsToOffer(policy({ defaultTools: list }), recommended, base, true), []);
|
|
46
|
-
assert.deepEqual(toolsToOffer(policy({}, { defaultTools: list }), recommended, base, true), []);
|
|
47
|
-
}
|
|
48
|
-
assert.deepEqual(toolsToOffer(policy({ defaultTools: ["-find"] }), recommended, base, true), ["grep", "ls"]);
|
|
49
|
-
assert.deepEqual(toolsToOffer(policy({}, { defaultTools: ["-grep"] }), recommended, base, true), ["find", "ls"]);
|
|
50
|
-
assert.deepEqual(toolsToOffer(policy({ defaultTools: ["+find"] }, { defaultTools: ["-find"] }), recommended, base, true), recommended);
|
|
51
|
-
assert.deepEqual(toolsToOffer(policy({ defaultTools: ["+find", "-find"] }), recommended, base, true), ["grep", "ls"]);
|
|
52
|
-
assert.deepEqual(toolsToOffer(policy({ defaultTools: ["+find"] }), recommended, base, false), []);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("CLI overrides skip setup and -- terminates option detection", () => {
|
|
56
|
-
for (const flag of ["--tools", "--tools=read", "-t", "--exclude-tools", "--exclude-tools=find", "-xt", "--no-builtin-tools", "-nbt", "--no-tools", "-nt"]) {
|
|
57
|
-
assert.equal(hasToolOverrides([flag]), true);
|
|
58
|
-
}
|
|
59
|
-
assert.equal(hasToolOverrides(["--", "--tools"]), false);
|
|
60
|
-
assert.equal(hasToolOverrides(["--model", "sonnet", "--extension", "./extension.ts"]), false);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("enable safely merges additive entries, unknown settings, namespace and file mode", async () => {
|
|
64
|
-
await fixture(async (f) => {
|
|
65
|
-
await f.global({ defaultTools: ["-write", "+codemode"], theme: "dark" });
|
|
66
|
-
await f.project({ defaultTools: ["-find", "+custom"], packages: ["npm:example"], piexis: { existing: 42 }, custom: { nested: true } });
|
|
67
|
-
const settings = await readSetupSettings(f.cwd, f.agentDir);
|
|
68
|
-
const originalGlobal = await readFile(f.globalPath, "utf8");
|
|
69
|
-
const mode = (await stat(f.projectPath)).mode & 0o777;
|
|
70
|
-
await saveSetupChoice(settings, "enabled", ["grep", "ls"], true);
|
|
71
|
-
const saved = JSON.parse(await readFile(f.projectPath, "utf8"));
|
|
72
|
-
assert.deepEqual(saved.defaultTools, ["-find", "+custom", "+grep", "+ls"]);
|
|
73
|
-
assert.deepEqual(saved.piexis, { existing: 42, recommendedTools: "enabled" });
|
|
74
|
-
assert.deepEqual(saved.custom, { nested: true });
|
|
75
|
-
assert.deepEqual(saved.packages, ["npm:example"]);
|
|
76
|
-
assert.equal(await readFile(f.globalPath, "utf8"), originalGlobal);
|
|
77
|
-
assert.equal((await stat(f.projectPath)).mode & 0o777, mode);
|
|
78
|
-
assert.deepEqual(await readdir(join(f.cwd, ".pi")), ["settings.json"]);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("fresh projects use additive configuration, with a valid plain-list fallback for older Pi", async () => {
|
|
83
|
-
for (const additive of [true, false]) {
|
|
84
|
-
await fixture(async (f) => {
|
|
85
|
-
await saveSetupChoice(await readSetupSettings(f.cwd, f.agentDir), "enabled", recommended, additive);
|
|
86
|
-
const saved = JSON.parse(await readFile(f.projectPath, "utf8"));
|
|
87
|
-
assert.deepEqual(saved.defaultTools, additive ? ["+grep", "+find", "+ls"] : [...base.slice(0, 4), ...recommended]);
|
|
88
|
-
assert.equal((await stat(f.projectPath)).mode & 0o777, 0o600);
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("Don't ask again records only a project preference and leaves defaultTools untouched", async () => {
|
|
94
|
-
await fixture(async (f) => {
|
|
95
|
-
await f.project({ defaultTools: ["-find"], theme: "orange" });
|
|
96
|
-
await saveSetupChoice(await readSetupSettings(f.cwd, f.agentDir), "disabled", recommended, true);
|
|
97
|
-
const saved = JSON.parse(await readFile(f.projectPath, "utf8"));
|
|
98
|
-
assert.deepEqual(saved.defaultTools, ["-find"]);
|
|
99
|
-
assert.equal(saved.theme, "orange");
|
|
100
|
-
assert.equal(saved.piexis.recommendedTools, "disabled");
|
|
101
|
-
assert.deepEqual(toolsToOffer(await readSetupSettings(f.cwd, f.agentDir), recommended, base, true), []);
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("malformed settings and non-regular files are never replaced", async () => {
|
|
106
|
-
for (const invalid of ["{ broken", "[]", "null", '{"defaultTools":"read"}', '{"defaultTools":[3]}', '{"piexis":false}']) {
|
|
107
|
-
await fixture(async (f) => {
|
|
108
|
-
await f.project(invalid);
|
|
109
|
-
await assert.rejects(readSetupSettings(f.cwd, f.agentDir));
|
|
110
|
-
assert.equal(await readFile(f.projectPath, "utf8"), invalid);
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
await fixture(async (f) => {
|
|
114
|
-
await f.global({ theme: "dark" });
|
|
115
|
-
await mkdir(join(f.cwd, ".pi"));
|
|
116
|
-
await symlink(f.globalPath, f.projectPath);
|
|
117
|
-
await assert.rejects(readSetupSettings(f.cwd, f.agentDir), /Not a regular/);
|
|
118
|
-
assert.deepEqual(JSON.parse(await readFile(f.globalPath, "utf8")), { theme: "dark" });
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("project settings cannot alias the user settings directory", async () => {
|
|
123
|
-
await fixture(async (f) => {
|
|
124
|
-
await f.global({ theme: "dark" });
|
|
125
|
-
await assert.rejects(readSetupSettings(f.cwd, join(f.cwd, ".pi")), /share a path/);
|
|
126
|
-
await symlink(f.agentDir, join(f.cwd, ".pi"), "dir");
|
|
127
|
-
await assert.rejects(readSetupSettings(f.cwd, f.agentDir), /share a path/);
|
|
128
|
-
assert.deepEqual(JSON.parse(await readFile(f.globalPath, "utf8")), { theme: "dark" });
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test("settings changes during the prompt and another writer's lock prevent overwrites", async () => {
|
|
133
|
-
for (const scope of ["project", "global"]) {
|
|
134
|
-
await fixture(async (f) => {
|
|
135
|
-
await f.project({ theme: "dark" });
|
|
136
|
-
const settings = await readSetupSettings(f.cwd, f.agentDir);
|
|
137
|
-
await f[scope]({ theme: "orange" });
|
|
138
|
-
const latest = await readFile(f.projectPath, "utf8");
|
|
139
|
-
await assert.rejects(saveSetupChoice(settings, "enabled", recommended, true), /Settings changed/);
|
|
140
|
-
assert.equal(await readFile(f.projectPath, "utf8"), latest);
|
|
141
|
-
assert.deepEqual(await readdir(join(f.cwd, ".pi")), ["settings.json"]);
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
await fixture(async (f) => {
|
|
145
|
-
await f.project({ theme: "dark" });
|
|
146
|
-
const settings = await readSetupSettings(f.cwd, f.agentDir);
|
|
147
|
-
await mkdir(`${f.projectPath}.lock`);
|
|
148
|
-
await assert.rejects(saveSetupChoice(settings, "enabled", recommended, true), { code: "EEXIST" });
|
|
149
|
-
assert.equal((await stat(`${f.projectPath}.lock`)).isDirectory(), true);
|
|
150
|
-
assert.deepEqual(JSON.parse(await readFile(f.projectPath, "utf8")), { theme: "dark" });
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("an aborted save does not create project settings", async () => {
|
|
155
|
-
await fixture(async (f) => {
|
|
156
|
-
const controller = new AbortController();
|
|
157
|
-
controller.abort();
|
|
158
|
-
await assert.rejects(saveSetupChoice(await readSetupSettings(f.cwd, f.agentDir), "enabled", recommended, true, controller.signal), { name: "AbortError" });
|
|
159
|
-
await assert.rejects(stat(join(f.cwd, ".pi")), { code: "ENOENT" });
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
async function environment(f, work) {
|
|
164
|
-
const oldDir = process.env.PI_CODING_AGENT_DIR;
|
|
165
|
-
const oldArgv = process.argv;
|
|
166
|
-
const streams = [process.stdin, process.stdout];
|
|
167
|
-
const saved = streams.map((stream) => Object.getOwnPropertyDescriptor(stream, "isTTY"));
|
|
168
|
-
process.env.PI_CODING_AGENT_DIR = f.agentDir;
|
|
169
|
-
process.argv = [process.execPath, "pi"];
|
|
170
|
-
streams.forEach((stream) => Object.defineProperty(stream, "isTTY", { configurable: true, value: true }));
|
|
171
|
-
try { await work(); } finally {
|
|
172
|
-
process.argv = oldArgv;
|
|
173
|
-
if (oldDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
|
174
|
-
else process.env.PI_CODING_AGENT_DIR = oldDir;
|
|
175
|
-
streams.forEach((stream, i) => saved[i] ? Object.defineProperty(stream, "isTTY", saved[i]) : delete stream.isTTY);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function harness(f, select = async () => "Enable") {
|
|
180
|
-
const handlers = new Map();
|
|
181
|
-
const notifications = [];
|
|
182
|
-
let active = [...base], prompts = 0, activations = 0;
|
|
183
|
-
let available = [...base.slice(0, 4), ...recommended].map((name) => ({ name, sourceInfo: { source: "builtin" } }));
|
|
184
|
-
available.push({ name: "subagent", sourceInfo: { source: "extension" } });
|
|
185
|
-
const pi = { on(name, handler) { handlers.set(name, handler); }, getActiveTools() { return active; }, getAllTools() { return available; }, setActiveTools(next) { active = next; activations++; } };
|
|
186
|
-
recommendedToolsExtension(pi);
|
|
187
|
-
const ctx = { cwd: f.cwd, mode: "tui", hasUI: true, model: { provider: "test", id: "default" }, isProjectTrusted: () => true, ui: {
|
|
188
|
-
async select(...args) { prompts++; return select(...args); },
|
|
189
|
-
notify(...args) { notifications.push(args); }
|
|
190
|
-
} };
|
|
191
|
-
return { ctx, notifications, start: () => handlers.get("session_start")({}, ctx), shutdown: () => handlers.get("session_shutdown")(),
|
|
192
|
-
set available(value) { available = value; }, get active() { return active; }, get prompts() { return prompts; }, get activations() { return activations; } };
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
test("automatic startup approval enables immediately, preserves active extensions and remembers choice", async () => {
|
|
196
|
-
await fixture(async (f) => environment(f, async () => {
|
|
197
|
-
const h = harness(f, async (title, options) => {
|
|
198
|
-
assert.match(title, /grep · find · ls/);
|
|
199
|
-
assert.deepEqual(options, ["Not now", "Enable", "Don't ask again"]);
|
|
200
|
-
return "Enable";
|
|
201
|
-
});
|
|
202
|
-
await h.start();
|
|
203
|
-
assert.deepEqual(h.active, [...base, ...recommended]);
|
|
204
|
-
assert.equal(h.activations, 1);
|
|
205
|
-
assert.equal(h.notifications[0][1], "info");
|
|
206
|
-
const saved = JSON.parse(await readFile(f.projectPath, "utf8"));
|
|
207
|
-
assert.equal(saved.piexis.recommendedTools, "enabled");
|
|
208
|
-
assert.deepEqual(JSON.parse(await readFile(join(f.cwd, ".pi", "agent.json"), "utf8")), {
|
|
209
|
-
$schema: agentConfigSchemaUrl, model: "test/default", small_model: "test/default", subagent_model: "test/default", delegate_exploration: false,
|
|
210
|
-
subagent_variant_overrides: {
|
|
211
|
-
plan: { model: "test/default", effort: "high" },
|
|
212
|
-
orchestrator: { model: "test/default", effort: "high" },
|
|
213
|
-
debug: { model: "test/default", effort: "high" },
|
|
214
|
-
ask: { model: "test/default", effort: "low" },
|
|
215
|
-
code: { model: "test/default", effort: "medium" },
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
assert.deepEqual(saved.defaultTools, supportsAdditiveTools ? recommended.map((name) => `+${name}`) : [...base.slice(0, 4), ...recommended]);
|
|
219
|
-
await h.start();
|
|
220
|
-
assert.equal(h.prompts, 1);
|
|
221
|
-
const restarted = harness(f);
|
|
222
|
-
await restarted.start();
|
|
223
|
-
assert.equal(restarted.prompts, 0);
|
|
224
|
-
}));
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
test("previously enabled projects are offered missing agent setup without changing tool policy", async () => {
|
|
228
|
-
await fixture(async (f) => environment(f, async () => {
|
|
229
|
-
const existing = { defaultTools: [...base.slice(0, 4), ...recommended], piexis: { recommendedTools: "enabled", custom: 42 }, theme: "orange" };
|
|
230
|
-
await f.project(existing);
|
|
231
|
-
const h = harness(f, async (title, options) => {
|
|
232
|
-
assert.match(title, /agent/i);
|
|
233
|
-
assert.deepEqual(options, ["Not now", "Enable", "Don't ask again"]);
|
|
234
|
-
return "Enable";
|
|
235
|
-
});
|
|
236
|
-
await h.start();
|
|
237
|
-
assert.equal(h.prompts, 1);
|
|
238
|
-
assert.equal(h.activations, 0);
|
|
239
|
-
const saved = JSON.parse(await readFile(f.projectPath, "utf8"));
|
|
240
|
-
assert.deepEqual(saved.defaultTools, existing.defaultTools);
|
|
241
|
-
assert.equal(saved.piexis.recommendedTools, "enabled");
|
|
242
|
-
assert.equal(saved.piexis.custom, 42);
|
|
243
|
-
assert.equal(saved.theme, "orange");
|
|
244
|
-
assert.equal(JSON.parse(await readFile(join(f.cwd, ".pi", "agent.json"), "utf8")).model, "test/default");
|
|
245
|
-
const restarted = harness(f);
|
|
246
|
-
await restarted.start();
|
|
247
|
-
assert.equal(restarted.prompts, 0);
|
|
248
|
-
}));
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
test("agent-only setup cancellation and dismissal preserve the earlier tool choice", async () => {
|
|
252
|
-
for (const choice of ["Not now", undefined, "Don't ask again"]) {
|
|
253
|
-
await fixture(async (f) => environment(f, async () => {
|
|
254
|
-
await f.project({ defaultTools: [], piexis: { recommendedTools: "enabled" }, custom: 42 });
|
|
255
|
-
const before = await readFile(f.projectPath, "utf8");
|
|
256
|
-
const h = harness(f, async () => choice);
|
|
257
|
-
await h.start();
|
|
258
|
-
assert.equal(h.prompts, 1);
|
|
259
|
-
assert.equal(h.activations, 0);
|
|
260
|
-
await assert.rejects(stat(join(f.cwd, ".pi", "agent.json")), { code: "ENOENT" });
|
|
261
|
-
if (choice === "Don't ask again") {
|
|
262
|
-
assert.deepEqual(JSON.parse(await readFile(f.projectPath, "utf8")), {
|
|
263
|
-
defaultTools: [], piexis: { recommendedTools: "enabled", agentSetup: "disabled" }, custom: 42
|
|
264
|
-
});
|
|
265
|
-
const restarted = harness(f);
|
|
266
|
-
await restarted.start();
|
|
267
|
-
assert.equal(restarted.prompts, 0);
|
|
268
|
-
} else assert.equal(await readFile(f.projectPath, "utf8"), before);
|
|
269
|
-
}));
|
|
270
|
-
}
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
test("agent-only approval preserves restrictive and older tool policies, including a prior tool decline", async () => {
|
|
274
|
-
for (const additive of [true, false]) {
|
|
275
|
-
await fixture(async (f) => {
|
|
276
|
-
const original = { defaultTools: [], piexis: { recommendedTools: "disabled" }, other: 42 };
|
|
277
|
-
await f.project(original);
|
|
278
|
-
await saveSetupChoice(await readSetupSettings(f.cwd, f.agentDir), "enabled", [], additive, undefined, "enabled");
|
|
279
|
-
assert.deepEqual(JSON.parse(await readFile(f.projectPath, "utf8")), { ...original, piexis: { ...original.piexis, agentSetup: "enabled" } });
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
await fixture(async (f) => environment(f, async () => {
|
|
283
|
-
await f.project({ defaultTools: [], piexis: { recommendedTools: "disabled" } });
|
|
284
|
-
const h = harness(f);
|
|
285
|
-
await h.start();
|
|
286
|
-
assert.equal(h.prompts, 1);
|
|
287
|
-
assert.equal(h.activations, 0);
|
|
288
|
-
assert.deepEqual(JSON.parse(await readFile(f.projectPath, "utf8")), {
|
|
289
|
-
defaultTools: [], piexis: { recommendedTools: "disabled", agentSetup: "enabled" }
|
|
290
|
-
});
|
|
291
|
-
assert.equal(JSON.parse(await readFile(join(f.cwd, ".pi", "agent.json"), "utf8")).model, "test/default");
|
|
292
|
-
}));
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
test("completed, unavailable or explicitly declined agent setup does not offer another prompt", async () => {
|
|
296
|
-
for (const name of ["agent.json", "agents.json", "unavailable", "declined"]) {
|
|
297
|
-
await fixture(async (f) => environment(f, async () => {
|
|
298
|
-
await f.project({ piexis: { recommendedTools: "enabled", ...(name === "declined" ? { agentSetup: "disabled" } : {}) } });
|
|
299
|
-
if (name.endsWith(".json")) await writeFile(join(f.cwd, ".pi", name), '{"model":"test/default"}');
|
|
300
|
-
const h = harness(f);
|
|
301
|
-
if (name === "unavailable") h.available = [...recommended].map((name) => ({ name, sourceInfo: { source: "builtin" } }));
|
|
302
|
-
await h.start();
|
|
303
|
-
assert.equal(h.prompts, 0);
|
|
304
|
-
}));
|
|
305
|
-
}
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
test("Enable preserves existing agent configuration and warns on initialization failures without undoing tools", async () => {
|
|
309
|
-
for (const name of ["agent.json", "agents.json"]) {
|
|
310
|
-
await fixture(async (f) => environment(f, async () => {
|
|
311
|
-
await mkdir(join(f.cwd, ".pi"));
|
|
312
|
-
const path = join(f.cwd, ".pi", name);
|
|
313
|
-
await writeFile(path, "{existing malformed configuration}");
|
|
314
|
-
const h = harness(f);
|
|
315
|
-
await h.start();
|
|
316
|
-
assert.equal(await readFile(path, "utf8"), "{existing malformed configuration}");
|
|
317
|
-
assert.deepEqual(h.active, [...base, ...recommended]);
|
|
318
|
-
assert.equal(h.notifications.some((notice) => notice[1] === "warning"), false);
|
|
319
|
-
if (name === "agents.json") await assert.rejects(stat(join(f.cwd, ".pi", "agent.json")), { code: "ENOENT" });
|
|
320
|
-
}));
|
|
321
|
-
}
|
|
322
|
-
for (const missingModel of [true, false]) {
|
|
323
|
-
await fixture(async (f) => environment(f, async () => {
|
|
324
|
-
const h = harness(f);
|
|
325
|
-
if (missingModel) h.ctx.model = undefined;
|
|
326
|
-
else await writeFile(join(f.agentDir, "agent.json"), "{bad");
|
|
327
|
-
await h.start();
|
|
328
|
-
assert.deepEqual(h.active, [...base, ...recommended]);
|
|
329
|
-
assert.equal(JSON.parse(await readFile(f.projectPath, "utf8")).piexis.recommendedTools, "enabled");
|
|
330
|
-
await assert.rejects(stat(join(f.cwd, ".pi", "agent.json")), { code: "ENOENT" });
|
|
331
|
-
assert.match(h.notifications.find((notice) => notice[1] === "warning")[0], /Agent configuration was not created/);
|
|
332
|
-
}));
|
|
333
|
-
}
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
test("Not now/Esc makes no writes; Don't ask again persists without activating tools", async () => {
|
|
337
|
-
for (const choice of ["Not now", undefined, "Don't ask again"]) {
|
|
338
|
-
await fixture(async (f) => environment(f, async () => {
|
|
339
|
-
const h = harness(f, async () => choice);
|
|
340
|
-
await h.start();
|
|
341
|
-
assert.equal(h.activations, 0);
|
|
342
|
-
await h.start();
|
|
343
|
-
assert.equal(h.prompts, 1);
|
|
344
|
-
await assert.rejects(stat(join(f.cwd, ".pi", "agent.json")), { code: "ENOENT" });
|
|
345
|
-
if (choice === "Don't ask again") {
|
|
346
|
-
const saved = JSON.parse(await readFile(f.projectPath, "utf8"));
|
|
347
|
-
assert.deepEqual(saved, { piexis: { recommendedTools: "disabled", agentSetup: "disabled" } });
|
|
348
|
-
const restarted = harness(f);
|
|
349
|
-
await restarted.start();
|
|
350
|
-
assert.equal(restarted.prompts, 0);
|
|
351
|
-
} else await assert.rejects(stat(join(f.cwd, ".pi")), { code: "ENOENT" });
|
|
352
|
-
}));
|
|
353
|
-
}
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
test("headless, non-TTY, untrusted, CLI-restricted and unavailable tools skip startup setup", async () => {
|
|
357
|
-
await fixture(async (f) => environment(f, async () => {
|
|
358
|
-
for (const patch of [{ mode: "print" }, { mode: "json" }, { mode: "rpc" }, { hasUI: false }, { isProjectTrusted: () => false }]) {
|
|
359
|
-
const h = harness(f);
|
|
360
|
-
Object.assign(h.ctx, patch);
|
|
361
|
-
await h.start();
|
|
362
|
-
assert.equal(h.prompts, 0);
|
|
363
|
-
}
|
|
364
|
-
Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: false });
|
|
365
|
-
const noTTY = harness(f);
|
|
366
|
-
await noTTY.start();
|
|
367
|
-
assert.equal(noTTY.prompts, 0);
|
|
368
|
-
Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: true });
|
|
369
|
-
process.argv.push("--no-builtin-tools");
|
|
370
|
-
const cli = harness(f);
|
|
371
|
-
await cli.start();
|
|
372
|
-
assert.equal(cli.prompts, 0);
|
|
373
|
-
process.argv.pop();
|
|
374
|
-
const unavailable = harness(f);
|
|
375
|
-
unavailable.available = [];
|
|
376
|
-
await unavailable.start();
|
|
377
|
-
assert.equal(unavailable.prompts, 0);
|
|
378
|
-
await assert.rejects(stat(join(f.cwd, ".pi")), { code: "ENOENT" });
|
|
379
|
-
}));
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
test("startup shutdown cancels pending dialog without activation or writes", async () => {
|
|
383
|
-
await fixture(async (f) => environment(f, async () => {
|
|
384
|
-
let shown;
|
|
385
|
-
const ready = new Promise((resolve) => { shown = resolve; });
|
|
386
|
-
const h = harness(f, (_title, _options, { signal }) => new Promise((resolve) => {
|
|
387
|
-
signal.addEventListener("abort", () => resolve(undefined), { once: true });
|
|
388
|
-
shown();
|
|
389
|
-
}));
|
|
390
|
-
const waiting = h.start();
|
|
391
|
-
await ready;
|
|
392
|
-
await h.start();
|
|
393
|
-
assert.equal(h.prompts, 1);
|
|
394
|
-
await h.shutdown();
|
|
395
|
-
await waiting;
|
|
396
|
-
assert.equal(h.activations, 0);
|
|
397
|
-
await assert.rejects(stat(join(f.cwd, ".pi")), { code: "ENOENT" });
|
|
398
|
-
}));
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
test("startup errors or changed tool availability warn without changing runtime tools", async () => {
|
|
402
|
-
await fixture(async (f) => environment(f, async () => {
|
|
403
|
-
await f.project("{ broken");
|
|
404
|
-
const invalid = harness(f);
|
|
405
|
-
await invalid.start();
|
|
406
|
-
assert.equal(invalid.prompts, 0);
|
|
407
|
-
assert.equal(invalid.notifications[0][1], "warning");
|
|
408
|
-
assert.equal(await readFile(f.projectPath, "utf8"), "{ broken");
|
|
409
|
-
await f.project({});
|
|
410
|
-
const changed = harness(f, async () => { changed.available = []; return "Enable"; });
|
|
411
|
-
await changed.start();
|
|
412
|
-
assert.equal(changed.activations, 0);
|
|
413
|
-
assert.deepEqual(JSON.parse(await readFile(f.projectPath, "utf8")), {});
|
|
414
|
-
assert.equal(changed.notifications[0][1], "warning");
|
|
415
|
-
await mkdir(`${f.projectPath}.lock`);
|
|
416
|
-
const locked = harness(f);
|
|
417
|
-
await locked.start();
|
|
418
|
-
assert.equal(locked.activations, 0);
|
|
419
|
-
assert.equal(locked.notifications[0][1], "warning");
|
|
420
|
-
assert.deepEqual(JSON.parse(await readFile(f.projectPath, "utf8")), {});
|
|
421
|
-
assert.equal((await stat(`${f.projectPath}.lock`)).isDirectory(), true);
|
|
422
|
-
}));
|
|
423
|
-
});
|
package/tests/resources.test.mjs
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { access, readFile, readdir } from "node:fs/promises";
|
|
3
|
-
import test from "node:test";
|
|
4
|
-
import askQuestion from "../extensions/ask-question.ts";
|
|
5
|
-
import ui from "../extensions/ui.ts";
|
|
6
|
-
|
|
7
|
-
const root = new URL("../", import.meta.url);
|
|
8
|
-
|
|
9
|
-
test("manifest loads only retained extensions and resources", async () => {
|
|
10
|
-
const manifest = JSON.parse(await readFile(new URL("package.json", root), "utf8"));
|
|
11
|
-
assert.deepEqual(manifest.pi.extensions, ["./extensions/ask-question.ts", "./extensions/ui.ts", "./extensions/recommended-tools.ts", "./extensions/agents.ts"]);
|
|
12
|
-
for (const paths of Object.values(manifest.pi)) {
|
|
13
|
-
for (const path of paths) await access(new URL(path, root));
|
|
14
|
-
}
|
|
15
|
-
assert.deepEqual((await readdir(new URL("extensions/", root))).sort(), ["agents.ts", "ask-question.ts", "recommended-tools.ts", "ui.ts"]);
|
|
16
|
-
assert.equal(manifest.dependencies?.["@anthropic-ai/sandbox-runtime"], undefined);
|
|
17
|
-
for (const removed of ["test:runtime", "test:sandbox", "eval:permissions"]) {
|
|
18
|
-
assert.equal(manifest.scripts[removed], undefined);
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test("AskQuestion works independently and retains non-TUI handling", async () => {
|
|
23
|
-
let tool;
|
|
24
|
-
askQuestion({ on() {}, registerTool(value) { tool = value; } });
|
|
25
|
-
assert.equal(tool.name, "AskQuestion");
|
|
26
|
-
assert.doesNotMatch(tool.description, /permission|mode commands/i);
|
|
27
|
-
const params = { questions: [{ question: "Which?", options: [{ label: "A" }] }] };
|
|
28
|
-
const headless = await tool.execute("id", params, undefined, undefined, { mode: "json" });
|
|
29
|
-
assert.equal(headless.details.status, "requires_interaction");
|
|
30
|
-
assert.deepEqual(headless.details.answers, []);
|
|
31
|
-
assert.equal(headless.details.cancelled, true);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("UI renders without mode controls or permission status dependencies", async () => {
|
|
35
|
-
const handlers = new Map();
|
|
36
|
-
ui({ on(name, handler) { handlers.set(name, handler); }, getThinkingLevel: () => "off" });
|
|
37
|
-
let header, footer;
|
|
38
|
-
const theme = { fg: (_color, text) => text, bold: (text) => text };
|
|
39
|
-
const ctx = {
|
|
40
|
-
mode: "tui", cwd: "/tmp/project", model: { id: "test-model" },
|
|
41
|
-
sessionManager: { getBranch: () => [], getEntries: () => [] },
|
|
42
|
-
getContextUsage: () => ({ percent: 12, tokens: 24_000, contextWindow: 200_000 }),
|
|
43
|
-
ui: {
|
|
44
|
-
setTitle() {},
|
|
45
|
-
setHeader(factory) { header = factory({}, theme); },
|
|
46
|
-
setFooter(factory) {
|
|
47
|
-
footer = factory({ requestRender() {} }, theme, {
|
|
48
|
-
onBranchChange: () => () => {}, getGitBranch: () => "main"
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
await handlers.get("session_start")({}, ctx);
|
|
54
|
-
for (const width of [60, 120]) {
|
|
55
|
-
assert.doesNotMatch(header.render(width).join("\n"), /Alt\+M|\/plan|permission/i);
|
|
56
|
-
const dashboard = footer.render(width).join("\n");
|
|
57
|
-
for (const value of [/ready/, /main/, /test-model/, /think off/, /ctx .*12%/]) {
|
|
58
|
-
assert.match(dashboard, value);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
await handlers.get("tool_call")({ toolName: "read" });
|
|
62
|
-
assert.match(footer.render(120)[0], /read/);
|
|
63
|
-
await handlers.get("agent_end")();
|
|
64
|
-
assert.match(footer.render(120)[0], /ready/);
|
|
65
|
-
footer.dispose();
|
|
66
|
-
});
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { readFile, readdir } from "node:fs/promises";
|
|
3
|
-
import test from "node:test";
|
|
4
|
-
|
|
5
|
-
const cases = [
|
|
6
|
-
["This API sometimes returns 500 errors.", "diagnosing-bugs", /unknown bugs|server errors/i],
|
|
7
|
-
["The cause is already known. Patch this null check and add a regression test.", "diagnosing-bugs", /known-cause small fix|narrow patch/i],
|
|
8
|
-
["Implement the feature described in SPEC.md.", "lean-build", /new feature|ticket or spec/i],
|
|
9
|
-
["Review this PR for correctness.", "code-review", /correctness/i],
|
|
10
|
-
["Review this PR for unnecessary complexity.", "code-review", /simplicity/i],
|
|
11
|
-
["Audit this repository for architecture problems.", "codebase-design", /architecture audit/i],
|
|
12
|
-
["Help me design the architecture for this service.", "codebase-design", /module and service architecture/i],
|
|
13
|
-
["Turn these requirements into a spec.", "grilling", /turn requirements.*spec/i],
|
|
14
|
-
["I have an idea but the requirements are unclear.", "grilling", /clarify/i],
|
|
15
|
-
["Refactor this module without changing behavior.", "safe-refactor", /preserving behavior/i],
|
|
16
|
-
["Migrate this database schema with zero downtime.", "migration", /compatibility-safe transitions/i],
|
|
17
|
-
["Implement this test-first.", "tdd", /test-first/i],
|
|
18
|
-
["Verify whether this implementation meets the acceptance criteria. Do not modify it.", "verify-and-stop", /acceptance conditions/i],
|
|
19
|
-
["Build an MCP server.", "mcp-builder", /MCP .*servers/i],
|
|
20
|
-
["Find a skill for deployment.", "find-skills", /discover and install agent skills/i],
|
|
21
|
-
["Create an Excel workbook.", "xlsx", /spreadsheet/i],
|
|
22
|
-
["Design the frontend for this dashboard.", "frontend-design", /product web interface/i],
|
|
23
|
-
["Keep chat responses brief without losing technical facts.", "caveman", /every technical fact kept/i],
|
|
24
|
-
["Build the smallest complete solution.", "ponytail", /smallest change that fully solves the task/i],
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
async function skillText(name) {
|
|
28
|
-
return readFile(new URL(`../skills/${name}/SKILL.md`, import.meta.url), "utf8");
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
test("routing contracts name one canonical skill for each core request", async () => {
|
|
32
|
-
for (const [prompt, skill, expected] of cases) {
|
|
33
|
-
const text = await skillText(skill);
|
|
34
|
-
assert.match(text, expected, `${prompt} should route to ${skill}`);
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("active canonical skills do not require removed setup workflows", async () => {
|
|
39
|
-
const text = await skillText("code-review");
|
|
40
|
-
assert.doesNotMatch(text, /issue-tracker\.md/i);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("only canonical workflows and bundled voice/build skills remain active", async () => {
|
|
44
|
-
const active = (await readdir(new URL("../skills/", import.meta.url), { withFileTypes: true }))
|
|
45
|
-
.filter((entry) => entry.isDirectory())
|
|
46
|
-
.map((entry) => entry.name)
|
|
47
|
-
.sort();
|
|
48
|
-
assert.deepEqual(active, [
|
|
49
|
-
"caveman", "code-review", "codebase-design", "diagnosing-bugs", "docx",
|
|
50
|
-
"find-skills", "frontend-design", "grilling", "lean-build", "mcp-builder", "migration", "pdf",
|
|
51
|
-
"ponytail", "pptx", "safe-refactor", "tdd", "verify-and-stop", "webapp-testing",
|
|
52
|
-
"writing-for-agents", "xlsx",
|
|
53
|
-
]);
|
|
54
|
-
});
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { createRequire } from "node:module";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { pathToFileURL } from "node:url";
|
|
7
|
-
import test from "node:test";
|
|
8
|
-
|
|
9
|
-
test("UI tip JSON reload bypasses Pi-loader legacy data and rejects invalid tips", async () => {
|
|
10
|
-
const hostRequire = createRequire(import.meta.resolve("@earendil-works/pi-coding-agent"));
|
|
11
|
-
const jitiPackage = pathToFileURL(hostRequire.resolve("jiti/package.json"));
|
|
12
|
-
const { createJiti } = await import(new URL("./lib/jiti-static.mjs", jitiPackage).href);
|
|
13
|
-
const root = await mkdtemp(join(tmpdir(), "piexis-ui-reload-"));
|
|
14
|
-
const entry = join(root, "extensions", "ui.ts");
|
|
15
|
-
const asset = join(root, "lib", "ui-tips.json");
|
|
16
|
-
const load = () => createJiti(import.meta.url, {
|
|
17
|
-
moduleCache: false,
|
|
18
|
-
alias: {
|
|
19
|
-
"@earendil-works/pi-tui": import.meta.resolve("@earendil-works/pi-tui"),
|
|
20
|
-
"@earendil-works/pi-coding-agent": import.meta.resolve("@earendil-works/pi-coding-agent")
|
|
21
|
-
}
|
|
22
|
-
}).import(entry, { default: true });
|
|
23
|
-
async function render(factory) {
|
|
24
|
-
const handlers = new Map();
|
|
25
|
-
let header;
|
|
26
|
-
factory({ on(name, handler) { handlers.set(name, handler); } });
|
|
27
|
-
await handlers.get("session_start")({}, {
|
|
28
|
-
mode: "tui", cwd: "/projects/demo",
|
|
29
|
-
ui: {
|
|
30
|
-
setTitle() {}, setFooter() {},
|
|
31
|
-
setHeader(make) { header = make({}, { fg: (_color, text) => text, bold: (text) => text }); }
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
return header.render(200).join("\n");
|
|
35
|
-
}
|
|
36
|
-
try {
|
|
37
|
-
await mkdir(join(root, "extensions"));
|
|
38
|
-
await mkdir(join(root, "lib"));
|
|
39
|
-
await writeFile(asset, JSON.stringify(["Legacy cached tip"]));
|
|
40
|
-
await writeFile(join(root, "lib", "agent-config.ts"), await readFile(new URL("../lib/agent-config.ts", import.meta.url), "utf8"));
|
|
41
|
-
await writeFile(join(root, "lib", "agents.ts"), await readFile(new URL("../lib/agents.ts", import.meta.url), "utf8"));
|
|
42
|
-
await writeFile(entry, 'import tips from "../lib/ui-tips.json" with { type: "json" }; export default () => tips;');
|
|
43
|
-
assert.deepEqual((await load())(), ["Legacy cached tip"]);
|
|
44
|
-
|
|
45
|
-
const tips = [
|
|
46
|
-
{ category: "Coding", text: "Fresh coding tip" },
|
|
47
|
-
{ category: "Style", text: "Fresh style tip" },
|
|
48
|
-
{ category: "Planning", text: "Fresh planning tip" }
|
|
49
|
-
];
|
|
50
|
-
await writeFile(asset, JSON.stringify(tips));
|
|
51
|
-
await writeFile(entry, await readFile(new URL("../extensions/ui.ts", import.meta.url), "utf8"));
|
|
52
|
-
const text = await render(await load());
|
|
53
|
-
for (const tip of tips) assert.ok(text.includes(`[${tip.category}] ${tip.text}`));
|
|
54
|
-
assert.doesNotMatch(text, /undefined|Legacy cached tip/);
|
|
55
|
-
|
|
56
|
-
const updated = tips.map((tip) => ({ ...tip, text: `Updated ${tip.text}` }));
|
|
57
|
-
await writeFile(asset, JSON.stringify(updated));
|
|
58
|
-
const factory = await load();
|
|
59
|
-
const reloaded = await render(factory);
|
|
60
|
-
for (const tip of updated) assert.ok(reloaded.includes(tip.text));
|
|
61
|
-
assert.doesNotMatch(reloaded, /undefined/);
|
|
62
|
-
|
|
63
|
-
for (const invalid of [null, [null], ["Legacy tip"], [{ category: "Coding" }], [{ category: "", text: "Tip" }]]) {
|
|
64
|
-
await writeFile(asset, JSON.stringify(invalid));
|
|
65
|
-
assert.throws(() => factory({ on() {} }), /Invalid ui-tips.json/);
|
|
66
|
-
}
|
|
67
|
-
} finally {
|
|
68
|
-
await rm(root, { recursive: true, force: true });
|
|
69
|
-
}
|
|
70
|
-
});
|