@bosun-sh/logbook 1.0.0

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 bosun.sh
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 ADDED
@@ -0,0 +1,361 @@
1
+ # logbook: kanban for ai agents
2
+
3
+ logbook is a kanban board implementation for autonomous agentic development, focusing on autonomous development and context window management.
4
+
5
+ → **new here?** see [quickstart.md](quickstart.md) to get running in 2 minutes.
6
+
7
+ ## problem
8
+
9
+ ai agents changed the way software teams worked, and with specification-driven development we encounter a rift: **agents don't manage their tasks as we do**.
10
+
11
+ ### what's the issue with this?
12
+
13
+ - hard for humans to track autonomous work properly: **"do you know what specific tasks your agent did?"**
14
+ - hard for agents to track tasks in-progress and done: **not a centralized way to track tasks so each instance haves to figure this out**
15
+ - existing tools add too much overload and are human-centered: **if an agent is going to use it, then it should be tailored for agents**
16
+
17
+ ## solution
18
+
19
+ logbook is a file-system based kanban board that uses jsonl files to enter one task per line in a structured and clean approach and gives the agent the right tools to use it:
20
+
21
+ ### tools
22
+
23
+ - the agent can call `list_tasks(status)` and receive a list of the tasks in that status _(in_progress by default)_
24
+ - the agent can call `current_task()` and receive the highest-priority in_progress task for the current session, resolved via this priority chain:
25
+
26
+ | priority | condition | action |
27
+ |----------|-----------|--------|
28
+ | 1 | task already assigned to this session | return highest priority (tie-break: oldest) |
29
+ | 2 | unassigned `in_progress` task | claim highest priority, return |
30
+ | 3 | `in_progress` task with a dead-session assignee | claim highest priority, return |
31
+ | 4 | `todo` task | auto-transition highest priority to `in_progress`, claim, return |
32
+ | 5 | nothing available | fail with `no_current_task` |
33
+ - the agent can call `update_task(id, new_status, comment)` to transition a task, add a comment, or reply to a `need_info` blocking comment
34
+ - the agent can call `create_task(input)` to open a new task in `backlog`, passing `predictedKTokens` so the server derives a Fibonacci estimation automatically
35
+ - the agent can call `edit_task(id, updates)` to change mutable fields without altering status
36
+
37
+ each one of these tools has the sole purpose of removing overload from the agent context, handling the _"heavy load"_ programmatically on the MCP server.
38
+
39
+ ## walkthrough
40
+
41
+ a complete agent session from start to done:
42
+
43
+ **1. agent starts — get current task**
44
+
45
+ ```
46
+ current_task()
47
+ → { id: "abc-123", title: "implement login endpoint", status: "in_progress", ... }
48
+ ```
49
+
50
+ **2. agent needs clarification — blocks on a question**
51
+
52
+ ```
53
+ update_task("abc-123", "need_info", {
54
+ title: "which auth provider?",
55
+ content: "should i use jwt or session-based auth? the spec doesn't say.",
56
+ kind: "need_info"
57
+ })
58
+ → hook fires: user is notified with the comment
59
+ ```
60
+
61
+ **3. user replies — task unblocked**
62
+
63
+ ```
64
+ update_task("abc-123", "in_progress", {
65
+ id: "<comment-id>",
66
+ reply: "use jwt, see the auth spec in docs/auth.md",
67
+ title: "jwt confirmed",
68
+ content: "jwt confirmed",
69
+ kind: "need_info"
70
+ })
71
+ → task returns to in_progress
72
+ ```
73
+
74
+ **4. agent finishes — submits for review**
75
+
76
+ ```
77
+ update_task("abc-123", "pending_review", {
78
+ title: "implementation complete",
79
+ content: "jwt login endpoint implemented, tests passing",
80
+ kind: "regular"
81
+ })
82
+ → review-spawn hook fires: review task created, reviewer agent spawned
83
+ ```
84
+
85
+ **5. reviewer approves — task closed**
86
+
87
+ ```
88
+ # reviewer agent calls:
89
+ current_task() → gets the review task
90
+ update_task("<review-task-id>", "done")
91
+ → original task abc-123 → done automatically
92
+ ```
93
+
94
+ ### how the agent knows logbook exists
95
+
96
+ add the logbook MCP server to your AI client config (see [configuration](#configuration)), then include these instructions in your agent's system prompt or `CLAUDE.md`:
97
+
98
+ ```
99
+ You are connected to the logbook MCP server. Call current_task() immediately at session start.
100
+ ```
101
+
102
+ the full system prompt is injected automatically when the MCP server connects.
103
+
104
+ ## architecture
105
+
106
+ - **runtime**: Bun / TypeScript
107
+ - **effect system**: Effect.ts — all async operations and errors are modeled as `Effect<A, E, R>`
108
+ - **architecture**: hexagonal (ports & adapters), organized by vertical slices per domain concept (task, hook)
109
+ - **validation**: Zod at every system boundary (MCP input, filesystem reads)
110
+ - **persistence**: JSONL — one task per line, append-only writes, full file scan for reads
111
+
112
+ JSONL was chosen for simplicity and agent-friendliness: a single line = a single task makes partial reads and diffs readable without tooling.
113
+
114
+ ### hooks
115
+
116
+ besides the tools that the agent call manually, each action performed in the kanban can have automatic _hooks_ executed right before or after.
117
+ the default hooks include:
118
+
119
+ - after moving a task to `need_info`, the user receives a notification with the comment left to be able to answer the question.
120
+ - after moving a task to `pending_review`, a reviewer sub-agent spawns and a review task is automatically generated for it.
121
+ - when a second task is moved to `in_progress`, a built-in hook fires and requires a comment justifying the overlap before proceeding.
122
+
123
+ but hooks can also be defined by the user as scripts in any language as long as it's installed in the system, under the "hooks/" directory, following this structure:
124
+
125
+ ```
126
+ hooks/
127
+ └── example_hook/
128
+ ├── config.yml
129
+ └── script.ts
130
+ ```
131
+
132
+ a minimal `config.yml` looks like:
133
+
134
+ ```yaml
135
+ # config.yml
136
+ event: task.status_changed # lifecycle event that triggers the hook
137
+ condition: "new_status == 'need_info'" # optional; JS-like expression
138
+ timeout_ms: 5000 # optional; default 5000
139
+ ```
140
+
141
+ you can base your config.yml in the default hooks-which have complete configuration files.
142
+
143
+ > note: as mentioned, you can change .ts for any language, but the .yml / .yaml is required for configuration.
144
+
145
+ #### review flow
146
+
147
+ when a task is moved to `pending_review`, the built-in `review-spawn` hook automatically creates a review task and spawns a reviewer sub-agent. the reviewer classifies every finding before acting:
148
+
149
+ ```mermaid
150
+ flowchart TD
151
+ PR[task: pending_review]
152
+ PR -->|review-spawn hook| SPAWN[review task created\nreviewer agent spawned]
153
+ SPAWN --> CL{classify findings}
154
+
155
+ CL -->|nice-to-have findings| TD["[tech debt] tasks created\nin backlog — silently"]
156
+
157
+ CL -->|must-fix found| MF[original → in_progress\nneed_info: must fix before re-submitting]
158
+ CL -->|consider only| CO[original → in_progress\nneed_info: implementer decides\nfix now or backlog]
159
+ CL -->|clean| DONE[original → done]
160
+
161
+ MF --> RD[review task → done]
162
+ CO --> RD
163
+ DONE --> RD
164
+
165
+ TD -.->|accompanies any outcome| RD
166
+ ```
167
+
168
+ | finding severity | original task | review task | side effect |
169
+ |-----------------|---------------|-------------|-------------|
170
+ | **must-fix** | `→ in_progress` + `need_info` | `→ done` | — |
171
+ | **consider** | `→ in_progress` + `need_info` | `→ done` | implementer replies: fix now or backlog |
172
+ | **nice-to-have** | unchanged | — | `[tech debt]` backlog task created |
173
+ | **clean** | `→ done` | `→ done` | — |
174
+
175
+ nice-to-have findings are always handled silently — they never block progress or ping the implementer.
176
+
177
+ #### why hooks?
178
+
179
+ hooks don't need to store information from one execution to the other, so the main principle here is: **"execute and forget"**, this way we can focus on the kanban and actual tasks.
180
+
181
+ ## contracts
182
+
183
+ the core types the server operates on:
184
+
185
+ ```ts
186
+ type Agent = {
187
+ id: string, // session_id assigned by the server on connection
188
+ title: string,
189
+ description: string
190
+ }
191
+
192
+ type Status = 'backlog' | 'todo' | 'need_info' | 'blocked' | 'in_progress' | 'pending_review' | 'done'
193
+
194
+ type Comment = {
195
+ id: string,
196
+ timestamp: Date,
197
+ title: string,
198
+ content: string,
199
+ reply: string, // user's reply, populated when responding to a need_info comment
200
+ kind: 'need_info' | 'regular' // drives the reply cycle — only need_info comments accept replies
201
+ }
202
+
203
+ type Task = {
204
+ project: string,
205
+ milestone: string,
206
+ id: string,
207
+ title: string,
208
+ definition_of_done: string,
209
+ description: string,
210
+ estimation: number, // fibonacci number derived from predictedKTokens at creation time
211
+ comments: Comment[],
212
+ assignee: Agent,
213
+ status: Status,
214
+ in_progress_since?: Date // set when task enters in_progress; used as tie-breaker in current_task
215
+ priority: number // integer ≥ 0; higher = more urgent; defaults to 0
216
+ }
217
+
218
+ // status defaults to 'in_progress'; results ordered by priority DESC
219
+ // project and milestone are optional; all provided filters compose (AND semantics)
220
+ type ListTasks = (options: { status: Status | '*', project?: string, milestone?: string }) => Task[]
221
+
222
+ // returns the highest-priority task for the current session using a priority chain:
223
+ // 1. own in_progress → 2. unassigned in_progress → 3. orphaned in_progress
224
+ // (dead-session assignee) → 4. highest-priority todo (auto-transitioned) → 5. no_current_task error.
225
+ // within each step, tasks are ordered by priority DESC, tie-broken by in_progress_since ASC.
226
+ // if a second task is moved to in_progress, a built-in hook fires and
227
+ // requires a comment justifying the overlap.
228
+ type GetCurrentTask = () => Task
229
+
230
+ // transitions a task to a new status; sessionId is injected server-side.
231
+ // to reply to a need_info comment, pass a comment with the existing comment's id and a reply string.
232
+ type UpdateTask = (id: string, new_status: Status, comment: CommentInput | null, sessionId: string) => void
233
+
234
+ type CommentInput = {
235
+ id?: string, // existing comment id — only when replying to a need_info comment
236
+ title: string,
237
+ content: string,
238
+ reply?: string, // reply text — only meaningful when id refers to a need_info comment
239
+ kind: 'need_info' | 'regular'
240
+ }
241
+
242
+ // creates a new task in backlog assigned to the calling session.
243
+ // predictedKTokens is mapped to a Fibonacci estimation by the server.
244
+ type CreateTask = (input: CreateTaskInput, sessionId: string) => Task
245
+
246
+ type CreateTaskInput = {
247
+ project: string,
248
+ milestone: string,
249
+ title: string,
250
+ definition_of_done: string,
251
+ description: string,
252
+ predictedKTokens: number, // positive number; server maps this to a Fibonacci estimation (max 20)
253
+ priority?: number // integer ≥ 0; defaults to 0
254
+ }
255
+
256
+ // edits mutable fields without changing status
257
+ type EditTask = (id: string, updates: EditTaskInput) => Task
258
+
259
+ type EditTaskInput = {
260
+ title?: string,
261
+ description?: string,
262
+ definition_of_done?: string,
263
+ predictedKTokens?: number, // re-derives estimation if provided
264
+ priority?: number // integer ≥ 0; re-assigns priority if provided
265
+ }
266
+ ```
267
+
268
+ each MCP session is treated as a distinct agent instance. the server assigns a `session_id` on connection and uses it to scope `GetCurrentTask` — no explicit agent ID needs to be passed by the caller.
269
+
270
+ ## install
271
+
272
+ ```bash
273
+ npm install -g @bosun-sh/logbook
274
+ ```
275
+
276
+ requires **bun ≥ 1.0.0** as the runtime ([install bun](https://bun.sh)).
277
+
278
+ verify the installation:
279
+
280
+ ```bash
281
+ logbook-mcp --version
282
+ ```
283
+
284
+ for a full onboarding walkthrough see [quickstart.md](quickstart.md).
285
+
286
+ ## configuration
287
+
288
+ ### quick setup
289
+
290
+ run `logbook-mcp init` in your project directory to scaffold `tasks.jsonl`, `hooks/`, and print the config snippets for your AI client.
291
+
292
+ ### environment variables
293
+
294
+ | Variable | Default | Description |
295
+ |----------|---------|-------------|
296
+ | `LOGBOOK_TASKS_FILE` | `./tasks.jsonl` | path to the JSONL task store |
297
+ | `LOGBOOK_HOOKS_DIR` | `./hooks` | directory scanned for custom hook definitions |
298
+ | `LOGBOOK_LOG_LEVEL` | `warn` | structured logger level: `debug`, `info`, `warn`, or `error` |
299
+
300
+ ### gitignore
301
+
302
+ `tasks.jsonl` and `sessions.json` are runtime files generated by the MCP server — they should not be committed to version control:
303
+
304
+ ```gitignore
305
+ tasks.jsonl
306
+ sessions.json
307
+ ```
308
+
309
+ > note: the logbook repo itself intentionally commits these files for dogfooding — that is the exception, not the rule.
310
+
311
+ ### client setup
312
+
313
+ **Claude Code** — add to `.claude/settings.json`:
314
+
315
+ ```json
316
+ {
317
+ "mcpServers": {
318
+ "logbook": {
319
+ "command": "logbook-mcp"
320
+ }
321
+ }
322
+ }
323
+ ```
324
+
325
+ **OpenCode** — add to `opencode.json`:
326
+
327
+ ```json
328
+ {
329
+ "mcp": {
330
+ "logbook": {
331
+ "type": "local",
332
+ "command": ["logbook-mcp"],
333
+ "enabled": true
334
+ }
335
+ }
336
+ }
337
+ ```
338
+
339
+ ## security
340
+
341
+ ### hook conditions are trusted code
342
+
343
+ hook `config.yml` files support an optional `condition` field (e.g. `"new_status == 'pending_review'"`). these conditions are compiled and evaluated as live JavaScript at runtime — equivalent in trust level to a shell script.
344
+
345
+ **what this means for you:**
346
+
347
+ - **only add hooks from sources you trust.** a malicious `config.yml` condition can execute arbitrary code in the process that runs the MCP server.
348
+ - **do not expose `LOGBOOK_HOOKS_DIR` to external write access.** if an untrusted process can write files under the hooks directory, it can inject conditions that execute as the MCP server's user.
349
+ - the built-in hooks shipped with logbook are safe — they use simple equality checks (`new_status == 'need_info'`).
350
+ - if a condition throws or is malformed, the hook is skipped silently and execution continues — it fails safe.
351
+
352
+ the security model here is the same as running a `Makefile` or a `.husky/` script: filesystem-level trust. as long as you control what goes into your hooks directory, you are safe.
353
+
354
+ ## stability
355
+
356
+ logbook follows semantic versioning. here is what is stable at v1.0.0:
357
+
358
+ - **mcp api**: tool names and required parameters will not change within a major version. optional parameters may be added.
359
+ - **jsonl format**: the serialized `Task` type in `tasks.jsonl` is stable. new optional fields may be added; existing fields will not be removed or renamed within a major version.
360
+ - **hook config schema**: the `event`, `condition`, and `timeout_ms` keys in `config.yml` are stable. new optional keys may be added.
361
+ - **breaking changes**: any breaking change will be preceded by a deprecation notice in the prior minor release and documented in `CHANGELOG.md`.
@@ -0,0 +1,3 @@
1
+ event: task.status_changed
2
+ condition: "new_status == 'need_info'"
3
+ timeout_ms: 5000
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env bun
2
+ import { readFile } from "node:fs/promises"
3
+
4
+ const taskId = process.env.LOGBOOK_TASK_ID ?? ""
5
+ const dataFile = process.env.LOGBOOK_TASKS_FILE ?? "./tasks.jsonl"
6
+
7
+ if (taskId === "") process.exit(0)
8
+
9
+ const readLines = async (filePath: string): Promise<readonly string[]> => {
10
+ const content = await readFile(filePath, "utf8").catch((e: unknown) => {
11
+ if (isEnoent(e)) return ""
12
+ throw e
13
+ })
14
+ return content.split("\n").filter((l) => l.trim() !== "")
15
+ }
16
+
17
+ interface RawComment {
18
+ id: string
19
+ timestamp: string
20
+ title: string
21
+ content: string
22
+ reply: string
23
+ kind: string
24
+ }
25
+
26
+ interface RawTask {
27
+ id: string
28
+ comments: RawComment[]
29
+ }
30
+
31
+ const parseTask = (line: string): RawTask | null => {
32
+ try {
33
+ return JSON.parse(line) as RawTask
34
+ } catch {
35
+ return null
36
+ }
37
+ }
38
+
39
+ const findBlockingComment = (task: RawTask): RawComment | null => {
40
+ // Find the most recent need_info comment with an empty reply
41
+ for (let i = task.comments.length - 1; i >= 0; i--) {
42
+ const comment = task.comments[i]
43
+ if (comment !== undefined && comment.kind === "need_info" && comment.reply === "") {
44
+ return comment
45
+ }
46
+ }
47
+ return null
48
+ }
49
+
50
+ const isEnoent = (e: unknown): boolean =>
51
+ typeof e === "object" && e !== null && (e as { code?: unknown }).code === "ENOENT"
52
+
53
+ const lines = await readLines(dataFile)
54
+
55
+ let found: RawTask | null = null
56
+ for (const line of lines) {
57
+ const task = parseTask(line)
58
+ if (task !== null && task.id === taskId) {
59
+ found = task
60
+ break
61
+ }
62
+ }
63
+
64
+ if (found === null) process.exit(0)
65
+
66
+ const comment = findBlockingComment(found)
67
+ if (comment === null) process.exit(0)
68
+
69
+ process.stdout.write(`[need_info] Task ${taskId}: ${comment.title}\n${comment.content}\n`)
@@ -0,0 +1,3 @@
1
+ event: task.status_changed
2
+ condition: "new_status == 'pending_review'"
3
+ timeout_ms: 30000
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env bun
2
+ import { appendFile, readFile } from "node:fs/promises"
3
+
4
+ const taskId = process.env.LOGBOOK_TASK_ID ?? ""
5
+ const dataFile = process.env.LOGBOOK_TASKS_FILE ?? "./tasks.jsonl"
6
+
7
+ if (taskId === "") process.exit(0)
8
+
9
+ const readLines = async (filePath: string): Promise<readonly string[]> => {
10
+ const content = await readFile(filePath, "utf8").catch((e: unknown) => {
11
+ if (isEnoent(e)) return ""
12
+ throw e
13
+ })
14
+ return content.split("\n").filter((l) => l.trim() !== "")
15
+ }
16
+
17
+ interface RawAgent {
18
+ id: string
19
+ title: string
20
+ description: string
21
+ }
22
+
23
+ interface RawTask {
24
+ project: string
25
+ milestone: string
26
+ id: string
27
+ title: string
28
+ definition_of_done: string
29
+ description: string
30
+ estimation: number
31
+ comments: unknown[]
32
+ assignee: RawAgent
33
+ status: string
34
+ in_progress_since?: string
35
+ }
36
+
37
+ const parseTask = (line: string): RawTask | null => {
38
+ try {
39
+ return JSON.parse(line) as RawTask
40
+ } catch {
41
+ return null
42
+ }
43
+ }
44
+
45
+ const isEnoent = (e: unknown): boolean =>
46
+ typeof e === "object" && e !== null && (e as { code?: unknown }).code === "ENOENT"
47
+
48
+ const lines = await readLines(dataFile)
49
+
50
+ let original: RawTask | null = null
51
+ for (const line of lines) {
52
+ const task = parseTask(line)
53
+ if (task !== null && task.id === taskId) {
54
+ original = task
55
+ break
56
+ }
57
+ }
58
+
59
+ if (original === null) process.exit(0)
60
+
61
+ // Guard: don't create a review-of-a-review (circular flow)
62
+ if (original.id.startsWith("review-")) process.exit(0)
63
+
64
+ const reviewId = `review-${original.id}`
65
+
66
+ // Idempotency check: skip if a task with the review id already exists
67
+ const alreadyExists = lines.some((line) => {
68
+ const task = parseTask(line)
69
+ return task !== null && task.id === reviewId
70
+ })
71
+
72
+ if (alreadyExists) process.exit(0)
73
+
74
+ const reviewTask: RawTask = {
75
+ project: original.project,
76
+ milestone: original.milestone,
77
+ id: reviewId,
78
+ title: `Review: ${original.title}`,
79
+ definition_of_done: "Review approved",
80
+ description: `Review task for ${original.id}`,
81
+ estimation: 1,
82
+ comments: [],
83
+ assignee: original.assignee,
84
+ status: "todo",
85
+ }
86
+
87
+ await appendFile(dataFile, `${JSON.stringify(reviewTask)}\n`, "utf8")
88
+
89
+ const { execSync } = await import("node:child_process")
90
+ const path = await import("node:path")
91
+ const projectRoot = path.dirname(process.env.LOGBOOK_TASKS_FILE ?? "./tasks.jsonl")
92
+ const mcpConfig = path.join(projectRoot, ".claude/mcp-config.json")
93
+ execSync(
94
+ `claude --model claude-haiku-4-5-20251001 --mcp-config ${mcpConfig} --agent reviewer -p "review task ${reviewId}"`,
95
+ { stdio: "inherit", env: { ...process.env, LOGBOOK_TASKS_FILE: dataFile } }
96
+ )
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@bosun-sh/logbook",
3
+ "version": "1.0.0",
4
+ "description": "File-system kanban board MCP server for AI agents",
5
+ "type": "module",
6
+ "bin": {
7
+ "logbook-mcp": "src/mcp/server.ts"
8
+ },
9
+ "files": [
10
+ "src/",
11
+ "hooks/",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "bun": ">=1.0.0"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public",
19
+ "registry": "https://registry.npmjs.org/"
20
+ },
21
+ "scripts": {
22
+ "start": "bun src/mcp/server.ts",
23
+ "test": "bun test",
24
+ "test:watch": "bun test --watch",
25
+ "test:unit": "bun test tests/unit/",
26
+ "test:e2e": "bun test tests/e2e/",
27
+ "typecheck": "tsc --noEmit",
28
+ "lint": "biome lint .",
29
+ "lint:fix": "biome lint --write .",
30
+ "format": "biome format --write .",
31
+ "check": "biome check .",
32
+ "prepare": "husky",
33
+ "prepublishOnly": "bun run typecheck",
34
+ "publish": "bun run typecheck && npm pack --dry-run && npm publish --ignore-scripts",
35
+ "publish:dev": "bun link ."
36
+ },
37
+ "dependencies": {
38
+ "effect": "^3.12.0",
39
+ "zod": "^3.24.1"
40
+ },
41
+ "devDependencies": {
42
+ "@biomejs/biome": "latest",
43
+ "@types/bun": "latest",
44
+ "husky": "^9.0.0",
45
+ "typescript": "^5.7.3"
46
+ }
47
+ }
@@ -0,0 +1,90 @@
1
+ import { access, mkdir, writeFile } from "node:fs/promises"
2
+ import { join } from "node:path"
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Config snippet constants (pure — no side effects)
6
+ // ---------------------------------------------------------------------------
7
+
8
+ const CLAUDE_CODE_SNIPPET = `Claude Code — add to .claude/settings.json:
9
+ {
10
+ "mcpServers": {
11
+ "logbook": {
12
+ "command": "logbook-mcp"
13
+ }
14
+ }
15
+ }`
16
+
17
+ const OPENCODE_SNIPPET = `OpenCode — add to opencode.json:
18
+ {
19
+ "mcp": {
20
+ "logbook": {
21
+ "type": "local",
22
+ "command": ["logbook-mcp"],
23
+ "enabled": true
24
+ }
25
+ }
26
+ }`
27
+
28
+ const GITIGNORE_SNIPPET = `.gitignore — add these runtime files:
29
+ tasks.jsonl
30
+ sessions.json`
31
+
32
+ const NEXT_STEPS = `Next steps:
33
+ 1. Add the config snippet for your AI client
34
+ 2. Run: LOGBOOK_TASKS_FILE=./tasks.jsonl logbook-mcp
35
+ 3. In your AI client, call current_task() to verify — expected: no_current_task
36
+ 4. See quickstart.md for the full walkthrough`
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Side-effecting helpers (file I/O at boundary)
40
+ // ---------------------------------------------------------------------------
41
+
42
+ const fileExists = async (path: string): Promise<boolean> => {
43
+ try {
44
+ await access(path)
45
+ return true
46
+ } catch {
47
+ return false
48
+ }
49
+ }
50
+
51
+ const scaffoldTasksFile = async (cwd: string): Promise<void> => {
52
+ const path = join(cwd, "tasks.jsonl")
53
+ if (await fileExists(path)) {
54
+ console.log("✓ tasks.jsonl already exists, skipping")
55
+ return
56
+ }
57
+ await writeFile(path, "", "utf8")
58
+ console.log("✓ tasks.jsonl created")
59
+ }
60
+
61
+ const scaffoldHooksDir = async (cwd: string): Promise<void> => {
62
+ const path = join(cwd, "hooks")
63
+ if (await fileExists(path)) {
64
+ console.log("✓ hooks/ already exists, skipping")
65
+ return
66
+ }
67
+ await mkdir(path, { recursive: false })
68
+ console.log("✓ hooks/ created")
69
+ }
70
+
71
+ const printSnippets = (): void => {
72
+ console.log("")
73
+ console.log(CLAUDE_CODE_SNIPPET)
74
+ console.log("")
75
+ console.log(OPENCODE_SNIPPET)
76
+ console.log("")
77
+ console.log(GITIGNORE_SNIPPET)
78
+ console.log("")
79
+ console.log(NEXT_STEPS)
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Entry point
84
+ // ---------------------------------------------------------------------------
85
+
86
+ export const runInit = async (cwd: string = process.cwd()): Promise<void> => {
87
+ await scaffoldTasksFile(cwd)
88
+ await scaffoldHooksDir(cwd)
89
+ printSnippets()
90
+ }