@comfanion/usethis_todo 0.1.6 → 0.1.7-dev.2
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/index.ts +78 -1
- package/package.json +1 -1
- package/tools.ts +75 -8
package/index.ts
CHANGED
|
@@ -2,6 +2,12 @@ import type { Plugin } from "@opencode-ai/plugin"
|
|
|
2
2
|
|
|
3
3
|
import { write, read, read_five, read_by_id, update } from "./tools"
|
|
4
4
|
|
|
5
|
+
interface TodoPruneState {
|
|
6
|
+
lastToolCallId: string | null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const pruneStates = new Map<string, TodoPruneState>()
|
|
10
|
+
|
|
5
11
|
const UsethisTodoPlugin: Plugin = async ({ directory, client }) => {
|
|
6
12
|
// Ensure storage directory exists on init
|
|
7
13
|
try {
|
|
@@ -12,6 +18,69 @@ const UsethisTodoPlugin: Plugin = async ({ directory, client }) => {
|
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
return {
|
|
21
|
+
"experimental.chat.messages.transform": async (_input: unknown, output: { messages: any[] }) => {
|
|
22
|
+
const messages = output.messages || []
|
|
23
|
+
const sessionID = messages[0]?.info?.sessionID
|
|
24
|
+
|
|
25
|
+
if (!sessionID) return
|
|
26
|
+
|
|
27
|
+
const state = pruneStates.get(sessionID)
|
|
28
|
+
if (!state?.lastToolCallId) return
|
|
29
|
+
|
|
30
|
+
const prunedToolNames = new Set([
|
|
31
|
+
"usethis_todo_write",
|
|
32
|
+
"usethis_todo_update",
|
|
33
|
+
"usethis_todo_read",
|
|
34
|
+
"usethis_todo_read_five",
|
|
35
|
+
"usethis_todo_read_by_id",
|
|
36
|
+
"todowrite",
|
|
37
|
+
"todoread",
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
// 1. Collect all "## TODO" user messages and all tool outputs
|
|
41
|
+
const todoTextParts: { part: any }[] = []
|
|
42
|
+
const todoToolParts: { part: any; isLast: boolean }[] = []
|
|
43
|
+
|
|
44
|
+
for (const msg of messages) {
|
|
45
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : []
|
|
46
|
+
for (const part of parts) {
|
|
47
|
+
// Tool outputs
|
|
48
|
+
if (part.type === "tool" && prunedToolNames.has(part.tool) && part.state?.status === "completed") {
|
|
49
|
+
todoToolParts.push({ part, isLast: part.callID === state.lastToolCallId })
|
|
50
|
+
}
|
|
51
|
+
// User "## TODO" snapshots
|
|
52
|
+
if (part.type === "text" && typeof part.text === "string" && part.text.startsWith("## TODO")) {
|
|
53
|
+
todoTextParts.push({ part })
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 2. Check if all tasks are done by parsing the latest snapshot
|
|
59
|
+
const lastSnapshot = todoTextParts[todoTextParts.length - 1]?.part.text || ""
|
|
60
|
+
const doneMatch = lastSnapshot.match(/\[(\d+)\/(\d+) done/)
|
|
61
|
+
const allDone = doneMatch && doneMatch[1] === doneMatch[2] && parseInt(doneMatch[1]) > 0
|
|
62
|
+
|
|
63
|
+
// 3. Prune tool outputs
|
|
64
|
+
for (const { part, isLast } of todoToolParts) {
|
|
65
|
+
if (allDone || !isLast) {
|
|
66
|
+
part.state.output = "[TODO output pruned - see latest]"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 4. Prune "## TODO" user messages
|
|
71
|
+
if (allDone) {
|
|
72
|
+
// All done → prune everything, no need to keep any snapshot
|
|
73
|
+
for (const { part } of todoTextParts) {
|
|
74
|
+
part.text = "[TODO completed - all tasks done]"
|
|
75
|
+
}
|
|
76
|
+
} else if (todoTextParts.length > 1) {
|
|
77
|
+
// Active tasks → keep only the last snapshot
|
|
78
|
+
for (let i = 0; i < todoTextParts.length - 1; i++) {
|
|
79
|
+
todoTextParts[i].part.text = "[TODO snapshot pruned - see latest]"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
15
84
|
tool: {
|
|
16
85
|
usethis_todo_write: write,
|
|
17
86
|
usethis_todo_read: read,
|
|
@@ -22,7 +91,15 @@ const UsethisTodoPlugin: Plugin = async ({ directory, client }) => {
|
|
|
22
91
|
|
|
23
92
|
// UI niceties + publish snapshot into the chat
|
|
24
93
|
"tool.execute.after": async (input, output) => {
|
|
25
|
-
if (!input.tool.startsWith("usethis_todo_")) return
|
|
94
|
+
if (!input.tool.startsWith("usethis_todo_") && input.tool !== "todowrite" && input.tool !== "todoread") return
|
|
95
|
+
|
|
96
|
+
// Update prune state with latest call ID
|
|
97
|
+
const sessionID = input.sessionID
|
|
98
|
+
if (sessionID) {
|
|
99
|
+
const state = pruneStates.get(sessionID) || { lastToolCallId: null }
|
|
100
|
+
state.lastToolCallId = input.callID
|
|
101
|
+
pruneStates.set(sessionID, state)
|
|
102
|
+
}
|
|
26
103
|
|
|
27
104
|
const out = output.output || ""
|
|
28
105
|
|
package/package.json
CHANGED
package/tools.ts
CHANGED
|
@@ -29,6 +29,45 @@ import fs from "fs/promises"
|
|
|
29
29
|
// Types
|
|
30
30
|
// ============================================================================
|
|
31
31
|
|
|
32
|
+
interface TodoFile {
|
|
33
|
+
path: string // File path (relative to project root)
|
|
34
|
+
role: "input" | "output" | "test" | "doc" | "config" | "main" | "graph"
|
|
35
|
+
addedAt: string // ISO timestamp when linked
|
|
36
|
+
addedBy: "manual" | "auto" // How it was added
|
|
37
|
+
lastAccessed?: string // Last read/write timestamp
|
|
38
|
+
toolIds?: number[] // Tool call IDs that touched this file
|
|
39
|
+
preserve?: boolean // Don't prune even on TODO complete
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface TodoTool {
|
|
43
|
+
toolName: string // Tool name (read, write, search, etc.)
|
|
44
|
+
callId: number // Tool call ID in session history
|
|
45
|
+
timestamp: string // ISO timestamp
|
|
46
|
+
filePath?: string // If tool operated on file
|
|
47
|
+
status: "success" | "error" // Tool execution status
|
|
48
|
+
tokens?: number // Tokens used by this tool result
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface TodoSearch {
|
|
52
|
+
query: string // Search query text
|
|
53
|
+
toolId: number // search() tool call ID
|
|
54
|
+
resultsCount: number // Number of results returned
|
|
55
|
+
timestamp: string // ISO timestamp
|
|
56
|
+
topResults?: string[] // Top file paths found
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface TodoContext {
|
|
60
|
+
totalTokens: number // Total tokens in all linked tools
|
|
61
|
+
createdAt: string // Task creation timestamp
|
|
62
|
+
startedAt?: string // When status → in_progress
|
|
63
|
+
completedAt?: string // When status → completed
|
|
64
|
+
estimatedCleanup: number // Estimated tokens prunable on complete
|
|
65
|
+
|
|
66
|
+
// Tracking
|
|
67
|
+
activeTracking: boolean // Is Mind currently tracking this TODO?
|
|
68
|
+
lastSync: string // Last time Mind synced metadata
|
|
69
|
+
}
|
|
70
|
+
|
|
32
71
|
interface Todo {
|
|
33
72
|
id: string // E01-S01-T01
|
|
34
73
|
content: string // Short task summary
|
|
@@ -39,6 +78,19 @@ interface Todo {
|
|
|
39
78
|
blockedBy?: string[] // IDs of blocking tasks
|
|
40
79
|
createdAt?: number
|
|
41
80
|
updatedAt?: number
|
|
81
|
+
|
|
82
|
+
// Mind extensions (backward compatible - optional)
|
|
83
|
+
files?: TodoFile[] // Associated files
|
|
84
|
+
tools?: TodoTool[] // Associated tool calls
|
|
85
|
+
searches?: TodoSearch[] // Associated searches
|
|
86
|
+
context?: TodoContext // Context metadata
|
|
87
|
+
|
|
88
|
+
// Cleanup configuration (optional)
|
|
89
|
+
cleanup?: {
|
|
90
|
+
onComplete?: "auto" | "manual" | "extract"
|
|
91
|
+
preserveFiles?: string[] // Glob patterns to preserve
|
|
92
|
+
preserveTools?: string[] // Tool names to preserve
|
|
93
|
+
}
|
|
42
94
|
}
|
|
43
95
|
|
|
44
96
|
interface NativeTodo {
|
|
@@ -260,11 +312,21 @@ function normalizeTodo(input: any): Todo {
|
|
|
260
312
|
// Auto transition: ready -> done when releases exist
|
|
261
313
|
const promotedStatus = status === "ready" && releases?.length ? "done" : status
|
|
262
314
|
|
|
263
|
-
|
|
315
|
+
// Initialize Mind extensions if provided (backward compatible)
|
|
316
|
+
const normalized: Todo = {
|
|
264
317
|
...input,
|
|
265
318
|
status: promotedStatus,
|
|
266
319
|
releases,
|
|
267
320
|
}
|
|
321
|
+
|
|
322
|
+
// Preserve existing Mind extensions if they exist
|
|
323
|
+
if (input.files) normalized.files = input.files
|
|
324
|
+
if (input.tools) normalized.tools = input.tools
|
|
325
|
+
if (input.searches) normalized.searches = input.searches
|
|
326
|
+
if (input.context) normalized.context = input.context
|
|
327
|
+
if (input.cleanup) normalized.cleanup = input.cleanup
|
|
328
|
+
|
|
329
|
+
return normalized
|
|
268
330
|
}
|
|
269
331
|
|
|
270
332
|
function prioRank(p?: string): number {
|
|
@@ -294,9 +356,14 @@ function todoLine(todo: Todo, byId: Map<string, Todo>): string {
|
|
|
294
356
|
const desc = todo.description?.trim() ? ` — ${todo.description.trim()}` : ""
|
|
295
357
|
const rel = todo.releases?.length ? ` [rel: ${todo.releases.join(", ")}]` : ""
|
|
296
358
|
const deps = todo.blockedBy?.length ? ` ← ${todo.blockedBy.join(", ")}` : ""
|
|
359
|
+
|
|
360
|
+
// Mind extensions
|
|
361
|
+
const files = todo.files?.length ? ` 📄${todo.files.length}` : ""
|
|
362
|
+
const tokens = todo.context?.totalTokens ? ` (${todo.context.totalTokens}t)` : ""
|
|
363
|
+
|
|
297
364
|
const ns = normalizeStatus(todo.status)
|
|
298
365
|
const icon = isBlocked(todo, byId) ? "⊗" : SI(ns)
|
|
299
|
-
return `${icon} ${PE(todo.priority)} ${todo.id}: ${todo.content}${desc}${rel}${deps}`
|
|
366
|
+
return `${icon} ${PE(todo.priority)} ${todo.id}: ${todo.content}${desc}${rel}${files}${tokens}${deps}`
|
|
300
367
|
}
|
|
301
368
|
|
|
302
369
|
function renderNestedTodoList(todos: Todo[], allTodos?: Todo[]): string {
|
|
@@ -411,17 +478,17 @@ function formatGraph(graph: TodoGraph): string {
|
|
|
411
478
|
// ============================================================================
|
|
412
479
|
|
|
413
480
|
export const write = tool({
|
|
414
|
-
description: "Create or update TODO list.
|
|
481
|
+
description: "Create or update TODO list. Use this for TODO. For better performance use ID for task relation",
|
|
415
482
|
args: {
|
|
416
483
|
todos: tool.schema.array(
|
|
417
484
|
tool.schema.object({
|
|
418
|
-
id: tool.schema.string().describe("Task ID in concat format: E01-S01-T01"),
|
|
419
|
-
content: tool.schema.string().describe("Short task summary"),
|
|
420
|
-
description: tool.schema.string().optional().describe("Full task description"),
|
|
421
|
-
releases: tool.schema.array(tool.schema.string()).optional().describe("Release
|
|
485
|
+
id: tool.schema.string().describe("Task ID in concat format: E01-S01-T01(for graph dependency)"),
|
|
486
|
+
content: tool.schema.string().describe("Short task summary or title"),
|
|
487
|
+
description: tool.schema.string().optional().describe("Full task description. Helps to remember what should be done"),
|
|
488
|
+
releases: tool.schema.array(tool.schema.string()).optional().describe("Set IDs for AUTO Release task from ready -> done(after review)"),
|
|
422
489
|
status: tool.schema.string().describe("todo | in_progress | ready | done"),
|
|
423
490
|
priority: tool.schema.string().describe("CRIT | HIGH | MED | LOW"),
|
|
424
|
-
blockedBy: tool.schema.array(tool.schema.string()).optional().describe("IDs of blocking tasks"),
|
|
491
|
+
blockedBy: tool.schema.array(tool.schema.string()).optional().describe("IDs of blocking tasks. this help you understand scope better"),
|
|
425
492
|
}),
|
|
426
493
|
).describe("Array of todos"),
|
|
427
494
|
},
|