claude-code-kanban 7.4.0 → 7.6.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 +5 -3
- package/cli.js +95 -29
- package/hub-app.json +5 -0
- package/lib/panes.js +41 -6
- package/lib/parsers.js +42 -3
- package/lib/retention.js +54 -6
- package/lib/scratchpad-dir.js +19 -2
- package/lib/session-cache.js +1 -1
- package/lib/session-events.js +174 -47
- package/lib/show.js +385 -0
- package/lib/task-dir.js +26 -0
- package/lib/terminal-client.js +14 -1
- package/lib/terminal-host.js +2 -1
- package/lib/terminal.js +28 -8
- package/package.json +1 -1
- package/plugin/plugins/claude-code-kanban/.claude-plugin/plugin.json +2 -5
- package/plugin/plugins/claude-code-kanban/hooks/doorbell.ts +136 -0
- package/plugin/plugins/claude-code-kanban/hooks/index.ts +4 -0
- package/plugin/plugins/claude-code-kanban/hooks/show.ts +120 -0
- package/plugin/plugins/claude-code-kanban/skills/dispatch/SKILL.md +2 -0
- package/plugin/plugins/claude-code-kanban/skills/kanban/SKILL.md +3 -3
- package/plugin/plugins/claude-code-kanban/skills/show/SKILL.md +45 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/NOTICE.md +27 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/chart.md +58 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/architecture.md +18 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/er.md +10 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/flow.md +13 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/layers.md +10 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/sequence.md +19 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/state.md +12 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/swimlane.md +10 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/timeline.md +9 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/tree.md +10 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/diagram.md +87 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/explain.md +98 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/mockup.md +30 -0
- package/plugin/plugins/claude-code-kanban/tests/doorbell.test.ts +133 -0
- package/plugin/plugins/claude-code-kanban/tests/show.test.ts +137 -0
- package/public/app.js +2151 -1108
- package/public/index.html +19 -36
- package/public/style.css +1047 -125
- package/public/terminal-frame.js +0 -2
- package/public/tree-highlight.js +137 -15
- package/server.js +237 -178
- package/lib/dispatch.js +0 -32
- package/plugin/plugins/claude-code-kanban/monitors.json +0 -8
- package/plugin/plugins/claude-code-kanban/scripts/postman.js +0 -82
- package/plugin/plugins/claude-code-kanban/skills/follow/SKILL.md +0 -47
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Explain card
|
|
2
|
+
|
|
3
|
+
A markdown card that shows how code works: what calls what, what a function decides, where things live, or what a change moves. Post `kind: "markdown"`; the board renders the fences below.
|
|
4
|
+
|
|
5
|
+
## Make one point
|
|
6
|
+
|
|
7
|
+
Each card makes one point about the code under discussion. Write the point as a sentence or two, then the view that proves it. Pick the smallest view that makes the point clear; add a second view to the card only when it shows what the first cannot.
|
|
8
|
+
|
|
9
|
+
| Showing | View | Fence |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| Runtime flow: who calls whom | Call stack | `callstack` |
|
|
12
|
+
| What a function decides | Pseudocode | `text` |
|
|
13
|
+
| UI structure, state and module boundaries | Component tree | `tsx` |
|
|
14
|
+
| Which file owns what | File tree | `text` |
|
|
15
|
+
| Messages between parts over time | Sequence | `mermaid` |
|
|
16
|
+
| What a change adds, removes or moves | A diff of the view above | see [Diff](#diff) |
|
|
17
|
+
|
|
18
|
+
Keep only the calls, files, props and states the point needs, with real names from the code. Give the card a `key` that names the topic; post a follow-up view, such as the diff after the fix, with the same key so it replaces the card.
|
|
19
|
+
|
|
20
|
+
## Call stack
|
|
21
|
+
|
|
22
|
+
The board draws the tree lines, links, focus and folds from this notation:
|
|
23
|
+
|
|
24
|
+
```callstack
|
|
25
|
+
handleCheckout src/routes/checkout.ts:24
|
|
26
|
+
validateCart throws on an empty cart
|
|
27
|
+
placeOrder * src/orders/place.ts:40
|
|
28
|
+
chargeCard src/payments/charge.ts:31
|
|
29
|
+
~ 2 SDK frames
|
|
30
|
+
PaymentsClient.charge node_modules/pay-sdk/client.js:112
|
|
31
|
+
HttpClient.request node_modules/pay-sdk/http.js:58
|
|
32
|
+
+ sendReceipt src/email/receipt.ts:9
|
|
33
|
+
- logOrder src/orders/log.ts:5
|
|
34
|
+
onPaymentWebhook src/routes/webhooks.ts:58
|
|
35
|
+
markOrderPaid src/orders/status.ts:22
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- The fence tag is `callstack`.
|
|
39
|
+
- Caller on top. Indent each callee 2 spaces under its caller; the board turns the spaces into tree lines. Several roots are fine.
|
|
40
|
+
- One frame per line: the function or method name as one identifier, spelled as in the code.
|
|
41
|
+
- The note goes after 2 or more spaces: a `path:line` relative to the project root, which the board links, or a few words. Line the notes up in one column.
|
|
42
|
+
- A `*` after the name, with one space, marks the frame to look at. One per stack.
|
|
43
|
+
- `+` or `-` in the first column marks a frame the change adds or removes. In a stack with any `+` or `-`, every other line starts with one space.
|
|
44
|
+
|
|
45
|
+
### Folds
|
|
46
|
+
|
|
47
|
+
A `~ N <what> frames` line stands for frames the reader can skip, such as framework, library or runtime code. It sits where those frames sit: under the frame that calls them, or at the top for framework callers above a handler.
|
|
48
|
+
|
|
49
|
+
- With frames indented under it, the line is a fold: the board shows it closed, and the reader clicks it to open.
|
|
50
|
+
- A fold that holds the `*` frame or a `+` or `-` frame starts open, so the point stays on screen.
|
|
51
|
+
- Folds nest: library frames that call runtime frames get a `~` each.
|
|
52
|
+
- With nothing under it, the line is a note that frames were left out. Write the frames out only when the reader may need them.
|
|
53
|
+
|
|
54
|
+
## Pseudocode
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
placeOrder(cart)
|
|
58
|
+
if any item is out of stock
|
|
59
|
+
return "sold out"
|
|
60
|
+
charge the card
|
|
61
|
+
if the charge fails
|
|
62
|
+
release the stock
|
|
63
|
+
send the receipt
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Component tree
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
<CheckoutPage> (src/routes/checkout.tsx)
|
|
70
|
+
useCart()
|
|
71
|
+
<OrderSummary>
|
|
72
|
+
<PayButton> (packages/ui)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## File tree
|
|
76
|
+
|
|
77
|
+
The one view drawn with box characters:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
src/
|
|
81
|
+
├── routes/ # HTTP handlers
|
|
82
|
+
├── orders/ # owns order state
|
|
83
|
+
└── payments/ # talks to the card provider
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Diff
|
|
87
|
+
|
|
88
|
+
Draw a change in the shape of the view it changes: a component tree diff for a component change, a file tree diff for a move. Mark each line with `+`, `-` or a space in the first column. A call stack takes the marks inside its `callstack` fence; every other view goes in a `diff` fence:
|
|
89
|
+
|
|
90
|
+
```diff
|
|
91
|
+
<CheckoutPage>
|
|
92
|
+
useCart()
|
|
93
|
+
<OrderSummary>
|
|
94
|
+
+ <PromoCodeInput />
|
|
95
|
+
<PayButton>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Show the whole block without marks when most of it is new, or when a diff would hide the order or the owner of each part.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Mockup card
|
|
2
|
+
|
|
3
|
+
A wireframe in an HTML card: a screen, a dialog, a form or a component, to agree on layout and wording before code.
|
|
4
|
+
|
|
5
|
+
## Content
|
|
6
|
+
|
|
7
|
+
- Real labels and real copy from the task: "Delete 3 sessions?", not "Lorem ipsum" or "Button".
|
|
8
|
+
- Plain boxes in place of logos, brand names and product chrome. A gray block (`--color-bg-subtle`) with a mono caption stands in for an image, chart or map.
|
|
9
|
+
- One screen or one state per card. For a flow, post one card per step under one `key` prefix, or show 2 or 3 states side by side when they fit.
|
|
10
|
+
- Mark open questions with numbered callouts: a small circle in `--color-accent` with a number, and the notes as a list under the sketch.
|
|
11
|
+
|
|
12
|
+
## Building blocks
|
|
13
|
+
|
|
14
|
+
The card already styles `button`, `button.primary`, `input`, `select`, `textarea`, `table` and headings from the tokens. Use them as they are.
|
|
15
|
+
|
|
16
|
+
```html
|
|
17
|
+
<style>
|
|
18
|
+
.m { border: 1px solid var(--color-border); border-radius: 8px; overflow: hidden; }
|
|
19
|
+
.m-bar { display: flex; gap: 8px; align-items: center; padding: 8px 12px; background: var(--color-bg-subtle); border-bottom: 1px solid var(--color-border); }
|
|
20
|
+
.m-body { padding: 12px; display: grid; gap: 8px; }
|
|
21
|
+
.m-img { background: var(--color-bg-subtle); border: 1px dashed var(--color-border); border-radius: 6px; min-height: 64px; display: grid; place-items: center; font: 11px var(--font-mono); color: var(--color-text-muted); }
|
|
22
|
+
.m-note { display: inline-grid; place-items: center; width: 18px; height: 18px; border-radius: 50%; background: var(--color-accent); color: #fff; font: 600 11px var(--font-sans); }
|
|
23
|
+
</style>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Fit the card
|
|
27
|
+
|
|
28
|
+
- A sketch narrower than the card sits centered (`margin-inline: auto`).
|
|
29
|
+
- A phone screen fits at real size, about 360 px. A desktop screen does not: sketch the part under discussion (one panel, the toolbar, the dialog), not the whole window.
|
|
30
|
+
- Scripts may toggle a state (open a menu, switch a tab) to show the interaction. Keep it to a few lines; the card has no storage and no network.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { On } from 'claude-code'
|
|
2
|
+
import { expect, mock, test } from 'claude-code/testing'
|
|
3
|
+
import { boardUrl, parseReply, RETRY_MS } from '../hooks/doorbell'
|
|
4
|
+
|
|
5
|
+
test('the board URL comes from CCK_URL, else from server.json', async () => {
|
|
6
|
+
expect(boardUrl('http://127.0.0.1:4000/', '{"port":3541}')).toBe('http://127.0.0.1:4000')
|
|
7
|
+
expect(boardUrl(undefined, '{"port":3541}')).toBe('http://127.0.0.1:3541')
|
|
8
|
+
expect(boardUrl(undefined, '{"port":"3541"}')).toBe(undefined)
|
|
9
|
+
expect(boardUrl(undefined, 'not json')).toBe(undefined)
|
|
10
|
+
expect(boardUrl(undefined, undefined)).toBe(undefined)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
type Reply = { status: number; events?: string[]; seqs?: number[]; board?: string }
|
|
14
|
+
|
|
15
|
+
function engine(on: On, replies: (() => Reply)[], submit?: () => Promise<unknown>) {
|
|
16
|
+
const urls: string[] = []
|
|
17
|
+
const prompts: string[] = []
|
|
18
|
+
let sessionId = 'sid-1'
|
|
19
|
+
on('fs.read', (_$, e) => {
|
|
20
|
+
if (e.path.replaceAll('\\', '/') === 'C:/cfg/.cck/server.json') return { value: '{"port":3541}' }
|
|
21
|
+
throw new Error(`ENOENT: ${e.path}`)
|
|
22
|
+
})
|
|
23
|
+
on('env.get', (_$, e) => ({ value: { CLAUDE_CONFIG_DIR: 'C:/cfg' }[e.name] }))
|
|
24
|
+
on('session.id', () => ({ value: sessionId }))
|
|
25
|
+
on('session.start', (_$, e) => ({ cwd: e.cwd }))
|
|
26
|
+
on('http.fetch', async (_$, e) => {
|
|
27
|
+
urls.push(e.url)
|
|
28
|
+
const reply = replies.shift()
|
|
29
|
+
if (!reply) return new Promise(() => {})
|
|
30
|
+
const { status, events = [], seqs, board } = reply()
|
|
31
|
+
return { value: { status, ok: status < 300, headers: {}, text: JSON.stringify({ events, seqs, board }) } }
|
|
32
|
+
})
|
|
33
|
+
on('prompt.submit', async (_$, e) => {
|
|
34
|
+
prompts.push(e.text)
|
|
35
|
+
if (submit) await submit()
|
|
36
|
+
return { text: e.text }
|
|
37
|
+
})
|
|
38
|
+
return { urls, prompts, clear: () => (sessionId = 'sid-2') }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test('a -p run does not poll the board', async ($, on) => {
|
|
42
|
+
const clock = mock.clock(on)
|
|
43
|
+
const board = engine(on, [])
|
|
44
|
+
await $.session.start({ cwd: 'C:/proj', surface: null, isInteractive: false })
|
|
45
|
+
await clock.settle()
|
|
46
|
+
expect(board.urls).toEqual([])
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('polls the board, submits a burst as one prompt, and drops the backlog for a new session id', async ($, on) => {
|
|
50
|
+
const clock = mock.clock(on)
|
|
51
|
+
const replies = [
|
|
52
|
+
() => ({ status: 200, events: ['line one', 'line two'] }),
|
|
53
|
+
() => ({ status: 503 }),
|
|
54
|
+
() => {
|
|
55
|
+
board.clear()
|
|
56
|
+
return { status: 200 }
|
|
57
|
+
},
|
|
58
|
+
() => ({ status: 200, events: ['after clear'] }),
|
|
59
|
+
]
|
|
60
|
+
const board = engine(on, replies)
|
|
61
|
+
await $.session.start({ cwd: 'C:/proj', surface: 'terminal', isInteractive: true })
|
|
62
|
+
await clock.settle()
|
|
63
|
+
|
|
64
|
+
const base = 'http://127.0.0.1:3541/api/sessions'
|
|
65
|
+
expect(board.prompts).toEqual(['line one\nline two'])
|
|
66
|
+
expect(board.urls).toEqual([
|
|
67
|
+
`${base}/sid-1/events?wait=25&board=&got=0&ack=0&first=1`,
|
|
68
|
+
`${base}/sid-1/events?wait=25&board=&got=0&ack=0`,
|
|
69
|
+
])
|
|
70
|
+
|
|
71
|
+
await clock.advance(RETRY_MS)
|
|
72
|
+
expect(board.urls.slice(2)).toEqual([
|
|
73
|
+
`${base}/sid-1/events?wait=25&board=&got=0&ack=0`,
|
|
74
|
+
`${base}/sid-2/events?wait=25&board=&got=0&ack=0&first=1&prev=sid-1`,
|
|
75
|
+
`${base}/sid-2/events?wait=25&board=&got=0&ack=0`,
|
|
76
|
+
])
|
|
77
|
+
expect(board.prompts).toEqual(['line one\nline two', 'after clear'])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('keeps polling while a submit waits, sends what came in as one prompt, and acks only after the submit', async ($, on) => {
|
|
81
|
+
const clock = mock.clock(on)
|
|
82
|
+
let release = () => {}
|
|
83
|
+
const replies = [
|
|
84
|
+
() => ({ status: 200, events: ['move'], seqs: [1], board: 'b1' }),
|
|
85
|
+
() => ({ status: 200, events: ['review one'], seqs: [2], board: 'b1' }),
|
|
86
|
+
() => ({ status: 200, events: ['review two', 'review one'], seqs: [3, 2], board: 'b1' }),
|
|
87
|
+
]
|
|
88
|
+
let held = true
|
|
89
|
+
const board = engine(on, replies, () => (held ? new Promise<void>(r => (release = () => ((held = false), r()))) : Promise.resolve()))
|
|
90
|
+
await $.session.start({ cwd: 'C:/proj', surface: 'terminal', isInteractive: true })
|
|
91
|
+
await clock.settle()
|
|
92
|
+
|
|
93
|
+
const base = 'http://127.0.0.1:3541/api/sessions/sid-1/events?wait=25'
|
|
94
|
+
expect(board.prompts).toEqual(['move'])
|
|
95
|
+
expect(board.urls).toEqual([
|
|
96
|
+
`${base}&board=&got=0&ack=0&first=1`,
|
|
97
|
+
`${base}&board=b1&got=1&ack=0`,
|
|
98
|
+
`${base}&board=b1&got=2&ack=0`,
|
|
99
|
+
`${base}&board=b1&got=3&ack=0`,
|
|
100
|
+
])
|
|
101
|
+
|
|
102
|
+
release()
|
|
103
|
+
await clock.settle()
|
|
104
|
+
expect(board.prompts).toEqual(['move', 'review one\nreview two'])
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('acks a submitted line on the next poll', async ($, on) => {
|
|
108
|
+
const clock = mock.clock(on)
|
|
109
|
+
const replies = [() => ({ status: 200, events: ['review'], seqs: [4], board: 'b1' }), () => ({ status: 200, board: 'b1' })]
|
|
110
|
+
const board = engine(on, replies)
|
|
111
|
+
await $.session.start({ cwd: 'C:/proj', surface: 'terminal', isInteractive: true })
|
|
112
|
+
await clock.settle()
|
|
113
|
+
expect(board.urls.at(-1)).toContain('board=b1&got=4&ack=4')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('starts the count again when the board restarts', async ($, on) => {
|
|
117
|
+
const clock = mock.clock(on)
|
|
118
|
+
const replies = [
|
|
119
|
+
() => ({ status: 200, events: ['old'], seqs: [9], board: 'b1' }),
|
|
120
|
+
() => ({ status: 200, board: 'b1' }),
|
|
121
|
+
() => ({ status: 200, events: ['new'], seqs: [1], board: 'b2' }),
|
|
122
|
+
]
|
|
123
|
+
const board = engine(on, replies)
|
|
124
|
+
await $.session.start({ cwd: 'C:/proj', surface: 'terminal', isInteractive: true })
|
|
125
|
+
await clock.settle()
|
|
126
|
+
expect(board.prompts).toEqual(['old', 'new'])
|
|
127
|
+
expect(board.urls.at(-1)).toContain('board=b2&got=1&ack=1')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('reads a reply from a board without acks', () => {
|
|
131
|
+
expect(parseReply('{"events":["a",3,"b"]}')).toEqual({ lines: [{ text: 'a', seq: undefined }, { text: 'b', seq: undefined }], board: '' })
|
|
132
|
+
expect(parseReply('{"events":["a"],"seqs":[5],"board":"b1"}')).toEqual({ lines: [{ text: 'a', seq: 5 }], board: 'b1' })
|
|
133
|
+
})
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { On, ToolCallArgs } from 'claude-code'
|
|
2
|
+
import { expect, test } from 'claude-code/testing'
|
|
3
|
+
import { boardPort, dropSkillLine, shownText, TOOL } from '../hooks/show'
|
|
4
|
+
|
|
5
|
+
const CCK_URL = 'http://127.0.0.1:4100'
|
|
6
|
+
const TERMINAL_ID = 'term-1'
|
|
7
|
+
const TOKEN_FILE = 'C:/cfg/.cck/terminal-tokens/4100.json'
|
|
8
|
+
const LISTING = [
|
|
9
|
+
'The following skills are available for use with the Skill tool:',
|
|
10
|
+
'',
|
|
11
|
+
'- claude-code-kanban:dispatch',
|
|
12
|
+
'- claude-code-kanban:show',
|
|
13
|
+
'- claude-code-kanban:kanban: Kanban board CLI',
|
|
14
|
+
'- meta:show-me: Help the user understand the current topic visually',
|
|
15
|
+
].join('\n')
|
|
16
|
+
|
|
17
|
+
type Reply = { status: number; body: unknown } | Error
|
|
18
|
+
type Request = { url: string; method?: string; headers?: Record<string, string>; body?: unknown }
|
|
19
|
+
|
|
20
|
+
function engine(on: On, env: Record<string, string>, reply?: Reply) {
|
|
21
|
+
const registered: string[] = []
|
|
22
|
+
const requests: Request[] = []
|
|
23
|
+
const files: Record<string, string> = { [TOKEN_FILE]: '{"token":"tok-abc"}' }
|
|
24
|
+
on('env.get', (_$, e) => ({ value: { CLAUDE_CONFIG_DIR: 'C:/cfg', ...env }[e.name] }))
|
|
25
|
+
on('fs.read', (_$, e) => {
|
|
26
|
+
const text = files[e.path.replaceAll('\\', '/')]
|
|
27
|
+
if (text === undefined) throw new Error(`ENOENT: ${e.path}`)
|
|
28
|
+
return { value: text }
|
|
29
|
+
})
|
|
30
|
+
on('session.id', () => ({ value: 'sid-1' }))
|
|
31
|
+
on('session.start', (_$, e) => ({ cwd: e.cwd }))
|
|
32
|
+
on('tool.register', (_$, e) => {
|
|
33
|
+
registered.push(e.name)
|
|
34
|
+
return { value: { tool: `mcp__claude-code-kanban__${e.name}` } }
|
|
35
|
+
})
|
|
36
|
+
on('prompt.attachment', (_$, e) => ({ text: e.text }))
|
|
37
|
+
on('http.fetch', (_$, e) => {
|
|
38
|
+
if (!e.url.includes('/show')) return new Promise(() => {})
|
|
39
|
+
const { method, headers, body } = (e.init ?? {}) as Omit<Request, 'url'>
|
|
40
|
+
requests.push({ url: e.url, method, headers, body: typeof body === 'string' ? JSON.parse(body) : body })
|
|
41
|
+
if (!reply) throw new Error('no reply')
|
|
42
|
+
if (reply instanceof Error) return { deny: reply.message }
|
|
43
|
+
return { value: { status: reply.status, ok: reply.status < 300, headers: {}, text: JSON.stringify(reply.body) } }
|
|
44
|
+
})
|
|
45
|
+
return { registered, requests, files }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const GATE = { CCK_URL, CCK_TERMINAL_ID: TERMINAL_ID }
|
|
49
|
+
const listing = { type: 'skill_listing', text: LISTING, origin: { kind: 'engine' } } as const
|
|
50
|
+
const call = (input: Record<string, unknown>) => ({ tool: TOOL, ...input }) as ToolCallArgs
|
|
51
|
+
|
|
52
|
+
test('helpers: the port of CCK_URL, the answer line, the listing filter', async () => {
|
|
53
|
+
expect(boardPort('http://127.0.0.1:4100')).toBe('4100')
|
|
54
|
+
expect(boardPort('http://localhost:3541/')).toBe('3541')
|
|
55
|
+
expect(boardPort('http://localhost')).toBe(undefined)
|
|
56
|
+
expect(shownText({ title: 'Plan', key: 'plan', index: 3, count: 7 })).toBe('Shown "Plan" (plan, 3/7)')
|
|
57
|
+
expect(shownText({ title: 'Map', key: null, index: 1, count: 1 })).toBe('Shown "Map" (1/1)')
|
|
58
|
+
expect(shownText({ title: 'Plan', key: 'plan', index: 1, count: 1, path: 'C:/s/p1.md' }, true)).toBe(
|
|
59
|
+
'Card "Plan" (plan, 1/1) is ready. Write it to C:/s/p1.md; the card refreshes on each save.',
|
|
60
|
+
)
|
|
61
|
+
expect(dropSkillLine(LISTING)).toBe(LISTING.replace('- claude-code-kanban:show\n', ''))
|
|
62
|
+
expect(dropSkillLine('- claude-code-kanban:show: Show a diagram\n- x')).toBe('- x')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('gate off: no tool, and the skill line is dropped', async ($, on) => {
|
|
66
|
+
const board = engine(on, { CCK_URL })
|
|
67
|
+
await $.session.start({ cwd: 'C:/proj', surface: 'terminal', isInteractive: true })
|
|
68
|
+
expect(board.registered).toEqual([])
|
|
69
|
+
const { text } = await $.prompt.attachment(listing)
|
|
70
|
+
expect(text).toBe(LISTING.replace('- claude-code-kanban:show\n', ''))
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('gate on: the tool is registered and the skill line stays', async ($, on) => {
|
|
74
|
+
const board = engine(on, GATE)
|
|
75
|
+
await $.session.start({ cwd: 'C:/proj', surface: 'terminal', isInteractive: true })
|
|
76
|
+
expect(board.registered).toEqual(['show'])
|
|
77
|
+
const { text } = await $.prompt.attachment(listing)
|
|
78
|
+
expect(text).toBe(LISTING)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('a -p run gets no tool', async ($, on) => {
|
|
82
|
+
const board = engine(on, GATE)
|
|
83
|
+
await $.session.start({ cwd: 'C:/proj', surface: null, isInteractive: false })
|
|
84
|
+
expect(board.registered).toEqual([])
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('posts the body with the terminal token and answers with the position', async ($, on) => {
|
|
88
|
+
const board = engine(on, GATE, {
|
|
89
|
+
status: 200,
|
|
90
|
+
body: { id: 'p1', title: 'Plan', key: 'plan', index: 2, count: 3, replaced: false },
|
|
91
|
+
})
|
|
92
|
+
await $.session.start({ cwd: 'C:/proj', surface: 'terminal', isInteractive: true })
|
|
93
|
+
const res = await $.tool.call(call({ title: 'Plan', key: 'plan', kind: 'markdown', file: 'C:/proj/plan.md' }))
|
|
94
|
+
expect(res.result).toBe('Shown "Plan" (plan, 2/3)')
|
|
95
|
+
expect(board.requests).toEqual([
|
|
96
|
+
{
|
|
97
|
+
url: `${CCK_URL}/api/terminals/${TERMINAL_ID}/show`,
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: { 'content-type': 'application/json', 'x-terminal-token': 'tok-abc' },
|
|
100
|
+
body: { sessionId: 'sid-1', title: 'Plan', key: 'plan', kind: 'markdown', file: 'C:/proj/plan.md' },
|
|
101
|
+
},
|
|
102
|
+
])
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('a claim sends no file and answers with the file to write', async ($, on) => {
|
|
106
|
+
const path = 'C:/s/.cck/show/p1.md'
|
|
107
|
+
const board = engine(on, GATE, { status: 200, body: { id: 'p1', title: 'Plan', key: 'plan', index: 1, count: 1, path } })
|
|
108
|
+
await $.session.start({ cwd: 'C:/proj', surface: 'terminal', isInteractive: true })
|
|
109
|
+
const res = await $.tool.call(call({ title: 'Plan', key: 'plan' }))
|
|
110
|
+
expect(res.result).toBe(`Card "Plan" (plan, 1/1) is ready. Write it to ${path}; the card refreshes on each save.`)
|
|
111
|
+
expect(board.requests[0]?.body).toEqual({ sessionId: 'sid-1', title: 'Plan', key: 'plan' })
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('a 400 answer reaches the model as an error with the server text', async ($, on) => {
|
|
115
|
+
const error = 'file not found: C:/proj/gone.md'
|
|
116
|
+
engine(on, GATE, { status: 400, body: { error } })
|
|
117
|
+
await $.session.start({ cwd: 'C:/proj', surface: 'terminal', isInteractive: true })
|
|
118
|
+
const res = await $.tool.call(call({ title: 'Gone', file: 'C:/proj/gone.md' }))
|
|
119
|
+
expect(res.deny).toBe(error)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('a network error reaches the model as an error', async ($, on) => {
|
|
123
|
+
engine(on, GATE, new Error('connect ECONNREFUSED 127.0.0.1:4100'))
|
|
124
|
+
await $.session.start({ cwd: 'C:/proj', surface: 'terminal', isInteractive: true })
|
|
125
|
+
const res = await $.tool.call(call({ title: 'Plan' }))
|
|
126
|
+
expect(res.deny).toContain(`Cannot reach cck at ${CCK_URL}`)
|
|
127
|
+
expect(res.deny).toContain('ECONNREFUSED')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('no token file: no request, an error that names the board', async ($, on) => {
|
|
131
|
+
const board = engine(on, GATE)
|
|
132
|
+
delete board.files[TOKEN_FILE]
|
|
133
|
+
await $.session.start({ cwd: 'C:/proj', surface: 'terminal', isInteractive: true })
|
|
134
|
+
const res = await $.tool.call(call({ title: 'Plan' }))
|
|
135
|
+
expect(res.deny).toContain('No terminal token')
|
|
136
|
+
expect(board.requests).toEqual([])
|
|
137
|
+
})
|