pi-agent-toolkit 0.1.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/dist/dotfiles/AGENTS.md +197 -0
- package/dist/dotfiles/APPEND_SYSTEM.md +78 -0
- package/dist/dotfiles/agent-modes.json +12 -0
- package/dist/dotfiles/agent-skills/exa-search/.env.example +4 -0
- package/dist/dotfiles/agent-skills/exa-search/SKILL.md +234 -0
- package/dist/dotfiles/agent-skills/exa-search/scripts/exa-api.cjs +197 -0
- package/dist/dotfiles/auth.json.template +5 -0
- package/dist/dotfiles/damage-control-rules.yaml +318 -0
- package/dist/dotfiles/extensions/btw.ts +1031 -0
- package/dist/dotfiles/extensions/commit-approval.ts +590 -0
- package/dist/dotfiles/extensions/context.ts +578 -0
- package/dist/dotfiles/extensions/control.ts +1748 -0
- package/dist/dotfiles/extensions/damage-control/index.ts +543 -0
- package/dist/dotfiles/extensions/damage-control/node_modules/.package-lock.json +22 -0
- package/dist/dotfiles/extensions/damage-control/package-lock.json +28 -0
- package/dist/dotfiles/extensions/damage-control/package.json +7 -0
- package/dist/dotfiles/extensions/dirty-repo-guard.ts +56 -0
- package/dist/dotfiles/extensions/exa-enforce.ts +51 -0
- package/dist/dotfiles/extensions/exa-search-tool.ts +384 -0
- package/dist/dotfiles/extensions/execute-command/index.ts +82 -0
- package/dist/dotfiles/extensions/files.ts +1112 -0
- package/dist/dotfiles/extensions/loop.ts +446 -0
- package/dist/dotfiles/extensions/pr-approval.ts +730 -0
- package/dist/dotfiles/extensions/qna-interactive.ts +532 -0
- package/dist/dotfiles/extensions/question-mode.ts +242 -0
- package/dist/dotfiles/extensions/require-session-name-on-exit.ts +141 -0
- package/dist/dotfiles/extensions/review.ts +2091 -0
- package/dist/dotfiles/extensions/session-breakdown.ts +1629 -0
- package/dist/dotfiles/extensions/term-notify.ts +150 -0
- package/dist/dotfiles/extensions/tilldone.ts +527 -0
- package/dist/dotfiles/extensions/todos.ts +2082 -0
- package/dist/dotfiles/extensions/tools.ts +146 -0
- package/dist/dotfiles/extensions/uv.ts +123 -0
- package/dist/dotfiles/global-skills/brainstorm/SKILL.md +10 -0
- package/dist/dotfiles/global-skills/cli-detector/SKILL.md +192 -0
- package/dist/dotfiles/global-skills/gh-issue-creator/SKILL.md +173 -0
- package/dist/dotfiles/global-skills/google-chat-cards-v2/SKILL.md +237 -0
- package/dist/dotfiles/global-skills/google-chat-cards-v2/references/bridge_tap_implementation.md +466 -0
- package/dist/dotfiles/global-skills/technical-docs/SKILL.md +204 -0
- package/dist/dotfiles/global-skills/technical-docs/references/diagrams.md +168 -0
- package/dist/dotfiles/global-skills/technical-docs/references/examples.md +449 -0
- package/dist/dotfiles/global-skills/technical-docs/scripts/validate_docs.py +352 -0
- package/dist/dotfiles/global-skills/whats-new/SKILL.md +159 -0
- package/dist/dotfiles/intercepted-commands/pip +7 -0
- package/dist/dotfiles/intercepted-commands/pip3 +7 -0
- package/dist/dotfiles/intercepted-commands/poetry +10 -0
- package/dist/dotfiles/intercepted-commands/python +104 -0
- package/dist/dotfiles/intercepted-commands/python3 +104 -0
- package/dist/dotfiles/mcp.json.template +32 -0
- package/dist/dotfiles/models.json +27 -0
- package/dist/dotfiles/settings.json +25 -0
- package/dist/index.js +1344 -0
- package/package.json +34 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
import { Markdown, type MarkdownTheme } from "@mariozechner/pi-tui";
|
|
5
|
+
|
|
6
|
+
function runCmux(args: string[]): void {
|
|
7
|
+
execFile("cmux", args, (error) => {
|
|
8
|
+
if (error) {
|
|
9
|
+
// Ignore quietly when cmux is unavailable or not attached.
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function notifyOsc777(title: string, body: string): void {
|
|
15
|
+
// OSC 777 format: ESC ] 777 ; notify ; title ; body BEL
|
|
16
|
+
// Supported by Ghostty, iTerm2, WezTerm, rxvt-unicode
|
|
17
|
+
process.stdout.write(`\x1b]777;notify;${title};${body}\x07`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isCmuxAvailable(): boolean {
|
|
21
|
+
return !!(process.env.CMUX_WORKSPACE_ID || process.env.CMUX_SURFACE_ID);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function execFileText(command: string, args: string[]): Promise<string | undefined> {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
execFile(command, args, (error, stdout) => {
|
|
27
|
+
if (error) {
|
|
28
|
+
resolve(undefined);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const text = stdout.trim();
|
|
32
|
+
resolve(text || undefined);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function compact(parts: Array<string | undefined>): string[] {
|
|
38
|
+
return parts.filter((part): part is string => Boolean(part && part.trim()));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function firstEnv(names: string[]): string | undefined {
|
|
42
|
+
for (const name of names) {
|
|
43
|
+
const value = process.env[name]?.trim();
|
|
44
|
+
if (value) return value;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function getLocationParts(cwd: string): Promise<{ location: string; branch?: string }> {
|
|
50
|
+
const topLevel = await execFileText("git", ["-C", cwd, "rev-parse", "--show-toplevel"]);
|
|
51
|
+
const branch = await execFileText("git", ["-C", cwd, "branch", "--show-current"]);
|
|
52
|
+
const location = basename(topLevel || cwd) || cwd;
|
|
53
|
+
return { location, branch };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getAgentTitle(): string {
|
|
57
|
+
const explicitLabel = firstEnv(["PI_NOTIFY_LABEL", "PI_AGENT_LABEL"]);
|
|
58
|
+
if (explicitLabel) return explicitLabel;
|
|
59
|
+
|
|
60
|
+
const subagentName = firstEnv(["PI_SUBAGENT_NAME", "SUBAGENT_NAME"]);
|
|
61
|
+
const subagentId = firstEnv(["PI_SUBAGENT_ID", "SUBAGENT_ID"]);
|
|
62
|
+
if (subagentName && subagentId) return `Pi Subagent ${subagentName} (#${subagentId})`;
|
|
63
|
+
if (subagentName) return `Pi Subagent ${subagentName}`;
|
|
64
|
+
if (subagentId) return `Pi Subagent #${subagentId}`;
|
|
65
|
+
return "Pi";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const isTextPart = (part: unknown): part is { type: "text"; text: string } =>
|
|
69
|
+
Boolean(part && typeof part === "object" && "type" in part && part.type === "text" && "text" in part);
|
|
70
|
+
|
|
71
|
+
function extractLastAssistantText(messages: Array<{ role?: string; content?: unknown }>): string | null {
|
|
72
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
73
|
+
const message = messages[i];
|
|
74
|
+
if (message?.role !== "assistant") continue;
|
|
75
|
+
|
|
76
|
+
const content = message.content;
|
|
77
|
+
if (typeof content === "string") return content.trim() || null;
|
|
78
|
+
|
|
79
|
+
if (Array.isArray(content)) {
|
|
80
|
+
const text = content.filter(isTextPart).map((part) => part.text).join("\n").trim();
|
|
81
|
+
return text || null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const plainMarkdownTheme: MarkdownTheme = {
|
|
90
|
+
heading: (text) => text,
|
|
91
|
+
link: (text) => text,
|
|
92
|
+
linkUrl: () => "",
|
|
93
|
+
code: (text) => text,
|
|
94
|
+
codeBlock: (text) => text,
|
|
95
|
+
codeBlockBorder: () => "",
|
|
96
|
+
quote: (text) => text,
|
|
97
|
+
quoteBorder: () => "",
|
|
98
|
+
hr: () => "",
|
|
99
|
+
listBullet: () => "",
|
|
100
|
+
bold: (text) => text,
|
|
101
|
+
italic: (text) => text,
|
|
102
|
+
strikethrough: (text) => text,
|
|
103
|
+
underline: (text) => text,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
function toPlainText(text: string): string {
|
|
107
|
+
const markdown = new Markdown(text, 0, 0, plainMarkdownTheme);
|
|
108
|
+
return markdown.render(80).join("\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function formatAssistantPreview(text: string | null, maxLength = 200): string {
|
|
112
|
+
if (!text) return "Turn complete";
|
|
113
|
+
const plain = toPlainText(text).replace(/\s+/g, " ").trim();
|
|
114
|
+
if (!plain) return "Turn complete";
|
|
115
|
+
return plain.length > maxLength ? `${plain.slice(0, maxLength - 1)}...` : plain;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function buildNotification(cwd: string, messages?: Array<{ role?: string; content?: unknown }>): Promise<{ title: string; subtitle: string; body: string }> {
|
|
119
|
+
const { location, branch } = await getLocationParts(cwd);
|
|
120
|
+
const workspaceId = process.env.CMUX_WORKSPACE_ID;
|
|
121
|
+
const surfaceId = process.env.CMUX_SURFACE_ID;
|
|
122
|
+
const title = getAgentTitle();
|
|
123
|
+
const subtitle = compact([location, branch, workspaceId, surfaceId]).join(" · ");
|
|
124
|
+
const lastText = messages ? extractLastAssistantText(messages) : null;
|
|
125
|
+
const body = formatAssistantPreview(lastText);
|
|
126
|
+
return { title, subtitle, body };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default function termNotifyExtension(pi: ExtensionAPI) {
|
|
130
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
131
|
+
const notification = await buildNotification(ctx.cwd, event.messages);
|
|
132
|
+
|
|
133
|
+
if (isCmuxAvailable()) {
|
|
134
|
+
// cmux: native notification + visual flash
|
|
135
|
+
runCmux([
|
|
136
|
+
"notify",
|
|
137
|
+
"--title",
|
|
138
|
+
notification.title,
|
|
139
|
+
"--subtitle",
|
|
140
|
+
notification.subtitle,
|
|
141
|
+
"--body",
|
|
142
|
+
notification.body,
|
|
143
|
+
]);
|
|
144
|
+
runCmux(["trigger-flash"]);
|
|
145
|
+
} else {
|
|
146
|
+
// Fallback: OSC 777 for Ghostty, iTerm2, WezTerm
|
|
147
|
+
notifyOsc777(notification.title, notification.body);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TillDone Extension — Togglable Task Discipline
|
|
3
|
+
*
|
|
4
|
+
* When enabled, the agent MUST define tasks before using other tools.
|
|
5
|
+
* Off by default. Toggle with /tasks (same pattern as question-mode).
|
|
6
|
+
*
|
|
7
|
+
* Commands:
|
|
8
|
+
* /tasks - Toggle task mode on/off
|
|
9
|
+
* /tasks on - Enable task discipline
|
|
10
|
+
* /tasks off - Disable and clear tasks
|
|
11
|
+
* /tasks status - Show current state
|
|
12
|
+
*
|
|
13
|
+
* When enabled:
|
|
14
|
+
* - Agent is blocked until it calls `tilldone add` to define tasks
|
|
15
|
+
* - Agent must toggle a task to "inprogress" before using other tools
|
|
16
|
+
* - Persistent widget shows current task below the editor
|
|
17
|
+
* - Status line shows progress
|
|
18
|
+
* - Auto-nudge when agent finishes with incomplete tasks
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { StringEnum } from "@mariozechner/pi-ai";
|
|
22
|
+
import type { ExtensionAPI, ExtensionContext, ToolCallEventResult } from "@mariozechner/pi-coding-agent";
|
|
23
|
+
import { truncateToWidth } from "@mariozechner/pi-tui";
|
|
24
|
+
import { Type } from "@sinclair/typebox";
|
|
25
|
+
|
|
26
|
+
// -- Types ------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
type TaskStatus = "idle" | "inprogress" | "done";
|
|
29
|
+
|
|
30
|
+
interface Task {
|
|
31
|
+
id: number;
|
|
32
|
+
text: string;
|
|
33
|
+
status: TaskStatus;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface TillDoneState {
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
tasks: Task[];
|
|
39
|
+
nextId: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface TillDoneDetails {
|
|
43
|
+
action: string;
|
|
44
|
+
tasks: Task[];
|
|
45
|
+
nextId: number;
|
|
46
|
+
error?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const STATUS_ICON: Record<TaskStatus, string> = { idle: "( )", inprogress: "(*)", done: "(x)" };
|
|
50
|
+
const NEXT_STATUS: Record<TaskStatus, TaskStatus> = { idle: "inprogress", inprogress: "done", done: "idle" };
|
|
51
|
+
|
|
52
|
+
// -- Extension --------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
export default function (pi: ExtensionAPI) {
|
|
55
|
+
let enabled = false;
|
|
56
|
+
let tasks: Task[] = [];
|
|
57
|
+
let nextId = 1;
|
|
58
|
+
let nudgedThisCycle = false;
|
|
59
|
+
|
|
60
|
+
// -- State helpers --------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
const makeDetails = (action: string, error?: string): TillDoneDetails => ({
|
|
63
|
+
action,
|
|
64
|
+
tasks: [...tasks],
|
|
65
|
+
nextId,
|
|
66
|
+
...(error ? { error } : {}),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
function persistState() {
|
|
70
|
+
pi.appendEntry<TillDoneState>("tilldone-state", {
|
|
71
|
+
enabled,
|
|
72
|
+
tasks: [...tasks],
|
|
73
|
+
nextId,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function restoreFromBranch(ctx: ExtensionContext) {
|
|
78
|
+
// Check custom state entries first
|
|
79
|
+
const branchEntries = ctx.sessionManager.getBranch();
|
|
80
|
+
let state: TillDoneState | undefined;
|
|
81
|
+
|
|
82
|
+
for (const entry of branchEntries) {
|
|
83
|
+
if (entry.type === "custom" && entry.customType === "tilldone-state") {
|
|
84
|
+
const data = entry.data as TillDoneState | undefined;
|
|
85
|
+
if (data) {
|
|
86
|
+
state = {
|
|
87
|
+
enabled: Boolean(data.enabled),
|
|
88
|
+
tasks: Array.isArray(data.tasks) ? data.tasks : [],
|
|
89
|
+
nextId: typeof data.nextId === "number" ? data.nextId : 1,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Also reconstruct from tool results (in case of older entries)
|
|
94
|
+
if (entry.type === "message" && entry.message.role === "toolResult" && entry.message.toolName === "tilldone") {
|
|
95
|
+
const details = entry.message.details as TillDoneDetails | undefined;
|
|
96
|
+
if (details && state?.enabled) {
|
|
97
|
+
state.tasks = details.tasks;
|
|
98
|
+
state.nextId = details.nextId;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (state) {
|
|
104
|
+
enabled = state.enabled;
|
|
105
|
+
tasks = state.tasks;
|
|
106
|
+
nextId = state.nextId;
|
|
107
|
+
} else {
|
|
108
|
+
enabled = false;
|
|
109
|
+
tasks = [];
|
|
110
|
+
nextId = 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
refreshUI(ctx);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// -- UI -------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
function refreshUI(ctx: ExtensionContext) {
|
|
119
|
+
if (!enabled) {
|
|
120
|
+
ctx.ui.setStatus("tilldone", undefined);
|
|
121
|
+
ctx.ui.setWidget("tilldone-current", undefined);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Status line: compact progress
|
|
126
|
+
const done = tasks.filter((t) => t.status === "done").length;
|
|
127
|
+
const total = tasks.length;
|
|
128
|
+
|
|
129
|
+
if (total === 0) {
|
|
130
|
+
ctx.ui.setStatus("tilldone", ctx.ui.theme.fg("warning", "TASKS: none"));
|
|
131
|
+
} else if (done === total) {
|
|
132
|
+
ctx.ui.setStatus("tilldone", ctx.ui.theme.fg("success", `TASKS: ${done}/${total} done`));
|
|
133
|
+
} else {
|
|
134
|
+
ctx.ui.setStatus(
|
|
135
|
+
"tilldone",
|
|
136
|
+
ctx.ui.theme.fg("accent", `TASKS: ${done}/${total}`),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Widget: show current inprogress task below editor
|
|
141
|
+
const current = tasks.find((t) => t.status === "inprogress");
|
|
142
|
+
if (!current) {
|
|
143
|
+
ctx.ui.setWidget("tilldone-current", undefined);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
ctx.ui.setWidget(
|
|
148
|
+
"tilldone-current",
|
|
149
|
+
(_tui, theme) => ({
|
|
150
|
+
render(width: number): string[] {
|
|
151
|
+
const cur = tasks.find((t) => t.status === "inprogress");
|
|
152
|
+
if (!cur) return [];
|
|
153
|
+
const line =
|
|
154
|
+
theme.fg("accent", ">> ") +
|
|
155
|
+
theme.fg("dim", "WORKING ON ") +
|
|
156
|
+
theme.fg("accent", `#${cur.id}`) +
|
|
157
|
+
theme.fg("dim", " - ") +
|
|
158
|
+
theme.fg("success", cur.text);
|
|
159
|
+
return [truncateToWidth(line, width)];
|
|
160
|
+
},
|
|
161
|
+
invalidate() {},
|
|
162
|
+
}),
|
|
163
|
+
{ placement: "belowEditor" },
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// -- Toggle ---------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
function enableTasks(ctx: ExtensionContext) {
|
|
170
|
+
if (enabled) {
|
|
171
|
+
ctx.ui.notify("Task mode is already enabled.", "info");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
enabled = true;
|
|
175
|
+
persistState();
|
|
176
|
+
refreshUI(ctx);
|
|
177
|
+
ctx.ui.notify("Task mode enabled. Agent must define tasks before working.", "info");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function disableTasks(ctx: ExtensionContext) {
|
|
181
|
+
if (!enabled) {
|
|
182
|
+
ctx.ui.notify("Task mode is already disabled.", "info");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
enabled = false;
|
|
186
|
+
tasks = [];
|
|
187
|
+
nextId = 1;
|
|
188
|
+
persistState();
|
|
189
|
+
refreshUI(ctx);
|
|
190
|
+
ctx.ui.notify("Task mode disabled. Tasks cleared.", "info");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// -- Command: /tasks ------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
pi.registerCommand("tasks", {
|
|
196
|
+
description: "Toggle task discipline mode (tilldone)",
|
|
197
|
+
handler: async (args, ctx) => {
|
|
198
|
+
const action = args.trim().toLowerCase();
|
|
199
|
+
|
|
200
|
+
if (action === "status") {
|
|
201
|
+
const mode = enabled ? "on" : "off";
|
|
202
|
+
const done = tasks.filter((t) => t.status === "done").length;
|
|
203
|
+
const inprog = tasks.filter((t) => t.status === "inprogress").length;
|
|
204
|
+
const idle = tasks.filter((t) => t.status === "idle").length;
|
|
205
|
+
const lines = [`task-mode:${mode} | ${tasks.length} tasks (${done} done, ${inprog} active, ${idle} idle)`];
|
|
206
|
+
for (const t of tasks) {
|
|
207
|
+
lines.push(` ${STATUS_ICON[t.status]} #${t.id} ${t.text}`);
|
|
208
|
+
}
|
|
209
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (action === "on" || action === "enable") {
|
|
214
|
+
enableTasks(ctx);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (action === "off" || action === "disable") {
|
|
219
|
+
disableTasks(ctx);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (action === "" || action === "toggle") {
|
|
224
|
+
if (enabled) {
|
|
225
|
+
disableTasks(ctx);
|
|
226
|
+
} else {
|
|
227
|
+
enableTasks(ctx);
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
ctx.ui.notify("Usage: /tasks [on|off|toggle|status]", "warning");
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// -- Blocking gate --------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
pi.on("tool_call", async (event): Promise<ToolCallEventResult | undefined> => {
|
|
239
|
+
if (!enabled) return undefined;
|
|
240
|
+
if (event.toolName === "tilldone") return undefined;
|
|
241
|
+
|
|
242
|
+
const pending = tasks.filter((t) => t.status !== "done");
|
|
243
|
+
const active = tasks.filter((t) => t.status === "inprogress");
|
|
244
|
+
|
|
245
|
+
if (tasks.length === 0) {
|
|
246
|
+
return {
|
|
247
|
+
block: true,
|
|
248
|
+
reason:
|
|
249
|
+
"[Task Mode] No tasks defined. You MUST use `tilldone add` to define your tasks before using any other tools. Plan your work first.",
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (pending.length === 0) {
|
|
253
|
+
return {
|
|
254
|
+
block: true,
|
|
255
|
+
reason:
|
|
256
|
+
"[Task Mode] All tasks are done. Use `tilldone add` for new tasks before using other tools.",
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
if (active.length === 0) {
|
|
260
|
+
return {
|
|
261
|
+
block: true,
|
|
262
|
+
reason:
|
|
263
|
+
"[Task Mode] No task is in progress. Use `tilldone toggle` to mark a task as inprogress before doing any work.",
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return undefined;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// -- System prompt injection when enabled ---------------------------------
|
|
271
|
+
|
|
272
|
+
pi.on("before_agent_start", async () => {
|
|
273
|
+
if (!enabled) return undefined;
|
|
274
|
+
|
|
275
|
+
const taskList = tasks
|
|
276
|
+
.map((t) => ` ${STATUS_ICON[t.status]} #${t.id} (${t.status}): ${t.text}`)
|
|
277
|
+
.join("\n");
|
|
278
|
+
|
|
279
|
+
const content = tasks.length > 0
|
|
280
|
+
? `[TASK MODE ACTIVE]\nYou have a task list managed by the tilldone tool. Current tasks:\n\n${taskList}\n\nRules:\n- Always toggle a task to inprogress before starting work on it.\n- Toggle to done when finished.\n- Only one task can be inprogress at a time.\n- Work through tasks systematically.`
|
|
281
|
+
: `[TASK MODE ACTIVE]\nTask discipline is enabled but no tasks are defined yet.\nYou MUST use the tilldone tool to add tasks before using any other tools.\nParse the user's request into concrete tasks.`;
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
message: {
|
|
285
|
+
customType: "tilldone-context",
|
|
286
|
+
content,
|
|
287
|
+
display: false,
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// -- Auto-nudge on agent_end ----------------------------------------------
|
|
293
|
+
|
|
294
|
+
pi.on("agent_end", async () => {
|
|
295
|
+
if (!enabled) return;
|
|
296
|
+
|
|
297
|
+
const incomplete = tasks.filter((t) => t.status !== "done");
|
|
298
|
+
if (incomplete.length === 0 || nudgedThisCycle) return;
|
|
299
|
+
|
|
300
|
+
nudgedThisCycle = true;
|
|
301
|
+
|
|
302
|
+
const taskList = incomplete
|
|
303
|
+
.map((t) => ` ${STATUS_ICON[t.status]} #${t.id} (${t.status}): ${t.text}`)
|
|
304
|
+
.join("\n");
|
|
305
|
+
|
|
306
|
+
pi.sendMessage(
|
|
307
|
+
{
|
|
308
|
+
customType: "tilldone-nudge",
|
|
309
|
+
content: `You still have ${incomplete.length} incomplete task(s):\n\n${taskList}\n\nContinue working on them or mark them done with tilldone toggle.`,
|
|
310
|
+
display: true,
|
|
311
|
+
},
|
|
312
|
+
{ triggerTurn: true },
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
pi.on("input", async () => {
|
|
317
|
+
nudgedThisCycle = false;
|
|
318
|
+
return { action: "continue" as const };
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// -- Register tilldone tool -----------------------------------------------
|
|
322
|
+
|
|
323
|
+
const TillDoneParams = Type.Object({
|
|
324
|
+
action: StringEnum(["add", "toggle", "remove", "update", "list", "clear"] as const),
|
|
325
|
+
text: Type.Optional(Type.String({ description: "Task text (for add/update)" })),
|
|
326
|
+
texts: Type.Optional(Type.Array(Type.String(), { description: "Multiple task texts (for batch add)" })),
|
|
327
|
+
id: Type.Optional(Type.Number({ description: "Task ID (for toggle/remove/update)" })),
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
pi.registerTool({
|
|
331
|
+
name: "tilldone",
|
|
332
|
+
label: "TillDone",
|
|
333
|
+
description:
|
|
334
|
+
"Manage the task list. Actions: add (text or texts[] for batch), toggle (id) cycles idle->inprogress->done, remove (id), update (id + text), list, clear. " +
|
|
335
|
+
"You MUST add tasks before using any other tools when task mode is active. " +
|
|
336
|
+
"Always toggle a task to inprogress before starting work on it, and to done when finished.",
|
|
337
|
+
|
|
338
|
+
promptSnippet:
|
|
339
|
+
"Manage the task list. Actions: add (text or texts[] for batch), toggle (id) cycles idle->inprogress->done, " +
|
|
340
|
+
"remove (id), update (id + text), list, clear. You MUST add tasks before using any other tools when task mode is active. " +
|
|
341
|
+
"Always toggle a task to inprogress before starting work on it, and to done when finished.",
|
|
342
|
+
|
|
343
|
+
promptGuidelines: [
|
|
344
|
+
"When task mode is active, call tilldone add before using any other tools.",
|
|
345
|
+
"Toggle a task to inprogress before starting work on it, and to done when finished.",
|
|
346
|
+
"Only one task can be inprogress at a time; toggling a new task auto-pauses the current one.",
|
|
347
|
+
"Use texts[] for batch adding multiple tasks in a single call.",
|
|
348
|
+
],
|
|
349
|
+
|
|
350
|
+
parameters: TillDoneParams,
|
|
351
|
+
|
|
352
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
353
|
+
switch (params.action) {
|
|
354
|
+
case "add": {
|
|
355
|
+
const items = params.texts?.length ? params.texts : params.text ? [params.text] : [];
|
|
356
|
+
if (items.length === 0) {
|
|
357
|
+
return {
|
|
358
|
+
content: [{ type: "text" as const, text: "Error: text or texts required for add" }],
|
|
359
|
+
details: makeDetails("add", "text required"),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const added: Task[] = [];
|
|
363
|
+
for (const item of items) {
|
|
364
|
+
const t: Task = { id: nextId++, text: item, status: "idle" };
|
|
365
|
+
tasks.push(t);
|
|
366
|
+
added.push(t);
|
|
367
|
+
}
|
|
368
|
+
const msg = added.length === 1
|
|
369
|
+
? `Added task #${added[0].id}: ${added[0].text}`
|
|
370
|
+
: `Added ${added.length} tasks: ${added.map((t) => `#${t.id}`).join(", ")}`;
|
|
371
|
+
refreshUI(ctx);
|
|
372
|
+
persistState();
|
|
373
|
+
return {
|
|
374
|
+
content: [{ type: "text" as const, text: msg }],
|
|
375
|
+
details: makeDetails("add"),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
case "toggle": {
|
|
380
|
+
if (params.id === undefined) {
|
|
381
|
+
return {
|
|
382
|
+
content: [{ type: "text" as const, text: "Error: id required for toggle" }],
|
|
383
|
+
details: makeDetails("toggle", "id required"),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
const task = tasks.find((t) => t.id === params.id);
|
|
387
|
+
if (!task) {
|
|
388
|
+
return {
|
|
389
|
+
content: [{ type: "text" as const, text: `Task #${params.id} not found` }],
|
|
390
|
+
details: makeDetails("toggle", `#${params.id} not found`),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
const prev = task.status;
|
|
394
|
+
task.status = NEXT_STATUS[task.status];
|
|
395
|
+
|
|
396
|
+
// Enforce single inprogress
|
|
397
|
+
const demoted: Task[] = [];
|
|
398
|
+
if (task.status === "inprogress") {
|
|
399
|
+
for (const t of tasks) {
|
|
400
|
+
if (t.id !== task.id && t.status === "inprogress") {
|
|
401
|
+
t.status = "idle";
|
|
402
|
+
demoted.push(t);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
let msg = `Task #${task.id}: ${prev} -> ${task.status}`;
|
|
408
|
+
if (demoted.length > 0) {
|
|
409
|
+
msg += `\n(Auto-paused ${demoted.map((t) => `#${t.id}`).join(", ")} -> idle. Only one task can be inprogress at a time.)`;
|
|
410
|
+
}
|
|
411
|
+
refreshUI(ctx);
|
|
412
|
+
persistState();
|
|
413
|
+
return {
|
|
414
|
+
content: [{ type: "text" as const, text: msg }],
|
|
415
|
+
details: makeDetails("toggle"),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
case "remove": {
|
|
420
|
+
if (params.id === undefined) {
|
|
421
|
+
return {
|
|
422
|
+
content: [{ type: "text" as const, text: "Error: id required for remove" }],
|
|
423
|
+
details: makeDetails("remove", "id required"),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
const idx = tasks.findIndex((t) => t.id === params.id);
|
|
427
|
+
if (idx === -1) {
|
|
428
|
+
return {
|
|
429
|
+
content: [{ type: "text" as const, text: `Task #${params.id} not found` }],
|
|
430
|
+
details: makeDetails("remove", `#${params.id} not found`),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const removed = tasks.splice(idx, 1)[0];
|
|
434
|
+
refreshUI(ctx);
|
|
435
|
+
persistState();
|
|
436
|
+
return {
|
|
437
|
+
content: [{ type: "text" as const, text: `Removed task #${removed.id}: ${removed.text}` }],
|
|
438
|
+
details: makeDetails("remove"),
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
case "update": {
|
|
443
|
+
if (params.id === undefined || !params.text) {
|
|
444
|
+
return {
|
|
445
|
+
content: [{ type: "text" as const, text: "Error: id and text required for update" }],
|
|
446
|
+
details: makeDetails("update", "id and text required"),
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
const toUpdate = tasks.find((t) => t.id === params.id);
|
|
450
|
+
if (!toUpdate) {
|
|
451
|
+
return {
|
|
452
|
+
content: [{ type: "text" as const, text: `Task #${params.id} not found` }],
|
|
453
|
+
details: makeDetails("update", `#${params.id} not found`),
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
const oldText = toUpdate.text;
|
|
457
|
+
toUpdate.text = params.text;
|
|
458
|
+
refreshUI(ctx);
|
|
459
|
+
persistState();
|
|
460
|
+
return {
|
|
461
|
+
content: [{ type: "text" as const, text: `Updated #${toUpdate.id}: "${oldText}" -> "${toUpdate.text}"` }],
|
|
462
|
+
details: makeDetails("update"),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
case "list": {
|
|
467
|
+
const result = {
|
|
468
|
+
content: [{
|
|
469
|
+
type: "text" as const,
|
|
470
|
+
text: tasks.length
|
|
471
|
+
? tasks.map((t) => `${STATUS_ICON[t.status]} #${t.id} (${t.status}): ${t.text}`).join("\n")
|
|
472
|
+
: "No tasks defined yet.",
|
|
473
|
+
}],
|
|
474
|
+
details: makeDetails("list"),
|
|
475
|
+
};
|
|
476
|
+
refreshUI(ctx);
|
|
477
|
+
return result;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
case "clear": {
|
|
481
|
+
if (tasks.length > 0) {
|
|
482
|
+
const confirmed = await ctx.ui.confirm(
|
|
483
|
+
"Clear all tasks?",
|
|
484
|
+
`This will remove all ${tasks.length} task(s). Continue?`,
|
|
485
|
+
{ timeout: 30000 },
|
|
486
|
+
);
|
|
487
|
+
if (!confirmed) {
|
|
488
|
+
return {
|
|
489
|
+
content: [{ type: "text" as const, text: "Clear cancelled by user." }],
|
|
490
|
+
details: makeDetails("clear", "cancelled"),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const count = tasks.length;
|
|
495
|
+
tasks = [];
|
|
496
|
+
nextId = 1;
|
|
497
|
+
refreshUI(ctx);
|
|
498
|
+
persistState();
|
|
499
|
+
return {
|
|
500
|
+
content: [{ type: "text" as const, text: `Cleared ${count} task(s)` }],
|
|
501
|
+
details: makeDetails("clear"),
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
default:
|
|
506
|
+
return {
|
|
507
|
+
content: [{ type: "text" as const, text: `Unknown action: ${params.action}` }],
|
|
508
|
+
details: makeDetails("list", `unknown action: ${params.action}`),
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// -- Session lifecycle ----------------------------------------------------
|
|
515
|
+
|
|
516
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
517
|
+
restoreFromBranch(ctx);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
521
|
+
restoreFromBranch(ctx);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
pi.on("session_fork", async (_event, ctx) => {
|
|
525
|
+
restoreFromBranch(ctx);
|
|
526
|
+
});
|
|
527
|
+
}
|