k0-board 0.4.0 → 0.5.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/.claude/skills/k0-changelog/SKILL.md +2 -2
- package/.claude/skills/k0-discuss/SKILL.md +105 -0
- package/.claude/skills/k0-discuss/references/rounds.md +280 -0
- package/.claude/skills/k0-epic/SKILL.md +196 -0
- package/.claude/skills/k0-links/.claude-plugin/plugin.json +6 -0
- package/.claude/skills/k0-links/hooks/hooks.json +1 -0
- package/.claude/skills/k0-links/hooks/links.js +128 -0
- package/.claude/skills/k0-links/hooks/register.js +204 -0
- package/.claude/skills/k0-next/SKILL.md +82 -0
- package/.claude/skills/k0-order/SKILL.md +123 -0
- package/.claude/skills/k0-plan/SKILL.md +156 -0
- package/.claude/skills/k0-split/SKILL.md +169 -0
- package/.claude/skills/k0-story/SKILL.md +108 -0
- package/.claude/skills/k0-ultracode/SKILL.md +251 -0
- package/.claude/skills/k0-ultracode/references/agent-brief.md +71 -0
- package/.claude/skills/k0-ultracode/references/modes.md +49 -0
- package/.claude/skills/k0-verify/SKILL.md +219 -0
- package/.claude/skills/k0-whatsnew/SKILL.md +95 -0
- package/.claude/skills/k0-work/SKILL.md +212 -0
- package/CHANGELOG.md +361 -1
- package/README.md +931 -111
- package/bin/k0-board.js +79 -11
- package/menubar/K0MenuBar.swift +35 -5
- package/package.json +13 -1
- package/platform/contract.js +30 -5
- package/platform/darwin/terminal.js +230 -56
- package/platform/index.js +2 -1
- package/platform/linux/index.js +3 -0
- package/platform/linux/tray.py +10 -10
- package/platform/shared/command.js +19 -3
- package/platform/shared/tmux.js +32 -14
- package/platform/win32/index.js +5 -2
- package/platform/win32/terminal.js +24 -8
- package/platform/win32/tray.ps1 +13 -13
- package/server/backlog.js +618 -0
- package/server/changelog.js +13 -13
- package/server/db.js +1800 -146
- package/server/files.js +60 -21
- package/server/git.js +53 -14
- package/server/idle.js +15 -13
- package/server/index.js +1251 -186
- package/server/launcher.js +122 -14
- package/server/machine.js +10 -1
- package/server/mirror.js +1048 -0
- package/server/prices.js +59 -0
- package/server/projects.js +57 -12
- package/server/servers.js +22 -1
- package/server/sessions.js +5 -5
- package/server/settings.js +25 -2
- package/server/update.js +324 -0
- package/server/usage.js +642 -0
- package/server/watcher.js +67 -13
- package/server/worktree.js +600 -0
- package/server/writer.js +98 -26
- package/web/awake.js +62 -0
- package/web/base.css +4 -0
- package/web/board.css +396 -16
- package/web/board.js +1182 -152
- package/web/changelog.js +3 -3
- package/web/files.js +73 -16
- package/web/index.html +109 -12
- package/web/list.css +536 -0
- package/web/list.js +1015 -0
- package/web/mentions.js +27 -9
- package/web/recency.js +10 -10
- package/web/units.js +1 -1
- package/web/usage.css +169 -0
- package/web/usage.html +78 -0
- package/web/usage.js +447 -0
- package/web/view.js +22 -3
- package/web/whatsnew.css +211 -0
- package/web/whatsnew.html +49 -0
- package/web/whatsnew.js +339 -0
package/bin/k0-board.js
CHANGED
|
@@ -75,6 +75,22 @@ function copyTree(from, to) {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/** The same files, byte for byte, and no others — on either side. */
|
|
79
|
+
function sameTree(a, b) {
|
|
80
|
+
const files = (dir) =>
|
|
81
|
+
fs
|
|
82
|
+
.readdirSync(dir, { withFileTypes: true, recursive: true })
|
|
83
|
+
.filter((entry) => entry.isFile())
|
|
84
|
+
.map((entry) => path.relative(dir, path.join(entry.parentPath, entry.name)))
|
|
85
|
+
.sort()
|
|
86
|
+
const left = files(a)
|
|
87
|
+
const right = files(b)
|
|
88
|
+
return (
|
|
89
|
+
left.length === right.length &&
|
|
90
|
+
left.every((f, i) => f === right[i] && fs.readFileSync(path.join(a, f)).equals(fs.readFileSync(path.join(b, f))))
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
78
94
|
// ── The tray icon ────────────────────────────────────────────────────────────
|
|
79
95
|
/**
|
|
80
96
|
* The three trays are three different things — a compiled Swift app, a Python script, a
|
|
@@ -258,7 +274,7 @@ async function install() {
|
|
|
258
274
|
note(`Where: ${extra.target}`)
|
|
259
275
|
}
|
|
260
276
|
|
|
261
|
-
// Last of all, on purpose: by now the imported
|
|
277
|
+
// Last of all, on purpose: by now the imported stories are on the board, so what opens is
|
|
262
278
|
// somebody's own work rather than an empty grid they would have to reload.
|
|
263
279
|
if (up && !flag('no-open')) {
|
|
264
280
|
say(`\n${bold('Done.')} The board is open at ${await openBoard()}`)
|
|
@@ -268,23 +284,74 @@ async function install() {
|
|
|
268
284
|
if (process.platform === 'win32') note(`If that name does not resolve, use http://127.0.0.1:${PORT}`)
|
|
269
285
|
}
|
|
270
286
|
|
|
287
|
+
// The commands you type at Claude Code, and every one of them ships in the package. Claude Code
|
|
288
|
+
// only ever looks in two places for a skill — the repository you have open, and your own
|
|
289
|
+
// `~/.claude/skills/` — and neither of them is where npm puts k0, so a command that is not copied
|
|
290
|
+
// out is a command that does not exist however carefully it was written. `k0-whatsnew` and
|
|
291
|
+
// `k0-changelog` are deliberately not here: k0 runs those two itself, from its own directory, and
|
|
292
|
+
// nobody types them.
|
|
293
|
+
const SKILLS = [
|
|
294
|
+
['k0-import', 'fills the board from sessions you already have'],
|
|
295
|
+
['k0-epic', 'an epic, discussed in rounds, then a tree of stories'],
|
|
296
|
+
['k0-story', 'one story, straight onto the board'],
|
|
297
|
+
['k0-discuss', 'the same rounds against a story that exists'],
|
|
298
|
+
['k0-split', 'a story that will not close, cut into tasks'],
|
|
299
|
+
['k0-plan', 'plan mode, with the decisions written in as constraints'],
|
|
300
|
+
['k0-work', 'the worktree, the work, the log, the merge back'],
|
|
301
|
+
['k0-ultracode', 'a whole epic, run by a manager with agents under it'],
|
|
302
|
+
['k0-verify', 'the counter-check, one decision at a time'],
|
|
303
|
+
['k0-next', 'what to pick up now, and why'],
|
|
304
|
+
['k0-order', 'priority and dependencies, dictated'],
|
|
305
|
+
// Not a command but a mod: nobody types it, Claude Code loads it from the same directory and it
|
|
306
|
+
// turns the files and artifacts named in a reply into links while k0 is running.
|
|
307
|
+
['k0-links', 'what the last reply pointed at, one click away above the prompt', 'mod'],
|
|
308
|
+
]
|
|
309
|
+
|
|
310
|
+
/** How the list shows an entry: a command by what you type, the mod by its name. */
|
|
311
|
+
const shown = ([name, , kind]) => (kind === 'mod' ? name : `/${name}`)
|
|
312
|
+
|
|
271
313
|
/**
|
|
272
|
-
* The
|
|
273
|
-
*
|
|
274
|
-
*
|
|
314
|
+
* The skills live in the repository, which is enough for anybody who cloned it. For anybody who
|
|
315
|
+
* installed with one command there is no repository to open Claude Code in, so they are offered
|
|
316
|
+
* as a copy into their own skills directory — where they work from any project.
|
|
317
|
+
*
|
|
318
|
+
* One question for all of them, and it is asked only about the ones that are not already there:
|
|
319
|
+
* somebody upgrading has answered it once, and being asked ten times is how a yes becomes a no.
|
|
320
|
+
*
|
|
321
|
+
* The ones that are already there are brought up to date without asking, for the same reason. A
|
|
322
|
+
* copy is taken once and Claude Code reads it from then on, so a command fixed in a new version
|
|
323
|
+
* went on running the old text for good — the yes was to having the commands, not to having the
|
|
324
|
+
* first version of them.
|
|
275
325
|
*/
|
|
276
326
|
async function offerSkill() {
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
|
|
327
|
+
const home = path.join(os.homedir(), '.claude', 'skills')
|
|
328
|
+
const source = (name) => path.join(APP_DIR, '.claude', 'skills', name)
|
|
329
|
+
const shipped = SKILLS.filter(([name]) => fs.existsSync(source(name)))
|
|
330
|
+
|
|
331
|
+
const stale = shipped.filter(([name]) => fs.existsSync(path.join(home, name)) && !sameTree(source(name), path.join(home, name)))
|
|
332
|
+
for (const [name] of stale) {
|
|
333
|
+
fs.rmSync(path.join(home, name), { recursive: true, force: true })
|
|
334
|
+
copyTree(source(name), path.join(home, name))
|
|
335
|
+
}
|
|
336
|
+
if (stale.length) ok(`brought up to date in ${home}: ${stale.map(shown).join(', ')}`)
|
|
337
|
+
|
|
338
|
+
const missing = shipped.filter(([name]) => !fs.existsSync(path.join(home, name)))
|
|
339
|
+
if (!missing.length) return
|
|
280
340
|
say('')
|
|
341
|
+
for (const entry of missing) note(`${bold(shown(entry))} ${dim(`— ${entry[1]}`)}`)
|
|
342
|
+
const commands = missing.filter(([, , kind]) => kind !== 'mod').length
|
|
343
|
+
const mods = missing.length - commands
|
|
344
|
+
const what = [
|
|
345
|
+
commands && `${bold(String(commands))} ${commands === 1 ? 'command' : 'commands'}`,
|
|
346
|
+
mods && `${mods === 1 ? 'the mod' : `${bold(String(mods))} mods`}`,
|
|
347
|
+
].filter(Boolean).join(' and ')
|
|
281
348
|
const yes = await confirm(
|
|
282
|
-
`Install
|
|
349
|
+
`Install ${what} for Claude Code?`,
|
|
283
350
|
{ fallback: true }
|
|
284
351
|
)
|
|
285
352
|
if (!yes) return
|
|
286
|
-
copyTree(
|
|
287
|
-
ok(`copied into ${
|
|
353
|
+
for (const [name] of missing) copyTree(path.join(APP_DIR, '.claude', 'skills', name), path.join(home, name))
|
|
354
|
+
ok(`copied into ${home}`)
|
|
288
355
|
}
|
|
289
356
|
|
|
290
357
|
/**
|
|
@@ -327,7 +394,7 @@ async function offerImport() {
|
|
|
327
394
|
const res = await api('/api/sessions/import', { items: items.slice(i, i + 30) })
|
|
328
395
|
created += res.created
|
|
329
396
|
}
|
|
330
|
-
ok(`${created}
|
|
397
|
+
ok(`${created} stories on the board`)
|
|
331
398
|
note('Run /k0-import inside Claude Code for titles written by reading the conversation.')
|
|
332
399
|
}
|
|
333
400
|
|
|
@@ -410,6 +477,7 @@ async function doctor() {
|
|
|
410
477
|
['place and raise windows', capabilities.terminal.windows],
|
|
411
478
|
['read a terminal to know when Claude Code is ready', capabilities.terminal.readScreen],
|
|
412
479
|
['leave a prompt unsent', capabilities.terminal.pasteWithoutSending],
|
|
480
|
+
['rename a running session when its title changes', capabilities.terminal.commands],
|
|
413
481
|
['resize the terminal font for driving mode', capabilities.terminal.font],
|
|
414
482
|
['keep the machine awake', capabilities.power.keepAwake],
|
|
415
483
|
['keep the screen awake', capabilities.power.keepDisplayAwake],
|
package/menubar/K0MenuBar.swift
CHANGED
|
@@ -113,6 +113,11 @@ final class ImagePaste {
|
|
|
113
113
|
try? FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
|
|
114
114
|
clearOld()
|
|
115
115
|
Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true) { _ in self.tick() }
|
|
116
|
+
// And again every ten minutes, which is the whole fix. Swept only here, `clearOld` tidied
|
|
117
|
+
// away the images of the session before this one and then never ran again: this app starts
|
|
118
|
+
// at login and stays up for weeks, so the folder simply grew — to 907 MB across 414
|
|
119
|
+
// screenshots, none of which anybody could still paste.
|
|
120
|
+
Timer.scheduledTimer(withTimeInterval: 600, repeats: true) { _ in self.clearOld() }
|
|
116
121
|
NSLog("k0: image pasting active (no permission required)")
|
|
117
122
|
}
|
|
118
123
|
|
|
@@ -154,15 +159,40 @@ final class ImagePaste {
|
|
|
154
159
|
return rep.representation(using: .png, properties: [:])
|
|
155
160
|
}
|
|
156
161
|
|
|
157
|
-
///
|
|
162
|
+
/// How long an image is worth keeping, and how much of it there may be at once.
|
|
163
|
+
///
|
|
164
|
+
/// Age alone was never a bound. One screenshot off a large display is ten megabytes, so an
|
|
165
|
+
/// afternoon of them can outgrow the disk long before anything is a day old — which is exactly
|
|
166
|
+
/// what happened. Past the ceiling the oldest go first, so what is left is what you might still
|
|
167
|
+
/// be about to paste.
|
|
168
|
+
private static let keepFor: TimeInterval = 86400
|
|
169
|
+
private static let keepUnder = 200 * 1024 * 1024
|
|
170
|
+
|
|
171
|
+
/// Yesterday's images are of no use to anybody, and neither is a gigabyte of today's.
|
|
158
172
|
private func clearOld() {
|
|
159
|
-
let cutoff = Date().addingTimeInterval(-86400)
|
|
160
173
|
let f = FileManager.default
|
|
161
|
-
|
|
174
|
+
let keys: [URLResourceKey] = [.contentModificationDateKey, .fileSizeKey]
|
|
175
|
+
guard let files = try? f.contentsOfDirectory(at: folder, includingPropertiesForKeys: keys)
|
|
162
176
|
else { return }
|
|
177
|
+
|
|
178
|
+
let cutoff = Date().addingTimeInterval(-Self.keepFor)
|
|
179
|
+
var kept: [(url: URL, at: Date, size: Int)] = []
|
|
163
180
|
for u in files {
|
|
164
|
-
let
|
|
165
|
-
|
|
181
|
+
let values = try? u.resourceValues(forKeys: Set(keys))
|
|
182
|
+
let at = values?.contentModificationDate ?? Date.distantPast
|
|
183
|
+
if at < cutoff {
|
|
184
|
+
try? f.removeItem(at: u)
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
kept.append((u, at, values?.fileSize ?? 0))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
var total = kept.reduce(0) { $0 + $1.size }
|
|
191
|
+
guard total > Self.keepUnder else { return }
|
|
192
|
+
for one in kept.sorted(by: { $0.at < $1.at }) {
|
|
193
|
+
if total <= Self.keepUnder { break }
|
|
194
|
+
try? f.removeItem(at: one.url)
|
|
195
|
+
total -= one.size
|
|
166
196
|
}
|
|
167
197
|
}
|
|
168
198
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "k0-board",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A board of sticky notes that drives your Claude Code sessions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -34,6 +34,18 @@
|
|
|
34
34
|
"!menubar/k0.app",
|
|
35
35
|
".claude/skills/k0-import",
|
|
36
36
|
".claude/skills/k0-changelog",
|
|
37
|
+
".claude/skills/k0-epic",
|
|
38
|
+
".claude/skills/k0-story",
|
|
39
|
+
".claude/skills/k0-plan",
|
|
40
|
+
".claude/skills/k0-work",
|
|
41
|
+
".claude/skills/k0-ultracode",
|
|
42
|
+
".claude/skills/k0-discuss",
|
|
43
|
+
".claude/skills/k0-split",
|
|
44
|
+
".claude/skills/k0-order",
|
|
45
|
+
".claude/skills/k0-verify",
|
|
46
|
+
".claude/skills/k0-next",
|
|
47
|
+
".claude/skills/k0-whatsnew",
|
|
48
|
+
".claude/skills/k0-links",
|
|
37
49
|
"README.md",
|
|
38
50
|
"CHANGELOG.md",
|
|
39
51
|
"LICENSE"
|
package/platform/contract.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* @property {boolean} terminal.readScreen can read back what is on screen
|
|
26
26
|
* @property {boolean} terminal.pasteWithoutSending can leave a prompt sitting unsent
|
|
27
27
|
* @property {boolean} terminal.title can rename a window from outside
|
|
28
|
+
* @property {boolean} terminal.commands can run a slash command inside a live session
|
|
28
29
|
* @property {object} power
|
|
29
30
|
* @property {boolean} power.keepAwake can stop idle sleep
|
|
30
31
|
* @property {boolean} power.keepDisplayAwake can stop the display sleeping
|
|
@@ -49,7 +50,14 @@
|
|
|
49
50
|
* so a capability added here later defaults to "no" everywhere instead of crashing.
|
|
50
51
|
*/
|
|
51
52
|
export const NO_CAPABILITIES = {
|
|
52
|
-
terminal: {
|
|
53
|
+
terminal: {
|
|
54
|
+
windows: false,
|
|
55
|
+
font: false,
|
|
56
|
+
readScreen: false,
|
|
57
|
+
pasteWithoutSending: false,
|
|
58
|
+
title: false,
|
|
59
|
+
commands: false,
|
|
60
|
+
},
|
|
53
61
|
power: { keepAwake: false, keepDisplayAwake: false, lidSleep: false, battery: false },
|
|
54
62
|
metrics: { pressure: false, swap: false },
|
|
55
63
|
servers: { run: false, ports: false, adopt: false },
|
|
@@ -71,10 +79,17 @@ export const NO_CAPABILITIES = {
|
|
|
71
79
|
* Opens a terminal running `command` and returns an opaque handle, or null if the
|
|
72
80
|
* platform has no notion of a handle it can address later.
|
|
73
81
|
* @property {(handle: string, title: string) => Promise<boolean>} setTitle
|
|
74
|
-
* @property {(handles: string[]
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
82
|
+
* @property {(handles: string[], opts?: {fontSize?: number, coverage?: number}) =>
|
|
83
|
+
* Promise<{touched: number, error?: string}>} applyMode
|
|
84
|
+
* Puts the windows the way the current mode wants them: the text at `fontSize`, the window at
|
|
85
|
+
* `coverage` of the free screen. One gesture and not two — a window left holding large text at
|
|
86
|
+
* a size measured for small text is worse than one that changed neither. What a platform cannot
|
|
87
|
+
* do it leaves alone: where the font belongs to the emulator's own profile, only the size of
|
|
88
|
+
* the window moves, and `terminal.font` says so.
|
|
89
|
+
* @property {(handles: string[], opts?: {coverage?: number}) => Promise<{touched: number}>} relayout
|
|
90
|
+
* @property {(handle: string, title?: string) => Promise<{ok: boolean, error?: string, handle?: string}>} focus
|
|
91
|
+
* Brings a story's window up. `title` is the second way of finding it when the handle has gone
|
|
92
|
+
* stale; a `handle` in the answer is the one that worked and is worth writing down.
|
|
78
93
|
* @property {(opts: {handle: string, pid?: number}) => Promise<{closed: boolean}>} close
|
|
79
94
|
* @property {(handle: string) => Promise<string|null>} readScreen
|
|
80
95
|
* What is on screen right now, or null when this platform cannot look.
|
|
@@ -82,6 +97,16 @@ export const NO_CAPABILITIES = {
|
|
|
82
97
|
* Puts `text` in front of the user WITHOUT submitting it.
|
|
83
98
|
* @property {(text: string, handle: string) => Promise<{written: boolean, error?: string}>} type
|
|
84
99
|
* Writes `text` and submits it. The fallback when `paste` is unavailable.
|
|
100
|
+
* @property {(text: string, handle: string, opts: {ready: (screen: string|null) => boolean,
|
|
101
|
+
* verify: (screen: string|null) => boolean}) => Promise<{sent: boolean, why?: string}>} command
|
|
102
|
+
* Runs a slash command — `/rename Foo` — inside the session in that window. The keys go in
|
|
103
|
+
* one by one, which is the only way Claude Code takes a command from outside: written as one
|
|
104
|
+
* block, the way `type` does it, the same line reaches it as a message. Only when the window
|
|
105
|
+
* already has the keyboard, never by taking it: raising a window under somebody's hands puts
|
|
106
|
+
* what they were typing into it. `ready` looks at the screen before the first key and says
|
|
107
|
+
* whether the input box is free to write in; `verify` looks after the last one and says whether
|
|
108
|
+
* exactly that line is under the cursor — only then is Enter pressed, and a line that came out
|
|
109
|
+
* wrong is deleted instead.
|
|
85
110
|
* @property {() => Promise<number>} defaultFontSize
|
|
86
111
|
*/
|
|
87
112
|
|