@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.
Files changed (3) hide show
  1. package/index.ts +43 -59
  2. package/package.json +1 -1
  3. 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, setNativeStorageBase } from "./tools"
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
- // Resolve the authoritative state path from OpenCode server (non-blocking).
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
- // 1. Collect all TODO-related parts
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 && snapshotParts.size === 0) return
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
- // 3. Prune old tool parts — clear BOTH input and output
55
+ // Prune old tool parts — clear BOTH input and output, keep only last
81
56
  for (const { part, isLast } of toolParts) {
82
- if (allDone || !isLast) {
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
- // UI niceties + publish snapshot into the chat
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 ? `📋 TODO: ${match[2]} tasks` : "📋 TODO updated"
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 ? `📝 ${match[1]}` : "📝 Task updated"
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 ? `📋 TODO [${match[1]}/${match[2]} done]` : "📋 TODO list"
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 = "📋 Next 5 tasks"
101
+ output.title = "Next 5 tasks"
140
102
  } else if (input.tool === "usethis_todo_read_by_id") {
141
- output.title = "📋 Task details"
103
+ output.title = "Task details"
142
104
  }
143
105
 
144
- // Native storage write handles sidebar updates now (via client.path.get()).
145
- // The old session.prompt snapshot was causing "table chunks not found" errors.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comfanion/usethis_todo",
3
- "version": "0.1.15-dev.0",
3
+ "version": "0.1.15-dev.10",
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
@@ -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
- try {
151
- const mod: any = await import("xdg-basedir")
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