@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.
- package/index.ts +33 -7
- package/package.json +1 -1
- 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
|
|
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
|
-
|
|
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
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")
|
|
@@ -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
|
-
|
|
139
|
-
|
|
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
|
|
160
|
-
const baseDirs = await getNativeDataDirs()
|
|
153
|
+
async function getStoragePaths(sid: string): Promise<string[]> {
|
|
161
154
|
const file = `${sid || "current"}.json`
|
|
162
|
-
|
|
163
|
-
}
|
|
155
|
+
const paths: string[] = []
|
|
164
156
|
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
242
|
+
const storagePaths = await getStoragePaths(sid)
|
|
208
243
|
const enhancedPath = getEnhancedPath(sid, directory)
|
|
209
|
-
|
|
210
|
-
|
|
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.
|
|
213
|
-
const nativeTodos = todos.map(toNative)
|
|
273
|
+
// 2. Write to Local Storage (for User visibility/Git)
|
|
214
274
|
try {
|
|
215
|
-
|
|
216
|
-
await
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
}
|