@rness/cli 0.18.1 → 0.20.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/dist/index.js CHANGED
@@ -62,7 +62,7 @@ import {
62
62
  workspaceDirs,
63
63
  writeFileAtomic,
64
64
  writeManifest
65
- } from "./chunk-JHU2G7K7.js";
65
+ } from "./chunk-WIZHU4WT.js";
66
66
 
67
67
  // src/index.ts
68
68
  async function run2(argv) {
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
+ "name": "rness",
4
+ "description": "The rness workspace in Claude Code: /rness:status, /rness:adr, /rness:spec, /rness:plan.",
5
+ "author": {
6
+ "name": "rness-dev",
7
+ "url": "https://rness.dev"
8
+ },
9
+ "homepage": "https://rness.dev",
10
+ "repository": "https://github.com/rness-dev/rness",
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "rness",
14
+ "agents",
15
+ "claude-code",
16
+ "adr",
17
+ "specification",
18
+ "plan"
19
+ ],
20
+ "types": "./types/index.d.ts"
21
+ }
@@ -0,0 +1 @@
1
+ { "modules": ["./register.tsx"] }
@@ -0,0 +1,499 @@
1
+ import { atom, read, update } from 'claude-code'
2
+ import type { EngineInterface, Register } from 'claude-code'
3
+
4
+ import type { RnessRow, RnessSnapshot } from '../types'
5
+
6
+ // The rness plugin's mod (spec 0029 §3): the workspace on screen, drawn from
7
+ // what the pinned CLI prints (`rness status --json`). Nothing is computed
8
+ // here: the CLI words every line, the module only draws and refreshes.
9
+ //
10
+ // `sync` writes this file whole into `.claude/skills/rness/hooks/`, at the
11
+ // workspace root and at the root of each clone, with `RNESS` set to the path
12
+ // of `.rness` from that place, as the settings hooks have it. One path, never
13
+ // a guess: a clone runs the workspace's pinned copy, never one its own tree
14
+ // could hold.
15
+
16
+ const snapshot = atom({ plugin: 'rness', key: 'snapshot' } as const, null)
17
+ const tab = atom({ plugin: 'rness', key: 'tab' } as const, 0)
18
+ const selected = atom({ plugin: 'rness', key: 'selected' } as const, 0)
19
+ const interactive = atom(
20
+ { plugin: 'rness', key: 'interactive' } as const,
21
+ false
22
+ )
23
+ const open = atom({ plugin: 'rness', key: 'open' } as const, null)
24
+
25
+ const PANE = 'rness'
26
+ const BIN = 'node_modules/@rness/cli/dist/bin/rness.js'
27
+ /** `.rness` from the project directory; `sync` writes `../../.rness` in a clone. */
28
+ const RNESS = '.rness'
29
+ const STATE = /[\\/]\.rness[\\/]/
30
+
31
+ /**
32
+ * Agent Pulse's option colours, which the CLI names for each row (spec 0017
33
+ * §3), as the surface names them: the board's purple and pink are the
34
+ * terminal's magenta, its orange the terminal's yellow.
35
+ */
36
+ const COLORS: Readonly<Record<string, string>> = {
37
+ gray: 'gray',
38
+ blue: 'blue',
39
+ green: 'green',
40
+ yellow: 'yellow',
41
+ orange: 'yellow',
42
+ red: 'red',
43
+ pink: 'magenta',
44
+ purple: 'magenta',
45
+ }
46
+
47
+ /** What one `Markdown` draws at most (10000 on Claude Code 2.1.289), with room. */
48
+ const PIECE = 9000
49
+
50
+ /** The project directory: the plugin lives in its `.claude/skills/rness`. */
51
+ const projectOf = (root: string): string =>
52
+ root.replace(/[\\/]\.claude[\\/]skills[\\/]rness[\\/]?$/, '')
53
+
54
+ const clamp = (n: number, low: number, high: number): number =>
55
+ Math.min(Math.max(n, low), high)
56
+
57
+ /**
58
+ * Blank lines under the rows, so that the body is always taller than the
59
+ * pane: in a pane ↑/↓ are scroll keys (`pane:scrollUp`/`Down` on 2.1.289),
60
+ * which raise `ui.scroll` — the selection's keys here — only while there is
61
+ * something to scroll; otherwise they walk the buttons.
62
+ */
63
+ const padOf = (bodyRows: number, rows: number): number =>
64
+ Math.max(0, bodyRows + 1 - (1 + rows))
65
+
66
+ // The values as the state holds them now, read outside a drawing: each
67
+ // reference a literal, so that the engine can list what the module reads.
68
+ const SNAPSHOT = { plugin: 'rness', key: 'snapshot' } as const
69
+ const TAB = { plugin: 'rness', key: 'tab' } as const
70
+ const SELECTED = { plugin: 'rness', key: 'selected' } as const
71
+ const OPEN = { plugin: 'rness', key: 'open' } as const
72
+ const INTERACTIVE = { plugin: 'rness', key: 'interactive' } as const
73
+ const snapshotNow = async ($: EngineInterface) =>
74
+ (await $.state.get(SNAPSHOT)).value ?? null
75
+ const tabNow = async ($: EngineInterface) => (await $.state.get(TAB)).value ?? 0
76
+ const selectedNow = async ($: EngineInterface) =>
77
+ (await $.state.get(SELECTED)).value ?? 0
78
+ const openNow = async ($: EngineInterface) =>
79
+ (await $.state.get(OPEN)).value ?? null
80
+
81
+ /**
82
+ * Reads the snapshot from the pinned CLI and keeps it; the status line
83
+ * follows, and so does the document open in the pane, read again. No
84
+ * pinned copy, or a run that fails: no snapshot, nothing drawn.
85
+ */
86
+ async function refresh($: EngineInterface): Promise<RnessSnapshot | null> {
87
+ let next: RnessSnapshot | null = null
88
+ try {
89
+ const project = projectOf($.plugin.root)
90
+ const bin = `${project}/${RNESS}/${BIN}`
91
+ if (await $.fs.exists(bin)) {
92
+ const ran = await $.process.run(
93
+ ['node', bin, 'status', '--json', '--cwd', project],
94
+ { cwd: project, timeoutMs: 20_000 }
95
+ )
96
+ if (ran.exitCode === 0) next = JSON.parse(ran.stdout) as RnessSnapshot
97
+ }
98
+ } catch {
99
+ next = null
100
+ }
101
+ await update($, snapshot, () => next)
102
+ $.ui.status(next?.statusLine)
103
+ const opened = await openNow($)
104
+ if (opened !== null) await show($, opened.path)
105
+ return next
106
+ }
107
+
108
+ /** A refresh off the dispatch that asked for it: a turn never waits for node. */
109
+ const later = ($: EngineInterface): void => {
110
+ $.clock.after(0, () => void refresh($))
111
+ }
112
+
113
+ /** The document at `path` in `.rness/`, read and shown in place of the list. */
114
+ async function show($: EngineInterface, path: string): Promise<void> {
115
+ let text: string | null
116
+ try {
117
+ text = await $.fs.read(`${projectOf($.plugin.root)}/${RNESS}/${path}`)
118
+ } catch {
119
+ text = null
120
+ }
121
+ await update($, open, () => ({ path, text }))
122
+ }
123
+
124
+ /** The ring onto the row at `path`; a site without the keys refuses, and that is fine. */
125
+ const ringOn = ($: EngineInterface, path: string): void => {
126
+ void $.ui.focus({ requestId: PANE, key: `open-${path}` }).catch(() => {})
127
+ }
128
+
129
+ /**
130
+ * Shows tab `i`: its newest row selected, the pane at its top, the ring on
131
+ * that row — from a digit, a click, or Tab and Shift+Tab (`cycleTab`).
132
+ */
133
+ async function showTab($: EngineInterface, i: number): Promise<void> {
134
+ const shown = await snapshotNow($)
135
+ await update($, tab, () => i)
136
+ await update($, selected, () => 0)
137
+ void $.ui.scroll({ in: PANE, to: 'start' }).catch(() => {})
138
+ const first = shown?.tabs[i]?.rows[0]
139
+ if (first !== undefined) ringOn($, first.path)
140
+ }
141
+
142
+ /** The next tab (`+1`) or the previous (`-1`), wrapping. */
143
+ async function cycleTab($: EngineInterface, by: 1 | -1): Promise<void> {
144
+ const shown = await snapshotNow($)
145
+ const n = shown?.tabs.length ?? 0
146
+ if (n === 0) return
147
+ const at = await tabNow($)
148
+ await showTab($, (at + by + n) % n)
149
+ }
150
+
151
+ /** The row of the document at `path`, whichever tab lists it. */
152
+ const rowOf = (shown: RnessSnapshot, path: string): RnessRow | undefined =>
153
+ shown.tabs.flatMap((t) => t.rows).find((row) => row.path === path)
154
+
155
+ /** `text` cut to `max` cells with an ellipsis, as the full-screen view cuts a title. */
156
+ const cut = (text: string, max: number): string =>
157
+ text.length <= max ? text : `${text.slice(0, Math.max(1, max - 1))}…`
158
+
159
+ /**
160
+ * The document without its front matter, as its issue's body starts (spec
161
+ * 0018 §3): the CLI's `stripFrontMatter`, the blank lines after it included.
162
+ */
163
+ const bodyOf = (text: string): string =>
164
+ text
165
+ .replace(/^\uFEFF/, '')
166
+ .replace(/^---\r?\n(?:[\s\S]*?\r?\n)?---(?:\r?\n|$)/, '')
167
+ .replace(/^(?:[ \t]*\r?\n)+/, '')
168
+
169
+ /** A line opening or closing a code fence, in a paragraph. */
170
+ const FENCES = /^(`{3,}|~{3,})/gm
171
+
172
+ /**
173
+ * `text` in pieces a `Markdown` can draw: cut at a blank line, a fenced block
174
+ * kept whole; a paragraph or a block longer than a piece is cut as it is. A
175
+ * control character other than tab and newline is dropped: a `Markdown`
176
+ * refuses it.
177
+ */
178
+ function pieces(text: string): string[] {
179
+ const out: string[] = []
180
+ let piece = ''
181
+ const put = (unit: string): void => {
182
+ if (piece !== '' && piece.length + 2 + unit.length > PIECE) {
183
+ out.push(piece)
184
+ piece = ''
185
+ }
186
+ piece = piece === '' ? unit : `${piece}\n\n${unit}`
187
+ while (piece.length > PIECE) {
188
+ out.push(piece.slice(0, PIECE))
189
+ piece = piece.slice(PIECE)
190
+ }
191
+ }
192
+ let block: string | null = null
193
+ for (const para of text.replace(/[^\P{Cc}\t\n]/gu, '').split('\n\n')) {
194
+ const opensOrCloses = (para.match(FENCES) ?? []).length % 2 === 1
195
+ if (block !== null) {
196
+ block = `${block}\n\n${para}`
197
+ if (opensOrCloses) {
198
+ put(block)
199
+ block = null
200
+ }
201
+ } else if (opensOrCloses) block = para
202
+ else put(para)
203
+ }
204
+ if (block !== null) put(block)
205
+ if (piece !== '') out.push(piece)
206
+ return out
207
+ }
208
+
209
+ export const register: Register = (on) => {
210
+ on('session.start', async ($, e, next) => {
211
+ await update($, interactive, () => e.isInteractive)
212
+ later($)
213
+ return next(e)
214
+ })
215
+
216
+ // An edit of `.rness/` changes what the band and the pane show.
217
+ on('tool.call', { tool: 'Edit' }, async ($, e, next) => {
218
+ const ran = await next(e)
219
+ if (STATE.test(e.file_path)) later($)
220
+ return ran
221
+ })
222
+ on('tool.call', { tool: 'Write' }, async ($, e, next) => {
223
+ const ran = await next(e)
224
+ if (STATE.test(e.file_path)) later($)
225
+ return ran
226
+ })
227
+
228
+ // What no tool shows: a `git pull`, a file written through Bash.
229
+ on('turn.complete', async ($, e, next) => {
230
+ if (e.agentId === undefined) later($)
231
+ return next(e)
232
+ })
233
+
234
+ // The notes to act on, whenever there are some (plan 0042). The banner is
235
+ // the session-start line's, said once in the transcript: drawn here too,
236
+ // it said the same thing twice.
237
+ on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
238
+ const shown = await read($, snapshot)
239
+ if (e.props.hasSurvey || shown === null || shown.notes.length === 0)
240
+ return next(e)
241
+ const { Box, Text } = $.ui.resolve(e)
242
+ return (
243
+ <Box flexDirection="column">
244
+ {shown.notes.map((note, i) => (
245
+ <Box key={`note-${i}`}>
246
+ <Text color="yellow" wrap="truncate-end">
247
+ {note}
248
+ </Text>
249
+ </Box>
250
+ ))}
251
+ </Box>
252
+ )
253
+ })
254
+
255
+ // `/rness:status` is the skill's name: answered here with the pane, it
256
+ // runs no model turn; with no snapshot, or no one to see a pane, the
257
+ // skill's tables answer it.
258
+ on('command.run', { command: 'rness:status' }, async ($, e, next) => {
259
+ // Nobody to see a pane (`claude -p`): the tables are the answer.
260
+ if (!(await $.state.get(INTERACTIVE)).value) return next(e)
261
+ const shown = (await snapshotNow($)) ?? (await refresh($))
262
+ if (shown === null) return next(e)
263
+ const wanted = e.args.trim().toLowerCase()
264
+ const index = shown.tabs.findIndex(
265
+ (t) => t.name.toLowerCase() === wanted || t.label.toLowerCase() === wanted
266
+ )
267
+ if (index !== -1) await update($, tab, () => index)
268
+ // The command asks for the list, from its first row, whatever the pane
269
+ // showed before.
270
+ await update($, selected, () => 0)
271
+ await update($, open, () => null)
272
+ // The pane takes the keys (plans 0042, 0043): ↑/↓ select a row, Enter
273
+ // opens it, Tab and Shift+Tab change the tab, a digit too, Esc and `q`
274
+ // close. A pane binds no ←/→ (Claude Code 2.1.289).
275
+ const opened = await $.ui.open({
276
+ id: PANE,
277
+ title: `${shown.workspace} · status`,
278
+ focus: true,
279
+ closeOnEscape: true,
280
+ })
281
+ // A surface that places no pane (an older desktop): the tables instead.
282
+ if (!opened.isPlaced) return next(e)
283
+ return {
284
+ text: 'The status of the workspace is open beside the conversation.',
285
+ }
286
+ })
287
+
288
+ // ↑/↓, PgUp/PgDn, Home/End are the pane's scroll keys (2.1.289): here they
289
+ // move the selection, and the window follows it, the selected row kept
290
+ // near the middle. A document open scrolls as the engine scrolls.
291
+ on(
292
+ 'ui.scroll',
293
+ { component: 'Pane', requestId: PANE },
294
+ async ($, e, next) => {
295
+ if (e.origin.kind !== 'person' || e.by === 0) return next(e)
296
+ const shown = await snapshotNow($)
297
+ if (shown === null || (await openNow($)) !== null) return next(e)
298
+ const rows = shown.tabs[await tabNow($)]?.rows ?? []
299
+ if (rows.length === 0) return next(e)
300
+ const last = rows.length - 1
301
+ const was = clamp(await selectedNow($), 0, last)
302
+ // An arrow is a row, Home and End are `contentRows`, a page key `bodyRows`.
303
+ const step =
304
+ Math.abs(e.by) <= 1
305
+ ? e.by
306
+ : Math.abs(e.by) >= e.contentRows
307
+ ? Math.sign(e.by) * rows.length
308
+ : Math.sign(e.by) * Math.max(1, e.bodyRows - 1)
309
+ const to = clamp(was + step, 0, last)
310
+ await update($, selected, () => to)
311
+ ringOn($, rows[to]!.path)
312
+ const header =
313
+ e.contentRows - rows.length - padOf(e.bodyRows, rows.length)
314
+ const half = Math.floor((e.bodyRows - 1) / 2)
315
+ const offset = clamp(
316
+ header + to - half,
317
+ 0,
318
+ Math.max(0, e.contentRows - e.bodyRows)
319
+ )
320
+ return next({ ...e, offset })
321
+ }
322
+ )
323
+
324
+ // Tab and Shift+Tab walk the ring: from the selected row they land on the
325
+ // `›` and `‹` beside it, drawn for that. The ring stays on the row and the
326
+ // tab changes, as Tab does in the full-screen `rness status`. A click on
327
+ // another row makes it the selection; the press that follows opens it.
328
+ on('ui.focus', { component: 'Pane', requestId: PANE }, async ($, e, next) => {
329
+ if (e.origin.kind !== 'person' || e.element === undefined) return next(e)
330
+ if (e.element === 'tab-next' || e.element === 'tab-prev') {
331
+ await cycleTab($, e.element === 'tab-next' ? 1 : -1)
332
+ return {}
333
+ }
334
+ if (e.element.startsWith('open-')) {
335
+ const path = e.element.slice('open-'.length)
336
+ const shown = await snapshotNow($)
337
+ const rows = shown?.tabs[await tabNow($)]?.rows ?? []
338
+ const i = rows.findIndex((row) => row.path === path)
339
+ if (i !== -1) await update($, selected, () => i)
340
+ }
341
+ return next(e)
342
+ })
343
+
344
+ on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
345
+ const { Box, Button, Link, Markdown, Text } = $.ui.resolve(e)
346
+ const shown = await read($, snapshot)
347
+ if (shown === null)
348
+ return <Text dimColor>rness: the workspace status cannot be read.</Text>
349
+ const opened = await read($, open)
350
+
351
+ // One document, in place of the list: `q` brings the list back, Esc
352
+ // still closes the pane. Its item on Agent Pulse is a link.
353
+ if (opened !== null) {
354
+ const row = rowOf(shown, opened.path)
355
+ return (
356
+ <Box flexDirection="column">
357
+ <Box flexDirection="row" gap={1}>
358
+ {[
359
+ <Button
360
+ key="back"
361
+ label="Back"
362
+ plain
363
+ hotkey="q"
364
+ autoFocus
365
+ onPress={() => update($, open, () => null)}
366
+ />,
367
+ ...(row?.link != null
368
+ ? [<Link key="pulse" href={row.link} label="Agent Pulse" />]
369
+ : []),
370
+ ]}
371
+ </Box>
372
+ <Box key="head">
373
+ {row === undefined ? (
374
+ <Text bold wrap="truncate-end">
375
+ {opened.path}
376
+ </Text>
377
+ ) : (
378
+ <Text wrap="truncate-end">
379
+ <Text bold>
380
+ {row.id} {row.title}
381
+ </Text>{' '}
382
+ <Text color={COLORS[row.color]}>{row.status ?? '?'}</Text>
383
+ </Text>
384
+ )}
385
+ </Box>
386
+ {opened.text === null ? (
387
+ <Box key="unread">
388
+ <Text dimColor>rness: {opened.path} cannot be read.</Text>
389
+ </Box>
390
+ ) : (
391
+ pieces(bodyOf(opened.text)).map((piece, i) => (
392
+ <Markdown key={`doc-${i}`} text={piece} />
393
+ ))
394
+ )}
395
+ </Box>
396
+ )
397
+ }
398
+
399
+ const at = clamp(await read($, tab), 0, Math.max(0, shown.tabs.length - 1))
400
+ const current = shown.tabs[at]
401
+ const rows = current?.rows ?? []
402
+ const sel = clamp(await read($, selected), 0, Math.max(0, rows.length - 1))
403
+ const width = e.props.bodyColumns ?? 80
404
+ const pad = padOf(e.props.scroll?.bodyRows ?? 0, rows.length)
405
+ return (
406
+ <Box flexDirection="column">
407
+ <Box flexDirection="row" gap={1} flexWrap="wrap">
408
+ {[
409
+ ...shown.tabs.map((t, i) => (
410
+ <Button
411
+ key={`tab-${t.name}`}
412
+ label={`${i < 9 ? `${i + 1} ` : ''}${t.label} (${t.rows.length})`}
413
+ {...(i < 9 ? { hotkey: String(i + 1) } : {})}
414
+ variant={i === at ? 'primary' : 'secondary'}
415
+ // The ring starts here only when the tab has no row.
416
+ {...(i === at && rows.length === 0
417
+ ? { autoFocus: true as const }
418
+ : {})}
419
+ onPress={() => void showTab($, i)}
420
+ />
421
+ )),
422
+ <Button
423
+ key="close"
424
+ label="Close"
425
+ plain
426
+ hotkey="q"
427
+ role="dismiss"
428
+ onPress={() => $.ui.close({ id: PANE })}
429
+ />,
430
+ ...(shown.pulse != null
431
+ ? [<Link key="pulse" href={shown.pulse} label="Agent Pulse" />]
432
+ : []),
433
+ <Text key="keys" dimColor>
434
+ ⇥ tab · ↑↓ row · ⏎ open
435
+ </Text>,
436
+ ]}
437
+ </Box>
438
+ {rows.length === 0 ? (
439
+ <Box key="empty">
440
+ <Text dimColor>Nothing here yet.</Text>
441
+ </Box>
442
+ ) : (
443
+ // Every row, keyed by path (two documents of one date share an
444
+ // id): Enter or a click opens it. The selected row carries the
445
+ // ring and, beside it, `‹` and `›`: where Shift+Tab and Tab land.
446
+ rows.map((row, i) => {
447
+ const status = row.status ?? '?'
448
+ const isSel = i === sel
449
+ const label = cut(
450
+ `${row.id} ${row.title}`,
451
+ width - status.length - (isSel ? 5 : 3)
452
+ )
453
+ return (
454
+ <Box key={`row-${row.path}`} flexDirection="row" gap={1}>
455
+ {[
456
+ isSel ? (
457
+ <Button
458
+ key="tab-prev"
459
+ plain
460
+ dimColor
461
+ label="‹"
462
+ onPress={() => void cycleTab($, -1)}
463
+ />
464
+ ) : (
465
+ <Text> </Text>
466
+ ),
467
+ <Button
468
+ key={`open-${row.path}`}
469
+ plain
470
+ label={label}
471
+ {...(isSel ? { autoFocus: true as const } : {})}
472
+ onPress={() => void show($, row.path)}
473
+ />,
474
+ <Text color={COLORS[row.color]}>{status}</Text>,
475
+ ...(isSel
476
+ ? [
477
+ <Button
478
+ key="tab-next"
479
+ plain
480
+ dimColor
481
+ label="›"
482
+ onPress={() => void cycleTab($, 1)}
483
+ />,
484
+ ]
485
+ : []),
486
+ ]}
487
+ </Box>
488
+ )
489
+ })
490
+ )}
491
+ {Array.from({ length: pad }, (_, i) => (
492
+ <Box key={`pad-${i}`}>
493
+ <Text> </Text>
494
+ </Box>
495
+ ))}
496
+ </Box>
497
+ )
498
+ })
499
+ }
@@ -0,0 +1,49 @@
1
+ // The values the rness mod keeps in `$.state` (spec 0029 §3.2). The
2
+ // snapshot is what `rness status --json` prints: `StatusSnapshot` in
3
+ // `src/commands/snapshot.ts`, read here as data.
4
+
5
+ export type RnessRow = {
6
+ id: string
7
+ title: string
8
+ status: string | null
9
+ path: string
10
+ /** The colour of its status on Agent Pulse, by the board's name (`yellow`). */
11
+ color: string
12
+ /** Its item on Agent Pulse; null while the workspace declares no pulse. */
13
+ link: string | null
14
+ }
15
+
16
+ export type RnessTab = { name: string; label: string; rows: RnessRow[] }
17
+
18
+ export type RnessSnapshot = {
19
+ version: string
20
+ workspace: string
21
+ scope: string | null
22
+ banner: string
23
+ statusLine: string
24
+ inProgress: string[]
25
+ notes: string[]
26
+ /** Agent Pulse, the board; null while none is declared. */
27
+ pulse: string | null
28
+ tabs: RnessTab[]
29
+ }
30
+
31
+ /** The document the pane shows in place of the list: its path, and its text, or null when it could not be read. */
32
+ export type RnessOpen = { path: string; text: string | null }
33
+
34
+ declare module 'claude-code' {
35
+ interface PluginState {
36
+ rness: {
37
+ /** The last snapshot; null while none could be read. */
38
+ snapshot: RnessSnapshot | null
39
+ /** The pane's tab, by its index in the snapshot. */
40
+ tab: number
41
+ /** The selected row of that tab, by its index: ↑/↓ move it, Enter opens it. */
42
+ selected: number
43
+ /** Whether someone watches the session: no pane for `claude -p`. */
44
+ interactive: boolean
45
+ /** The document open in the pane; null while the list shows. */
46
+ open: RnessOpen | null
47
+ }
48
+ }
49
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rness/cli",
3
- "version": "0.18.1",
3
+ "version": "0.20.0",
4
4
  "description": "Configuration-plane CLI for rness workspaces: resolve, validate and sync org-level context for AI coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,6 +23,10 @@
23
23
  "files": [
24
24
  "dist",
25
25
  "scaffold",
26
+ "mod/.claude-plugin/plugin.json",
27
+ "mod/hooks/hooks.json",
28
+ "mod/hooks/register.tsx",
29
+ "mod/types/index.d.ts",
26
30
  "README.md",
27
31
  "LICENSE"
28
32
  ],