@comfanion/usethis_todo 0.1.15-dev.9 → 0.1.16-dev.1

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 +33 -10
  2. package/package.json +1 -1
  3. package/tools.ts +132 -66
package/index.ts CHANGED
@@ -2,7 +2,7 @@ import type { Plugin } from "@opencode-ai/plugin"
2
2
  import path from "path"
3
3
  import fs from "fs/promises"
4
4
 
5
- import { write, read, read_five, read_by_id, update } from "./tools"
5
+ import { write, read, read_five, read_by_id, update, setNativeStorageBase } from "./tools"
6
6
 
7
7
  interface TodoPruneState {
8
8
  lastToolCallId: string | null
@@ -10,7 +10,14 @@ interface TodoPruneState {
10
10
 
11
11
  const pruneStates = new Map<string, TodoPruneState>()
12
12
 
13
- const UsethisTodoPlugin: Plugin = async ({ directory }) => {
13
+ const UsethisTodoPlugin: Plugin = async ({ directory, client }) => {
14
+ // Resolve the authoritative state path from OpenCode server (non-blocking).
15
+ // Must NOT await — server may block until plugin init completes → deadlock.
16
+ client.path.get().then((pathInfo: any) => {
17
+ const state = pathInfo?.data?.state
18
+ if (state) setNativeStorageBase(state)
19
+ }).catch(() => {})
20
+
14
21
  // Ensure storage directory exists on init
15
22
  try {
16
23
  const todoDir = path.join(directory, ".opencode", "session-todo")
@@ -62,20 +69,18 @@ const UsethisTodoPlugin: Plugin = async ({ directory }) => {
62
69
  },
63
70
 
64
71
  tool: {
65
- // Enhanced tools (original names)
72
+ // Enhanced tools only — NO native overrides
73
+ // Native todowrite/todoread stay untouched → Bus.publish fires → sidebar works
66
74
  usethis_todo_write: write,
67
75
  usethis_todo_read: read,
68
76
  usethis_todo_read_five: read_five,
69
77
  usethis_todo_read_by_id: read_by_id,
70
78
  usethis_todo_update: update,
71
- // Override native tools — same implementation, native names
72
- todowrite: write,
73
- todoread: read,
74
79
  },
75
80
 
76
- // Set nicer titles in TUI + track prune state
81
+ // Set nicer titles in TUI + track prune state (enhanced tools only)
77
82
  "tool.execute.after": async (input, output) => {
78
- if (!input.tool.startsWith("usethis_todo_") && input.tool !== "todowrite" && input.tool !== "todoread") return
83
+ if (!input.tool.startsWith("usethis_todo_")) return
79
84
 
80
85
  // Update prune state with latest call ID
81
86
  const sessionID = input.sessionID
@@ -88,13 +93,13 @@ const UsethisTodoPlugin: Plugin = async ({ directory }) => {
88
93
  const out = output.output || ""
89
94
 
90
95
  // Set a nicer title in TUI
91
- if (input.tool === "usethis_todo_write" || input.tool === "todowrite") {
96
+ if (input.tool === "usethis_todo_write") {
92
97
  const match = out.match(/\[(\d+)\/(\d+) done/)
93
98
  output.title = match ? `TODO: ${match[2]} tasks` : "TODO updated"
94
99
  } else if (input.tool === "usethis_todo_update") {
95
100
  const match = out.match(/^✅ (.+)$/m)
96
101
  output.title = match ? match[1] : "Task updated"
97
- } else if (input.tool === "usethis_todo_read" || input.tool === "todoread") {
102
+ } else if (input.tool === "usethis_todo_read") {
98
103
  const match = out.match(/\[(\d+)\/(\d+) done, (\d+) in progress\]/)
99
104
  output.title = match ? `TODO [${match[1]}/${match[2]} done]` : "TODO list"
100
105
  } else if (input.tool === "usethis_todo_read_five") {
@@ -102,6 +107,24 @@ const UsethisTodoPlugin: Plugin = async ({ directory }) => {
102
107
  } else if (input.tool === "usethis_todo_read_by_id") {
103
108
  output.title = "Task details"
104
109
  }
110
+
111
+ // Publish TODO snapshot into chat for write/update ops
112
+ const isWriteOp = input.tool === "usethis_todo_write"
113
+ || input.tool === "usethis_todo_update"
114
+ if (!isWriteOp) return
115
+
116
+ const text = ["## TODO", "", out].join("\n")
117
+ try {
118
+ await client?.session?.prompt?.({
119
+ path: { id: input.sessionID },
120
+ body: {
121
+ noReply: true,
122
+ parts: [{ type: "text", text }],
123
+ },
124
+ })
125
+ } catch {
126
+ // non-fatal
127
+ }
105
128
  },
106
129
  }
107
130
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comfanion/usethis_todo",
3
- "version": "0.1.15-dev.9",
3
+ "version": "0.1.16-dev.1",
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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * TODO Tool with Dependencies & Priority — v3 (dual storage)
2
+ * TODO Tool with Dependencies & Priority — v3 (unified storage)
3
3
  *
4
4
  * 4 commands:
5
5
  * usethis_todo_write({ todos: [...] }) - create/update TODO list
@@ -9,15 +9,13 @@
9
9
  * usethis_todo_update(id, field, value) - update any task field
10
10
  *
11
11
  * Storage:
12
- * Enhanced: .opencode/session-todos/{sid}.json (title, blockedBy, graph)
13
- * Native: ~/.local/share/opencode/storage/todo/{sid}.json (TUI display)
12
+ * Unified: Native OpenCode storage (TUI compatible)
13
+ * Path resolved via client.path.get()
14
14
  *
15
15
  * Features:
16
- * - Hierarchical IDs: E01-S01-T01
17
- * - Dependencies: blockedBy field
18
- * - Priority: CRIT | HIGH | MED | LOW (auto-sorted)
19
- * - Graph: shows available, blocked, parallel tasks
20
- * - Dual write: native OpenCode storage for TUI integration
16
+ * - Stores FULL enhanced schema (blockedBy, priority, etc) in the native file
17
+ * - Maps statuses to native values (pending/completed) for Sidebar compatibility
18
+ * - Preserves original statuses in `usethisStatus` field
21
19
  */
22
20
 
23
21
  import { tool } from "@opencode-ai/plugin"
@@ -79,6 +77,10 @@ interface Todo {
79
77
  createdAt?: number
80
78
  updatedAt?: number
81
79
 
80
+ // Shadow fields for native compatibility
81
+ usethisStatus?: string // The REAL status (todo, ready, etc)
82
+ usethisPriority?: string // The REAL priority (CRIT, HIGH, etc)
83
+
82
84
  // Mind extensions (backward compatible - optional)
83
85
  files?: TodoFile[] // Associated files
84
86
  tools?: TodoTool[] // Associated tool calls
@@ -93,13 +95,6 @@ interface Todo {
93
95
  }
94
96
  }
95
97
 
96
- interface NativeTodo {
97
- id: string
98
- content: string // "title: content" combined
99
- status: string // pending | in_progress | completed | cancelled
100
- priority: string // high | medium | low
101
- }
102
-
103
98
  interface TodoGraph {
104
99
  todos: Todo[]
105
100
  available: string[]
@@ -108,19 +103,24 @@ interface TodoGraph {
108
103
  }
109
104
 
110
105
  // ============================================================================
111
- // Storage — dual write
106
+ // Storage — Dual Write
112
107
  // ============================================================================
113
108
 
109
+ let _nativeStorageBase: string | null = null
110
+
111
+ /**
112
+ * Set the native storage base path (OpenCode's state directory).
113
+ * Called once during plugin initialization with the result of client.path.get().
114
+ */
115
+ export function setNativeStorageBase(statePath: string): void {
116
+ _nativeStorageBase = statePath
117
+ }
118
+
114
119
  // Resolve project directory (context.directory may be undefined via MCP)
115
120
  function dir(directory?: string): string {
116
121
  return directory || process.env.OPENCODE_PROJECT_DIR || process.cwd()
117
122
  }
118
123
 
119
- // Enhanced storage path (project-local)
120
- function getEnhancedPath(sid: string, directory?: string): string {
121
- return path.join(dir(directory), ".opencode", "session-todo", `${sid || "current"}.json`)
122
- }
123
-
124
124
  async function logAction(directory: string, action: string, details: string): Promise<void> {
125
125
  try {
126
126
  const logPath = path.join(dir(directory), ".opencode", "todo.log")
@@ -150,71 +150,133 @@ async function getNativeDataDirs(): Promise<string[]> {
150
150
  return [...dirs]
151
151
  }
152
152
 
153
- async function getNativePaths(sid: string): Promise<string[]> {
154
- const baseDirs = await getNativeDataDirs()
153
+ async function getStoragePaths(sid: string): Promise<string[]> {
155
154
  const file = `${sid || "current"}.json`
156
- return baseDirs.map((base) => path.join(base, "opencode", "storage", "todo", file))
157
- }
155
+ const paths: string[] = []
158
156
 
159
- // Map our format native format
160
- function toNative(todo: Todo): NativeTodo {
161
- // Status mapping: our → native
162
- const statusMap: Record<string, string> = {
163
- todo: "pending",
164
- in_progress: "in_progress",
165
- ready: "in_progress", // native has no "ready"
166
- finished: "in_progress", // back-compat
167
- done: "completed", // native uses "completed" not "done"
168
- cancelled: "cancelled",
157
+ // 1. Authoritative path from OpenCode server
158
+ if (_nativeStorageBase) {
159
+ paths.push(path.join(_nativeStorageBase, "storage", "todo", file))
169
160
  }
170
- // Priority mapping: CRIT/HIGH/MED/LOW → high/medium/low
171
- const prioMap: Record<string, string> = {
172
- CRIT: "high",
173
- HIGH: "high",
174
- MED: "medium",
175
- LOW: "low",
161
+
162
+ // 2. Well-known data dirs (fallbacks)
163
+ const baseDirs = await getNativeDataDirs()
164
+ for (const base of baseDirs) {
165
+ paths.push(path.join(base, "opencode", "storage", "todo", file))
176
166
  }
167
+
168
+ return [...new Set(paths)] // Unique paths
169
+ }
177
170
 
178
- const deps = todo.blockedBy?.length ? ` [← ${todo.blockedBy.join(", ")}]` : ""
179
- const desc = todo.description?.trim() ? ` — ${todo.description.trim()}` : ""
180
- const rel = todo.releases?.length ? ` [rel: ${todo.releases.join(", ")}]` : ""
171
+ // ============================================================================
172
+ // Mapping Logic (Enhanced <-> Native)
173
+ // ============================================================================
181
174
 
182
- return {
183
- id: todo.id,
184
- content: `${todo.content}${desc}${rel}${deps}`,
185
- status: statusMap[todo.status] || "pending",
186
- priority: prioMap[todo.priority] || "medium",
175
+ function toNativeStatus(status: string): string {
176
+ switch (status) {
177
+ case "todo": return "pending"
178
+ case "in_progress": return "in_progress"
179
+ case "ready": return "in_progress" // Sidebar has no 'ready'
180
+ case "done": return "completed"
181
+ case "cancelled": return "cancelled"
182
+ default: return "pending"
187
183
  }
188
184
  }
189
185
 
186
+ function fromNativeStatus(nativeStatus: string, originalStatus?: string): string {
187
+ // If we have the original status stored, prefer it
188
+ if (originalStatus) return originalStatus
189
+
190
+ // Otherwise map back (lossy for ready/todo distinction)
191
+ switch (nativeStatus) {
192
+ case "pending": return "todo"
193
+ case "in_progress": return "in_progress"
194
+ case "completed": return "done"
195
+ case "cancelled": return "cancelled"
196
+ default: return "todo"
197
+ }
198
+ }
199
+
200
+ function toNativePriority(priority: string): string {
201
+ switch (priority) {
202
+ case "CRIT": return "high"
203
+ case "HIGH": return "high"
204
+ case "MED": return "medium"
205
+ case "LOW": return "low"
206
+ default: return "medium"
207
+ }
208
+ }
209
+
210
+ // ============================================================================
211
+ // Read / Write
212
+ // ============================================================================
213
+
190
214
  async function readTodos(sid: string, directory?: string): Promise<Todo[]> {
191
215
  try {
192
- const raw = JSON.parse(await fs.readFile(getEnhancedPath(sid, directory), "utf-8"))
193
- if (!Array.isArray(raw)) return []
194
- return raw.map((t: any) => normalizeTodo(t))
216
+ const storagePaths = await getStoragePaths(sid)
217
+ // Try to read from any available path, starting with authoritative
218
+ for (const storagePath of storagePaths) {
219
+ try {
220
+ const raw = JSON.parse(await fs.readFile(storagePath, "utf-8"))
221
+ if (Array.isArray(raw)) {
222
+ return raw.map((t: any) => {
223
+ const realStatus = fromNativeStatus(t.status, t.usethisStatus)
224
+ const realPriority = t.usethisPriority || t.priority
225
+ return normalizeTodo({ ...t, status: realStatus, priority: realPriority })
226
+ })
227
+ }
228
+ } catch { continue }
229
+ }
230
+ return []
195
231
  } catch {
196
232
  return []
197
233
  }
198
234
  }
199
235
 
236
+ // Enhanced storage path (project-local) - restored for visibility/backup
237
+ function getEnhancedPath(sid: string, directory?: string): string {
238
+ return path.join(dir(directory), ".opencode", "session-todo", `${sid || "current"}.json`)
239
+ }
240
+
200
241
  async function writeTodos(todos: Todo[], sid: string, directory?: string): Promise<void> {
201
- // 1. Enhanced storage (our full format)
242
+ const storagePaths = await getStoragePaths(sid)
202
243
  const enhancedPath = getEnhancedPath(sid, directory)
203
- await fs.mkdir(path.dirname(enhancedPath), { recursive: true })
204
- await fs.writeFile(enhancedPath, JSON.stringify(todos, null, 2), "utf-8")
244
+
245
+ const storageTodos = todos.map(t => {
246
+ const nativeStatus = toNativeStatus(t.status)
247
+ const nativePriority = toNativePriority(t.priority)
248
+
249
+ return {
250
+ ...t,
251
+ status: nativeStatus,
252
+ priority: nativePriority,
253
+ usethisStatus: t.status,
254
+ usethisPriority: t.priority
255
+ }
256
+ })
257
+
258
+ const json = JSON.stringify(storageTodos, null, 2)
259
+
260
+ // 1. Write to Global Storages (for Sidebar)
261
+ await Promise.allSettled(
262
+ storagePaths.map(async (p) => {
263
+ try {
264
+ await fs.mkdir(path.dirname(p), { recursive: true })
265
+ await fs.writeFile(p, json, "utf-8")
266
+ await logAction(directory || "", "debug", `Writing to global: ${p}`)
267
+ } catch (e) {
268
+ await logAction(directory || "", "error", `Global write failed for ${p}: ${String(e)}`)
269
+ }
270
+ })
271
+ )
205
272
 
206
- // 2. Native storage (for TUI display)
207
- const nativeTodos = todos.map(toNative)
273
+ // 2. Write to Local Storage (for User visibility/Git)
208
274
  try {
209
- const nativePaths = await getNativePaths(sid)
210
- await Promise.allSettled(
211
- nativePaths.map(async (nativePath) => {
212
- await fs.mkdir(path.dirname(nativePath), { recursive: true })
213
- await fs.writeFile(nativePath, JSON.stringify(nativeTodos, null, 2), "utf-8")
214
- }),
215
- )
216
- } catch {
217
- // Native write failure is non-fatal
275
+ await fs.mkdir(path.dirname(enhancedPath), { recursive: true })
276
+ await fs.writeFile(enhancedPath, json, "utf-8")
277
+ await logAction(directory || "", "debug", `Writing to local: ${enhancedPath}`)
278
+ } catch (e) {
279
+ await logAction(directory || "", "error", `Local write failed: ${String(e)}`)
218
280
  }
219
281
  }
220
282
 
@@ -319,6 +381,10 @@ function normalizeTodo(input: any): Todo {
319
381
  if (input.searches) normalized.searches = input.searches
320
382
  if (input.context) normalized.context = input.context
321
383
  if (input.cleanup) normalized.cleanup = input.cleanup
384
+
385
+ // Preserve shadow fields
386
+ if (input.usethisStatus) normalized.usethisStatus = input.usethisStatus
387
+ if (input.usethisPriority) normalized.usethisPriority = input.usethisPriority
322
388
 
323
389
  return normalized
324
390
  }