@dungle-scrubs/tallow 0.8.28 → 0.9.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.
Files changed (32) hide show
  1. package/dist/config.d.ts +1 -1
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +1 -1
  4. package/dist/config.js.map +1 -1
  5. package/dist/install.d.ts.map +1 -1
  6. package/dist/install.js +2 -9
  7. package/dist/install.js.map +1 -1
  8. package/dist/interactive-mode-patch.d.ts.map +1 -1
  9. package/dist/interactive-mode-patch.js +20 -9
  10. package/dist/interactive-mode-patch.js.map +1 -1
  11. package/extensions/_icons/__tests__/icons.test.ts +0 -1
  12. package/extensions/_icons/index.ts +0 -2
  13. package/extensions/context-fork/__tests__/context-fork.test.ts +9 -0
  14. package/extensions/health/index.ts +1 -1
  15. package/extensions/render-stabilizer/__tests__/render-stabilizer.test.ts +42 -0
  16. package/extensions/render-stabilizer/extension.json +5 -0
  17. package/extensions/render-stabilizer/index.ts +66 -0
  18. package/extensions/subagent-tool/__tests__/auto-cheap-model.test.ts +66 -6
  19. package/extensions/subagent-tool/__tests__/model-router-explicit-resolution.test.ts +79 -5
  20. package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts +47 -0
  21. package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts.map +1 -1
  22. package/node_modules/@mariozechner/pi-tui/dist/tui.js +139 -5
  23. package/node_modules/@mariozechner/pi-tui/dist/tui.js.map +1 -1
  24. package/node_modules/@mariozechner/pi-tui/src/tui.ts +142 -5
  25. package/package.json +1 -1
  26. package/schemas/settings.schema.json +0 -5
  27. package/extensions/plan-mode-tool/__tests__/e2e.mjs +0 -350
  28. package/extensions/plan-mode-tool/__tests__/index.test.ts +0 -213
  29. package/extensions/plan-mode-tool/__tests__/utils.test.ts +0 -381
  30. package/extensions/plan-mode-tool/extension.json +0 -22
  31. package/extensions/plan-mode-tool/index.ts +0 -583
  32. package/extensions/plan-mode-tool/utils.ts +0 -257
@@ -1,583 +0,0 @@
1
- /**
2
- * Plan Mode Extension
3
- *
4
- * Read-only exploration mode for safe code analysis.
5
- * When enabled, only read-only tools are available.
6
- *
7
- * Features:
8
- * - /plan-mode command or Ctrl+Alt+P to toggle
9
- * - Strict fail-closed tool allowlist while plan mode is active
10
- * - Bash restricted to allowlisted read-only commands
11
- * - Extracts numbered plan steps from "Plan:" sections
12
- * - Delegates execution tracking to the tasks extension
13
- */
14
-
15
- import type { AgentMessage } from "@mariozechner/pi-agent-core";
16
- import type { AssistantMessage, TextContent } from "@mariozechner/pi-ai";
17
- import type { KeybindingsManager } from "@mariozechner/pi-coding-agent";
18
- import {
19
- CustomEditor,
20
- type ExtensionAPI,
21
- type ExtensionContext,
22
- } from "@mariozechner/pi-coding-agent";
23
- import {
24
- type EditorTheme,
25
- Key,
26
- Loader,
27
- type TUI,
28
- truncateToWidth,
29
- visibleWidth,
30
- } from "@mariozechner/pi-tui";
31
- import { Type } from "@sinclair/typebox";
32
- import { getIcon } from "../_icons/index.js";
33
- import { renderBorderedBox } from "../_shared/bordered-box.js";
34
- import {
35
- detectPlanIntent,
36
- extractTodoItems,
37
- isPlanModeToolAllowed,
38
- isSafeCommand,
39
- PLAN_MODE_ALLOWED_TOOLS,
40
- stripPlanIntent,
41
- type TodoItem,
42
- } from "./utils.js";
43
-
44
- /**
45
- * Type guard to check if a message is an assistant message.
46
- * @param m - The message to check
47
- * @returns true if the message is from the assistant
48
- */
49
- function isAssistantMessage(m: AgentMessage): m is AssistantMessage {
50
- return m.role === "assistant" && Array.isArray(m.content);
51
- }
52
-
53
- /**
54
- * Extracts all text content from an assistant message.
55
- * @param message - The assistant message to extract text from
56
- * @returns Concatenated text content
57
- */
58
- function getTextContent(message: AssistantMessage): string {
59
- return message.content
60
- .filter((block): block is TextContent => block.type === "text")
61
- .map((block) => block.text)
62
- .join("\n");
63
- }
64
-
65
- /** Plan mode label shown in the editor border */
66
- const PLAN_LABEL = ` ${getIcon("plan_mode")} PLAN `;
67
-
68
- /**
69
- * Custom editor that renders a warning-colored border in plan mode.
70
- * Extends CustomEditor to preserve all app keybindings.
71
- */
72
- class PlanModeEditor extends CustomEditor {
73
- constructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) {
74
- super(tui, { ...theme, borderColor: (s: string) => `\x1b[33m${s}\x1b[39m` }, keybindings);
75
- }
76
-
77
- /**
78
- * Renders the editor with a PLAN label in the top border.
79
- * @param width - Available width
80
- * @returns Array of rendered lines
81
- */
82
- override render(width: number): string[] {
83
- const lines = super.render(width);
84
- if (lines.length > 0) {
85
- const label = `\x1b[33;1m${PLAN_LABEL}\x1b[22;39m`;
86
- const first = lines[0];
87
- const vis = visibleWidth(first);
88
- const labelVis = visibleWidth(PLAN_LABEL);
89
- if (vis >= labelVis + 4) {
90
- lines[0] = truncateToWidth(first, width - labelVis, "") + label;
91
- }
92
- }
93
- return lines;
94
- }
95
- }
96
-
97
- /**
98
- * Registers the plan mode extension with Pi.
99
- * Provides read-only exploration mode with progress tracking.
100
- * @param pi - The Pi extension API
101
- */
102
- export default function planModeExtension(pi: ExtensionAPI): void {
103
- let planModeEnabled = false;
104
- let todoItems: TodoItem[] = [];
105
- let normalModeTools: string[] = [];
106
-
107
- /**
108
- * Capture the active tools used outside plan mode.
109
- *
110
- * @returns Snapshot of normal-mode tools
111
- */
112
- function captureNormalModeTools(): string[] {
113
- const activeTools = pi.getActiveTools();
114
- normalModeTools =
115
- activeTools.length > 0 ? [...activeTools] : pi.getAllTools().map((t) => t.name);
116
- return normalModeTools;
117
- }
118
-
119
- /**
120
- * Resolve allowlisted tools that exist in the current session.
121
- *
122
- * @returns Plan-mode tool list constrained to the strict allowlist
123
- */
124
- function getPlanModeTools(): string[] {
125
- const availableTools = new Set(pi.getAllTools().map((t) => t.name));
126
- return PLAN_MODE_ALLOWED_TOOLS.filter((name) => availableTools.has(name));
127
- }
128
-
129
- /**
130
- * Apply strict read-only tool policy for plan mode.
131
- *
132
- * @returns The active allowlisted tool names
133
- */
134
- function applyPlanModeTools(): string[] {
135
- const tools = getPlanModeTools();
136
- pi.setActiveTools(tools);
137
- return tools;
138
- }
139
-
140
- /**
141
- * Restore the normal tool set captured before plan mode was enabled.
142
- *
143
- * @returns Restored normal-mode tool names
144
- */
145
- function restoreNormalModeTools(): string[] {
146
- if (normalModeTools.length === 0) {
147
- captureNormalModeTools();
148
- }
149
- pi.setActiveTools(normalModeTools);
150
- return normalModeTools;
151
- }
152
-
153
- pi.registerFlag("plan", {
154
- description: "Start in plan mode (read-only exploration)",
155
- type: "boolean",
156
- default: false,
157
- });
158
-
159
- /**
160
- * Updates visual indicators: footer status, editor border, and widgets.
161
- * Plan mode gets a warning-colored editor border with PLAN label,
162
- * a custom footer bar, and the todo widget when executing.
163
- * @param ctx - The extension context
164
- */
165
- function updateStatus(ctx: ExtensionContext): void {
166
- // Footer status — plan mode only
167
- if (planModeEnabled) {
168
- ctx.ui.setStatus(
169
- "plan-mode",
170
- ctx.ui.theme.fg("warning", `${getIcon("plan_mode")} PLAN MODE — read-only`)
171
- );
172
- } else {
173
- ctx.ui.setStatus("plan-mode", undefined);
174
- }
175
-
176
- // Editor border: warning-colored in plan mode, default otherwise
177
- if (planModeEnabled) {
178
- ctx.ui.setEditorComponent(
179
- (tui, theme, keybindings) => new PlanModeEditor(tui, theme, keybindings)
180
- );
181
- } else {
182
- ctx.ui.setEditorComponent(undefined);
183
- }
184
-
185
- // Full-width banner above editor — plan mode only.
186
- if (planModeEnabled) {
187
- ctx.ui.setWidget("plan-banner", (_tui, theme) => {
188
- const label = " PLAN MODE — READ ONLY ";
189
- return {
190
- render: (width: number) => [
191
- theme.bg("customMessageBg", theme.fg("customMessageLabel", label.padEnd(width))),
192
- ],
193
- invalidate() {},
194
- };
195
- });
196
- } else {
197
- ctx.ui.setWidget("plan-banner", undefined);
198
- }
199
-
200
- ctx.ui.setWidget("plan-todos", undefined);
201
- }
202
-
203
- /**
204
- * Toggles plan mode on or off.
205
- * @param ctx - The extension context
206
- */
207
- function togglePlanMode(ctx: ExtensionContext): void {
208
- planModeEnabled = !planModeEnabled;
209
- todoItems = [];
210
-
211
- if (planModeEnabled) {
212
- captureNormalModeTools();
213
- const tools = applyPlanModeTools();
214
- ctx.ui.notify(`Plan mode enabled. Strict read-only tools: ${tools.join(", ")}`);
215
- } else {
216
- restoreNormalModeTools();
217
- ctx.ui.notify("Plan mode disabled. Previous tool access restored.");
218
- }
219
- updateStatus(ctx);
220
- persistState();
221
- }
222
-
223
- /**
224
- * Persists the current plan mode state to the session.
225
- */
226
- function persistState(): void {
227
- pi.appendEntry("plan-mode", {
228
- enabled: planModeEnabled,
229
- normalTools: normalModeTools,
230
- todos: todoItems,
231
- });
232
- }
233
-
234
- pi.registerCommand("plan-mode", {
235
- description: "Toggle plan mode (read-only exploration)",
236
- handler: async (_args, ctx) => togglePlanMode(ctx),
237
- });
238
-
239
- pi.registerCommand("todos", {
240
- description: "Show current plan todo list",
241
- handler: async (_args, ctx) => {
242
- if (todoItems.length === 0) {
243
- ctx.ui.notify("No todos. Create a plan first with /plan-mode", "info");
244
- return;
245
- }
246
- const list = todoItems
247
- .map(
248
- (item, i) =>
249
- `${i + 1}. ${item.completed ? getIcon("success") : getIcon("idle")} ${item.text}`
250
- )
251
- .join("\n");
252
- ctx.ui.notify(`Plan Progress:\n${list}`, "info");
253
- },
254
- });
255
-
256
- pi.registerShortcut(Key.ctrlAlt("p"), {
257
- description: "Toggle plan mode",
258
- handler: async (ctx) => togglePlanMode(ctx),
259
- });
260
-
261
- // Tool for the agent to toggle plan mode programmatically
262
- pi.registerTool({
263
- name: "plan_mode",
264
- label: "plan_mode",
265
- description: `Toggle plan mode on or off. Plan mode is a strict read-only exploration mode for safe code analysis.
266
-
267
- When enabled:
268
- - Only allowlisted read-only tools are available (read, bash, grep, find, ls, questionnaire, plan_mode)
269
- - All other tools are blocked fail-closed (including extension tools)
270
- - Bash is additionally restricted to safe read-only commands
271
-
272
- Use action "enable" to enter plan mode, "disable" to exit, or "status" to check current state.`,
273
- parameters: Type.Object({
274
- action: Type.Union(
275
- [Type.Literal("enable"), Type.Literal("disable"), Type.Literal("status")],
276
- {
277
- description: "Whether to enable, disable, or check plan mode status",
278
- }
279
- ),
280
- }),
281
-
282
- /**
283
- * Toggles plan mode or reports current status.
284
- * @param _toolCallId - Unique identifier for this tool call
285
- * @param params - Action to perform (enable/disable/status)
286
- * @param _signal - Abort signal
287
- * @param _onUpdate - Update callback
288
- * @param ctx - Extension context
289
- * @returns Tool result with the new state
290
- */
291
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
292
- const { action } = params;
293
-
294
- if (action === "status") {
295
- const mode = planModeEnabled ? "planning" : "normal";
296
- const tools = planModeEnabled ? getPlanModeTools() : pi.getActiveTools();
297
- return {
298
- content: [
299
- {
300
- type: "text",
301
- text: `Plan mode: ${mode}\nActive tools: ${tools.join(", ")}${
302
- todoItems.length > 0
303
- ? `\nTodos: ${todoItems.filter((t) => t.completed).length}/${todoItems.length} completed`
304
- : ""
305
- }`,
306
- },
307
- ],
308
- details: {},
309
- };
310
- }
311
-
312
- const shouldEnable = action === "enable";
313
-
314
- if (shouldEnable === planModeEnabled) {
315
- return {
316
- content: [
317
- {
318
- type: "text",
319
- text: `Plan mode is already ${shouldEnable ? "enabled" : "disabled"}.`,
320
- },
321
- ],
322
- details: {},
323
- };
324
- }
325
-
326
- planModeEnabled = shouldEnable;
327
- todoItems = [];
328
-
329
- let activeTools: string[];
330
- if (planModeEnabled) {
331
- captureNormalModeTools();
332
- activeTools = applyPlanModeTools();
333
- } else {
334
- activeTools = restoreNormalModeTools();
335
- }
336
-
337
- if (ctx.hasUI) {
338
- updateStatus(ctx);
339
- }
340
- persistState();
341
-
342
- return {
343
- content: [
344
- {
345
- type: "text",
346
- text: planModeEnabled
347
- ? `Plan mode enabled. Strict allowlist active: ${activeTools.join(", ")}. All other tools are blocked.`
348
- : "Plan mode disabled. Previous tool access restored.",
349
- },
350
- ],
351
- details: {},
352
- };
353
- },
354
- });
355
-
356
- // Enforce strict plan-mode allowlist and safe bash commands
357
- pi.on("tool_call", async (event, ctx) => {
358
- if (!planModeEnabled) return;
359
-
360
- if (!isPlanModeToolAllowed(event.toolName)) {
361
- const reason =
362
- `Plan mode: tool "${event.toolName}" blocked (not in strict read-only allowlist). ` +
363
- "Disable plan mode first to use this tool.";
364
- ctx.ui?.notify(`⛔ ${reason}`, "error");
365
- return { block: true, reason };
366
- }
367
-
368
- if (event.toolName !== "bash") return;
369
-
370
- const command =
371
- typeof event.input.command === "string" ? event.input.command : String(event.input.command);
372
- if (!isSafeCommand(command)) {
373
- const reason =
374
- "Plan mode: bash command blocked (not in read-only command allowlist). " +
375
- `Disable plan mode first to run it.\nCommand: ${command}`;
376
- ctx.ui?.notify(`⛔ ${reason}`, "error");
377
- return { block: true, reason };
378
- }
379
- });
380
-
381
- // Auto-enable plan mode when a human interactive session explicitly signals planning intent.
382
- pi.on("input", async (event, ctx) => {
383
- // No-op if already in plan mode
384
- if (planModeEnabled) {
385
- return { action: "continue" as const };
386
- }
387
-
388
- // Headless/orchestrated prompts should never toggle workflow modes via string matching.
389
- if (!ctx.hasUI || event.source !== "interactive") {
390
- return { action: "continue" as const };
391
- }
392
-
393
- if (!detectPlanIntent(event.text)) {
394
- return { action: "continue" as const };
395
- }
396
-
397
- // Auto-enable plan mode
398
- planModeEnabled = true;
399
- captureNormalModeTools();
400
- applyPlanModeTools();
401
- updateStatus(ctx);
402
- persistState();
403
-
404
- ctx.ui?.notify(
405
- "Plan mode auto-enabled (detected planning intent). Use /plan-mode or Ctrl+Alt+P to disable.",
406
- "info"
407
- );
408
-
409
- // Strip the plan-intent phrase, keep the actual request
410
- const stripped = stripPlanIntent(event.text);
411
- if (stripped !== event.text) {
412
- return { action: "transform" as const, text: stripped };
413
- }
414
- return { action: "continue" as const };
415
- });
416
-
417
- // Filter out stale plan mode context when not in plan mode
418
- pi.on("context", async (event) => {
419
- if (planModeEnabled) return;
420
-
421
- return {
422
- messages: event.messages.filter((m) => {
423
- const msg = m as AgentMessage & { customType?: string };
424
- if (msg.customType === "plan-mode-context") return false;
425
- if (msg.role !== "user") return true;
426
-
427
- const content = msg.content;
428
- if (typeof content === "string") {
429
- return !content.includes("[PLAN MODE ACTIVE]");
430
- }
431
- if (Array.isArray(content)) {
432
- return !content.some(
433
- (c) => c.type === "text" && (c as TextContent).text?.includes("[PLAN MODE ACTIVE]")
434
- );
435
- }
436
- return true;
437
- }),
438
- };
439
- });
440
-
441
- // Inject plan/execution context before agent starts
442
- pi.on("before_agent_start", async () => {
443
- if (planModeEnabled) {
444
- return {
445
- message: {
446
- customType: "plan-mode-context",
447
- content: `[PLAN MODE ACTIVE]
448
- You are in plan mode - a read-only exploration mode for safe code analysis.
449
-
450
- Restrictions:
451
- - You can only use strict allowlisted read-only tools: read, bash, grep, find, ls, questionnaire, plan_mode
452
- - All other tools are blocked fail-closed (including edit, write, bg_bash, subagent, and mcp__* tools)
453
- - Bash is additionally restricted to an allowlist of read-only commands
454
-
455
- Ask clarifying questions using the questionnaire tool.
456
- Use bash only for safe inspection commands.
457
-
458
- Create a detailed numbered plan under a "Plan:" header:
459
-
460
- Plan:
461
- 1. First step description
462
- 2. Second step description
463
- ...
464
-
465
- Do NOT attempt to make changes - just describe what you would do.`,
466
- display: false,
467
- },
468
- };
469
- }
470
- });
471
-
472
- // Handle plan completion and plan mode UI
473
- pi.on("agent_end", async (event, ctx) => {
474
- if (!(planModeEnabled && ctx.hasUI)) return;
475
-
476
- // Extract todos from last assistant message
477
- const lastAssistant = [...event.messages].reverse().find(isAssistantMessage);
478
- if (lastAssistant) {
479
- const extracted = extractTodoItems(getTextContent(lastAssistant));
480
- if (extracted.length > 0) {
481
- todoItems = extracted;
482
- }
483
- }
484
-
485
- // Show plan steps in a bordered widget above the editor
486
- if (todoItems.length > 0) {
487
- ctx.ui.setWidget("plan-steps", (_tui, theme) => ({
488
- render(width: number): string[] {
489
- const stepLines = todoItems.map(
490
- (t) => `${theme.fg("muted", `${getIcon("pending")} `)}${t.text}`
491
- );
492
- return renderBorderedBox(stepLines, width, {
493
- title: `PLAN (${todoItems.length} steps)`,
494
- style: "rounded",
495
- borderColorFn: (s: string) => theme.fg("warning", s),
496
- titleColorFn: (s: string) => theme.fg("warning", s),
497
- });
498
- },
499
- invalidate() {},
500
- }));
501
- }
502
-
503
- ctx.ui.setWorkingMessage(Loader.HIDE);
504
-
505
- const choice = await ctx.ui.select("Plan mode - what next?", [
506
- "Execute the plan",
507
- "Stay in plan mode",
508
- "Refine the plan",
509
- ]);
510
-
511
- // Clear the plan steps widget after user makes a choice
512
- ctx.ui.setWidget("plan-steps", undefined);
513
-
514
- if (choice?.startsWith("Execute")) {
515
- const steps = [...todoItems];
516
- planModeEnabled = false;
517
- todoItems = [];
518
- restoreNormalModeTools();
519
- updateStatus(ctx);
520
- persistState();
521
-
522
- const stepList = steps.map((t) => `${t.step}. ${t.text}`).join("\n");
523
- const execMessage =
524
- steps.length > 0
525
- ? `Execute this plan. Create tasks to track each step, then work through them:\n\n${stepList}`
526
- : "Execute the plan you just created.";
527
- pi.sendMessage(
528
- { customType: "plan-mode-execute", content: execMessage, display: true },
529
- { triggerTurn: true }
530
- );
531
- } else if (choice === "Stay in plan mode") {
532
- ctx.ui.notify("Staying in plan mode. Continue refining or ask follow-up questions.", "info");
533
- } else if (choice === "Refine the plan") {
534
- const refinement = await ctx.ui.editor("Refine the plan:", "");
535
- if (refinement?.trim()) {
536
- pi.sendUserMessage(refinement.trim());
537
- } else {
538
- ctx.ui.notify("No refinement provided. Plan unchanged.", "info");
539
- }
540
- }
541
- });
542
-
543
- // Restore state on session start/resume
544
- pi.on("session_start", async (_event, ctx) => {
545
- if (pi.getFlag("plan") === true) {
546
- planModeEnabled = true;
547
- }
548
-
549
- const entries = ctx.sessionManager.getEntries();
550
-
551
- // Restore persisted state
552
- const planModeEntry = entries
553
- .filter(
554
- (e: { type: string; customType?: string }) =>
555
- e.type === "custom" && e.customType === "plan-mode"
556
- )
557
- .pop() as
558
- | {
559
- data?: {
560
- enabled?: boolean;
561
- normalTools?: string[];
562
- todos?: TodoItem[];
563
- };
564
- }
565
- | undefined;
566
-
567
- if (planModeEntry?.data) {
568
- planModeEnabled = planModeEntry.data.enabled ?? planModeEnabled;
569
- normalModeTools = planModeEntry.data.normalTools ?? normalModeTools;
570
- todoItems = planModeEntry.data.todos ?? todoItems;
571
- }
572
-
573
- if (normalModeTools.length === 0) {
574
- captureNormalModeTools();
575
- }
576
- if (planModeEnabled) {
577
- applyPlanModeTools();
578
- } else {
579
- restoreNormalModeTools();
580
- }
581
- updateStatus(ctx);
582
- });
583
- }