k0-board 0.1.2 → 0.3.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
@@ -40,7 +40,8 @@ npx k0-board
40
40
  ```
41
41
 
42
42
  That is the whole thing. It tells you everything it is about to change, asks, and only then does
43
- it. `k0-board uninstall` undoes all of it.
43
+ it — and when it is finished it opens the board in your browser, so there is no address to write
44
+ down anywhere. `k0-board uninstall` undoes all of it.
44
45
 
45
46
  k0 is published on npm as [`k0-board`](https://www.npmjs.com/package/k0-board): one package, no
46
47
  dependencies, nothing to compile. `npx` fetches it, runs the installer once and leaves nothing
@@ -69,8 +70,9 @@ that and went out from a laptop.)
69
70
  | `k0-board doctor` | what this machine can and cannot do, and why |
70
71
 
71
72
  `--yes` does not ask. `--from-source` points the service at the directory you are in instead of
72
- copying it, which is what you want while working on k0 itself. `--no-<name>` skips one optional
73
- change — `--no-lid-sleep`, `--no-shift-enter`. `K0_PORT` moves the board off 4319.
73
+ copying it, which is what you want while working on k0 itself. `--no-open` finishes without
74
+ opening a browser, which is what you want over SSH. `--no-<name>` skips one optional change —
75
+ `--no-lid-sleep`, `--no-shift-enter`. `K0_PORT` moves the board off 4319.
74
76
 
75
77
  From a clone, if you would rather look first:
76
78
 
@@ -83,7 +85,10 @@ node bin/k0-board.js start
83
85
  That runs the server in the foreground and opens the board. Nothing is installed and nothing
84
86
  outside the directory is touched.
85
87
 
86
- The board lives at <http://k0.localhost:4319>.
88
+ The board lives at <http://k0.localhost:4319>, which is the address both `install` and `start`
89
+ open for you. If that name does not resolve on your machine — which happens on Windows, because
90
+ only browsers are obliged to know that `.localhost` means this computer — k0 opens
91
+ <http://127.0.0.1:4319> instead, and says so.
87
92
 
88
93
  ### Requirements
89
94
 
@@ -128,7 +133,7 @@ Nothing on this list happens before you have seen it and said yes. Everything on
128
133
  | What | Where | Why |
129
134
  |---|---|---|
130
135
  | A copy of k0 | `~/.k0/app` | so the service does not point at a temporary npx cache |
131
- | Your board, logs and cache | `~/.k0/` | `%LOCALAPPDATA%\k0` on Windows |
136
+ | Your board, settings, logs and cache | `~/.k0/` | `%LOCALAPPDATA%\k0` on Windows |
132
137
  | A service that starts at login | LaunchAgents · systemd user unit · Task Scheduler | so k0 is simply there |
133
138
  | **macOS only** — one administrator rule | `/etc/sudoers.d/k0-pmset` | to keep the Mac awake with the lid closed. Asks for your password once, and grants exactly two command lines |
134
139
  | **macOS only** — one key binding | your Terminal profile (backed up first) | so Shift+Enter starts a new line |
@@ -195,8 +200,10 @@ k0 makes **no network requests**. Not one, not even for a font.
195
200
  - **double click** — brings that session's terminal back to the front, even if you had minimised
196
201
  it. It is what stops you losing track of which window is which.
197
202
 
198
- - **Resume** — appears once you have closed the terminal: it reopens the same conversation exactly
199
- where it was.
203
+ - **Resume** — appears once the terminal has been closed, by you or by k0 after a long silence: it
204
+ reopens the same conversation exactly where it was. The card's prompt is **not** put back in —
205
+ that conversation answered it hours ago, and typing it in again would be at best something stale
206
+ in the way.
200
207
 
201
208
  - **Done** — closes the work **and its terminal**, with no questions: undo it with **Reopen**, and
202
209
  pick the conversation up again with **Resume**. It is not on backlog cards: they never started.
@@ -205,6 +212,16 @@ k0 makes **no network requests**. Not one, not even for a font.
205
212
  still inside it makes macOS put up the "terminate running processes" dialog, and the window does
206
213
  not close at all.
207
214
 
215
+ - **Close** — a link at the end of the row, quieter than the buttons because it is the one you
216
+ reach for least: it closes the terminal and **not the work**. The session is stopped, its window
217
+ goes, and the machine gets the memory back; the card stays exactly where it is, with the colour
218
+ it had — the one asking you something stays red, the one holding a plan up stays amber, the one
219
+ at your turn stays yellow — and **Resume** takes it from there. It is what a dozen windows open
220
+ and a computer running out of air are answered with.
221
+
222
+ It is **not there while the session is working**: stopping one mid-thought is not memory saved,
223
+ it is work lost. Wait for it to come back to you, and then close it.
224
+
208
225
  The only thing that asks for confirmation is throwing a card away.
209
226
 
210
227
  Cards reorder themselves inside a column: first the ones waiting for you, then the ones grinding
@@ -218,17 +235,53 @@ right, left, up, down, and diagonally in the corners — faster the closer you a
218
235
  where the board ends. The thin shadow along one side says there is still something over there.
219
236
  Holding a mouse button keeps the board still, so a click near the edge lands.
220
237
 
221
- Bottom right there is the **zoom** and a percentage that, clicked, goes back to 100%; the button
238
+ Bottom right there is the **zoom**: + and − move it ten per cent at a time — 90%, 80%, 70% and so
239
+ on, between 30% and 200% — and the percentage between them, clicked, goes back to 100%. The button
222
240
  next to it shrinks just enough to fit the whole board on the screen. On a trackpad, two fingers
223
- move the view and a pinch zooms. Where you were is still there after a reload.
241
+ move the view and a pinch zooms to anything in between; the next click of + or − picks the nearest
242
+ step from there. Where you were is still there after a reload.
224
243
 
225
244
  The pills at the top are both legend and filter: click one and that status leaves the board. They
226
245
  stay as you left them.
227
246
 
228
- **The last column on the right is `Others`**: the repositories that have no column because they
229
- have no cards. Headings only, in alphabetical order. It is there for two things — getting into
230
- the files of a directory you are not working on, and giving birth to the first card there with
231
- its `+`.
247
+ **The board keeps itself down to what you are working on.** A repository you have not touched
248
+ since the day before your last piece of work folds away by itself: its column leaves the board
249
+ and its name appears in `Old`, over on the right. There is nothing to switch on and nothing to
250
+ tidy up — with a dozen repositories open, you arrive in the morning and the three or four you are
251
+ actually on are the ones standing there, side by side.
252
+
253
+ The day is counted back from **your last piece of work, not from the clock**, and that is what
254
+ makes it survive a weekend: on Monday the freshest thing on the board is Friday evening, so what
255
+ you find is Friday — not an empty board on the one morning you most need to see where you left
256
+ off. The moment you start something on Monday the line moves with you.
257
+
258
+ Two things never fold. **A column with something going on in it** — working, planning, asking you
259
+ something, holding a plan up for approval — stays where it is however old it is: hiding the one
260
+ that is waiting for you is the opposite of what the board is for. A terminal simply left open at
261
+ **Your turn** is not one of those: it is the resting state of every window you have not closed,
262
+ and a session you finished with three weeks ago would otherwise hold its column at full width for
263
+ ever. It folds like anything else — and folding closes nothing, so the terminal is still open and
264
+ the card is still exactly where you left it. And **nothing folds while you are looking at it**: within one visit the set of columns can only grow, and the board tidies itself
265
+ on the next reload, so a column never disappears out from under your hands.
266
+
267
+ **The last two columns on the right are `Old` and `Others`**, and they are the same list twice
268
+ over: a `+`, a name and the git mark, in alphabetical order. `Old` is the repositories that have
269
+ work on the board, just not lately; `Others` is the ones that have no column because they have no
270
+ cards at all. **Click a row in `Old` and the column comes back** at full width for the rest of the
271
+ visit — with everything that was in it, which is why the row itself says nothing about how much
272
+ that is or how long it has been there. Both columns are also there for the same two things:
273
+ getting into the files of a directory you are not working on, and giving birth to the first card
274
+ there with the `+`.
275
+
276
+ Next to a column's `+` there is one more button, which **puts that column away now** instead of
277
+ waiting for it to go quiet on its own — for when you are finished with a repository for the day
278
+ and would rather not look at it. On a column with something going on in it the button goes faint
279
+ and refuses, and says why if you hover it — it does not disappear, because a column runs and goes
280
+ back to your turn every few seconds and it would spend the session blinking.
281
+ Unlike the automatic fold, this one is remembered across reloads, until you fetch the column back
282
+ or something in it wakes up. It **closes nothing**: no session is stopped and no terminal is
283
+ shut. It is the board that gets smaller, not the work — what closes a session is `Close` or `Done`
284
+ on a card.
232
285
 
233
286
  ### The git mark
234
287
 
@@ -248,6 +301,93 @@ k0 **never runs `git fetch`**. It only looks at what your git already knows abou
248
301
  itself; a push made by somebody else does not show until you fetch. On a repository with no
249
302
  remote, pushing is not mentioned at all.
250
303
 
304
+ ### The globe: is the site up
305
+
306
+ Last on a column's heading, after the git mark, a small **globe** says whether that repository's
307
+ dev server is running. **Grey** is off, **green** is up, and it **breathes** while it is coming
308
+ up. **Red** means it was started and did not come up — hover it for the reason and the log.
309
+
310
+ **One click switches it on or off. Two restart it.** And when it is green the repository's
311
+ **name is underlined with a green dashed line** and becomes a link: click it and the site opens
312
+ in another tab, on the port it is really listening on. The line is the same green as the globe
313
+ and stays for as long as the server does, so a repository that is up is one you can tell apart
314
+ at a glance, from across the board, without going near it. Hover the globe to read the rest — the port, the command it was
315
+ started with, and where its log is.
316
+
317
+ A repository gets a globe when its `package.json` has a **`dev`** script, or failing that a
318
+ **`serve`** one. Nothing to configure and nothing to fill in. `start` is deliberately not on that
319
+ list: by convention it runs what has been *built*, which is not what this is for.
320
+
321
+ Three things are worth knowing, because they are the point.
322
+
323
+ **The server outlives the session.** k0 starts it detached and owned by nobody. Close the session
324
+ that asked for it, close every session, restart k0 itself — the server stays up and the globe
325
+ stays green. This is the whole reason it exists: a dev server started inside a session belongs to
326
+ that session, and dies with it.
327
+
328
+ **k0 never asks the server whether it is up.** It makes no network requests, here as everywhere
329
+ else. "Up" means the process is alive *and* the operating system says it is holding a TCP port
330
+ open. That is stronger evidence than a reply would be, and it is where the port in the tooltip
331
+ comes from — k0 never guesses a port, it reads the one your server actually chose.
332
+
333
+ **A server you started yourself is recognised.** Start one by hand in a terminal and the globe
334
+ goes green on its own within a few seconds, with the right port, and switches it off if you ask
335
+ it to. A globe that only knew about its own servers would sit grey next to a site that is plainly
336
+ running, which is worse than no globe at all. On Windows this one part is missing — nothing there
337
+ can read another process's working directory — and k0 says so rather than showing those servers
338
+ as off.
339
+
340
+ ---
341
+
342
+ ## The ChangeLog
343
+
344
+ The git mark answers "is this safe" for one repository, right now. The **ChangeLog** answers a
345
+ different question — *what have I actually been doing* — across all of them at once. It is the
346
+ list-shaped button next to the `+`, and it opens in a page of its own.
347
+
348
+ It reads like a release note rather than a report. An opening paragraph saying how the day went,
349
+ then one block per repository: a line saying what is different now, a short paragraph, a few
350
+ points, and — the part that matters most — **what is done but not out yet**. A commit sitting on
351
+ your machine, three changelog lines written but not closed into a version, four files never
352
+ committed. That is the distinction the whole page is built on: what left here, and what only
353
+ looks finished.
354
+
355
+ Four windows across the top: **Today**, **Yesterday**, **Week**, **Month**. It opens on none of
356
+ them: it opens on **the last day you actually worked**. On a Monday that is Friday, because a
357
+ page that comes up empty after every weekend is a page you stop opening.
358
+
359
+ **Only what you touched in that window appears.** A repository that has been dirty since March
360
+ and that you have not opened since is not news, and a summary that repeats it every morning is
361
+ one nobody is still reading by Thursday. Something has to have happened there — a commit, a file
362
+ saved, a card moved — before k0 will tell you what is still outstanding in it. And only **your**
363
+ commits are counted: k0 asks git which email you commit with in that repository and shows that
364
+ person's day, so a repository shared with other people still tells you about yours.
365
+
366
+ Below every block the facts are folded away, one line to open them: the commits themselves, each
367
+ marked green if it is online and amber if it is still only here. That is what to open when you do
368
+ not believe the paragraph above it.
369
+
370
+ ### Who writes it
371
+
372
+ k0 does not. **k0 makes no network requests**, and that is worth more than a better paragraph: a
373
+ model of its own would mean a key to keep, an account to configure, and your commit messages
374
+ leaving this machine.
375
+
376
+ It does not need one. If you are running k0 you already have Claude Code installed, signed in and
377
+ paid for — that is the whole point of the board — so the model is already here. k0 works out the
378
+ facts, hands them to Claude Code, and shows what comes back. No terminal opens, nothing appears
379
+ on the board, and there is nothing to set up: the skill that does the writing ships inside the
380
+ package, the same way `/k0-import` does.
381
+
382
+ It is written **in your language**, without a setting anywhere, because it is written out of your
383
+ own commit messages and card titles.
384
+
385
+ Nothing is saved. Every time you open the page it is written again, and changing the window
386
+ writes it again — which is why there is no stale summary to clear and no history to prune.
387
+ "Today" changes while you are reading it, so keeping a copy would be wrong more often than it
388
+ helped. If Claude Code is not on the machine the page still opens: you get the facts, and a line
389
+ saying why the rest is missing.
390
+
251
391
  ---
252
392
 
253
393
  ## The files of a session
@@ -366,10 +506,55 @@ which on an old session says nothing about this minute. And summing the memory o
366
506
  counts shared memory twice, so the total is a little generous: it is the usual approximation, and
367
507
  it is the right one for the question that matters — which one do I close to feel better.
368
508
 
509
+ And once you have decided, **`Close` on that card is how you give the memory back**: the session
510
+ is stopped and its window goes, the work stays on the board as it was, and `Resume` picks the
511
+ conversation up where it left off. Nothing has to be declared finished to stop weighing.
512
+
369
513
  Reading the process table costs little on a calm machine and a lot on a struggling one, which is
370
514
  exactly when you look at it. So it is sampled every three seconds, and **only while the board is
371
515
  open**.
372
516
 
517
+ ### And the ones you forget, k0 closes for you
518
+
519
+ The paragraph above assumes you noticed. Most of the time nobody does: the window that is costing
520
+ you a gigabyte is the one you stopped thinking about yesterday. So **after twelve hours with
521
+ nothing happening, k0 does that `Close` itself** — the session is stopped, its window goes, and the
522
+ memory comes back.
523
+
524
+ Nothing is lost by it. The card stays exactly where it is with the status it had, and **Resume**
525
+ picks the conversation up where it was. That is the whole reason this is safe: a closed terminal
526
+ has never been a lost session.
527
+
528
+ **Only yellow is ever touched** — *Your turn*, the ball in your court and nothing happening. The
529
+ rest is left alone, and each for its own reason:
530
+
531
+ - **Working** and **Planning** are somebody mid-thought. Stopping one is not memory saved, it is
532
+ work lost — the same rule the `Close` link has always followed.
533
+ - **Needs answer** and **Needs approval** would lose the very thing they were showing you. A
534
+ question and a finished plan are drawn by the terminal and are not written to the transcript
535
+ until they are answered, so closing that window throws the question away.
536
+ - A session sitting in a **shell** is left alone as well. That is a shell you dropped into, and
537
+ there may be a command of yours running in it; k0 cannot see what it is, so k0 does not touch it.
538
+ On the board it reads as yellow like any other, which is exactly why this one has to be said.
539
+
540
+ And a window you were using an hour ago is never closed, whatever the card's history says. k0 has
541
+ four clocks for a session — two of its own and two Claude Code keeps — and **the newest one wins**.
542
+ Being wrong in that direction leaves a window open until tomorrow; being wrong the other way closes
543
+ a terminal you were about to go back to.
544
+
545
+ A card k0 tidied away says so: where one you closed yourself reads *session closed*, this one reads
546
+ *closed automatically*. Same italic, same place, one word different.
547
+
548
+ And the memory chip at the end of the top bar says it is watching, and from when: **RAM 79% · CPU
549
+ 19% · closes at 8h**, in lighter type than the numbers beside it. That is the whole announcement,
550
+ and it is on that chip rather than anywhere else because that is where the memory is already being
551
+ talked about — something that closes your windows for you should never be a surprise. Hover for
552
+ the sentence. Switched off, the chip says nothing at all rather than *0h*, which would read as
553
+ "closes immediately", and the hover is what tells you it is off.
554
+
555
+ Twelve hours is what it does if you never say otherwise. `closeIdleTerminalsAfterHours` in the
556
+ settings file changes it, and **`0` switches it off** — see below.
557
+
373
558
  ---
374
559
 
375
560
  ## Filling the board with what you have already done
@@ -580,6 +765,38 @@ Two side effects of the Cmd+V k0 uses to write your prompt, worth knowing:
580
765
 
581
766
  ---
582
767
 
768
+ ## Settings
769
+
770
+ There is no settings page on the board, and there is not going to be one. The board is for what
771
+ changes during a working day; a number you set once and then never look at again is the opposite of
772
+ that, and putting it on screen would cost the one page that has to stay readable in exchange for
773
+ nothing.
774
+
775
+ So the settings are a file, and **the file is the list**:
776
+
777
+ ```
778
+ ~/.k0/config.json macOS and Linux
779
+ %LOCALAPPDATA%\k0\config.json Windows
780
+ ```
781
+
782
+ k0 writes it on the first run with **every setting already in it at its default**, so opening it is
783
+ how you find out what there is to change. It sits beside the board rather than inside the app —
784
+ `npx` unpacks into a cache npm is free to wipe, and `k0-board install` overwrites the app
785
+ directory, so a setting kept in there would not survive an update. Change it and k0 picks it up by
786
+ itself on the next round: nothing to restart.
787
+
788
+ | | |
789
+ |---|---|
790
+ | `closeIdleTerminalsAfterHours` | How long a yellow card's terminal may sit there before k0 closes it and gives the memory back. `12` if you say nothing. **`0` switches it off.** Anything under an hour is treated as an hour — below that this stops being a tidy-up and starts closing windows while you are using them. |
791
+
792
+ Delete a line to go back to its default; a file k0 cannot read is ignored altogether and the
793
+ defaults hold, rather than k0 refusing to start over a stray comma.
794
+
795
+ **`k0-board doctor` prints the same list**, with the value actually in force and where the file is,
796
+ for the times you would rather ask than go and look.
797
+
798
+ ---
799
+
583
800
  ## How it is put together
584
801
 
585
802
  No dependencies, nothing to compile. Change a file, reload the page.
@@ -595,33 +812,61 @@ platform/
595
812
  (menubar/, which is also what posts the notifications and reads the permission)
596
813
  linux/ tmux and any emulator, systemd-inhibit, systemd user units, a GTK tray
597
814
  win32/ PowerShell and Windows Terminal, execution state, Task Scheduler, a WinForms tray
598
- shared/ what more than one of them needs: tmux, running commands, reading a process table
815
+ shared/ what more than one of them needs: tmux, running commands, reading a process
816
+ table, and reading who is holding which port
599
817
  server/
600
818
  index.js the http server, the API, and the watching loop that runs every second
601
- db.js SQLite (node:sqlite): the card, session_event and pref tables
602
- paths.js where the board, the logs and the cache live
819
+ guard.js who is allowed to talk to this server at all: the Host and the Origin
820
+ db.js SQLite (node:sqlite): the card, session_event, pref and dev_server tables —
821
+ docs/database.md
822
+ paths.js where the board, the logs, the settings and the cache live
823
+ settings.js the few things you can change, and the file that is the list of them —
824
+ ~/.k0/config.json, written out with everything in it and re-read when it changes
825
+ idle.js which terminals have sat still long enough to be closed and given back. Only the
826
+ deciding: it kills nothing, so every rule in it can be proved without a machine
603
827
  watcher.js reads Claude Code's own files and derives the statuses; also renames a session
604
- git.js the only one that talks to git: what is committed, what is pushed, whose it is
828
+ git.js the only one that talks to git: what is committed, what is pushed, whose it is,
829
+ and — for the ChangeLog — what the commits actually said
830
+ changelog.js gathers what happened in a window, out of git and out of session_event. Decides
831
+ which repositories are worth mentioning at all, and stores nothing
832
+ writer.js hands those facts to the Claude Code already on this machine and gets the words
833
+ back. The only place k0 starts a model, and it never leaves the machine to do it
605
834
  launcher.js starts and resumes sessions, through the platform's terminal
835
+ servers.js the dev server of a repository: what starts it, whether it is up, and on what
836
+ port. The only thing k0 starts that is meant to outlive k0
606
837
  mode.js the four modes: how awake to keep the machine, and whether the text goes large
607
838
  projects.js the repositories, in the order you last used them
608
839
  sessions.js digs already-lived sessions out of the transcripts, to import as cards
609
840
  files.js the only one that reads the projects' disk: what is there, what changed, what it says
610
841
  machine.js the only one that looks at processes and memory: what it all costs, and who is costing it
611
842
  pdf.js the document on paper, printed by a headless browser
612
- web/ the two pages (html, css, js served exactly as they are)
843
+ web/ the three pages (html, css, js served exactly as they are)
613
844
  index.html the board — board.js, board.css, view.js. A note is never taller than it is
614
845
  wide: the title, the age and the buttons always show, the text in the middle is
615
846
  what gets clipped
616
847
  files.html the file viewer — files.js, files.css
617
- base.css colours, fonts and scale: the house variables, shared by both pages
848
+ changelog.html what you have been doing — changelog.js, changelog.css. The only page in k0 you
849
+ read top to bottom instead of looking at, so it is the only one that scrolls
850
+ base.css colours, fonts and scale: the house variables, shared by all three pages
618
851
  md.js markdown laid out, written by hand because nothing here is compiled
852
+ recency.js which repositories are still warm, and which fold away into `Old`
619
853
  fuzzy.js searching the names: the letters you type, in the order you type them
620
854
  mentions.js which of a repository's files a piece of text names
621
855
  refs.js what a name written inside a document points at, and which of the nine READMEs
622
856
  .claude/skills/
623
857
  k0-import/ the skill that fills the board with sessions you have already had
624
- test/ npm test — plain scripts, no framework. See CONTRIBUTING.md
858
+ k0-changelog/ the one that turns the facts of a stretch of work into something readable. k0
859
+ calls it by itself when the ChangeLog is opened, so it is in the package too
860
+ changelog/ the one that writes the changelog and the documentation when work is finished.
861
+ For whoever works on k0, not for whoever uses it: it is not in the package
862
+ commit-push-deploy/
863
+ the road out: the documents, the tests, the commit, the push, and the tag that
864
+ publishes. Also for whoever works on k0, and also not in the package
865
+ docs/
866
+ database.md the shape of the database as it is now: the four tables, column by column
867
+ testing.md how the tests are written, run and measured, and what is left untested on purpose
868
+ test/ npm test — Node's own runner, no framework. See docs/testing.md
869
+ harness.mjs check(label, got, want), and the sections the labels are grouped under
625
870
  ```
626
871
 
627
872
  ### What it reads, and what it never does
@@ -637,6 +882,13 @@ writes for itself:
637
882
  Sessions are launched with `--session-id` (k0 chooses the id, so the card ↔ session link is
638
883
  certain), `-n` for the name, `--resume` to pick one up, and `--permission-mode plan` on new ones.
639
884
 
885
+ The ChangeLog runs Claude Code a second way: `claude -p /k0-changelog`, with the facts on
886
+ standard input and no terminal at all. It is the one time k0 starts a model, and it still opens
887
+ no socket of its own — the request is Claude Code's, made with your own account, from your own
888
+ machine. Those runs cannot come back as cards: `sessions.js` drops `claude -p` when it looks for
889
+ sessions to import, which is also how it tells a hundred and forty real sessions from eight
890
+ hundred automated ones.
891
+
640
892
  ### The traps
641
893
 
642
894
  Field notes the code takes for granted.
@@ -665,6 +917,23 @@ Field notes the code takes for granted.
665
917
  - **The session file appears before the interface is ready to receive.** Writing at that moment
666
918
  loses the first characters and swallows the Enter. So k0 waits until it can see the input box —
667
919
  and where a platform cannot read a terminal's screen, it waits a fixed moment and says so.
920
+ - **A service's PATH is not a shell's PATH.** Under launchd it is barely more than
921
+ `/usr/bin:/bin`, and `npm` and `node` usually arrive from a version manager whose PATH only
922
+ exists inside a shell. So a dev server is started through a **login shell** — which is the same
923
+ reason `findClaude` ends up asking one, and it is also what puts the project's own `.nvmrc` in
924
+ force.
925
+ - **`npm run dev` is not the server, it is the thing that starts it.** SIGTERM to npm alone leaves
926
+ `vite` holding the port, which is why the old per-repo skills all ended up reaching for
927
+ `pkill -f`. k0 starts the server **detached**, so it leads a process group of its own, and
928
+ stops the *group*. But only when the process it was told about really is that group's leader:
929
+ a server somebody started by hand in a shell without job control can share its group with the
930
+ **shell**, and signalling that would close the user's terminal to stop a dev server. Where the
931
+ leader is somebody else, the processes are signalled one at a time instead.
932
+ - **A remembered pid is only a number, and the system hands numbers out again.** A row written
933
+ before a reboot can point at a stranger's process — and that row is what a click on the globe
934
+ would kill. So a remembered server is only believed while the process at that pid is still
935
+ running the command it was started with, and `stop` reads the machine before it signals
936
+ anything rather than trusting what it stored.
668
937
  - **`do script … in tab 1 of window id N`** writes into the interface with no simulated keystrokes
669
938
  and no permissions, but it **always adds the Enter**. You can hold the Enter back with a
670
939
  trailing backslash, but the backslash stays on screen and forces two presses: tried and rejected.
package/bin/k0-board.js CHANGED
@@ -48,6 +48,7 @@ if (Number(process.versions.node.split('.')[0]) < MIN_NODE) {
48
48
 
49
49
  const { name: platformName, capabilities, notes, service, extras, shell } = await import('../platform/index.js')
50
50
  const paths = await import('../server/paths.js')
51
+ const settings = await import('../server/settings.js')
51
52
 
52
53
  const APP_DIR = flag('from-source') ? SOURCE : paths.APP_DIR
53
54
  const ENTRY = path.join(APP_DIR, 'server', 'index.js')
@@ -126,6 +127,31 @@ async function waitForServer(seconds = 10) {
126
127
  return false
127
128
  }
128
129
 
130
+ // ── Opening the board ────────────────────────────────────────────────────────
131
+ /**
132
+ * Opens the board in a browser and answers with the address it used.
133
+ *
134
+ * `k0.localhost` is the name the board deserves, and inside a browser it is guaranteed to mean
135
+ * this machine — RFC 6761 reserves the whole of `.localhost` for loopback and every browser
136
+ * honours it. A resolver is under no such obligation, and on Windows usually declines. So the
137
+ * name is tried first, from here, and the number is what we fall back to: a board at a duller
138
+ * address beats a blank page at a prettier one.
139
+ *
140
+ * Not being able to open anything at all is not a failure. Installing over SSH, or on a machine
141
+ * with no session to put a window in, is an ordinary thing to do, and the install has already
142
+ * succeeded by the time we get here.
143
+ */
144
+ async function openBoard() {
145
+ let url = `http://k0.localhost:${PORT}`
146
+ try {
147
+ await fetch(`${url}/api/status`, { signal: AbortSignal.timeout(1500) })
148
+ } catch {
149
+ url = `http://127.0.0.1:${PORT}`
150
+ }
151
+ await shell.openBrowser(url).catch(() => {})
152
+ return url
153
+ }
154
+
129
155
  // ── install ──────────────────────────────────────────────────────────────────
130
156
  async function install() {
131
157
  say(`\n${bold('k0')} — the board that drives your Claude Code sessions`)
@@ -218,7 +244,8 @@ async function install() {
218
244
  appDir: APP_DIR,
219
245
  tray,
220
246
  })
221
- if (await waitForServer()) ok(`k0 is up: ${bold(`http://k0.localhost:${PORT}`)}`)
247
+ const up = await waitForServer()
248
+ if (up) ok(`k0 is up: ${bold(`http://k0.localhost:${PORT}`)}`)
222
249
  else warn(`the server is not answering yet — look in ${paths.LOG_DIR}`)
223
250
 
224
251
  // ── The two invitations ────────────────────────────────────────────────────
@@ -231,6 +258,12 @@ async function install() {
231
258
  note(`Where: ${extra.target}`)
232
259
  }
233
260
 
261
+ // Last of all, on purpose: by now the imported cards are on the board, so what opens is
262
+ // somebody's own work rather than an empty grid they would have to reload.
263
+ if (up && !flag('no-open')) {
264
+ say(`\n${bold('Done.')} The board is open at ${await openBoard()}`)
265
+ return
266
+ }
234
267
  say(`\n${bold('Done.')} The board is at http://k0.localhost:${PORT}`)
235
268
  if (process.platform === 'win32') note(`If that name does not resolve, use http://127.0.0.1:${PORT}`)
236
269
  }
@@ -334,16 +367,17 @@ async function uninstall() {
334
367
  // ── start ────────────────────────────────────────────────────────────────────
335
368
  /** Runs the server in the foreground and opens the board. No service, nothing installed. */
336
369
  async function start() {
370
+ const open = !flag('no-open')
337
371
  if (await alive()) {
338
372
  ok(`k0 is already running on http://k0.localhost:${PORT}`)
339
- await shell.openBrowser(`http://k0.localhost:${PORT}`).catch(() => {})
373
+ if (open) await openBoard()
340
374
  return
341
375
  }
342
376
  const child = spawn(process.execPath, [path.join(SOURCE, 'server', 'index.js')], {
343
377
  stdio: 'inherit',
344
378
  env: { ...process.env, K0_PORT: String(PORT) },
345
379
  })
346
- if (await waitForServer()) await shell.openBrowser(`http://k0.localhost:${PORT}`).catch(() => {})
380
+ if ((await waitForServer()) && open) await openBoard()
347
381
  child.on('exit', (code) => process.exit(code ?? 0))
348
382
  }
349
383
 
@@ -387,6 +421,19 @@ async function doctor() {
387
421
  ]
388
422
  for (const [what, yes] of rows) (yes ? ok : warn)(what)
389
423
 
424
+ // The settings, in full, with what is actually in force. k0 has no settings page and is not
425
+ // getting one, so this — and the file itself — is where the list lives. Printing every key
426
+ // rather than only the ones somebody has changed is the point: it is an inventory.
427
+ step('Settings')
428
+ const conf = settings.status()
429
+ say(dim(` ${conf.path}${conf.exists ? '' : ' (not written yet — the defaults are in force)'}`))
430
+ if (conf.broken) warn('this file could not be read: it is being ignored and the defaults are in force')
431
+ for (const [key, value] of Object.entries(conf.values)) {
432
+ const dflt = settings.DEFAULTS[key]
433
+ const off = key === 'closeIdleTerminalsAfterHours' && value === 0
434
+ say(` ${bold(key)} ${off ? 'off' : value}${value === dflt ? dim(' (default)') : dim(` (default ${dflt})`)}`)
435
+ }
436
+
390
437
  const explained = Object.entries(notes)
391
438
  if (explained.length) {
392
439
  step('Why not')
@@ -434,6 +481,7 @@ ${bold('k0-board')} — a board of sticky notes that drives your Claude Code ses
434
481
  Options
435
482
  ${bold('--yes')} do not ask, assume yes
436
483
  ${bold('--from-source')} point the service at this directory instead of copying it
484
+ ${bold('--no-open')} do not open the board in a browser at the end
437
485
  ${bold('--no-<name>')} skip one optional change, e.g. --no-lid-sleep
438
486
  `)
439
487
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "k0-board",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "A board of sticky notes that drives your Claude Code sessions",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -32,14 +32,17 @@
32
32
  "web",
33
33
  "menubar",
34
34
  "!menubar/k0.app",
35
- ".claude/skills",
35
+ ".claude/skills/k0-import",
36
+ ".claude/skills/k0-changelog",
36
37
  "README.md",
37
38
  "CHANGELOG.md",
38
39
  "LICENSE"
39
40
  ],
40
41
  "scripts": {
41
42
  "start": "node server/index.js",
42
- "test": "node test/clean.test.mjs && node test/states.test.mjs && node test/files.test.mjs && node test/mentions.test.mjs && node test/refs.test.mjs && node test/machine.test.mjs && node test/mode.test.mjs",
43
+ "test": "node --test \"test/*.test.mjs\"",
44
+ "test:watch": "node --test --watch \"test/*.test.mjs\"",
45
+ "coverage": "node --test --experimental-test-coverage --test-coverage-exclude=\"platform/**\" --test-coverage-exclude=\"test/**\" \"test/*.test.mjs\"",
43
46
  "doctor": "node bin/k0-board.js doctor"
44
47
  }
45
48
  }
@@ -33,6 +33,10 @@
33
33
  * @property {object} metrics
34
34
  * @property {boolean} metrics.pressure the kernel publishes a memory-pressure verdict
35
35
  * @property {boolean} metrics.swap swap usage is readable
36
+ * @property {object} servers
37
+ * @property {boolean} servers.run can start and stop a project's dev server
38
+ * @property {boolean} servers.ports can tell which port a process is listening on
39
+ * @property {boolean} servers.adopt can recognise a server started outside k0
36
40
  * @property {object} service
37
41
  * @property {boolean} service.autostart can register a service that starts at login
38
42
  * @property {boolean} tray a menu bar / system tray icon is available
@@ -47,6 +51,7 @@ export const NO_CAPABILITIES = {
47
51
  terminal: { windows: false, font: false, readScreen: false, pasteWithoutSending: false, title: false },
48
52
  power: { keepAwake: false, keepDisplayAwake: false, lidSleep: false, battery: false },
49
53
  metrics: { pressure: false, swap: false },
54
+ servers: { run: false, ports: false, adopt: false },
50
55
  service: { autostart: false },
51
56
  tray: false,
52
57
  revealInFileManager: false,
@@ -101,6 +106,30 @@ export const NO_CAPABILITIES = {
101
106
  * @property {() => Promise<number>} pressureLevel 1 normal, 2 warning, 4 critical
102
107
  */
103
108
 
109
+ /**
110
+ * The dev server a project runs while you are working on it — the one the globe in the column
111
+ * heading switches on and off. k0 starts it detached, so it outlives both the session that
112
+ * asked for it and k0 itself; what this adapter adds is the two things only the operating
113
+ * system knows, and the one gesture Node cannot make portably.
114
+ *
115
+ * @typedef {object} ServersAdapter
116
+ * @property {() => {run: boolean, ports: boolean, adopt: boolean}} capabilities
117
+ * @property {() => Promise<Map<number, Set<number>>>} listeners
118
+ * Every process holding a TCP port open, and which ports. This is how k0 knows a server is
119
+ * up: not by asking it, which would be a network request, but by seeing it hold the socket.
120
+ * @property {(pids: number[]) => Promise<Map<number, string>>} cwds
121
+ * Where each of those processes is working from, which is what ties one to a repository.
122
+ * An adapter that cannot say returns an empty map and reports `adopt: false`.
123
+ * @property {(command: string) => {file: string, args: string[]}} shell
124
+ * How to run `command` inside a project. It goes through a LOGIN shell on Unix, and that is
125
+ * not a flourish: the server runs from a launch agent whose PATH is barely more than
126
+ * /usr/bin:/bin, while npm and node usually arrive from a version manager whose PATH only
127
+ * exists inside a shell. It is the same reason `findClaude` ends up in a shell.
128
+ * @property {(root: number, pids: number[], signal?: string) => Promise<boolean>} stop
129
+ * Stops the server and everything it started. `pids` is the whole subtree, `root` included:
130
+ * killing the top process alone leaves the real server holding the port.
131
+ */
132
+
104
133
  /**
105
134
  * @typedef {object} ShellAdapter
106
135
  * @property {(absPath: string) => Promise<unknown>} revealInFileManager
@@ -129,6 +158,7 @@ export const NO_CAPABILITIES = {
129
158
  * @property {TerminalAdapter} terminal
130
159
  * @property {PowerAdapter} power
131
160
  * @property {MetricsAdapter} metrics
161
+ * @property {ServersAdapter} servers
132
162
  * @property {ShellAdapter} shell
133
163
  * @property {ServiceAdapter} service
134
164
  */