@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.
- package/index.ts +8 -1
- package/package.json +1 -1
- 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
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 — 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
|
|
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
|
-
//
|
|
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",
|
|
156
|
+
// 1. Prefer authoritative path from OpenCode server
|
|
157
|
+
if (_nativeStorageBase) {
|
|
158
|
+
return path.join(_nativeStorageBase, "storage", "todo", file)
|
|
169
159
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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
|
}
|