@comfanion/usethis_todo 0.1.15-dev.0 → 0.1.15-dev.10
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 +43 -59
- package/package.json +1 -1
- package/tools.ts +3 -28
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
|
|
5
|
+
import { write, read, read_five, read_by_id, update } from "./tools"
|
|
6
6
|
|
|
7
7
|
interface TodoPruneState {
|
|
8
8
|
lastToolCallId: string | null
|
|
@@ -11,19 +11,7 @@ interface TodoPruneState {
|
|
|
11
11
|
const pruneStates = new Map<string, TodoPruneState>()
|
|
12
12
|
|
|
13
13
|
const UsethisTodoPlugin: Plugin = async ({ directory, client }) => {
|
|
14
|
-
//
|
|
15
|
-
// Must NOT await — server may block until plugin init completes → deadlock.
|
|
16
|
-
// Wrapped in try-catch because client.path may not exist (sync TypeError).
|
|
17
|
-
try {
|
|
18
|
-
client?.path?.get()?.then((pathInfo: any) => {
|
|
19
|
-
const state = pathInfo?.data?.state
|
|
20
|
-
if (state) setNativeStorageBase(state)
|
|
21
|
-
}).catch(() => {})
|
|
22
|
-
} catch {
|
|
23
|
-
// client.path not available in this SDK version — fall back to guessed paths
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Ensure enhanced storage directory exists on init
|
|
14
|
+
// Ensure storage directory exists on init
|
|
27
15
|
try {
|
|
28
16
|
const todoDir = path.join(directory, ".opencode", "session-todo")
|
|
29
17
|
await fs.mkdir(todoDir, { recursive: true })
|
|
@@ -51,67 +39,41 @@ const UsethisTodoPlugin: Plugin = async ({ directory, client }) => {
|
|
|
51
39
|
"todoread",
|
|
52
40
|
])
|
|
53
41
|
|
|
54
|
-
//
|
|
42
|
+
// Collect all TODO-related tool parts
|
|
55
43
|
const toolParts: { part: any; isLast: boolean }[] = []
|
|
56
|
-
const snapshotParts = new Set<any>()
|
|
57
|
-
let lastSnapshotPart: any = null
|
|
58
44
|
|
|
59
45
|
for (const msg of messages) {
|
|
60
46
|
for (const part of (msg.parts || [])) {
|
|
61
|
-
// Tool parts (contain both state.input and state.output)
|
|
62
47
|
if (part.type === "tool" && prunedToolNames.has(part.tool) && part.state?.status === "completed") {
|
|
63
48
|
toolParts.push({ part, isLast: part.callID === state.lastToolCallId })
|
|
64
49
|
}
|
|
65
|
-
// User "## TODO" snapshot text parts
|
|
66
|
-
if (part.type === "text" && typeof part.text === "string" && part.text.startsWith("## TODO")) {
|
|
67
|
-
snapshotParts.add(part)
|
|
68
|
-
lastSnapshotPart = part
|
|
69
|
-
}
|
|
70
50
|
}
|
|
71
51
|
}
|
|
72
52
|
|
|
73
|
-
if (toolParts.length === 0
|
|
74
|
-
|
|
75
|
-
// 2. Check if all tasks are done by parsing the latest snapshot
|
|
76
|
-
const lastSnapshotText = lastSnapshotPart?.text || ""
|
|
77
|
-
const doneMatch = lastSnapshotText.match(/\[(\d+)\/(\d+) done/)
|
|
78
|
-
const allDone = doneMatch && doneMatch[1] === doneMatch[2] && parseInt(doneMatch[1]) > 0
|
|
53
|
+
if (toolParts.length === 0) return
|
|
79
54
|
|
|
80
|
-
//
|
|
55
|
+
// Prune old tool parts — clear BOTH input and output, keep only last
|
|
81
56
|
for (const { part, isLast } of toolParts) {
|
|
82
|
-
if (
|
|
57
|
+
if (!isLast) {
|
|
83
58
|
part.state.output = "[TODO pruned]"
|
|
84
59
|
if (part.state.input) part.state.input = {}
|
|
85
60
|
}
|
|
86
61
|
}
|
|
87
|
-
|
|
88
|
-
// 4. Remove old "## TODO" snapshot parts entirely from messages
|
|
89
|
-
// Keep only the last snapshot (or none if allDone)
|
|
90
|
-
const snapshotsToRemove = new Set(snapshotParts)
|
|
91
|
-
if (!allDone && lastSnapshotPart) {
|
|
92
|
-
snapshotsToRemove.delete(lastSnapshotPart)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (snapshotsToRemove.size > 0) {
|
|
96
|
-
for (const msg of messages) {
|
|
97
|
-
if (!Array.isArray(msg.parts)) continue
|
|
98
|
-
msg.parts = msg.parts.filter((p: any) => !snapshotsToRemove.has(p))
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// 5. Remove messages that became empty after part removal
|
|
103
|
-
output.messages = messages.filter((msg: any) => (msg.parts || []).length > 0)
|
|
104
62
|
},
|
|
105
63
|
|
|
106
64
|
tool: {
|
|
65
|
+
// Enhanced tools (original names)
|
|
107
66
|
usethis_todo_write: write,
|
|
108
67
|
usethis_todo_read: read,
|
|
109
68
|
usethis_todo_read_five: read_five,
|
|
110
69
|
usethis_todo_read_by_id: read_by_id,
|
|
111
70
|
usethis_todo_update: update,
|
|
71
|
+
// Override native tools — same implementation, native names
|
|
72
|
+
todowrite: write,
|
|
73
|
+
todoread: read,
|
|
112
74
|
},
|
|
113
75
|
|
|
114
|
-
//
|
|
76
|
+
// Set nicer titles in TUI + track prune state
|
|
115
77
|
"tool.execute.after": async (input, output) => {
|
|
116
78
|
if (!input.tool.startsWith("usethis_todo_") && input.tool !== "todowrite" && input.tool !== "todoread") return
|
|
117
79
|
|
|
@@ -126,25 +88,47 @@ const UsethisTodoPlugin: Plugin = async ({ directory, client }) => {
|
|
|
126
88
|
const out = output.output || ""
|
|
127
89
|
|
|
128
90
|
// Set a nicer title in TUI
|
|
129
|
-
if (input.tool === "usethis_todo_write") {
|
|
91
|
+
if (input.tool === "usethis_todo_write" || input.tool === "todowrite") {
|
|
130
92
|
const match = out.match(/\[(\d+)\/(\d+) done/)
|
|
131
|
-
output.title = match ?
|
|
93
|
+
output.title = match ? `TODO: ${match[2]} tasks` : "TODO updated"
|
|
132
94
|
} else if (input.tool === "usethis_todo_update") {
|
|
133
95
|
const match = out.match(/^✅ (.+)$/m)
|
|
134
|
-
output.title = match ?
|
|
135
|
-
} else if (input.tool === "usethis_todo_read") {
|
|
96
|
+
output.title = match ? match[1] : "Task updated"
|
|
97
|
+
} else if (input.tool === "usethis_todo_read" || input.tool === "todoread") {
|
|
136
98
|
const match = out.match(/\[(\d+)\/(\d+) done, (\d+) in progress\]/)
|
|
137
|
-
output.title = match ?
|
|
99
|
+
output.title = match ? `TODO [${match[1]}/${match[2]} done]` : "TODO list"
|
|
138
100
|
} else if (input.tool === "usethis_todo_read_five") {
|
|
139
|
-
output.title = "
|
|
101
|
+
output.title = "Next 5 tasks"
|
|
140
102
|
} else if (input.tool === "usethis_todo_read_by_id") {
|
|
141
|
-
output.title = "
|
|
103
|
+
output.title = "Task details"
|
|
142
104
|
}
|
|
143
105
|
|
|
144
|
-
//
|
|
145
|
-
|
|
106
|
+
// Publish snapshot into chat (helps when sidebar doesn't refresh)
|
|
107
|
+
const publishTools = new Set([
|
|
108
|
+
"usethis_todo_write",
|
|
109
|
+
"usethis_todo_update",
|
|
110
|
+
"usethis_todo_read",
|
|
111
|
+
"usethis_todo_read_five",
|
|
112
|
+
"usethis_todo_read_by_id",
|
|
113
|
+
])
|
|
114
|
+
|
|
115
|
+
if (!publishTools.has(input.tool)) return
|
|
116
|
+
|
|
117
|
+
const text = ["## TODO", "", out].join("\n")
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
await client?.session?.prompt?.({
|
|
121
|
+
path: { id: input.sessionID },
|
|
122
|
+
body: {
|
|
123
|
+
noReply: true,
|
|
124
|
+
parts: [{ type: "text", text }],
|
|
125
|
+
},
|
|
126
|
+
})
|
|
127
|
+
} catch {
|
|
128
|
+
// non-fatal
|
|
129
|
+
}
|
|
146
130
|
},
|
|
147
131
|
}
|
|
148
132
|
}
|
|
149
133
|
|
|
150
|
-
export default UsethisTodoPlugin;
|
|
134
|
+
export default UsethisTodoPlugin;
|
package/package.json
CHANGED
package/tools.ts
CHANGED
|
@@ -111,18 +111,6 @@ interface TodoGraph {
|
|
|
111
111
|
// Storage — dual write
|
|
112
112
|
// ============================================================================
|
|
113
113
|
|
|
114
|
-
// Native storage base path (set by plugin init via client.path.get())
|
|
115
|
-
let _nativeStorageBase: string | null = null
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Set the native storage base path (OpenCode's state directory).
|
|
119
|
-
* Called once during plugin initialization with the result of client.path.get().
|
|
120
|
-
* This ensures we write to the exact path OpenCode reads from for the sidebar.
|
|
121
|
-
*/
|
|
122
|
-
export function setNativeStorageBase(statePath: string): void {
|
|
123
|
-
_nativeStorageBase = statePath
|
|
124
|
-
}
|
|
125
|
-
|
|
126
114
|
// Resolve project directory (context.directory may be undefined via MCP)
|
|
127
115
|
function dir(directory?: string): string {
|
|
128
116
|
return directory || process.env.OPENCODE_PROJECT_DIR || process.cwd()
|
|
@@ -147,14 +135,8 @@ async function getNativeDataDirs(): Promise<string[]> {
|
|
|
147
135
|
const dirs = new Set<string>()
|
|
148
136
|
|
|
149
137
|
// 1) xdg-basedir (what OpenCode itself uses)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (mod?.xdgData && typeof mod.xdgData === "string") {
|
|
153
|
-
dirs.add(mod.xdgData)
|
|
154
|
-
}
|
|
155
|
-
} catch {
|
|
156
|
-
// ignore
|
|
157
|
-
}
|
|
138
|
+
// Removed dynamic import to avoid "chunk not found" errors in some environments
|
|
139
|
+
// relying on standard env vars and paths instead
|
|
158
140
|
|
|
159
141
|
// 2) explicit XDG override
|
|
160
142
|
if (process.env.XDG_DATA_HOME) {
|
|
@@ -169,15 +151,8 @@ async function getNativeDataDirs(): Promise<string[]> {
|
|
|
169
151
|
}
|
|
170
152
|
|
|
171
153
|
async function getNativePaths(sid: string): Promise<string[]> {
|
|
172
|
-
const file = `${sid || "current"}.json`
|
|
173
|
-
|
|
174
|
-
// Prefer the authoritative path from OpenCode server API
|
|
175
|
-
if (_nativeStorageBase) {
|
|
176
|
-
return [path.join(_nativeStorageBase, "storage", "todo", file)]
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Fallback: guess from well-known data dirs
|
|
180
154
|
const baseDirs = await getNativeDataDirs()
|
|
155
|
+
const file = `${sid || "current"}.json`
|
|
181
156
|
return baseDirs.map((base) => path.join(base, "opencode", "storage", "todo", file))
|
|
182
157
|
}
|
|
183
158
|
|