kandown 0.3.4 → 0.4.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/README.md CHANGED
@@ -13,214 +13,122 @@
13
13
  <a href="https://www.npmjs.com/package/kandown"><img src="https://img.shields.io/npm/v/kandown?color=cb3837&label=npm" alt="npm version"></a>
14
14
  <a href="https://www.npmjs.com/package/kandown"><img src="https://img.shields.io/npm/dm/kandown?color=blue" alt="npm downloads"></a>
15
15
  <img src="https://img.shields.io/badge/license-MIT-green" alt="MIT License">
16
- <img src="https://img.shields.io/badge/languages-48-purple" alt="48 languages">
17
16
  <img src="https://img.shields.io/badge/local--first-100%25-orange" alt="100% local-first">
18
17
  </p>
19
18
 
20
- <p align="center">
21
- <a href="#-quick-start">Quick Start</a> ·
22
- <a href="#-features">Features</a> ·
23
- <a href="#-cli-commands">CLI</a> ·
24
- <a href="#-board-tui">Board TUI</a> ·
25
- <a href="#-ai-agent-integration">AI Agents</a> ·
26
- <a href="#-keyboard-shortcuts">Shortcuts</a> ·
27
- <a href="#-development">Development</a>
28
- </p>
29
-
30
19
  ---
31
20
 
32
- ## 🚀 Quick Start
33
-
34
- ### 1. Install globally
21
+ ## Description
35
22
 
36
- ```bash
37
- npm i -g kandown
38
- ```
23
+ Kandown is a **local-first Kanban board** where your tasks live as plain Markdown files. No cloud, no account, no vendor lock-in — just a `.kandown/` folder in your project that you own forever.
39
24
 
40
- ### 2. Initialize in any project
25
+ - **Your data is portable** — tasks are `.md` files versioned with git, readable by any text editor or AI agent
26
+ - **AI-agent friendly** — Claude, Codex, Gemini, Goose, Aider, and OpenCode can read and update tasks directly
27
+ - **Dual interface** — a polished web UI + a full terminal TUI, both running entirely offline
28
+ - **Single-file deployment** — `kandown.html` is a self-contained app you can open in any browser
41
29
 
42
- ```bash
43
- cd my-project
44
- kandown init
45
- ```
30
+ ---
46
31
 
47
- ### 3. Launch your board
32
+ ## Installation
48
33
 
49
34
  ```bash
50
- kandown
35
+ npm install -g kandown
51
36
  ```
52
37
 
53
- > That's it. A local web UI opens in your browser, and an interactive board TUI appears in your terminal. All your data stays in `.kandown/` as plain markdown — versioned with git, readable by humans and AI agents alike.
38
+ **Requirements:** Node.js 18+
54
39
 
55
40
  ---
56
41
 
57
- ## 💡 Why Kandown?
42
+ ## Usage
58
43
 
59
- Most kanban tools trap your data in their cloud. Kandown does the opposite.
60
-
61
- | Traditional Kanban | Kandown |
62
- |---|---|
63
- | ☁️ Cloud-hosted, needs an account | 📁 100% local, no account |
64
- | 🔒 Proprietary data format | 📝 Plain Markdown + JSON |
65
- | 💰 Paid plans for teams | 🆓 Free & open-source (MIT) |
66
- | 🤖 No AI integration | 🤖 AI-agent native (Claude, Codex, Gemini…) |
67
- | 🔄 Requires sync/internet | 📂 Git-diffable, offline-first |
68
- | 🏗️ Backend, database, infra | 🚫 Zero backend, zero dependencies |
44
+ ### Initialize in any project
69
45
 
70
- **Your tasks are just `.md` files.** Move them, `grep` them, edit them in Vim, let an AI agent process them — they're yours.
71
-
72
- ---
73
-
74
- ## 📦 What Gets Installed
46
+ ```bash
47
+ cd my-project
48
+ kandown init
49
+ ```
75
50
 
76
- Running `kandown init` creates a `.kandown/` folder in your project:
51
+ This creates a `.kandown/` folder with:
77
52
 
78
53
  ```
79
54
  .kandown/
80
- ├── kandown.html Single-file web app (opens in any browser)
81
- ├── kandown.json Project preferences, columns, appearance
82
- ├── tasks/ One markdown file per task (source of truth)
83
- ├── AGENT.md AI-agent quick reference
84
- └── README.md ← Project-local user guide
55
+ ├── kandown.html # Single-file web app
56
+ ├── kandown.json # Project config (columns, appearance)
57
+ ├── tasks/ # One .md file per task
58
+ └── AGENT.md # AI-agent quick reference
59
+ ```
60
+
61
+ ### Launch the board
62
+
63
+ ```bash
64
+ kandown
85
65
  ```
86
66
 
87
- It also copies `AGENT_KANDOWN.md` to the project root and wires it into your `AGENTS.md` / `CLAUDE.md` so AI agents know how to manage your board automatically.
67
+ This opens:
68
+ - A **web UI** in your browser (board view, list view, task editor)
69
+ - A **terminal TUI** for keyboard-driven workflow (works over SSH, no browser needed)
70
+
71
+ ### CLI Commands
72
+
73
+ | Command | Description |
74
+ |---|---|
75
+ | `kandown` | Launch web UI + board TUI |
76
+ | `kandown init` | Initialize in current project |
77
+ | `kandown board` | TUI only (no browser) |
78
+ | `kandown settings` | Terminal settings editor |
79
+ | `kandown update` | Update `kandown.html` to latest |
80
+ | `kandown help` | CLI help |
88
81
 
89
82
  ---
90
83
 
91
- ## Features
84
+ ## Features
92
85
 
93
86
  ### Board & Views
94
87
 
95
88
  | Feature | Description |
96
89
  |---|---|
97
- | 🗂️ Board view | Horizontal kanban with drag-and-drop between columns |
98
- | 📋 List view | Dense table-like view with the same filters & search |
99
- | 🔍 Content search | Search titles, body, subtasks, tags, assignee, priority — with highlighted previews |
100
- | ⌨️ Command palette | `⌘K` / `Ctrl+K` for quick actions and task lookup |
101
- | 🏷️ Custom columns | Add, rename, delete columns unknown statuses appear as temp columns |
102
- | 🗑️ Guarded deletion | Double-click the trash icon to delete — no accidental losses |
90
+ | Board view | Horizontal kanban with drag-and-drop |
91
+ | List view | Dense table with filters & search |
92
+ | Content search | Search titles, body, subtasks, tags, assignee, priority |
93
+ | Command palette | `⌘K` / `Ctrl+K` for quick actions |
94
+ | Custom columns | Add, rename, delete columns freely |
95
+ | Guarded deletion | Double-click to delete — no accidents |
103
96
 
104
97
  ### Task Management
105
98
 
106
99
  | Feature | Description |
107
100
  |---|---|
108
- | ✏️ Rich task drawer | Edit title, metadata, subtasks, and body via WYSIWYG markdown editor |
109
- | ☑️ Subtasks | Full checklist support with progress tracking on cards |
110
- | 📊 Metadata fields | Priority, assignee, tags, due date, owner type — toggle each on/off |
111
- | 👤 Owner filtering | Separate human tasks from AI-agent tasks |
112
- | External-change detection | Warns when a file was modified outside the app (Reload / Overwrite / Cancel) |
101
+ | Rich task drawer | WYSIWYG markdown editor for title, metadata, subtasks, body |
102
+ | Subtasks | Full checklist with progress tracking on cards |
103
+ | Metadata fields | Priority, assignee, tags, due date, owner type |
104
+ | Owner filtering | Filter human vs AI-agent tasks separately |
105
+ | External-change detection | Warns when files are modified outside the app |
113
106
 
114
107
  ### Appearance & UX
115
108
 
116
109
  | Feature | Description |
117
110
  |---|---|
118
- | 🎨 5 built-in skins | Kandown, Graphite, Sage, Cobalt, Rose |
119
- | 🌗 Theme modes | Auto, Light, Dark |
120
- | 🔤 5 font stacks | Inter, System, Serif, Mono, Rounded |
121
- | 🌊 Animated backgrounds | WebGL fluid simulation (LiquidEther) |
122
- | 🔔 Notifications | Browser + in-page sound alerts for status changes, edits, subtask completions |
123
- | 🌍 48 languages | Full i18n support |
124
- | 📌 Recent projects | Stored in IndexedDB for quick reopening |
111
+ | 5 built-in skins | Kandown, Graphite, Sage, Cobalt, Rose |
112
+ | Theme modes | Auto (system), Light, Dark |
113
+ | 5 font stacks | Inter, System, Serif, Mono, Rounded |
114
+ | Animated backgrounds | WebGL fluid simulation (LiquidEther) |
115
+ | 48 languages | Full i18n support |
116
+ | Notifications | Browser + sound alerts for status changes, edits, completions |
125
117
 
126
- ---
127
-
128
- ## 🖥️ CLI Commands
129
-
130
- ### Usage
131
-
132
- ```bash
133
- # If installed globally (recommended):
134
- kandown [command] [options]
135
-
136
- # Or with npx (no install needed):
137
- npx kandown [command] [options]
138
- ```
139
-
140
- ### Commands
141
-
142
- | Command | Description |
143
- |---|---|
144
- | `kandown` | Launch web UI + board TUI (recommended daily workflow) |
145
- | `kandown init` | Initialize Kandown in the current project |
146
- | `kandown board` | Open the board TUI only (no browser) |
147
- | `kandown settings` | Open the terminal settings editor |
148
- | `kandown update` | Update `kandown.html` to the latest version |
149
- | `kandown help` | Print CLI help |
150
-
151
- ### Init Options
152
-
153
- | Flag | Description |
154
- |---|---|
155
- | `--path <dir>` | Install `.kandown` at a custom location |
156
- | `--force` | Overwrite existing installation |
157
- | `--no-agents` | Skip AI agent docs setup |
158
-
159
- ### Serve Options
160
-
161
- | Flag | Description |
162
- |---|---|
163
- | `--port <number>` | Use a specific port (default: auto-scan 2048–2060) |
164
-
165
- ---
166
-
167
- ## 📟 Board TUI
168
-
169
- The board TUI is a full-screen terminal kanban built with [Ink](https://github.com/vadimdemedes/ink). Same columns and tasks as the web UI — plus the ability to launch AI agents on any task.
170
-
171
- ```
172
- KANDOWN tmux My Project
173
- ──────────────────────────────────────────────────────────────
174
- Backlog (3) │ Todo (2) │ In Progress │ Review (1) │ Done
175
- ──────────────│──────────────│──────────────│────────────│──────
176
- ▸ t9 │ t16 │ (empty) │ t18 │ ...
177
- t10 │ t7 │ │
178
- t11 │ │
179
- ```
118
+ ### AI Agent Integration
180
119
 
181
- ### TUI Navigation
182
-
183
- | Key | Action |
184
- |---|---|
185
- | `h` / `l` or `←` / `→` | Move between columns |
186
- | `j` / `k` or `↑` / `↓` | Move between tasks |
187
- | `Enter` | Open task detail (scrollable) |
188
- | `a` | Launch AI agent picker |
189
- | `r` | Reload files from disk |
190
- | `q` / `Esc` | Quit or go back |
191
-
192
- ---
193
-
194
- ## 🤖 AI Agent Integration
195
-
196
- Press `a` in the board TUI to launch an AI agent on the selected task. Kandown auto-detects installed agents and builds a context-rich prompt from `AGENT_KANDOWN.md` + the task file.
197
-
198
- ### Supported Agents
120
+ Press `a` in the board TUI to launch an AI agent on any task. Supported agents:
199
121
 
200
122
  | Agent | Binary | Launch mode |
201
123
  |---|---|---|
202
- | **Claude Code** | `claude` | Interactive session with initial prompt |
203
- | **OpenAI Codex** | `codex` | Interactive session |
204
- | **Gemini CLI** | `gemini` | `-p` flag for initial prompt |
205
- | **Goose** | `goose` | `run --text` for non-interactive |
206
- | **Aider** | `aider` | `--message` for initial prompt |
207
- | **OpenCode** | `opencode` | TUI, context written to `/tmp` |
208
-
209
- ### How it works
124
+ | Claude Code | `claude` | Interactive session |
125
+ | OpenAI Codex | `codex` | Interactive session |
126
+ | Gemini CLI | `gemini` | `-p` flag for initial prompt |
127
+ | Goose | `goose` | `run --text` for non-interactive |
128
+ | Aider | `aider` | `--message` for initial prompt |
129
+ | OpenCode | `opencode` | TUI, context written to `/tmp` |
210
130
 
211
- 1. Selecting an agent sets the task status to **In Progress**
212
- 2. A system prompt is built from `AGENT_KANDOWN.md` + the task file
213
- 3. Context is written to `/tmp/kandown-<id>-context.md`
214
- 4. **In tmux**: opens in a 50%-wide split pane (TUI stays visible)
215
- 5. **Without tmux**: exits TUI and hands the terminal to the agent
216
-
217
- > **Tip:** AI agents can also edit task files directly — Kandown detects external changes and syncs automatically.
218
-
219
- ---
220
-
221
- ## ⌨️ Keyboard Shortcuts
222
-
223
- ### Web UI
131
+ ### Keyboard Shortcuts
224
132
 
225
133
  | Shortcut | Action |
226
134
  |---|---|
@@ -231,149 +139,42 @@ Press `a` in the board TUI to launch an AI agent on the selected task. Kandown a
231
139
  | `R` | Reload from disk |
232
140
  | `/` | Focus search |
233
141
  | `Esc` | Close drawer / palette |
234
- | `⌘S` / `Ctrl+S` | Save task in drawer |
142
+ | `⌘S` / `Ctrl+S` | Save task |
235
143
  | `⌘⌫` / `Ctrl+Backspace` | Delete task (with confirmation) |
236
144
 
237
- ---
238
-
239
- ## 📄 Data Model
240
-
241
- ### Task files — `tasks/<id>.md`
145
+ ### Data Model
242
146
 
243
- Each task is a standalone markdown file with YAML frontmatter:
147
+ Each task is a standalone Markdown file:
244
148
 
245
149
  ```markdown
246
150
  ---
247
151
  id: t1
248
152
  title: Implement user auth
249
153
  status: Todo
250
- order: 0
251
154
  priority: P1
252
155
  tags: [backend, security]
253
156
  assignee: chacha
254
157
  created: 2026-04-10
255
- ownerType: human
256
158
  ---
257
159
 
258
160
  # Implement user auth
259
161
 
260
162
  ## Context
261
- Why this task exists and what it solves.
163
+ Why this task exists.
262
164
 
263
165
  ## Subtasks
264
166
  - [ ] Set up OAuth provider
265
167
  - [x] Create user model
266
168
  - [ ] Add session middleware
267
-
268
- ## Notes
269
- Extra details, links, decisions.
270
- ```
271
-
272
- ### Configuration — `kandown.json`
273
-
274
- <details>
275
- <summary>Full default configuration</summary>
276
-
277
- ```json
278
- {
279
- "ui": {
280
- "language": "en",
281
- "theme": "auto",
282
- "skin": "kandown",
283
- "font": "inter"
284
- },
285
- "agent": {
286
- "suggestFollowUp": false,
287
- "maxSuggestions": 3
288
- },
289
- "board": {
290
- "taskPrefix": "t",
291
- "defaultPriority": "P3",
292
- "defaultOwnerType": "human"
293
- },
294
- "fields": {
295
- "priority": false,
296
- "assignee": false,
297
- "tags": false,
298
- "dueDate": false,
299
- "ownerType": false,
300
- "tools": false
301
- },
302
- "notifications": {
303
- "browser": false,
304
- "sound": false,
305
- "soundId": "soft",
306
- "statusChanges": true,
307
- "taskEdits": true,
308
- "subtaskCompletions": true,
309
- "editDebounceMs": 2000
310
- }
311
- }
312
169
  ```
313
170
 
314
- </details>
315
-
316
- | Section | Key options |
317
- |---|---|
318
- | `ui` | Language (48 locales), theme mode, skin, font |
319
- | `board` | Task ID prefix, default priority/owner |
320
- | `fields` | Toggle visibility of priority, assignee, tags, due date, owner type |
321
- | `notifications` | Browser alerts, sound cues, debounce timing |
322
-
323
- Disabled fields are hidden from cards, list view, the task drawer, and metadata filters.
324
-
325
- ---
326
-
327
- ## 🌐 Browser Support
328
-
329
- | Browser | Supported |
330
- |---|---|
331
- | Chrome | ✅ |
332
- | Edge | ✅ |
333
- | Brave | ✅ |
334
- | Opera | ✅ |
335
- | Firefox | ❌ (no File System Access API) |
336
- | Safari | ❌ (no File System Access API) |
337
-
338
- > **Server mode** (`kandown` CLI command) works in all browsers — it proxies file operations through a local REST API, bypassing the File System Access API requirement.
339
-
340
171
  ---
341
172
 
342
- ## 🎨 Appearance
343
-
344
- ### Skins
345
-
346
- | Skin | Description |
347
- |---|---|
348
- | `kandown` | Crisp neutral default |
349
- | `graphite` | Soft gray surfaces |
350
- | `sage` | Calm green-gray planning palette |
351
- | `cobalt` | Cool blue accent, restrained surfaces |
352
- | `rose` | Warm ink with rose accent |
353
-
354
- ### Theme Modes
355
-
356
- | Mode | Behavior |
357
- |---|---|
358
- | `auto` | Follows system `prefers-color-scheme` |
359
- | `light` | Force light theme |
360
- | `dark` | Force dark theme |
361
-
362
- ### Fonts
363
-
364
- | Font | Stack |
365
- |---|---|
366
- | `inter` | Inter-first sans stack |
367
- | `system` | Native platform sans stack |
368
- | `serif` | Editorial serif stack |
369
- | `mono` | Monospace stack |
370
- | `rounded` | Rounded system stack |
371
-
372
- ---
173
+ ## Contributing
373
174
 
374
- ## 🛠️ Development
175
+ Contributions are welcome! Please read the existing code style and conventions before submitting PRs.
375
176
 
376
- ### Setup
177
+ ### Development Setup
377
178
 
378
179
  ```bash
379
180
  git clone https://github.com/vava-nessa/kandown.git
@@ -386,107 +187,24 @@ pnpm dev # Web UI at localhost:5173
386
187
 
387
188
  | Script | Description |
388
189
  |---|---|
389
- | `pnpm dev` | Vite dev server for the web UI |
390
- | `pnpm dev:cli` | Watch-mode build for the CLI TUI |
391
- | `pnpm build` | Full build: typecheck → web app → CLI TUI |
392
- | `pnpm build:cli` | One-shot CLI TUI build |
393
- | `pnpm preview` | Preview the Vite production build |
394
- | `pnpm typecheck` | TypeScript check without emitting |
395
-
396
- ### Dev Workflow (CLI / TUI)
397
-
398
- ```bash
399
- # Terminal 1 — watch and rebuild on save
400
- pnpm dev:cli
401
-
402
- # Terminal 2 — test the board TUI
403
- node bin/kandown.js board
404
-
405
- # Or full experience (web server + browser + TUI):
406
- node bin/kandown.js --port 3000
407
- ```
408
-
409
- ### Build Output
410
-
411
- | File | Purpose |
412
- |---|---|
413
- | `dist/index.html` | Single-file web app → copied as `kandown.html` |
414
- | `bin/tui.js` | Bundled Ink TUI → used by `kandown board` and `kandown settings` |
415
-
416
- ---
417
-
418
- ## 🏗️ Architecture
419
-
420
- <details>
421
- <summary>Project structure</summary>
422
-
423
- ```
424
- kandown/
425
- ├── bin/
426
- │ ├── kandown.js # CLI entrypoint (hand-rolled, no deps)
427
- │ └── tui.js # Generated TUI bundle (tsup)
428
- ├── src/
429
- │ ├── App.tsx # Web app shell + global shortcuts
430
- │ ├── main.tsx # React/Vite browser entrypoint
431
- │ ├── cli/ # Ink terminal UI source
432
- │ │ ├── tui.tsx # Ink entrypoint (alternate screen buffer)
433
- │ │ ├── app.tsx # TUI screen router
434
- │ │ ├── lib/ # Node-side helpers (config, board, agents, launcher)
435
- │ │ └── screens/ # board.tsx, agent-picker.tsx, settings.tsx
436
- │ ├── components/ # React UI components (web)
437
- │ ├── hooks/ # React hooks
438
- │ ├── lib/ # Domain model, parser, serializer, store, theme, i18n
439
- │ └── styles/ # Tailwind layers + CSS tokens
440
- ├── templates/ # Files copied by `kandown init`
441
- ├── dist/index.html # Generated single-file web app
442
- ├── vite.config.ts
443
- ├── tsup.config.ts
444
- └── package.json
445
- ```
446
-
447
- </details>
448
-
449
- <details>
450
- <summary>Runtime flow</summary>
451
-
452
- 1. `main.tsx` mounts the React app
453
- 2. `App` renders header + board / list / settings / empty state
454
- 3. User selects a folder → File System Access API (or server mode)
455
- 4. `filesystem.ts` resolves `.kandown/`, `tasks/`, `kandown.json`
456
- 5. `store.ts` loads config, applies theme, scans tasks, saves recent project handles
457
- 6. `parser.ts` converts task markdown into typed data
458
- 7. Mutations flow back through store → `serializer.ts` → `filesystem.ts`
459
-
460
- </details>
461
-
462
- <details>
463
- <summary>Key modules</summary>
464
-
465
- | Module | Role |
466
- |---|---|
467
- | `src/lib/store.ts` | Zustand state machine — project loading, board mutations, drawer, search, config |
468
- | `src/lib/filesystem.ts` | File System Access API + IndexedDB helpers |
469
- | `src/lib/parser.ts` | Markdown parsing, subtask extraction, content search |
470
- | `src/lib/serializer.ts` | Markdown serialization |
471
- | `src/lib/theme.ts` | Theme engine — skins, fonts, CSS variables |
472
- | `src/lib/watcher.ts` | 500ms polling watcher with SHA-256 hashing for external change detection |
473
- | `src/lib/i18n/index.ts` | 48-locale translation system |
474
-
475
- </details>
190
+ | `pnpm dev` | Vite dev server for web UI |
191
+ | `pnpm dev:cli` | Watch-mode build for CLI TUI |
192
+ | `pnpm build` | Full build: typecheck → web app → CLI |
193
+ | `pnpm build:cli` | CLI TUI build only |
194
+ | `pnpm preview` | Preview production build |
195
+ | `pnpm typecheck` | TypeScript check |
476
196
 
477
197
  ---
478
198
 
479
- ## 📐 Design Constraints
199
+ ## License
480
200
 
481
- - **Local-first, backend-free** no servers, no sync vendors
482
- - **Markdown is the canonical data format** — always human-readable
483
- - **Task files are the single source of truth** — no index files to sync
484
- - **Single-file web app** — `kandown.html` bundles everything
485
- - **Project config in `kandown.json`** — columns, appearance, fields, notifications
486
- - **Browser state in IndexedDB** — only for convenience (recent project handles)
201
+ [MIT](LICENSE) use it, fork it, ship it.
487
202
 
488
203
  ---
489
204
 
490
- ## 📜 License
491
-
492
- [MIT](LICENSE) use it, fork it, ship it.
205
+ <p align="center">
206
+ <a href="#-quick-start">Quick Start</a> ·
207
+ <a href="#-features">Features</a> ·
208
+ <a href="#-cli-commands">CLI</a> ·
209
+ <a href="#-ai-agent-integration">AI Agents</a>
210
+ </p>
package/bin/kandown.js CHANGED
@@ -18,6 +18,7 @@
18
18
  * → parseArgs — parses shared CLI flags
19
19
  * → cmdInit — installs `.kandown`
20
20
  * → cmdUpdate — refreshes installed kandown.html
21
+ * → injectServerRoot — injects the CLI server root into single-file HTML
21
22
  * → createServeServer — creates the local zero-dependency HTTP server
22
23
  * → cmdServe — opens the web UI over localhost and launches the board TUI
23
24
  * → main — dispatches CLI commands
@@ -333,6 +334,11 @@ function cmdInit(rawArgs) {
333
334
  const kandownPath = args.path;
334
335
  const kandownDir = resolve(cwd, kandownPath);
335
336
 
337
+ if (existsSync(join(cwd, '.kandown', 'board.md'))) {
338
+ err(`Kandown already initialized in this directory. To reinitialize, remove the .kandown folder first.`);
339
+ process.exit(1);
340
+ }
341
+
336
342
  log('');
337
343
  info(`Installing kandown in ${c.bold}${kandownPath}/${c.reset}`);
338
344
  log('');
@@ -549,6 +555,22 @@ function deleteTask(res, kandownDir, id) {
549
555
  }
550
556
  }
551
557
 
558
+ /**
559
+ * 📖 The single-file Vite bundle can contain literal strings such as
560
+ * `</head>` from HTML parser libraries. Use the last closing head tag so the
561
+ * CLI does not inject server-mode globals into bundled JavaScript text.
562
+ */
563
+ function injectServerRoot(html, kandownDir) {
564
+ const marker = '</head>';
565
+ const markerIndex = html.toLowerCase().lastIndexOf(marker);
566
+ const safeRoot = JSON.stringify(kandownDir).replace(/</g, '\\u003c');
567
+ const script = `<script>window.__KANDOWN_ROOT__ = ${safeRoot};</script>\n`;
568
+
569
+ if (markerIndex === -1) return script + html;
570
+
571
+ return html.slice(0, markerIndex) + script + html.slice(markerIndex);
572
+ }
573
+
552
574
  function handleApi(req, res, url, kandownDir) {
553
575
  const parts = url.pathname.replace('/api/', '').split('/');
554
576
  const resource = parts[0];
@@ -583,10 +605,7 @@ function serveApp(res, kandownDir) {
583
605
 
584
606
  try {
585
607
  const html = readFileSync(htmlPath, 'utf8');
586
- const injected = html.replace(
587
- '</head>',
588
- `<script>window.__KANDOWN_ROOT__ = ${JSON.stringify(kandownDir)};</script>\n</head>`,
589
- );
608
+ const injected = injectServerRoot(html, kandownDir);
590
609
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
591
610
  res.end(injected);
592
611
  } catch (e) {