@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.
Files changed (3) hide show
  1. package/index.ts +78 -1
  2. package/package.json +1 -1
  3. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comfanion/usethis_todo",
3
- "version": "0.1.6",
3
+ "version": "0.1.7-dev.2",
4
4
  "description": "OpenCode plugin: enhanced TODO tools (dual storage + dependency graph)",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
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
- return {
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. TODOv2 (Prefer this instead of TODO)",
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 identifiers"),
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
  },