@xynogen/pix-optimizer 1.0.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/src/caveman.ts ADDED
@@ -0,0 +1,402 @@
1
+ /**
2
+ * caveman.ts — pure logic + Pi extension
3
+ *
4
+ * Pure helpers exported for tests; caveman(pi) is the extension entry,
5
+ * called by index.ts alongside rtk(pi).
6
+ */
7
+
8
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
9
+ import { homedir } from "node:os";
10
+ import { join } from "node:path";
11
+ import type {
12
+ ExtensionAPI,
13
+ ExtensionCommandContext,
14
+ ExtensionContext,
15
+ } from "@earendil-works/pi-coding-agent";
16
+ import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
17
+ import type { OptimizerHandle, OptimizerStatus } from "./status.ts";
18
+ import {
19
+ Container,
20
+ type SettingItem,
21
+ SettingsList,
22
+ Text,
23
+ } from "@earendil-works/pi-tui";
24
+
25
+ // ── Levels ────────────────────────────────────────────────────────────────────
26
+
27
+ export const LEVELS = [
28
+ "off",
29
+ "lite",
30
+ "full",
31
+ "ultra",
32
+ "micro",
33
+ ] as const;
34
+
35
+ export type Level = (typeof LEVELS)[number];
36
+
37
+ export const STOP_ALIASES = new Set(["off", "stop", "quit", "0"]);
38
+
39
+ // Numeric shortcuts: /caveman 1|2|3
40
+ export const LEVEL_NUMBERS: Record<string, Level> = {
41
+ "1": "lite",
42
+ "2": "full",
43
+ "3": "ultra",
44
+ };
45
+
46
+ export const CAVEMAN_COMMAND_OPTIONS = [
47
+ { value: "1", label: "1 (lite)", description: "Professional, no fluff" },
48
+ { value: "2", label: "2 (full)", description: "Classic caveman" },
49
+ { value: "3", label: "3 (ultra)", description: "Maximum compression" },
50
+ { value: "lite", label: "lite", description: "Professional, no fluff" },
51
+ { value: "full", label: "full", description: "Classic caveman" },
52
+ { value: "ultra", label: "ultra", description: "Maximum compression" },
53
+ { value: "micro", label: "micro", description: "Experimental prompt-minimized mode" },
54
+ { value: "off", label: "off", description: "Disable caveman mode" },
55
+ { value: "stop", label: "stop", description: "Disable caveman mode" },
56
+ { value: "quit", label: "quit", description: "Disable caveman mode" },
57
+ { value: "config", label: "config", description: "Open settings dialog" },
58
+ ] as const;
59
+
60
+ // ── Status labels ─────────────────────────────────────────────────────────────
61
+
62
+ export const STATUS_LABELS: Record<Exclude<Level, "off">, string> = {
63
+ lite: "LITE",
64
+ full: "FULL",
65
+ ultra: "ULTRA",
66
+ micro: "MICRO",
67
+ };
68
+
69
+ // ── Prompt fragments ──────────────────────────────────────────────────────────
70
+
71
+ const BASE = `\
72
+ IMPORTANT: You are in CAVEMAN MODE. Respond terse like smart caveman. \
73
+ All technical substance stay. Only fluff die.
74
+
75
+ Rules:
76
+ - Drop articles (a/an/the), filler (just/really/basically/actually/simply), \
77
+ pleasantries, hedging
78
+ - Fragments OK. Short synonyms preferred. Technical terms exact
79
+ - Code blocks unchanged. Errors quoted exact
80
+ - Pattern: [thing] [action] [reason]. [next step].
81
+
82
+ Bad: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
83
+ Good: "Bug in auth middleware. Token expiry check use \`<\` not \`<=\`. Fix:"`;
84
+
85
+ const MICRO_PROMPT = `# Token efficiency
86
+ Respond like smart caveman. Cut all filler, keep technical substance.
87
+ - Drop articles (a, an, the), filler (just, really, basically, actually).
88
+ - Drop pleasantries (sure, certainly, happy to).
89
+ - No hedging. Fragments fine. Short synonyms.
90
+ - Technical terms stay exact. Code blocks unchanged.
91
+ - Pattern: [thing] [action] [reason]. [next step].`;
92
+
93
+ const INTENSITY: Record<Exclude<Level, "off" | "micro">, string> = {
94
+ lite: `\
95
+ No filler/hedging. Keep articles + full sentences. Professional but tight.
96
+ Example: "Your component re-renders because you create a new object reference each render. Wrap it in \`useMemo\`."`,
97
+
98
+ full: `\
99
+ Drop articles, fragments OK, short synonyms.
100
+ Example: "New object ref each render. Inline object prop = new ref = re-render. Wrap in \`useMemo\`."`,
101
+
102
+ ultra: `\
103
+ Abbreviate (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y).
104
+ Example: "Inline obj prop → new ref → re-render. \`useMemo\`."`,
105
+ };
106
+
107
+ const SAFETY = `\
108
+ Auto-clarity: drop caveman for security warnings, irreversible action confirmations, \
109
+ or when user is confused. Resume after.
110
+ Boundaries: write normal code. Only compress explanations. "stop caveman" or "normal mode" reverts.`;
111
+
112
+ /**
113
+ * Build the system prompt injection for a given level.
114
+ * Returns empty string when level is "off".
115
+ */
116
+ export function buildPrompt(level: Level): string {
117
+ if (level === "off") return "";
118
+ if (level === "micro") return MICRO_PROMPT;
119
+ return [BASE, "", `Intensity: ${INTENSITY[level]}`, "", SAFETY].join("\n");
120
+ }
121
+
122
+ // ── Level resolution ──────────────────────────────────────────────────────────
123
+
124
+ /**
125
+ * Resolve a raw command arg to a Level, or return null if unrecognised.
126
+ * Handles stop aliases (stop/quit → "off") and valid level names.
127
+ */
128
+ export function resolveLevel(arg: string): Level | null {
129
+ const a = arg.trim().toLowerCase();
130
+ if (STOP_ALIASES.has(a)) return "off";
131
+ if (LEVEL_NUMBERS[a]) return LEVEL_NUMBERS[a];
132
+ if (LEVELS.includes(a as Level)) return a as Level;
133
+ return null;
134
+ }
135
+
136
+ /**
137
+ * Help text shown when /caveman is run with no argument.
138
+ */
139
+ export function buildHelp(current: Level): string {
140
+ const statusLine =
141
+ current === "off" ? "off" : `${STATUS_LABELS[current]} (${current})`;
142
+ return [
143
+ `Caveman mode: ${statusLine}`,
144
+ "",
145
+ "Usage: /caveman <level>",
146
+ " 1 lite - professional, no fluff",
147
+ " 2 full - classic caveman",
148
+ " 3 ultra - maximum compression",
149
+ " 0 off - disable (aliases: off, stop, quit)",
150
+ "",
151
+ "Other levels: micro",
152
+ " config - open settings dialog",
153
+ ].join("\n");
154
+ }
155
+
156
+ /**
157
+ * Toggle: off → full, anything else → off.
158
+ */
159
+ export function toggleLevel(current: Level): Level {
160
+ return current === "off" ? "full" : "off";
161
+ }
162
+
163
+ // ── Config ────────────────────────────────────────────────────────────────────
164
+
165
+ export interface CavemanConfig {
166
+ /** Level to apply on new sessions. "off" means don't auto-enable. */
167
+ defaultLevel: Level;
168
+ /** Whether to show the status bar indicator. */
169
+ showStatus: boolean;
170
+ }
171
+
172
+ export const DEFAULT_CONFIG: CavemanConfig = {
173
+ defaultLevel: "full",
174
+ showStatus: true,
175
+ };
176
+
177
+ export const CONFIG_PATH = join(homedir(), ".pi", "agent", "caveman.json");
178
+
179
+ export function parseConfig(raw: unknown): CavemanConfig {
180
+ const parsed = raw as Record<string, unknown>;
181
+ return {
182
+ defaultLevel: LEVELS.includes(parsed?.defaultLevel as Level)
183
+ ? (parsed.defaultLevel as Level)
184
+ : DEFAULT_CONFIG.defaultLevel,
185
+ showStatus:
186
+ typeof parsed?.showStatus === "boolean"
187
+ ? parsed.showStatus
188
+ : DEFAULT_CONFIG.showStatus,
189
+ };
190
+ }
191
+
192
+ let _saveQueue: Promise<void> = Promise.resolve();
193
+
194
+ export async function loadConfig(): Promise<CavemanConfig> {
195
+ try {
196
+ const raw = await readFile(CONFIG_PATH, "utf8");
197
+ return parseConfig(JSON.parse(raw));
198
+ } catch {
199
+ return { ...DEFAULT_CONFIG };
200
+ }
201
+ }
202
+
203
+ export async function saveConfig(config: CavemanConfig): Promise<void> {
204
+ const snapshot = JSON.stringify(config, null, 2) + "\n";
205
+ _saveQueue = _saveQueue.then(async () => {
206
+ await mkdir(join(homedir(), ".pi", "agent"), { recursive: true });
207
+ await writeFile(CONFIG_PATH, snapshot, "utf8");
208
+ });
209
+ return _saveQueue;
210
+ }
211
+
212
+ // ── Pi extension ────────────────────────────────────────────────────────────
213
+
214
+ export function caveman(pi: ExtensionAPI, status: OptimizerStatus): OptimizerHandle {
215
+ let level: Level = "off";
216
+ let config: CavemanConfig = { ...DEFAULT_CONFIG };
217
+ let configLoadPromise: Promise<void> | null = null;
218
+
219
+ const ensureConfigLoaded = async () => {
220
+ if (!configLoadPromise) {
221
+ configLoadPromise = (async () => {
222
+ config = await loadConfig();
223
+ if (level === "off" && config.defaultLevel !== "off") {
224
+ level = config.defaultLevel;
225
+ }
226
+ })();
227
+ }
228
+ await configLoadPromise;
229
+ };
230
+
231
+ // -- Status: report into the shared optimizer indicator. --
232
+
233
+ function syncStatus(ctx: Pick<ExtensionContext, "ui">) {
234
+ const on = level !== "off" && config.showStatus;
235
+ status.set("caveman", on, ctx);
236
+ }
237
+
238
+ // Inject caveman prompt via before_agent_start
239
+ pi.on("before_agent_start", async (event, _ctx) => {
240
+ const prompt = buildPrompt(level);
241
+ if (!prompt) return undefined;
242
+ const existing = event.systemPrompt ?? "";
243
+ return { systemPrompt: prompt + "\n\n" + existing };
244
+ });
245
+
246
+ // -- Restore state on session load --
247
+
248
+ pi.on("session_start", async (_event, ctx) => {
249
+ await ensureConfigLoaded();
250
+
251
+ let sessionLevel: Level | null = null;
252
+ for (const entry of ctx.sessionManager.getEntries()) {
253
+ if (entry.type === "custom" && entry.customType === "caveman-level") {
254
+ sessionLevel = (entry.data as { level: Level })?.level ?? null;
255
+ }
256
+ }
257
+
258
+ if (sessionLevel !== null) {
259
+ level = sessionLevel;
260
+ } else if (config.defaultLevel !== "off") {
261
+ level = config.defaultLevel;
262
+ pi.appendEntry("caveman-level", { level });
263
+ }
264
+
265
+ syncStatus(ctx);
266
+ });
267
+
268
+ pi.on("agent_start", async (_event, ctx) => { syncStatus(ctx); });
269
+ pi.on("agent_end", async (_event, ctx) => { syncStatus(ctx); });
270
+ pi.on("session_shutdown", async () => {});
271
+
272
+ // -- Subcommand handler (dispatched by the merged /opt router) --
273
+
274
+ async function run(args: string, ctx: ExtensionCommandContext): Promise<void> {
275
+ const arg = args.trim().toLowerCase();
276
+
277
+ // No argument → show help
278
+ if (!arg) {
279
+ ctx.ui.notify(buildHelp(level), "info");
280
+ return;
281
+ }
282
+
283
+ if (arg === "config") {
284
+ await openConfig(ctx);
285
+ return;
286
+ }
287
+
288
+ const resolved = resolveLevel(arg);
289
+ if (resolved === null) {
290
+ ctx.ui.notify(
291
+ `Unknown: "${arg}". Use 1/2/3, ${LEVELS.join(", ")}, stop, quit, or config`,
292
+ "error",
293
+ );
294
+ return;
295
+ }
296
+ level = resolved;
297
+
298
+ pi.appendEntry("caveman-level", { level });
299
+ syncStatus(ctx);
300
+
301
+ ctx.ui.notify(
302
+ level === "off"
303
+ ? "Caveman mode off."
304
+ : `Caveman: ${STATUS_LABELS[level]}`,
305
+ "info",
306
+ );
307
+ }
308
+
309
+ function complete(prefix: string) {
310
+ const normalized = prefix.trim().toLowerCase();
311
+ const items = CAVEMAN_COMMAND_OPTIONS.filter((item) =>
312
+ item.value.startsWith(normalized),
313
+ );
314
+ return items.length > 0 ? items.map((i) => ({ ...i })) : null;
315
+ }
316
+
317
+ // -- /caveman config: interactive SettingsList --
318
+
319
+ async function openConfig(ctx: ExtensionContext) {
320
+ await ensureConfigLoaded();
321
+
322
+ await ctx.ui.custom((_tui, theme, _kb, done) => {
323
+ const items: SettingItem[] = [
324
+ {
325
+ id: "defaultLevel",
326
+ label: "Default level for new sessions",
327
+ currentValue: config.defaultLevel,
328
+ values: [...LEVELS],
329
+ },
330
+ {
331
+ id: "showStatus",
332
+ label: "Show status bar",
333
+ currentValue: config.showStatus ? "on" : "off",
334
+ values: ["on", "off"],
335
+ },
336
+ ];
337
+
338
+ const container = new Container();
339
+ container.addChild(new Text(theme.fg("accent", theme.bold(" Caveman Config")), 0, 0));
340
+ container.addChild(new Text(theme.fg("dim", " Saved to ~/.pi/agent/caveman.json"), 0, 0));
341
+ container.addChild(new Text(theme.fg("dim", " Default level applies to future sessions."), 0, 0));
342
+ container.addChild(new Text("", 0, 0));
343
+
344
+ const applySettingChange = (id: string, newValue: string) => {
345
+ if (id === "defaultLevel" && LEVELS.includes(newValue as Level)) {
346
+ config.defaultLevel = newValue as Level;
347
+ } else if (id === "showStatus") {
348
+ config.showStatus = newValue === "on";
349
+ }
350
+ saveConfig(config);
351
+ syncStatus(ctx);
352
+ };
353
+
354
+ const settingsList = new SettingsList(
355
+ items,
356
+ Math.min(items.length + 2, 10),
357
+ getSettingsListTheme(),
358
+ applySettingChange,
359
+ () => done(undefined),
360
+ );
361
+
362
+ container.addChild(settingsList);
363
+ container.addChild(new Text(theme.fg("dim", " ←→/hl/tab change • ↑↓/jk move • esc close"), 0, 0));
364
+
365
+ const cycleSelectedValue = (direction: -1 | 1) => {
366
+ const selectedIndex = (settingsList as unknown as { selectedIndex: number }).selectedIndex;
367
+ const item = items[selectedIndex];
368
+ if (!item?.values?.length) return;
369
+ const currentIndex = item.values.indexOf(item.currentValue);
370
+ const nextIndex = (currentIndex + direction + item.values.length) % item.values.length;
371
+ const newValue = item.values[nextIndex]!;
372
+ item.currentValue = newValue;
373
+ settingsList.updateValue(item.id, newValue);
374
+ applySettingChange(item.id, newValue);
375
+ };
376
+
377
+ return {
378
+ render: (w: number) => container.render(w),
379
+ invalidate: () => container.invalidate(),
380
+ handleInput: (data: string) => {
381
+ if (data === "j") data = "\u001b[B";
382
+ else if (data === "k") data = "\u001b[A";
383
+ else if (data === "h") { cycleSelectedValue(-1); _tui.requestRender(); return; }
384
+ else if (data === "l" || data === "\u001b[C" || data === "\t") { cycleSelectedValue(1); _tui.requestRender(); return; }
385
+ else if (data === "\u001b[D") { cycleSelectedValue(-1); _tui.requestRender(); return; }
386
+ else if (data === "\u001b" || data === "q") { done(undefined); return; }
387
+
388
+ const result = settingsList.handleInput(data);
389
+ if (result === "done") done(undefined);
390
+ else _tui.requestRender();
391
+ },
392
+ };
393
+ });
394
+ }
395
+
396
+ return {
397
+ name: "caveman",
398
+ help: "caveman <1|2|3|lite|full|ultra|micro|off|config> — terse output",
399
+ run,
400
+ complete,
401
+ };
402
+ }
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * pix-optimizer — token-optimization suite for Pi Coding Agent.
3
+ *
4
+ * Three tools, combined into one extension + one command:
5
+ * - caveman: terse-output system prompt
6
+ * - rtk: prefixes shell commands with `rtk` + injects RTK prompt
7
+ * - toon: jq + TOON guidance for dense JSON (+ bundled skill)
8
+ *
9
+ * They share ONE status-bar cell (⛏ ⚔ ✂, only enabled tools shown) and ONE
10
+ * command (/opt <tool> [args]). index.ts wires lifecycle hooks via each
11
+ * module, then registers the merged command from their handles.
12
+ */
13
+
14
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
+ import { caveman } from "./caveman.ts";
16
+ import { rtk } from "./rtk.ts";
17
+ import { json } from "./json.ts";
18
+ import { OptimizerStatus, type OptimizerHandle, type OptimizerTool } from "./status.ts";
19
+ import { registerOptCommand } from "./opt.ts";
20
+
21
+ export default function optimizer(pi: ExtensionAPI) {
22
+ const status = new OptimizerStatus();
23
+
24
+ // Each module registers its own lifecycle hooks and returns a handle the
25
+ // merged /opt command dispatches to.
26
+ const handles: Record<OptimizerTool, OptimizerHandle> = {
27
+ caveman: caveman(pi, status),
28
+ rtk: rtk(pi, status),
29
+ toon: json(pi, status),
30
+ };
31
+
32
+ registerOptCommand(pi, handles);
33
+ }
@@ -0,0 +1,177 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import {
3
+ JSON_SYSTEM_PROMPT,
4
+ adviseToon,
5
+ isUniformObjectArray,
6
+ mentionsJson,
7
+ objectDepth,
8
+ skillDir,
9
+ } from "./json.ts";
10
+
11
+ describe("JSON_SYSTEM_PROMPT", () => {
12
+ it("mentions the jq + toon pipeline", () => {
13
+ expect(JSON_SYSTEM_PROMPT).toContain("jq");
14
+ expect(JSON_SYSTEM_PROMPT).toContain("toon");
15
+ });
16
+
17
+ it("teaches both encode and decode directions", () => {
18
+ expect(JSON_SYSTEM_PROMPT).toContain("toon -d");
19
+ });
20
+
21
+ it("warns to keep JSON for API contracts", () => {
22
+ expect(JSON_SYSTEM_PROMPT.toLowerCase()).toContain("contract");
23
+ });
24
+
25
+ it("references the toon-json skill", () => {
26
+ expect(JSON_SYSTEM_PROMPT).toContain("toon-json");
27
+ });
28
+ });
29
+
30
+ describe("mentionsJson (injection gate)", () => {
31
+ it("matches json in any case", () => {
32
+ expect(mentionsJson("parse this JSON")).toBe(true);
33
+ expect(mentionsJson("a Json file")).toBe(true);
34
+ expect(mentionsJson("json blob")).toBe(true);
35
+ });
36
+
37
+ it("matches related tokens", () => {
38
+ expect(mentionsJson("pipe through jq")).toBe(true);
39
+ expect(mentionsJson("convert to toon")).toBe(true);
40
+ expect(mentionsJson("a .jsonl dataset")).toBe(true);
41
+ expect(mentionsJson("the openapi spec")).toBe(true);
42
+ expect(mentionsJson("swagger doc")).toBe(true);
43
+ expect(mentionsJson("some js object")).toBe(true);
44
+ });
45
+
46
+ it("does not match unrelated prompts", () => {
47
+ expect(mentionsJson("refactor the auth module")).toBe(false);
48
+ expect(mentionsJson("fix the bug")).toBe(false);
49
+ });
50
+
51
+ it("is word-bounded (no false positives inside words)", () => {
52
+ expect(mentionsJson("adjust the layout")).toBe(false); // contains 'js'
53
+ expect(mentionsJson("a jsx component")).toBe(false); // jsx != js
54
+ expect(mentionsJson("jsonify is a typo")).toBe(false); // jsonify != json
55
+ });
56
+
57
+ it("handles empty / nullish input", () => {
58
+ expect(mentionsJson("")).toBe(false);
59
+ expect(mentionsJson(undefined)).toBe(false);
60
+ expect(mentionsJson(null)).toBe(false);
61
+ });
62
+ });
63
+
64
+ describe("isUniformObjectArray", () => {
65
+ it("true for same-keyed flat objects", () => {
66
+ expect(
67
+ isUniformObjectArray([
68
+ { id: 1, name: "a" },
69
+ { id: 2, name: "b" },
70
+ ]),
71
+ ).toBe(true);
72
+ });
73
+
74
+ it("false for differing keys", () => {
75
+ expect(
76
+ isUniformObjectArray([{ id: 1 }, { id: 2, extra: true }]),
77
+ ).toBe(false);
78
+ });
79
+
80
+ it("false when a value is itself an object", () => {
81
+ expect(
82
+ isUniformObjectArray([
83
+ { id: 1, meta: { x: 1 } },
84
+ { id: 2, meta: { x: 2 } },
85
+ ]),
86
+ ).toBe(false);
87
+ });
88
+
89
+ it("false for empty array", () => {
90
+ expect(isUniformObjectArray([])).toBe(false);
91
+ });
92
+
93
+ it("false for array of primitives", () => {
94
+ expect(isUniformObjectArray([1, 2, 3])).toBe(false);
95
+ });
96
+ });
97
+
98
+ describe("objectDepth", () => {
99
+ it("primitive is depth 0", () => {
100
+ expect(objectDepth(42)).toBe(0);
101
+ expect(objectDepth("x")).toBe(0);
102
+ });
103
+
104
+ it("flat object is depth 1", () => {
105
+ expect(objectDepth({ a: 1, b: 2 })).toBe(1);
106
+ });
107
+
108
+ it("nested object counts each level", () => {
109
+ expect(objectDepth({ a: { b: { c: 1 } } })).toBe(3);
110
+ });
111
+
112
+ it("array nesting counts too", () => {
113
+ expect(objectDepth([{ a: [1, 2] }])).toBe(3);
114
+ });
115
+ });
116
+
117
+ describe("adviseToon", () => {
118
+ it("recommends TOON for uniform object arrays (sweet spot)", () => {
119
+ const advice = adviseToon([
120
+ { id: 1, role: "admin" },
121
+ { id: 2, role: "user" },
122
+ ]);
123
+ expect(advice.useToon).toBe(true);
124
+ expect(advice.reason).toContain("tabular");
125
+ });
126
+
127
+ it("recommends TOON for primitive arrays", () => {
128
+ expect(adviseToon([1, 2, 3]).useToon).toBe(true);
129
+ });
130
+
131
+ it("recommends TOON for shallow objects", () => {
132
+ expect(adviseToon({ a: 1, b: 2 }).useToon).toBe(true);
133
+ });
134
+
135
+ it("rejects array of arrays (TOON's worse case)", () => {
136
+ const advice = adviseToon([
137
+ [1, 2],
138
+ [3, 4],
139
+ ]);
140
+ expect(advice.useToon).toBe(false);
141
+ expect(advice.reason).toContain("array of arrays");
142
+ });
143
+
144
+ it("rejects empty array", () => {
145
+ expect(adviseToon([]).useToon).toBe(false);
146
+ });
147
+
148
+ it("rejects deeply nested objects beyond maxDepth", () => {
149
+ const deep = { a: { b: { c: { d: { e: 1 } } } } }; // depth 5
150
+ const advice = adviseToon(deep, 4);
151
+ expect(advice.useToon).toBe(false);
152
+ expect(advice.reason).toContain("depth");
153
+ });
154
+
155
+ it("rejects non-uniform object arrays", () => {
156
+ const advice = adviseToon([{ id: 1 }, { id: 2, extra: true }]);
157
+ expect(advice.useToon).toBe(false);
158
+ });
159
+
160
+ it("rejects primitive scalars", () => {
161
+ expect(adviseToon(42).useToon).toBe(false);
162
+ expect(adviseToon("hi").useToon).toBe(false);
163
+ });
164
+
165
+ it("honours a custom maxDepth", () => {
166
+ const d2 = { a: { b: 1 } }; // depth 2
167
+ expect(adviseToon(d2, 1).useToon).toBe(false);
168
+ expect(adviseToon(d2, 2).useToon).toBe(true);
169
+ });
170
+ });
171
+
172
+ describe("skillDir", () => {
173
+ it("points at the bundled toon-json skill", () => {
174
+ const dir = skillDir();
175
+ expect(dir.endsWith("skills/toon-json")).toBe(true);
176
+ });
177
+ });