@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.
- package/index.ts +33 -10
- package/package.json +1 -1
- 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
|
|
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_")
|
|
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"
|
|
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"
|
|
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
package/tools.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TODO Tool with Dependencies & Priority — v3 (
|
|
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
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Unified: Native OpenCode storage (TUI compatible)
|
|
13
|
+
* Path resolved via client.path.get()
|
|
14
14
|
*
|
|
15
15
|
* Features:
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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 —
|
|
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
|
|
154
|
-
const baseDirs = await getNativeDataDirs()
|
|
153
|
+
async function getStoragePaths(sid: string): Promise<string[]> {
|
|
155
154
|
const file = `${sid || "current"}.json`
|
|
156
|
-
|
|
157
|
-
}
|
|
155
|
+
const paths: string[] = []
|
|
158
156
|
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
171
|
+
// ============================================================================
|
|
172
|
+
// Mapping Logic (Enhanced <-> Native)
|
|
173
|
+
// ============================================================================
|
|
181
174
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
242
|
+
const storagePaths = await getStoragePaths(sid)
|
|
202
243
|
const enhancedPath = getEnhancedPath(sid, directory)
|
|
203
|
-
|
|
204
|
-
|
|
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.
|
|
207
|
-
const nativeTodos = todos.map(toNative)
|
|
273
|
+
// 2. Write to Local Storage (for User visibility/Git)
|
|
208
274
|
try {
|
|
209
|
-
|
|
210
|
-
await
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
}
|