@xynogen/pix-core 0.2.4 → 0.3.0
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 +11 -17
- package/skills/ask-user/SKILL.md +0 -48
- package/src/commands/agent-sop/agent-sop.ts +0 -58
- package/src/commands/clear/clear.ts +0 -32
- package/src/commands/diff/diff.ts +0 -32
- package/src/commands/models/models.test.ts +0 -95
- package/src/commands/models/models.ts +0 -367
- package/src/commands/models/patch-builtin.test.ts +0 -66
- package/src/commands/models/patch-builtin.ts +0 -120
- package/src/commands/tools.test.ts +0 -15
- package/src/commands/update/update.test.ts +0 -112
- package/src/commands/update/update.ts +0 -271
- package/src/index.ts +0 -45
- package/src/lib/data.ts +0 -33
- package/src/nudge/capability.test.ts +0 -258
- package/src/nudge/capability.ts +0 -189
- package/src/nudge/index.ts +0 -17
- package/src/nudge/tools.test.ts +0 -157
- package/src/nudge/tools.ts +0 -212
- package/src/tool/ask/ask.test.ts +0 -243
- package/src/tool/ask/components.ts +0 -55
- package/src/tool/ask/helpers.ts +0 -77
- package/src/tool/ask/index.ts +0 -130
- package/src/tool/ask/questionnaire.ts +0 -693
- package/src/tool/ask/rpc.ts +0 -84
- package/src/tool/ask/schema.ts +0 -69
- package/src/tool/ask/single-select-layout.test.ts +0 -124
- package/src/tool/ask/single-select-layout.ts +0 -237
- package/src/tool/ask/types.ts +0 -17
- package/src/tool/todo/todo.test.ts +0 -646
- package/src/tool/todo/todo.ts +0 -218
- package/src/tool/toolbox/toolbox.test.ts +0 -314
- package/src/tool/toolbox/toolbox.ts +0 -570
- package/src/ui/diagnostics.ts +0 -145
- package/src/ui/footer.ts +0 -513
- package/src/ui/welcome.test.ts +0 -124
- package/src/ui/welcome.ts +0 -369
package/src/tool/todo/todo.ts
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* todo.ts — durable execution checklist tool
|
|
3
|
-
*
|
|
4
|
-
* Extracted from the plan extension: the checklist is BUILD-phase execution
|
|
5
|
-
* state that survives context compaction and session restore (persisted via
|
|
6
|
-
* appendEntry("todo-state")). It is universal — other tools and workflow
|
|
7
|
-
* extensions (like plan) drive it — so it lives in pix-core and registers the
|
|
8
|
-
* `todo` tool. State, persistence, and restore are owned end to end here; the
|
|
9
|
-
* checklist is seeded by the model via the tool's `set` action.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
14
|
-
import { Type } from "typebox";
|
|
15
|
-
|
|
16
|
-
export type TodoStatus = "pending" | "in_progress" | "done" | "blocked";
|
|
17
|
-
|
|
18
|
-
export interface TodoItem {
|
|
19
|
-
id: number;
|
|
20
|
-
text: string;
|
|
21
|
-
status: TodoStatus;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const TODO_GLYPH: Record<TodoStatus, string> = {
|
|
25
|
-
pending: "○",
|
|
26
|
-
in_progress: "◐",
|
|
27
|
-
done: "●",
|
|
28
|
-
blocked: "⊘",
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/** Theme color key per status — drives both glyph and (for active) row tint. */
|
|
32
|
-
const TODO_COLOR: Record<TodoStatus, string> = {
|
|
33
|
-
pending: "muted",
|
|
34
|
-
in_progress: "accent",
|
|
35
|
-
done: "success",
|
|
36
|
-
blocked: "error",
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export type TodoTheme = {
|
|
40
|
-
fg: (color: string, text: string) => string;
|
|
41
|
-
bold: (text: string) => string;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/** Colored checklist for the TUI: glyphs tinted by status, active row bold. */
|
|
45
|
-
export function renderTodoLines(items: TodoItem[], theme: TodoTheme): string {
|
|
46
|
-
if (!items.length) return theme.fg("muted", "(no todos)");
|
|
47
|
-
const done = items.filter((t) => t.status === "done").length;
|
|
48
|
-
const head = theme.fg("muted", `Todos ${done}/${items.length} done:`);
|
|
49
|
-
const lines = items.map((t) => {
|
|
50
|
-
const color = TODO_COLOR[t.status];
|
|
51
|
-
const glyph = theme.fg(color, TODO_GLYPH[t.status]);
|
|
52
|
-
const body = `${t.id}. ${t.text}`;
|
|
53
|
-
// Highlight the in-flight task so the eye lands on it first.
|
|
54
|
-
const label =
|
|
55
|
-
t.status === "in_progress"
|
|
56
|
-
? theme.bold(theme.fg("accent", body))
|
|
57
|
-
: theme.fg(t.status === "done" ? "muted" : "text", body);
|
|
58
|
-
return `${glyph} ${label}`;
|
|
59
|
-
});
|
|
60
|
-
return `${head}\n${lines.join("\n")}`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const parseItems = (raw: string): string[] =>
|
|
64
|
-
raw
|
|
65
|
-
.split("\n")
|
|
66
|
-
.map((l) => l.replace(/^\s*(?:\d+[.)]|[-*•])\s*/, "").trim())
|
|
67
|
-
.filter(Boolean);
|
|
68
|
-
|
|
69
|
-
export default function registerTodo(pi: ExtensionAPI): void {
|
|
70
|
-
let todos: TodoItem[] = [];
|
|
71
|
-
let nextTodoId = 1;
|
|
72
|
-
|
|
73
|
-
function persistTodos() {
|
|
74
|
-
pi.appendEntry("todo-state", { todos, nextTodoId });
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function todoSummary(): string {
|
|
78
|
-
if (!todos.length) return "(no todos)";
|
|
79
|
-
const done = todos.filter((t) => t.status === "done").length;
|
|
80
|
-
const lines = todos.map(
|
|
81
|
-
(t) => `${TODO_GLYPH[t.status]} ${t.id}. ${t.text}`,
|
|
82
|
-
);
|
|
83
|
-
return `Todos ${done}/${todos.length} done:\n${lines.join("\n")}`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Durable execution checklist for BUILD mode. Survives context compaction
|
|
87
|
-
// and session restore. Workflows like plan instruct the model to seed it
|
|
88
|
-
// from a plan's "Implementation Phases" so it stays anchored to plan.md.
|
|
89
|
-
pi.registerTool({
|
|
90
|
-
name: "todo",
|
|
91
|
-
label: "Todo",
|
|
92
|
-
description:
|
|
93
|
-
"Track BUILD-phase execution progress. Durable across context compaction. Actions: list, set (replace all items from newline/numbered text), add, update (change one item's status), clear.",
|
|
94
|
-
promptSnippet:
|
|
95
|
-
"todo(action, items?, id?, status?, text?) — action: list|set|add|update|clear. Use to track implementation progress, especially when executing a plan.",
|
|
96
|
-
promptGuidelines: [
|
|
97
|
-
"When you start executing a multi-step plan in BUILD mode, seed the todo list with `todo(action:'set', items: <plan Implementation Phases>)`.",
|
|
98
|
-
"Mark each item in_progress before working it and done when finished via `todo(action:'update', id, status)`.",
|
|
99
|
-
"Call `todo(action:'list')` to recover your place after long runs or context compaction.",
|
|
100
|
-
],
|
|
101
|
-
parameters: Type.Object({
|
|
102
|
-
action: Type.Union(
|
|
103
|
-
[
|
|
104
|
-
Type.Literal("list"),
|
|
105
|
-
Type.Literal("set"),
|
|
106
|
-
Type.Literal("add"),
|
|
107
|
-
Type.Literal("update"),
|
|
108
|
-
Type.Literal("clear"),
|
|
109
|
-
],
|
|
110
|
-
{ description: "Operation to perform" },
|
|
111
|
-
),
|
|
112
|
-
items: Type.Optional(
|
|
113
|
-
Type.String({
|
|
114
|
-
description:
|
|
115
|
-
"For set/add: newline-separated or numbered list of todo texts.",
|
|
116
|
-
}),
|
|
117
|
-
),
|
|
118
|
-
id: Type.Optional(
|
|
119
|
-
Type.Number({ description: "For update: target todo id." }),
|
|
120
|
-
),
|
|
121
|
-
status: Type.Optional(
|
|
122
|
-
Type.Union(
|
|
123
|
-
[
|
|
124
|
-
Type.Literal("pending"),
|
|
125
|
-
Type.Literal("in_progress"),
|
|
126
|
-
Type.Literal("done"),
|
|
127
|
-
Type.Literal("blocked"),
|
|
128
|
-
],
|
|
129
|
-
{ description: "For update: new status." },
|
|
130
|
-
),
|
|
131
|
-
),
|
|
132
|
-
text: Type.Optional(
|
|
133
|
-
Type.String({
|
|
134
|
-
description: "For update: replacement text (optional).",
|
|
135
|
-
}),
|
|
136
|
-
),
|
|
137
|
-
}),
|
|
138
|
-
renderResult(_result, _options, theme) {
|
|
139
|
-
return new Text(renderTodoLines(todos, theme as TodoTheme), 0, 0);
|
|
140
|
-
},
|
|
141
|
-
|
|
142
|
-
async execute(_id, params) {
|
|
143
|
-
// AgentToolResult now requires a `details` field. These todo results have
|
|
144
|
-
// no structured details, so emit `undefined` via small local helpers.
|
|
145
|
-
const ok = (text: string) => ({
|
|
146
|
-
content: [{ type: "text" as const, text }],
|
|
147
|
-
details: undefined,
|
|
148
|
-
});
|
|
149
|
-
const fail = (text: string) => ({
|
|
150
|
-
content: [{ type: "text" as const, text }],
|
|
151
|
-
details: undefined,
|
|
152
|
-
isError: true,
|
|
153
|
-
});
|
|
154
|
-
switch (params.action) {
|
|
155
|
-
case "list":
|
|
156
|
-
return ok(todoSummary());
|
|
157
|
-
|
|
158
|
-
case "set": {
|
|
159
|
-
const texts = parseItems(params.items ?? "");
|
|
160
|
-
if (!texts.length) return fail("set requires non-empty `items`.");
|
|
161
|
-
nextTodoId = 1;
|
|
162
|
-
todos = texts.map((text) => ({
|
|
163
|
-
id: nextTodoId++,
|
|
164
|
-
text,
|
|
165
|
-
status: "pending" as TodoStatus,
|
|
166
|
-
}));
|
|
167
|
-
persistTodos();
|
|
168
|
-
return ok(todoSummary());
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
case "add": {
|
|
172
|
-
const texts = parseItems(params.items ?? "");
|
|
173
|
-
if (!texts.length) return fail("add requires non-empty `items`.");
|
|
174
|
-
for (const text of texts)
|
|
175
|
-
todos.push({ id: nextTodoId++, text, status: "pending" });
|
|
176
|
-
persistTodos();
|
|
177
|
-
return ok(todoSummary());
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
case "update": {
|
|
181
|
-
const t = todos.find((x) => x.id === params.id);
|
|
182
|
-
if (!t) return fail(`No todo with id ${params.id}.`);
|
|
183
|
-
if (params.status) t.status = params.status;
|
|
184
|
-
if (params.text) t.text = params.text;
|
|
185
|
-
persistTodos();
|
|
186
|
-
return ok(todoSummary());
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
case "clear":
|
|
190
|
-
todos = [];
|
|
191
|
-
nextTodoId = 1;
|
|
192
|
-
persistTodos();
|
|
193
|
-
return ok("Todos cleared.");
|
|
194
|
-
|
|
195
|
-
default:
|
|
196
|
-
return fail(`Unknown action: ${String(params.action)}`);
|
|
197
|
-
}
|
|
198
|
-
},
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
// Restore the checklist from session entries so it survives restart.
|
|
202
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
203
|
-
const entries = ctx.sessionManager.getEntries() as Array<{
|
|
204
|
-
type: string;
|
|
205
|
-
customType?: string;
|
|
206
|
-
data?: { todos?: TodoItem[]; nextTodoId?: number };
|
|
207
|
-
}>;
|
|
208
|
-
const lastTodo = entries
|
|
209
|
-
.filter((e) => e.type === "custom" && e.customType === "todo-state")
|
|
210
|
-
.pop();
|
|
211
|
-
if (Array.isArray(lastTodo?.data?.todos)) {
|
|
212
|
-
todos = lastTodo.data.todos;
|
|
213
|
-
nextTodoId =
|
|
214
|
-
lastTodo.data.nextTodoId ??
|
|
215
|
-
todos.reduce((m, t) => Math.max(m, t.id + 1), 1);
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
}
|
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import registerToolbox, {
|
|
6
|
-
buildRows,
|
|
7
|
-
parseTargets,
|
|
8
|
-
renderList,
|
|
9
|
-
type ToggleOps,
|
|
10
|
-
type ToolRow,
|
|
11
|
-
toggleTool,
|
|
12
|
-
} from "./toolbox.ts";
|
|
13
|
-
|
|
14
|
-
// ─── Fixtures ───────────────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
const toolInfo = (name: string, source = "builtin") =>
|
|
17
|
-
({
|
|
18
|
-
name,
|
|
19
|
-
description: `${name} does things.`,
|
|
20
|
-
parameters: {},
|
|
21
|
-
sourceInfo: { source, path: "", scope: "user", origin: "package" },
|
|
22
|
-
}) as never;
|
|
23
|
-
|
|
24
|
-
const mcpToolInfo = (name: string) => toolInfo(name, "mcp:context7");
|
|
25
|
-
|
|
26
|
-
// ─── buildRows ──────────────────────────────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
describe("buildRows", () => {
|
|
29
|
-
test("excludes core tools (bash, edit, read, write)", () => {
|
|
30
|
-
const rows = buildRows([
|
|
31
|
-
toolInfo("bash"),
|
|
32
|
-
toolInfo("edit"),
|
|
33
|
-
toolInfo("read"),
|
|
34
|
-
toolInfo("write"),
|
|
35
|
-
toolInfo("grep"),
|
|
36
|
-
toolInfo("ls"),
|
|
37
|
-
]);
|
|
38
|
-
expect(rows.map((r) => r.name)).toEqual(["grep", "ls"]);
|
|
39
|
-
expect(rows.every((r) => !r.mcp)).toBe(true);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("flags MCP tools", () => {
|
|
43
|
-
const rows = buildRows([mcpToolInfo("ctx_search")]);
|
|
44
|
-
expect(rows[0].mcp).toBe(true);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("empty input returns empty", () => {
|
|
48
|
-
expect(buildRows([])).toEqual([]);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// ─── parseTargets ───────────────────────────────────────────────────────────
|
|
53
|
-
|
|
54
|
-
describe("parseTargets", () => {
|
|
55
|
-
test("splits on commas, spaces, newlines", () => {
|
|
56
|
-
expect(parseTargets("ls, find grep\nfetch")).toEqual([
|
|
57
|
-
"ls",
|
|
58
|
-
"find",
|
|
59
|
-
"grep",
|
|
60
|
-
"fetch",
|
|
61
|
-
]);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("dedupes", () => {
|
|
65
|
-
expect(parseTargets("ls, ls")).toEqual(["ls"]);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("empty yields empty", () => {
|
|
69
|
-
expect(parseTargets("")).toEqual([]);
|
|
70
|
-
expect(parseTargets(" , ")).toEqual([]);
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// ─── renderList ─────────────────────────────────────────────────────────────
|
|
75
|
-
|
|
76
|
-
describe("renderList", () => {
|
|
77
|
-
const rows: ToolRow[] = [
|
|
78
|
-
{ name: "read", description: "Read files.", mcp: false },
|
|
79
|
-
{ name: "grep", description: "Search files.", mcp: false },
|
|
80
|
-
{ name: "ctx", description: "MCP search.", mcp: true },
|
|
81
|
-
];
|
|
82
|
-
const isActive = (n: string) => n === "read";
|
|
83
|
-
|
|
84
|
-
test("shows status for each tool", () => {
|
|
85
|
-
const out = renderList(rows, isActive);
|
|
86
|
-
expect(out).toContain("✓ active read");
|
|
87
|
-
expect(out).toContain("# gated grep");
|
|
88
|
-
expect(out).toContain("# gated ctx");
|
|
89
|
-
expect(out).toContain("[MCP]");
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test("filters by query", () => {
|
|
93
|
-
const out = renderList(rows, isActive, "grep");
|
|
94
|
-
expect(out).toContain("grep");
|
|
95
|
-
expect(out).not.toContain("read");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test("no match message", () => {
|
|
99
|
-
const out = renderList(rows, isActive, "zzz");
|
|
100
|
-
expect(out).toContain('No tools matched "zzz"');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("empty rows", () => {
|
|
104
|
-
expect(renderList([], isActive)).toContain("No tools registered");
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// ─── toggleTool ─────────────────────────────────────────────────────────────
|
|
109
|
-
|
|
110
|
-
describe("toggleTool", () => {
|
|
111
|
-
const rows: ToolRow[] = [
|
|
112
|
-
{ name: "read", description: "Read.", mcp: false },
|
|
113
|
-
{ name: "grep", description: "Search.", mcp: false },
|
|
114
|
-
];
|
|
115
|
-
|
|
116
|
-
test("enable calls onActivate", () => {
|
|
117
|
-
let called = "";
|
|
118
|
-
const ops: ToggleOps = {
|
|
119
|
-
onActivate: (n) => {
|
|
120
|
-
called = n;
|
|
121
|
-
return true;
|
|
122
|
-
},
|
|
123
|
-
onDeactivate: () => false,
|
|
124
|
-
isActive: () => false,
|
|
125
|
-
};
|
|
126
|
-
const msg = toggleTool("enable", "grep", rows, ops);
|
|
127
|
-
expect(called).toBe("grep");
|
|
128
|
-
expect(msg).toContain("Enabled grep");
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
test("disable calls onDeactivate", () => {
|
|
132
|
-
let called = "";
|
|
133
|
-
const ops: ToggleOps = {
|
|
134
|
-
onActivate: () => false,
|
|
135
|
-
onDeactivate: (n) => {
|
|
136
|
-
called = n;
|
|
137
|
-
return true;
|
|
138
|
-
},
|
|
139
|
-
isActive: () => true,
|
|
140
|
-
};
|
|
141
|
-
const msg = toggleTool("disable", "read", rows, ops);
|
|
142
|
-
expect(called).toBe("read");
|
|
143
|
-
expect(msg).toContain("Disabled read");
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("unknown tool returns error", () => {
|
|
147
|
-
const ops: ToggleOps = {
|
|
148
|
-
onActivate: () => false,
|
|
149
|
-
onDeactivate: () => false,
|
|
150
|
-
isActive: () => false,
|
|
151
|
-
};
|
|
152
|
-
expect(toggleTool("enable", "nope", rows, ops)).toContain("Unknown");
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test("already active returns already message", () => {
|
|
156
|
-
const ops: ToggleOps = {
|
|
157
|
-
onActivate: () => false,
|
|
158
|
-
onDeactivate: () => false,
|
|
159
|
-
isActive: () => true,
|
|
160
|
-
};
|
|
161
|
-
expect(toggleTool("enable", "read", rows, ops)).toContain("already");
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
test("already gated returns already message", () => {
|
|
165
|
-
const ops: ToggleOps = {
|
|
166
|
-
onActivate: () => false,
|
|
167
|
-
onDeactivate: () => false,
|
|
168
|
-
isActive: () => false,
|
|
169
|
-
};
|
|
170
|
-
expect(toggleTool("disable", "read", rows, ops)).toContain("already");
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
// ─── Integration: /toolbox command ──────────────────────────────────────────
|
|
175
|
-
|
|
176
|
-
// Isolate from real ~/.pi/agent/toolbox.json on disk
|
|
177
|
-
let tmpAgentDir: string;
|
|
178
|
-
beforeAll(() => {
|
|
179
|
-
tmpAgentDir = mkdtempSync(join(tmpdir(), "toolbox-test-"));
|
|
180
|
-
process.env.PI_CODING_AGENT_DIR = tmpAgentDir;
|
|
181
|
-
});
|
|
182
|
-
afterAll(() => {
|
|
183
|
-
delete process.env.PI_CODING_AGENT_DIR;
|
|
184
|
-
try {
|
|
185
|
-
rmSync(tmpAgentDir, { recursive: true });
|
|
186
|
-
} catch {
|
|
187
|
-
// temp dir may already be gone — safe to ignore
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
function makeHost(toolNames: string[]) {
|
|
192
|
-
const handlers: Record<string, Array<(p: unknown) => unknown>> = {};
|
|
193
|
-
let active: string[] = [...toolNames];
|
|
194
|
-
const commands: Array<{ name: string; handler: Function }> = [];
|
|
195
|
-
const pi = {
|
|
196
|
-
on(ev: string, fn: (p: unknown) => unknown) {
|
|
197
|
-
(handlers[ev] ??= []).push(fn);
|
|
198
|
-
},
|
|
199
|
-
emit(ev: string, payload: unknown, ctx?: unknown) {
|
|
200
|
-
return Promise.all(
|
|
201
|
-
(handlers[ev] ?? []).map((f) => (f as Function)(payload, ctx)),
|
|
202
|
-
);
|
|
203
|
-
},
|
|
204
|
-
getAllTools() {
|
|
205
|
-
return toolNames.map((name) => ({
|
|
206
|
-
name,
|
|
207
|
-
description: `${name} does things.`,
|
|
208
|
-
parameters: {},
|
|
209
|
-
sourceInfo: { source: "builtin" },
|
|
210
|
-
}));
|
|
211
|
-
},
|
|
212
|
-
getActiveTools() {
|
|
213
|
-
return active;
|
|
214
|
-
},
|
|
215
|
-
setActiveTools(names: string[]) {
|
|
216
|
-
active = [...names];
|
|
217
|
-
},
|
|
218
|
-
getCommands() {
|
|
219
|
-
return [];
|
|
220
|
-
},
|
|
221
|
-
appendEntry() {},
|
|
222
|
-
registerTool() {},
|
|
223
|
-
registerCommand(name: string, def: { handler: Function }) {
|
|
224
|
-
commands.push({ name, handler: def.handler });
|
|
225
|
-
},
|
|
226
|
-
} as never;
|
|
227
|
-
const emit = (ev: string, payload: unknown, ctx?: unknown) =>
|
|
228
|
-
Promise.all((handlers[ev] ?? []).map((f) => (f as Function)(payload, ctx)));
|
|
229
|
-
return {
|
|
230
|
-
pi,
|
|
231
|
-
emit,
|
|
232
|
-
getActive: () => active,
|
|
233
|
-
command: (name: string) => commands.find((c) => c.name === name),
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function makeCtx() {
|
|
238
|
-
const notes: Array<{ text: string; level?: string }> = [];
|
|
239
|
-
const ctx = {
|
|
240
|
-
ui: {
|
|
241
|
-
notify(text: string, level?: string) {
|
|
242
|
-
notes.push({ text, level });
|
|
243
|
-
},
|
|
244
|
-
},
|
|
245
|
-
} as never;
|
|
246
|
-
return { ctx, notes };
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
describe("/toolbox command", () => {
|
|
250
|
-
const ALL = ["read", "write", "bash", "grep", "find"];
|
|
251
|
-
|
|
252
|
-
async function boot() {
|
|
253
|
-
const host = makeHost(ALL);
|
|
254
|
-
registerToolbox(host.pi);
|
|
255
|
-
// session_start triggers init
|
|
256
|
-
await host.emit("session_start", {}, {});
|
|
257
|
-
return host;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
test("registers a /toolbox command", async () => {
|
|
261
|
-
const host = await boot();
|
|
262
|
-
expect(host.command("toolbox")).toBeDefined();
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
test("bare /toolbox falls back to listing when no custom UI", async () => {
|
|
266
|
-
const host = await boot();
|
|
267
|
-
const { ctx, notes } = makeCtx();
|
|
268
|
-
await host.command("toolbox")?.handler("", ctx);
|
|
269
|
-
expect(notes.length).toBe(1);
|
|
270
|
-
// only non-core tools shown, all start active
|
|
271
|
-
expect(notes[0].text).toContain("✓ active grep");
|
|
272
|
-
expect(notes[0].text).toContain("✓ active find");
|
|
273
|
-
// core tools excluded from toolbox
|
|
274
|
-
expect(notes[0].text).not.toContain(" read");
|
|
275
|
-
expect(notes[0].text).not.toContain(" bash");
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
test("/toolbox list shows non-core tools with status", async () => {
|
|
279
|
-
const host = await boot();
|
|
280
|
-
const { ctx, notes } = makeCtx();
|
|
281
|
-
await host.command("toolbox")?.handler("list", ctx);
|
|
282
|
-
expect(notes[0].text).toContain("grep");
|
|
283
|
-
expect(notes[0].text).toContain("find");
|
|
284
|
-
expect(notes[0].text).not.toContain(" bash");
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
test("/toolbox list <query> filters", async () => {
|
|
288
|
-
const host = await boot();
|
|
289
|
-
const { ctx, notes } = makeCtx();
|
|
290
|
-
await host.command("toolbox")?.handler("list fin", ctx);
|
|
291
|
-
expect(notes[0].text).toContain("find");
|
|
292
|
-
expect(notes[0].text).not.toContain("✓ active read");
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
test("opens interactive picker when ctx.ui.custom exists", async () => {
|
|
296
|
-
const host = await boot();
|
|
297
|
-
let customCalled = 0;
|
|
298
|
-
const notes: Array<{ text: string; level?: string }> = [];
|
|
299
|
-
const ctx = {
|
|
300
|
-
ui: {
|
|
301
|
-
notify(text: string, level?: string) {
|
|
302
|
-
notes.push({ text, level });
|
|
303
|
-
},
|
|
304
|
-
async custom() {
|
|
305
|
-
customCalled++;
|
|
306
|
-
return null;
|
|
307
|
-
},
|
|
308
|
-
},
|
|
309
|
-
} as never;
|
|
310
|
-
await host.command("toolbox")?.handler("", ctx);
|
|
311
|
-
expect(customCalled).toBe(1);
|
|
312
|
-
expect(notes.length).toBe(0);
|
|
313
|
-
});
|
|
314
|
-
});
|