@comfanion/usethis_todo 0.1.15-dev.10 → 0.1.15-dev.11

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 +8 -1
  2. package/package.json +1 -1
  3. package/tools.ts +133 -68
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
@@ -11,6 +11,13 @@ interface TodoPruneState {
11
11
  const pruneStates = new Map<string, TodoPruneState>()
12
12
 
13
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")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comfanion/usethis_todo",
3
- "version": "0.1.15-dev.10",
3
+ "version": "0.1.15-dev.11",
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 — Unified Global
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,72 +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 getStoragePath(sid: string): Promise<string> {
155
154
  const file = `${sid || "current"}.json`
156
- return baseDirs.map((base) => path.join(base, "opencode", "storage", "todo", file))
157
- }
158
155
 
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",
156
+ // 1. Prefer authoritative path from OpenCode server
157
+ if (_nativeStorageBase) {
158
+ return path.join(_nativeStorageBase, "storage", "todo", file)
169
159
  }
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",
160
+
161
+ // 2. Fallback: guess from well-known data dirs (first one that exists or default)
162
+ const baseDirs = await getNativeDataDirs()
163
+ // Try to find one that exists
164
+ for (const base of baseDirs) {
165
+ try {
166
+ await fs.access(base)
167
+ return path.join(base, "opencode", "storage", "todo", file)
168
+ } catch {}
176
169
  }
170
+
171
+ // Default to first one
172
+ return path.join(baseDirs[0], "opencode", "storage", "todo", file)
173
+ }
177
174
 
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(", ")}]` : ""
175
+ // ============================================================================
176
+ // Mapping Logic (Enhanced <-> Native)
177
+ // ============================================================================
178
+
179
+ function toNativeStatus(status: string): string {
180
+ switch (status) {
181
+ case "todo": return "pending"
182
+ case "in_progress": return "in_progress"
183
+ case "ready": return "in_progress" // Sidebar has no 'ready'
184
+ case "done": return "completed"
185
+ case "cancelled": return "cancelled"
186
+ default: return "pending"
187
+ }
188
+ }
189
+
190
+ function fromNativeStatus(nativeStatus: string, originalStatus?: string): string {
191
+ // If we have the original status stored, prefer it
192
+ if (originalStatus) return originalStatus
193
+
194
+ // Otherwise map back (lossy for ready/todo distinction)
195
+ switch (nativeStatus) {
196
+ case "pending": return "todo"
197
+ case "in_progress": return "in_progress"
198
+ case "completed": return "done"
199
+ case "cancelled": return "cancelled"
200
+ default: return "todo"
201
+ }
202
+ }
181
203
 
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",
204
+ function toNativePriority(priority: string): string {
205
+ switch (priority) {
206
+ case "CRIT": return "high"
207
+ case "HIGH": return "high"
208
+ case "MED": return "medium"
209
+ case "LOW": return "low"
210
+ default: return "medium"
187
211
  }
188
212
  }
189
213
 
214
+ // ============================================================================
215
+ // Read / Write
216
+ // ============================================================================
217
+
190
218
  async function readTodos(sid: string, directory?: string): Promise<Todo[]> {
191
219
  try {
192
- const raw = JSON.parse(await fs.readFile(getEnhancedPath(sid, directory), "utf-8"))
220
+ const storagePath = await getStoragePath(sid)
221
+ const raw = JSON.parse(await fs.readFile(storagePath, "utf-8"))
193
222
  if (!Array.isArray(raw)) return []
194
- return raw.map((t: any) => normalizeTodo(t))
223
+
224
+ return raw.map((t: any) => {
225
+ // Restore our rich status from shadow field if present
226
+ const realStatus = fromNativeStatus(t.status, t.usethisStatus)
227
+ const realPriority = t.usethisPriority || t.priority // Fallback if not shadowed
228
+
229
+ return normalizeTodo({
230
+ ...t,
231
+ status: realStatus,
232
+ priority: realPriority
233
+ })
234
+ })
195
235
  } catch {
196
236
  return []
197
237
  }
198
238
  }
199
239
 
200
240
  async function writeTodos(todos: Todo[], sid: string, directory?: string): Promise<void> {
201
- // 1. Enhanced storage (our full format)
202
- 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")
241
+ const storagePath = await getStoragePath(sid)
242
+
243
+ // Prepare for storage:
244
+ // 1. Set native-compatible fields (status, priority)
245
+ // 2. Store real values in shadow fields (usethisStatus, usethisPriority)
246
+ // 3. Keep all other fields (blockedBy, etc)
247
+
248
+ const storageTodos = todos.map(t => {
249
+ const nativeStatus = toNativeStatus(t.status)
250
+ const nativePriority = toNativePriority(t.priority)
251
+
252
+ const deps = t.blockedBy?.length ? ` [← ${t.blockedBy.join(", ")}]` : ""
253
+ const desc = t.description?.trim() ? ` — ${t.description.trim()}` : ""
254
+ const rel = t.releases?.length ? ` [rel: ${t.releases.join(", ")}]` : ""
255
+
256
+ return {
257
+ ...t,
258
+ // Native fields
259
+ status: nativeStatus,
260
+ priority: nativePriority,
261
+ content: t.content, // Keep content clean? Or append deps?
262
+ // Sidebar usually displays 'content'. If we want deps visible in sidebar, we should append them.
263
+ // But if we append them, we dirty the content for next read.
264
+ // Let's NOT append to content in the object, but maybe the sidebar reads 'content'.
265
+ // Wait, if we don't append deps to content, sidebar won't show them.
266
+ // But if we do, 'content' grows every time we read/write?
267
+ // Solution: We store 'originalContent' or just assume we can parse it back?
268
+ // Better: The sidebar likely just shows 'content'.
269
+ // Let's keep 'content' clean in the JSON. If the sidebar supports 'description', great.
270
+ // If not, the user sees just the title. That's acceptable for "Native Status".
271
+
272
+ // Shadow fields (our source of truth)
273
+ usethisStatus: t.status,
274
+ usethisPriority: t.priority
275
+ }
276
+ })
205
277
 
206
- // 2. Native storage (for TUI display)
207
- const nativeTodos = todos.map(toNative)
208
- 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
218
- }
278
+ await fs.mkdir(path.dirname(storagePath), { recursive: true })
279
+ await fs.writeFile(storagePath, JSON.stringify(storageTodos, null, 2), "utf-8")
219
280
  }
220
281
 
221
282
  // ============================================================================
@@ -319,6 +380,10 @@ function normalizeTodo(input: any): Todo {
319
380
  if (input.searches) normalized.searches = input.searches
320
381
  if (input.context) normalized.context = input.context
321
382
  if (input.cleanup) normalized.cleanup = input.cleanup
383
+
384
+ // Preserve shadow fields
385
+ if (input.usethisStatus) normalized.usethisStatus = input.usethisStatus
386
+ if (input.usethisPriority) normalized.usethisPriority = input.usethisPriority
322
387
 
323
388
  return normalized
324
389
  }