opencode-session-todo 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexandru Nedelcu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,32 @@
1
- # Temporary Holding Version
1
+ # OpenCode TODO
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **A task list that stays with your session.**
4
+
5
+ Session-local [todo.txt](https://github.com/todotxt/todo.txt) for OpenCode V2.
6
+ Tools for the agent. A sidebar for you. Plain text you can edit.
7
+
8
+ ## Get started
9
+
10
+ Add to `opencode.jsonc`:
11
+
12
+ ```jsonc
13
+ {
14
+ "$schema": "https://opencode.ai/config.json",
15
+ "plugins": [
16
+ {
17
+ "package": "opencode-session-todo",
18
+ "options": {
19
+ "sidebarLimit": 5,
20
+ },
21
+ },
22
+ ],
23
+ }
24
+ ```
25
+
26
+ Reload OpenCode and reopen the TUI.
27
+
28
+ - **Read and write** with `todoread` and `todowrite`, with conflict detection and change diffs.
29
+ - **Keep tasks in view** in the sidebar; open `/todos` for the full list.
30
+ - **Own the file** at `.agents/sessions/<session-id>/todo.txt`.
31
+
32
+ [Usage & permissions](USAGE.md) · [Development & publishing](docs/development.md) · [MIT](LICENSE)
package/USAGE.md ADDED
@@ -0,0 +1,83 @@
1
+ # Session TODOs
2
+
3
+ ## Setup
4
+
5
+ Configure the plugin in `opencode.jsonc`:
6
+
7
+ ```json
8
+ {
9
+ "$schema": "https://opencode.ai/config.json",
10
+ "plugins": [{ "package": "opencode-session-todo", "options": { "sidebarLimit": 5 } }]
11
+ }
12
+ ```
13
+
14
+ Then reload OpenCode:
15
+
16
+ ```sh
17
+ opencode reload
18
+ ```
19
+
20
+ Reopen the TUI after configuration changes. `sidebarLimit` is a positive integer, default **5**.
21
+
22
+ ## Tools
23
+
24
+ Only root sessions expose these tools and receive the TODO lifecycle instructions. Subagents see neither tool.
25
+
26
+ - `todoread({})` reads the current session's tasks and file path. Missing files return an empty list.
27
+ - `todowrite({ previous, todos })` replaces the ordered list only when the stored list still equals `previous`; take `previous` from the latest `todoread` or `todowrite` result. A mismatch fails with an instruction to `todoread` and retry. The result reports a before/after diff. An empty `todos` array clears the list.
28
+
29
+ ```json
30
+ {
31
+ "previous": [],
32
+ "todos": [
33
+ { "content": "Run the tests", "status": "in_progress", "priority": "high" }
34
+ ]
35
+ }
36
+ ```
37
+
38
+ Statuses: `pending`, `in_progress`, `completed`, `cancelled`. Priorities: `high`, `medium`, `low`.
39
+ At most one task may be `in_progress`. Content must be one nonempty trimmed line,
40
+ cannot begin with a priority/date, and cannot contain `status:` or `pri:` tokens.
41
+
42
+ ## TUI
43
+
44
+ - Click **TODO** or its `▼` / `▶` arrow to expand or collapse the sidebar list.
45
+ - Above `sidebarLimit` total tasks, the sidebar shows only unfinished tasks, capped at the limit,
46
+ with the active task first.
47
+ - **Show all …**, `/todos`, or **Ctrl+P → Toggle session todos** opens the full scrollable panel.
48
+ - Close with **Close ×**, **Esc** while focused, or the same command-palette action.
49
+ - Each `todowrite` pops a toast with the before/after diff of the change.
50
+ - Manual file edits appear within two seconds. Collapsing does not change the tasks.
51
+
52
+ ## Storage
53
+
54
+ ```text
55
+ <project/worktree>/.agents/sessions/<session-id>/todo.txt
56
+ ```
57
+
58
+ ```text
59
+ (A) Run the tests status:in_progress
60
+ (B) Review the changes
61
+ x Read the docs pri:C
62
+ x Superseded approach pri:B status:cancelled
63
+ ```
64
+
65
+ `A` / `B` / `C` map to high / medium / low; other priority letters are unsupported.
66
+ Add `.agents/sessions/` to your project's ignore rules to keep these files out of version control.
67
+
68
+ ## Permissions
69
+
70
+ Both tools use `todowrite`. Writes require agent/session rules resolving to `allow`; `ask` and
71
+ `deny` are rejected. Approval prompts, saved approvals, and experimental policies are unsupported.
72
+ Read-only agents should deny `todowrite`.
73
+
74
+ ## Commands
75
+
76
+ From the repository root:
77
+
78
+ ```sh
79
+ bun run check
80
+ bun test
81
+ bun run format # dprint fmt
82
+ bun run format:check # dprint check
83
+ ```
package/dist/tui.js ADDED
@@ -0,0 +1,433 @@
1
+ // @bun
2
+ // tui.tsx
3
+ import { createTextNode as _$createTextNode } from "@opentui/solid";
4
+ import { insertNode as _$insertNode } from "@opentui/solid";
5
+ import { createComponent as _$createComponent } from "@opentui/solid";
6
+ import { effect as _$effect } from "@opentui/solid";
7
+ import { insert as _$insert } from "@opentui/solid";
8
+ import { memo as _$memo } from "@opentui/solid";
9
+ import { setProp as _$setProp } from "@opentui/solid";
10
+ import { createElement as _$createElement } from "@opentui/solid";
11
+ import { Plugin } from "@opencode/plugin/tui";
12
+ import { Session as Session2 } from "@opencode/schema/session";
13
+ import { Effect as Effect2, Result, Schema as Schema4 } from "effect";
14
+ import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js";
15
+
16
+ // rpc.ts
17
+ import { Rpc } from "@opencode/schema/rpc";
18
+ import { Session } from "@opencode/schema/session";
19
+ import { Schema as Schema2 } from "effect";
20
+
21
+ // src/model.ts
22
+ import { Schema } from "effect";
23
+ var Content = Schema.String.check(Schema.isMinLength(1), Schema.isTrimmed(), Schema.isPattern(/^[^\r\n\x00]+$/), Schema.makeFilter((value) => !/^(?:\([A-Z]\) |\d{4}-\d{2}-\d{2}(?: |$))/.test(value) && !/(?:^|\s)(?:status|pri):/.test(value), {
24
+ message: "Task text must not start with a priority/date or contain reserved status:/pri: tokens"
25
+ })).pipe(Schema.brand("TodoContent"));
26
+ var Status = Schema.Literals(["pending", "in_progress", "completed", "cancelled"]);
27
+ var Priority = Schema.Literals(["high", "medium", "low"]);
28
+ var Todo = Schema.Struct({ content: Content, status: Status, priority: Priority });
29
+ var Todos = Schema.Array(Todo).check(Schema.makeFilter((todos) => {
30
+ let active = false;
31
+ for (const todo of todos) {
32
+ if (todo.status !== "in_progress")
33
+ continue;
34
+ if (active)
35
+ return false;
36
+ active = true;
37
+ }
38
+ return true;
39
+ }, { message: "At most one todo may be in_progress" })).pipe(Schema.brand("TodoList"));
40
+ var SessionName = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9][A-Za-z0-9_-]*$/)).pipe(Schema.brand("TodoSessionName"));
41
+ var Snapshot = Schema.Struct({ path: Schema.String, todos: Todos });
42
+
43
+ class TodoError extends Schema.TaggedError()("TodoError", {
44
+ message: Schema.String
45
+ }) {
46
+ }
47
+
48
+ // rpc.ts
49
+ var TodoRpc = Rpc.define({
50
+ id: "session-todo",
51
+ methods: {
52
+ read: {
53
+ input: Schema2.toStandardSchemaV1(Schema2.Struct({ sessionID: Session.ID })),
54
+ output: Schema2.toStandardSchemaV1(Snapshot),
55
+ errors: { storage: Schema2.toStandardSchemaV1(Schema2.Struct({ message: Schema2.String })) }
56
+ }
57
+ },
58
+ events: {
59
+ updated: {
60
+ schema: Schema2.toStandardSchemaV1(Schema2.Struct({ sessionID: Session.ID, diff: Schema2.String }))
61
+ }
62
+ }
63
+ });
64
+
65
+ // src/sidebar.ts
66
+ import { Effect, Schema as Schema3 } from "effect";
67
+ var SidebarLimit = Schema3.Int.check(Schema3.isGreaterThan(0)).pipe(Schema3.brand("TodoSidebarLimit"));
68
+ var SidebarOptions = Schema3.Struct({
69
+ sidebarLimit: SidebarLimit.pipe(Schema3.withDecodingDefaultKey(Effect.succeed(5)))
70
+ });
71
+ var incomplete = (todo) => todo.status === "pending" || todo.status === "in_progress";
72
+ var projectTodos = (todos, mode) => {
73
+ if (mode === "full" || todos.length <= mode) {
74
+ return {
75
+ kind: "all",
76
+ items: todos,
77
+ remaining: todos.reduce((count, todo) => count + Number(incomplete(todo)), 0)
78
+ };
79
+ }
80
+ const items = [];
81
+ let remaining = 0;
82
+ for (const todo of todos) {
83
+ if (!incomplete(todo))
84
+ continue;
85
+ remaining += 1;
86
+ if (todo.status === "in_progress") {
87
+ if (items.length === mode)
88
+ items.pop();
89
+ items.unshift(todo);
90
+ } else if (items.length < mode)
91
+ items.push(todo);
92
+ }
93
+ return { kind: "limited", items, remaining, total: todos.length };
94
+ };
95
+
96
+ // tui.tsx
97
+ var symbols = {
98
+ pending: "[ ]",
99
+ in_progress: "[\u2022]",
100
+ completed: "[\u2713]",
101
+ cancelled: "[\u2013]"
102
+ };
103
+ var colorFor = (status, theme) => {
104
+ switch (status) {
105
+ case "pending":
106
+ return theme.text.base;
107
+ case "in_progress":
108
+ return theme.text.feedback.warning.base;
109
+ case "completed":
110
+ return theme.text.muted;
111
+ case "cancelled":
112
+ return theme.text.feedback.error.base;
113
+ }
114
+ };
115
+ var Message = Schema4.Struct({
116
+ message: Schema4.String
117
+ });
118
+ var messageFor = (error) => Schema4.is(Message)(error) ? error.message : String(error);
119
+ var tui_default = Plugin.define({
120
+ id: "session-todo",
121
+ setup(ctx) {
122
+ const options = Schema4.decodeUnknownResult(SidebarOptions)(ctx.options);
123
+ if (Result.isFailure(options)) {
124
+ ctx.ui.toast.show({
125
+ title: "Invalid TODO sidebar options",
126
+ message: options.failure.message,
127
+ variant: "error"
128
+ });
129
+ return () => {};
130
+ }
131
+ const api = ctx.client.rpc(TodoRpc);
132
+ const [sidebarView, updateSidebarView] = ctx.storage.store("sidebar", {
133
+ initial: {
134
+ open: true
135
+ }
136
+ });
137
+ const notify = api.events.on("updated", (event) => {
138
+ if (event.data.diff === "")
139
+ return;
140
+ ctx.ui.toast.show({
141
+ title: "TODO updated",
142
+ message: event.data.diff,
143
+ variant: "info",
144
+ sessionID: event.data.sessionID
145
+ });
146
+ });
147
+ const List = (props) => {
148
+ const [view, setView] = createSignal({
149
+ status: "loading"
150
+ });
151
+ createEffect(() => {
152
+ const sessionID = props.sessionID;
153
+ setView({
154
+ status: "loading"
155
+ });
156
+ const controller = new AbortController;
157
+ let running = false;
158
+ const refresh = () => {
159
+ if (running || controller.signal.aborted)
160
+ return;
161
+ running = true;
162
+ const request = Schema4.decodeUnknownEffect(Session2.ID)(sessionID).pipe(Effect2.flatMap((id) => Effect2.tryPromise({
163
+ try: () => api.read({
164
+ sessionID: id
165
+ }, {
166
+ signal: controller.signal,
167
+ location: ctx.data.session.get(sessionID)?.location ?? ctx.location ?? ctx.data.location.default()
168
+ }),
169
+ catch: (error) => error
170
+ })));
171
+ Effect2.runPromise(request.pipe(Effect2.match({
172
+ onSuccess: (snapshot) => ({
173
+ status: "ready",
174
+ snapshot
175
+ }),
176
+ onFailure: (error) => ({
177
+ status: "error",
178
+ message: messageFor(error)
179
+ })
180
+ }))).then((next) => {
181
+ if (!controller.signal.aborted)
182
+ setView(next);
183
+ }).catch((error) => {
184
+ if (!controller.signal.aborted) {
185
+ setView({
186
+ status: "error",
187
+ message: messageFor(error)
188
+ });
189
+ }
190
+ }).finally(() => {
191
+ running = false;
192
+ });
193
+ };
194
+ refresh();
195
+ const timer = setInterval(refresh, 2000);
196
+ const unsubscribe = api.events.on("updated", (event) => {
197
+ if (event.data.sessionID === sessionID)
198
+ refresh();
199
+ });
200
+ onCleanup(() => {
201
+ controller.abort();
202
+ clearInterval(timer);
203
+ unsubscribe();
204
+ });
205
+ });
206
+ const snapshot = () => {
207
+ const current = view();
208
+ return current.status === "ready" ? current.snapshot : undefined;
209
+ };
210
+ const projection = createMemo(() => {
211
+ const current = snapshot();
212
+ return current === undefined ? undefined : projectTodos(current.todos, props.full ? "full" : options.success.sidebarLimit);
213
+ });
214
+ const todos = () => projection()?.items ?? [];
215
+ const active = () => projection()?.remaining ?? 0;
216
+ const more = () => {
217
+ const current = projection();
218
+ return current?.kind === "limited" ? current.total : undefined;
219
+ };
220
+ const error = () => {
221
+ const current = view();
222
+ return current.status === "error" ? current.message : undefined;
223
+ };
224
+ const visible = () => Boolean(props.full || snapshot()?.todos.length || error());
225
+ return _$createComponent(Show, {
226
+ get when() {
227
+ return visible();
228
+ },
229
+ get children() {
230
+ var _el$ = _$createElement("box"), _el$2 = _$createElement("box"), _el$4 = _$createElement("text"), _el$5 = _$createElement("b"), _el$7 = _$createElement("span"), _el$8 = _$createTextNode(` (`), _el$0 = _$createTextNode(` remaining)`);
231
+ _$insertNode(_el$, _el$2);
232
+ _$setProp(_el$, "flexDirection", "column");
233
+ _$setProp(_el$, "gap", 0);
234
+ _$setProp(_el$, "paddingTop", 1);
235
+ _$insertNode(_el$2, _el$4);
236
+ _$setProp(_el$2, "flexDirection", "row");
237
+ _$setProp(_el$2, "gap", 1);
238
+ _$setProp(_el$2, "onMouseDown", (event) => {
239
+ if (props.full)
240
+ return;
241
+ event.stopPropagation();
242
+ updateSidebarView((draft) => {
243
+ draft.open = !draft.open;
244
+ }).catch((error) => {
245
+ ctx.ui.toast.show({
246
+ title: "Todo sidebar",
247
+ message: `Cannot save expanded state: ${messageFor(error)}`,
248
+ variant: "error"
249
+ });
250
+ });
251
+ });
252
+ _$insert(_el$2, _$createComponent(Show, {
253
+ get when() {
254
+ return !props.full;
255
+ },
256
+ get children() {
257
+ var _el$3 = _$createElement("text");
258
+ _$insert(_el$3, () => sidebarView.open ? "\u25BC" : "\u25B6");
259
+ _$effect((_$p) => _$setProp(_el$3, "fg", ctx.theme.text.base, _$p));
260
+ return _el$3;
261
+ }
262
+ }), _el$4);
263
+ _$insertNode(_el$4, _el$5);
264
+ _$insertNode(_el$4, _el$7);
265
+ _$insertNode(_el$5, _$createTextNode(`TODO`));
266
+ _$insertNode(_el$7, _el$8);
267
+ _$insertNode(_el$7, _el$0);
268
+ _$insert(_el$7, active, _el$0);
269
+ _$insert(_el$, _$createComponent(Show, {
270
+ get when() {
271
+ return props.full || sidebarView.open;
272
+ },
273
+ get children() {
274
+ return [_$createComponent(Show, {
275
+ get when() {
276
+ return props.full;
277
+ },
278
+ get children() {
279
+ var _el$1 = _$createElement("text");
280
+ _$insert(_el$1, () => snapshot()?.path ?? "Loading todo.txt\u2026");
281
+ _$effect((_$p) => _$setProp(_el$1, "fg", ctx.theme.text.muted, _$p));
282
+ return _el$1;
283
+ }
284
+ }), _$createComponent(Show, {
285
+ get when() {
286
+ return error();
287
+ },
288
+ get children() {
289
+ var _el$10 = _$createElement("text"), _el$11 = _$createTextNode(`Cannot read todos: `);
290
+ _$insertNode(_el$10, _el$11);
291
+ _$insert(_el$10, error, null);
292
+ _$effect((_$p) => _$setProp(_el$10, "fg", ctx.theme.text.feedback.error.base, _$p));
293
+ return _el$10;
294
+ }
295
+ }), _$createComponent(Show, {
296
+ get when() {
297
+ return _$memo(() => !!(props.full && view().status === "ready"))() && todos().length === 0;
298
+ },
299
+ get children() {
300
+ var _el$12 = _$createElement("text");
301
+ _$insertNode(_el$12, _$createTextNode(`No todos for this session.`));
302
+ _$effect((_$p) => _$setProp(_el$12, "fg", ctx.theme.text.muted, _$p));
303
+ return _el$12;
304
+ }
305
+ }), _$createComponent(For, {
306
+ get each() {
307
+ return todos();
308
+ },
309
+ children: (todo) => (() => {
310
+ var _el$17 = _$createElement("text"), _el$18 = _$createTextNode(` `), _el$19 = _$createTextNode(` \xB7 `);
311
+ _$insertNode(_el$17, _el$18);
312
+ _$insertNode(_el$17, _el$19);
313
+ _$insert(_el$17, () => symbols[todo.status], _el$18);
314
+ _$insert(_el$17, () => todo.content, _el$19);
315
+ _$insert(_el$17, () => todo.priority, null);
316
+ _$effect((_$p) => _$setProp(_el$17, "fg", colorFor(todo.status, ctx.theme), _$p));
317
+ return _el$17;
318
+ })()
319
+ }), _$createComponent(Show, {
320
+ get when() {
321
+ return more();
322
+ },
323
+ get children() {
324
+ var _el$14 = _$createElement("text"), _el$15 = _$createTextNode(`Show all `), _el$16 = _$createTextNode(` todos \u2192`);
325
+ _$insertNode(_el$14, _el$15);
326
+ _$insertNode(_el$14, _el$16);
327
+ _$setProp(_el$14, "onMouseDown", (event) => {
328
+ event.stopPropagation();
329
+ ctx.ui.panel.open("session-todo.list");
330
+ });
331
+ _$insert(_el$14, more, _el$16);
332
+ _$effect((_$p) => _$setProp(_el$14, "fg", ctx.theme.text.feedback.info.base, _$p));
333
+ return _el$14;
334
+ }
335
+ })];
336
+ }
337
+ }), null);
338
+ _$effect((_p$) => {
339
+ var _v$ = ctx.theme.text.base, _v$2 = {
340
+ fg: ctx.theme.text.muted
341
+ };
342
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$4, "fg", _v$, _p$.e));
343
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$7, "style", _v$2, _p$.t));
344
+ return _p$;
345
+ }, {
346
+ e: undefined,
347
+ t: undefined
348
+ });
349
+ return _el$;
350
+ }
351
+ });
352
+ };
353
+ const sidebar = ctx.ui.slot({
354
+ append: "sidebar.content",
355
+ render: (props) => _$createComponent(List, {
356
+ get sessionID() {
357
+ return props.sessionID;
358
+ }
359
+ })
360
+ });
361
+ const Panel = (props) => {
362
+ ctx.keymap.layer(() => ({
363
+ mode: "global",
364
+ enabled: props.focused,
365
+ commands: [{
366
+ bind: "escape",
367
+ title: "Close session todos",
368
+ run: props.close
369
+ }]
370
+ }));
371
+ return (() => {
372
+ var _el$20 = _$createElement("box"), _el$21 = _$createElement("text"), _el$23 = _$createElement("scrollbox");
373
+ _$insertNode(_el$20, _el$21);
374
+ _$insertNode(_el$20, _el$23);
375
+ _$setProp(_el$20, "flexDirection", "column");
376
+ _$setProp(_el$20, "flexGrow", 1);
377
+ _$setProp(_el$20, "padding", 1);
378
+ _$insertNode(_el$21, _$createTextNode(`Close \xD7 \xB7 Esc when focused \xB7 Ctrl+P to toggle`));
379
+ _$setProp(_el$21, "onMouseDown", (event) => {
380
+ event.stopPropagation();
381
+ props.close();
382
+ });
383
+ _$setProp(_el$23, "flexGrow", 1);
384
+ _$insert(_el$23, _$createComponent(List, {
385
+ get sessionID() {
386
+ return props.sessionID;
387
+ },
388
+ full: true
389
+ }));
390
+ _$effect((_$p) => _$setProp(_el$21, "fg", ctx.theme.text.base, _$p));
391
+ return _el$20;
392
+ })();
393
+ };
394
+ const panel = ctx.ui.slot({
395
+ append: "session.panel",
396
+ render: (props) => _$createComponent(Show, {
397
+ get when() {
398
+ return props.name === "session-todo.list";
399
+ },
400
+ get children() {
401
+ return _$createComponent(Panel, props);
402
+ }
403
+ })
404
+ });
405
+ ctx.keymap.layer(() => ({
406
+ mode: "global",
407
+ commands: [{
408
+ id: "session-todo.list",
409
+ title: "Toggle session todos",
410
+ group: "Session",
411
+ palette: true,
412
+ slash: {
413
+ name: "todos"
414
+ },
415
+ enabled: ctx.ui.router.current().type === "session",
416
+ run: () => {
417
+ if (ctx.ui.panel.current()?.name === "session-todo.list")
418
+ ctx.ui.panel.close();
419
+ else
420
+ ctx.ui.panel.open("session-todo.list");
421
+ }
422
+ }]
423
+ }));
424
+ return () => {
425
+ sidebar();
426
+ panel();
427
+ notify();
428
+ };
429
+ }
430
+ });
431
+ export {
432
+ tui_default as default
433
+ };
@@ -0,0 +1,28 @@
1
+ # Development
2
+
3
+ Requires Bun >= 1.4.2.
4
+
5
+ ```sh
6
+ bun ci
7
+ make test
8
+ ```
9
+
10
+ `make test` runs typecheck, tests, and formatting checks.
11
+ `make upgrade-dependencies` updates pinned dependencies and the lockfile.
12
+
13
+ ## Package entry points
14
+
15
+ The package ships TypeScript sources for OpenCode's Bun runtime and precompiled Solid ESM for the TUI. Run `bun run build` before loading the local package; packing builds the TUI automatically.
16
+
17
+ - `opencode-session-todo`: tools and RPC handlers.
18
+ - `opencode-session-todo/tui`: sidebar and full panel.
19
+ - `opencode-session-todo/rpc`: shared RPC definitions.
20
+
21
+ ## Publishing
22
+
23
+ ```sh
24
+ npm login
25
+ make publish
26
+ ```
27
+
28
+ `make publish` runs the checks and a package dry-run before `npm publish --access public`.