@ian-pascoe/pi-todo 0.1.3 → 0.2.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/README.md +12 -1
- package/package.json +4 -4
- package/skills/pi-todo/SKILL.md +1 -0
- package/src/pi-todo-extension.ts +22 -1
- package/src/todo-list.ts +37 -6
- package/src/troubleshooting-skill.ts +9 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`@ian-pascoe/pi-todo` gives Pi agents a minimal, session-native **Todo List** without imposing a planning workflow.
|
|
4
4
|
|
|
5
|
-
Requires Node `>=22.19.0` and Pi `>=0.
|
|
5
|
+
Requires Node `>=22.19.0` and Pi `>=0.99.0`.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -24,12 +24,23 @@ The `todo` tool supports five actions:
|
|
|
24
24
|
| `remove` | `id` | Removes one Task |
|
|
25
25
|
| `clear` | — | Removes every Task and resets IDs |
|
|
26
26
|
|
|
27
|
+
A Pi `codemode` script receives the result as an object (the tool declares an `outputSchema`) instead of the text (its field names are already single words, so they match Pi's snake_case convention); the model still reads the same text, and failures still throw:
|
|
28
|
+
|
|
29
|
+
| Action | Script value |
|
|
30
|
+
| ---------------- | ---------------------------------------------------------------------------- |
|
|
31
|
+
| `list` | `{ action: "list", tasks }`, each Task `{ id, title, description?, status }` |
|
|
32
|
+
| `add` / `update` | `{ action, task }` with the resulting Task |
|
|
33
|
+
| `remove` | `{ action: "remove", id }` |
|
|
34
|
+
| `clear` | `{ action: "clear", cleared }` with the number of Tasks removed |
|
|
35
|
+
|
|
27
36
|
Task status is `pending`, `active`, or `completed`; `add` defaults to `pending`. Tasks are flat, duplicate titles are allowed, and multiple Tasks may be active. Set `description` to `null` during `update` to remove it.
|
|
28
37
|
|
|
29
38
|
Each changed Todo List is projected from its immutable session state entry as a hidden full snapshot at a fixed conversation position. Mutations inside a tool group appear after all sibling results; later requests keep earlier snapshots in place. Clearing the list produces an explicit empty snapshot. The system prompt and tool description do not change.
|
|
30
39
|
|
|
31
40
|
After compaction, a fixed baseline immediately after the summary restores the state from before the retained Tail. Retained and newer mutations follow chronologically. This preserves previously written conversation cache prefixes between checkpoints; it does not guarantee provider cache hits.
|
|
32
41
|
|
|
42
|
+
`todo` declares MCP-style `annotations`: not read-only, but non-destructive (it only appends to the session's own journal), not idempotent, and closed-world. Pi reports them through `pi.getAllTools()` for permission extensions and does not send them to model providers.
|
|
43
|
+
|
|
33
44
|
## UI and persistence
|
|
34
45
|
|
|
35
46
|
In interactive mode, a compact widget above the editor shows status counts and up to five Tasks. Tool calls and results use custom transcript rendering. `/todo clear` confirms before manually clearing the list.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ian-pascoe/pi-todo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Minimal session-native Todo List for Pi agents.",
|
|
6
6
|
"keywords": [
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"provenance": true
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@earendil-works/pi-ai": "
|
|
37
|
-
"@earendil-works/pi-coding-agent": "
|
|
38
|
-
"@earendil-works/pi-tui": "
|
|
36
|
+
"@earendil-works/pi-ai": ">=0.99.0",
|
|
37
|
+
"@earendil-works/pi-coding-agent": ">=0.99.0",
|
|
38
|
+
"@earendil-works/pi-tui": ">=0.99.0",
|
|
39
39
|
"typebox": "*"
|
|
40
40
|
},
|
|
41
41
|
"engines": {
|
package/skills/pi-todo/SKILL.md
CHANGED
package/src/pi-todo-extension.ts
CHANGED
|
@@ -8,13 +8,16 @@ import {
|
|
|
8
8
|
createEmptyTodoState,
|
|
9
9
|
TODO_ACTIONS,
|
|
10
10
|
TODO_STATUSES,
|
|
11
|
+
TodoToolOutputSchema,
|
|
11
12
|
todoStatusMarker,
|
|
12
13
|
type TodoActionInput,
|
|
13
14
|
type TodoStateSnapshot,
|
|
14
15
|
type TodoStatus,
|
|
15
16
|
type TodoTask,
|
|
17
|
+
TodoOperationError,
|
|
16
18
|
type TodoToolDetails,
|
|
17
19
|
} from "./todo-list.js";
|
|
20
|
+
import { TROUBLESHOOTING_HINT } from "./troubleshooting-skill.js";
|
|
18
21
|
|
|
19
22
|
const TODO_STATE_ENTRY_TYPE = "pi-todo-state";
|
|
20
23
|
const JOURNAL_FAULT = Symbol.for("@ian-pascoe/pi-todo/journal-fault");
|
|
@@ -158,13 +161,31 @@ export default function piTodoExtension(pi: ExtensionAPI): void {
|
|
|
158
161
|
label: "Todo",
|
|
159
162
|
description: "Manage the current session branch's Todo List.",
|
|
160
163
|
parameters: TodoParameters,
|
|
164
|
+
annotations: {
|
|
165
|
+
readOnlyHint: false,
|
|
166
|
+
destructiveHint: false,
|
|
167
|
+
idempotentHint: false,
|
|
168
|
+
openWorldHint: false,
|
|
169
|
+
},
|
|
170
|
+
outputSchema: TodoToolOutputSchema,
|
|
161
171
|
executionMode: "sequential",
|
|
162
172
|
async execute(_toolCallId, params, signal, _onUpdate, context) {
|
|
163
173
|
signal?.throwIfAborted();
|
|
164
|
-
|
|
174
|
+
let result: ReturnType<typeof runTodoAction>;
|
|
175
|
+
try {
|
|
176
|
+
result = runTodoAction(params, context);
|
|
177
|
+
} catch (cause) {
|
|
178
|
+
// Task-operation errors are model-fixable input errors; anything else is a journal failure.
|
|
179
|
+
if (cause instanceof TodoOperationError) throw cause;
|
|
180
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
181
|
+
throw new Error(`${message}\n\n${TROUBLESHOOTING_HINT}`, { cause });
|
|
182
|
+
}
|
|
165
183
|
return {
|
|
166
184
|
content: [{ type: "text", text: result.message }],
|
|
167
185
|
details: result.details,
|
|
186
|
+
// Separate from `details` so the session's render shape and the script-facing value can
|
|
187
|
+
// evolve independently; today both use the same snake_case-compatible field names.
|
|
188
|
+
structuredContent: structuredClone(result.details),
|
|
168
189
|
};
|
|
169
190
|
},
|
|
170
191
|
renderCall: (params, theme) => {
|
package/src/todo-list.ts
CHANGED
|
@@ -41,10 +41,15 @@ export type TodoActionInput = {
|
|
|
41
41
|
readonly status?: TodoStatus;
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Render details and the tool's structured result. The action discriminates the list from the
|
|
46
|
+
* Task a mutation added or updated, the ID it removed, and the count it cleared.
|
|
47
|
+
*/
|
|
45
48
|
export type TodoToolDetails =
|
|
46
49
|
| { readonly action: "list"; readonly tasks: readonly TodoTask[] }
|
|
47
|
-
| { readonly action:
|
|
50
|
+
| { readonly action: "add" | "update"; readonly task: TodoTask }
|
|
51
|
+
| { readonly action: "remove"; readonly id: TodoTaskId }
|
|
52
|
+
| { readonly action: "clear"; readonly cleared: number };
|
|
48
53
|
|
|
49
54
|
const PositiveSafeIntegerRecord = Type.Integer({
|
|
50
55
|
minimum: 1,
|
|
@@ -57,6 +62,32 @@ const TodoTaskRecord = Type.Object({
|
|
|
57
62
|
status: StringEnum(TODO_STATUSES),
|
|
58
63
|
});
|
|
59
64
|
|
|
65
|
+
/**
|
|
66
|
+
* JSON Schema of the `todo` tool's `structuredContent`, which codemode scripts receive instead of
|
|
67
|
+
* the model-facing text. Flat so Pi's one-line script declaration stays compact; `action` says
|
|
68
|
+
* which other field is present.
|
|
69
|
+
*/
|
|
70
|
+
export const TodoToolOutputSchema = Type.Object(
|
|
71
|
+
{
|
|
72
|
+
action: StringEnum(TODO_ACTIONS, { description: "The operation that ran" }),
|
|
73
|
+
tasks: Type.Optional(
|
|
74
|
+
Type.Array(TodoTaskRecord, { description: "list: every Task in ID order" }),
|
|
75
|
+
),
|
|
76
|
+
task: Type.Optional(TodoTaskRecord),
|
|
77
|
+
id: Type.Optional(
|
|
78
|
+
Type.Integer({
|
|
79
|
+
minimum: 1,
|
|
80
|
+
maximum: Number.MAX_SAFE_INTEGER,
|
|
81
|
+
description: "remove: ID of the removed Task",
|
|
82
|
+
}),
|
|
83
|
+
),
|
|
84
|
+
cleared: Type.Optional(
|
|
85
|
+
Type.Integer({ minimum: 0, description: "clear: number of Tasks removed" }),
|
|
86
|
+
),
|
|
87
|
+
},
|
|
88
|
+
{ additionalProperties: false },
|
|
89
|
+
);
|
|
90
|
+
|
|
60
91
|
/** Serialized shape stored in a `pi-todo-state` session entry. */
|
|
61
92
|
export const TodoStateRecord = Type.Object({
|
|
62
93
|
nextId: PositiveSafeIntegerRecord,
|
|
@@ -158,7 +189,7 @@ export function applyTodoAction(
|
|
|
158
189
|
ok: true,
|
|
159
190
|
state: { nextId: state.nextId + 1, tasks: [...state.tasks, task] },
|
|
160
191
|
message: `Added Task #${task.id}`,
|
|
161
|
-
details: { action: "add" },
|
|
192
|
+
details: { action: "add", task },
|
|
162
193
|
};
|
|
163
194
|
}
|
|
164
195
|
|
|
@@ -190,7 +221,7 @@ export function applyTodoAction(
|
|
|
190
221
|
tasks: state.tasks.filter((candidate) => candidate.id !== task.id),
|
|
191
222
|
},
|
|
192
223
|
message: `Removed Task #${task.id}`,
|
|
193
|
-
details: { action: "remove" },
|
|
224
|
+
details: { action: "remove", id: task.id },
|
|
194
225
|
};
|
|
195
226
|
}
|
|
196
227
|
if (
|
|
@@ -224,7 +255,7 @@ export function applyTodoAction(
|
|
|
224
255
|
),
|
|
225
256
|
},
|
|
226
257
|
message: `Updated Task #${task.id}`,
|
|
227
|
-
details: { action: "update" },
|
|
258
|
+
details: { action: "update", task: updatedTask },
|
|
228
259
|
};
|
|
229
260
|
}
|
|
230
261
|
|
|
@@ -234,7 +265,7 @@ export function applyTodoAction(
|
|
|
234
265
|
ok: true,
|
|
235
266
|
state: count > 0 || state.nextId !== 1 ? createEmptyTodoState() : state,
|
|
236
267
|
message: `Cleared ${count} ${count === 1 ? "Task" : "Tasks"}`,
|
|
237
|
-
details: { action: "clear" },
|
|
268
|
+
details: { action: "clear", cleared: count },
|
|
238
269
|
};
|
|
239
270
|
}
|
|
240
271
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
|
|
3
|
+
/** Absolute path of this package's user-invoked troubleshooting Skill. */
|
|
4
|
+
export const TROUBLESHOOTING_SKILL_PATH = fileURLToPath(
|
|
5
|
+
new URL("../skills/pi-todo/SKILL.md", import.meta.url),
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
/** Model-facing pointer appended to failures this package's Skill diagnoses. */
|
|
9
|
+
export const TROUBLESHOOTING_HINT = `For diagnosis, read the pi-todo troubleshooting Skill at ${TROUBLESHOOTING_SKILL_PATH}.`;
|