@comfanion/usethis_todo 0.1.15-dev.8 → 0.1.15

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 -7
  2. package/package.json +1 -1
  3. package/tools.ts +134 -74
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,20 @@ 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
81
  // Set nicer titles in TUI + track prune state
77
82
  "tool.execute.after": async (input, output) => {
78
- if (!input.tool.startsWith("usethis_todo_") && input.tool !== "todowrite" && input.tool !== "todoread") return
83
+ const isEnhanced = input.tool.startsWith("usethis_todo_")
84
+ const isNative = input.tool === "todowrite" || input.tool === "todoread"
85
+ if (!isEnhanced && !isNative) return
79
86
 
80
87
  // Update prune state with latest call ID
81
88
  const sessionID = input.sessionID
@@ -102,6 +109,25 @@ const UsethisTodoPlugin: Plugin = async ({ directory }) => {
102
109
  } else if (input.tool === "usethis_todo_read_by_id") {
103
110
  output.title = "Task details"
104
111
  }
112
+
113
+ // Publish TODO snapshot into chat for write/update ops
114
+ const isWriteOp = input.tool === "usethis_todo_write"
115
+ || input.tool === "usethis_todo_update"
116
+ || input.tool === "todowrite"
117
+ if (!isWriteOp) return
118
+
119
+ const text = ["## TODO", "", out].join("\n")
120
+ try {
121
+ await client?.session?.prompt?.({
122
+ path: { id: input.sessionID },
123
+ body: {
124
+ noReply: true,
125
+ parts: [{ type: "text", text }],
126
+ },
127
+ })
128
+ } catch {
129
+ // non-fatal
130
+ }
105
131
  },
106
132
  }
107
133
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comfanion/usethis_todo",
3
- "version": "0.1.15-dev.8",
3
+ "version": "0.1.15",
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")
@@ -135,14 +135,8 @@ async function getNativeDataDirs(): Promise<string[]> {
135
135
  const dirs = new Set<string>()
136
136
 
137
137
  // 1) xdg-basedir (what OpenCode itself uses)
138
- try {
139
- const mod: any = await import("xdg-basedir")
140
- if (mod?.xdgData && typeof mod.xdgData === "string") {
141
- dirs.add(mod.xdgData)
142
- }
143
- } catch {
144
- // ignore
145
- }
138
+ // Removed dynamic import to avoid "chunk not found" errors in some environments
139
+ // relying on standard env vars and paths instead
146
140
 
147
141
  // 2) explicit XDG override
148
142
  if (process.env.XDG_DATA_HOME) {
@@ -156,71 +150,133 @@ async function getNativeDataDirs(): Promise<string[]> {
156
150
  return [...dirs]
157
151
  }
158
152
 
159
- async function getNativePaths(sid: string): Promise<string[]> {
160
- const baseDirs = await getNativeDataDirs()
153
+ async function getStoragePaths(sid: string): Promise<string[]> {
161
154
  const file = `${sid || "current"}.json`
162
- return baseDirs.map((base) => path.join(base, "opencode", "storage", "todo", file))
163
- }
155
+ const paths: string[] = []
164
156
 
165
- // Map our format native format
166
- function toNative(todo: Todo): NativeTodo {
167
- // Status mapping: our → native
168
- const statusMap: Record<string, string> = {
169
- todo: "pending",
170
- in_progress: "in_progress",
171
- ready: "in_progress", // native has no "ready"
172
- finished: "in_progress", // back-compat
173
- done: "completed", // native uses "completed" not "done"
174
- cancelled: "cancelled",
157
+ // 1. Authoritative path from OpenCode server
158
+ if (_nativeStorageBase) {
159
+ paths.push(path.join(_nativeStorageBase, "storage", "todo", file))
175
160
  }
176
- // Priority mapping: CRIT/HIGH/MED/LOW → high/medium/low
177
- const prioMap: Record<string, string> = {
178
- CRIT: "high",
179
- HIGH: "high",
180
- MED: "medium",
181
- 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))
182
166
  }
167
+
168
+ return [...new Set(paths)] // Unique paths
169
+ }
183
170
 
184
- const deps = todo.blockedBy?.length ? ` [← ${todo.blockedBy.join(", ")}]` : ""
185
- const desc = todo.description?.trim() ? ` — ${todo.description.trim()}` : ""
186
- const rel = todo.releases?.length ? ` [rel: ${todo.releases.join(", ")}]` : ""
171
+ // ============================================================================
172
+ // Mapping Logic (Enhanced <-> Native)
173
+ // ============================================================================
174
+
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"
183
+ }
184
+ }
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
+ }
187
199
 
188
- return {
189
- id: todo.id,
190
- content: `${todo.content}${desc}${rel}${deps}`,
191
- status: statusMap[todo.status] || "pending",
192
- priority: prioMap[todo.priority] || "medium",
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"
193
207
  }
194
208
  }
195
209
 
210
+ // ============================================================================
211
+ // Read / Write
212
+ // ============================================================================
213
+
196
214
  async function readTodos(sid: string, directory?: string): Promise<Todo[]> {
197
215
  try {
198
- const raw = JSON.parse(await fs.readFile(getEnhancedPath(sid, directory), "utf-8"))
199
- if (!Array.isArray(raw)) return []
200
- 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 []
201
231
  } catch {
202
232
  return []
203
233
  }
204
234
  }
205
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
+
206
241
  async function writeTodos(todos: Todo[], sid: string, directory?: string): Promise<void> {
207
- // 1. Enhanced storage (our full format)
242
+ const storagePaths = await getStoragePaths(sid)
208
243
  const enhancedPath = getEnhancedPath(sid, directory)
209
- await fs.mkdir(path.dirname(enhancedPath), { recursive: true })
210
- 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
+ )
211
272
 
212
- // 2. Native storage (for TUI display)
213
- const nativeTodos = todos.map(toNative)
273
+ // 2. Write to Local Storage (for User visibility/Git)
214
274
  try {
215
- const nativePaths = await getNativePaths(sid)
216
- await Promise.allSettled(
217
- nativePaths.map(async (nativePath) => {
218
- await fs.mkdir(path.dirname(nativePath), { recursive: true })
219
- await fs.writeFile(nativePath, JSON.stringify(nativeTodos, null, 2), "utf-8")
220
- }),
221
- )
222
- } catch {
223
- // 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)}`)
224
280
  }
225
281
  }
226
282
 
@@ -325,6 +381,10 @@ function normalizeTodo(input: any): Todo {
325
381
  if (input.searches) normalized.searches = input.searches
326
382
  if (input.context) normalized.context = input.context
327
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
328
388
 
329
389
  return normalized
330
390
  }