@standardagents/code-plugin-sdk 1.0.0-alpha.0 → 1.0.0-alpha.10-rows.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/REFERENCE.md ADDED
@@ -0,0 +1,536 @@
1
+ # Plugin SDK reference
2
+
3
+ The public declarations live in
4
+ `packages/plugin-sdk/src/index.d.ts`. This page groups the stable concepts for
5
+ plugin authors.
6
+
7
+ The bundled SDK and the npm package use the same public declarations. This
8
+ reference describes SDK `1.0.0-alpha.5-usage.0`. Plugins should declare the
9
+ version published for their product build in both `peerDependencies` and
10
+ `devDependencies`; the manifest `apiVersion` remains `1`.
11
+
12
+ ## Definition and context
13
+
14
+ `definePlugin({ id, activate })` creates the default plugin export. The
15
+ activation function receives a `PluginContext` with the immutable manifest,
16
+ producer identity, abort signal, typed request function, surface publishers,
17
+ subscriptions, schedules, lifecycle operations, and host capabilities.
18
+
19
+ `section`, `card`, `slot`, `badge`, `panel`, and `overlay` each return a
20
+ publisher for a declared contribution of that kind; `canvas(id, spec)`
21
+ returns a canvas publisher for any declaration that draws content.
22
+ Publishers expose `replace(content)` and `clear()`. A canvas publisher also
23
+ exposes `write(ansi)` and `focus(capture)`. Registration objects return a
24
+ disposable subscription. Key registrations support one pending chord update.
25
+
26
+ ## Manifest
27
+
28
+ `PluginManifest` contains `apiVersion`, `id`, `name`, `version`, `entry`,
29
+ `singleton`, `order`, `capabilities`, `contributions`, and `hookTimeoutMs`.
30
+ Contribution declarations contain a stable ID, surface kind, anchor, title,
31
+ merge mode, width, menu position, palette group, chord, URL pattern, and
32
+ action ID as appropriate.
33
+
34
+ A `card` declaration draws a sidebar card. Its optional `border` boolean
35
+ defaults to `true`. Set `border: false` for a borderless group: the host
36
+ retains the author title, content padding, focus indication, and activation
37
+ behavior, and removes the outline and bottom border row. This preference
38
+ is declared by each plugin on its own cards; other surface kinds reject it.
39
+ The nested view node named `card` is a separate content element.
40
+
41
+ `validateManifest`
42
+ refuses a card whose anchor is not `plugins`. Two optional fields control
43
+ panels:
44
+
45
+ - `presentation` is `popover` (the default), `column`, or `pane`. On a panel
46
+ it sets how the panel opens. On a card or command it sets how the panel
47
+ named by `opens` opens. Other kinds cannot declare a presentation.
48
+ - `opens` is the ID of a panel that a card or command opens when the viewer
49
+ activates it. `validateManifest` refuses an `opens` value that does not name
50
+ a declared panel.
51
+
52
+ `PRESENTATIONS` lists the accepted presentation values.
53
+
54
+ Account-level sidebar cards prefer a live producer on the viewing machine.
55
+ This keeps card actions and attached panels local to that viewer. If no live
56
+ local producer exists, the host uses its deterministic remote-source fallback;
57
+ remote source authority restrictions still apply. Plugins aggregate shared
58
+ account data themselves through the public state API.
59
+
60
+ An anchor determines the entity that owns a contribution. Machine anchors use
61
+ a machine entity, project anchors use a project entity, and pane anchors use a
62
+ pane entity. The `plugins` and `account` anchors are account-level surfaces;
63
+ `section` may use a section entity. A machine contribution normally omits its
64
+ generation so the host resolves the current generation. The host rejects a
65
+ contribution whose entity belongs to another machine or project.
66
+
67
+ ## Sources and collections
68
+
69
+ A plugin installs from a Git repository or an npm package. A source is one
70
+ plugin or a collection. `PluginCollection` is the parsed
71
+ `standard-plugins.json` at the source root: `schema` is `1` and `plugins`
72
+ holds 1 to 256 `PluginCollectionEntry` values. Each entry has an `id` in the
73
+ plugin id pattern and a relative POSIX `path` with no leading slash, no
74
+ backslash, and no empty, `.`, or `..` segment. Ids and paths are unique.
75
+ `standard plugin install <source> --plugin <id>` selects one entry.
76
+
77
+ `validateCollection(value)` returns a frozen collection or throws a
78
+ `PluginError` with code `invalid_collection`.
79
+ `resolveCollection({ collection, packageJson })` returns the validated
80
+ collection when a file is present; otherwise a valid `standardPlugin`
81
+ manifest in `packageJson` yields one entry at path `""`. `COLLECTION_FILE`
82
+ is the file name.
83
+
84
+ ## Dependencies and publishing
85
+
86
+ A plugin with `dependencies` or `optionalDependencies` ships a lockfile
87
+ beside its `package.json`. `lockfileRequirement({ packageJson, sourceKind })`
88
+ returns `{ required, dependencies, lockfiles }`: for `"npm"` the accepted
89
+ name is `npm-shrinkwrap.json`; for `"git"` it is `package-lock.json` or
90
+ `npm-shrinkwrap.json`. The SDK itself stays under `peerDependencies`.
91
+
92
+ `checkPackageForPublish({ packageJson, files, sourceKind, requireManifest,
93
+ expectedId })` is pure and returns `PublishProblem` values with a `code` and
94
+ a `message`. Codes cover a missing lockfile, the SDK under `dependencies`, a
95
+ missing peer declaration, a missing or invalid manifest, an entry absent from
96
+ `files`, an id that differs from `expectedId`, and a private package.
97
+
98
+ The package's `standard-plugin` command applies these rules to a directory:
99
+ `check [dir] [--source npm|git]` prints problems and exits with status 1
100
+ when it finds one, and `pack [dir]` runs `check`, runs `npm shrinkwrap` when
101
+ dependencies lack `npm-shrinkwrap.json`, and then prints the
102
+ `npm pack --dry-run` file list.
103
+
104
+ ## Content
105
+
106
+ Native content uses rows, styled text, or badges. A row can carry an identity,
107
+ provider revision, styled spans, an action ID, a meter, spark samples, and a
108
+ divider marker. Canvas content declares canonical columns, rows, transparency,
109
+ shade, input capture, and optional hover metadata.
110
+
111
+ View content, `{ kind: 'view', root }`, is a tree of host-rendered nodes. The
112
+ host draws it with the viewer's theme, symbols, focus, and hit testing.
113
+
114
+ Each contribution kind accepts a fixed set of content kinds, and
115
+ `replace(content)` throws a `PluginError` for any other pairing:
116
+
117
+ | Contribution | Rows, text | Canvas | View | Badge |
118
+ |---|---|---|---|---|
119
+ | `section`, `card`, `panel` | yes | yes | yes | no |
120
+ | `slot`, `overlay` | yes | yes | no | no |
121
+ | `badge` | no | no | no | yes |
122
+ | `menu`, `command`, `key`, `link` | no | no | no | no |
123
+
124
+ ### Custom interfaces with canvas
125
+
126
+ Canvas is the fully supported path for a plugin that draws its own
127
+ interface. A section, card, or panel can show a canvas in every
128
+ presentation: a sidebar card, a pop-over, a column, or a plugin pane.
129
+ `canvas.columns` is the intrinsic width that the host uses to size the card,
130
+ column, or plugin pane. The view helpers below are optional, and a plugin can
131
+ combine a view card with a canvas panel.
132
+
133
+ `write(ansi)` draws into a grid that the host keeps for the canvas. The grid
134
+ keeps its cells between writes, so a plugin that redraws a whole frame starts
135
+ it with `\x1b[2J\x1b[H`. The host honours these sequences:
136
+
137
+ - SGR (`CSI … m`): reset, bold, dim, italic, underline, inverse, hidden,
138
+ strikethrough, their resets, the 8 and 16 colour forms, and `38`/`48`
139
+ with `5;n` or `2;r;g;b` (colon forms too).
140
+ - Cursor positioning: `CUP`/`HVP` (`H`, `f`), `CUU`, `CUD`, `CUF`, `CUB`,
141
+ `CNL`, `CPL`, `CHA`, and `VPA`, clamped to the canvas.
142
+ - Erasing: `ED` (`J`), `EL` (`K`), and `ECH` (`X`), with the current
143
+ background colour.
144
+ - Carriage return, line feed (which also returns to the first column),
145
+ tab, and backspace.
146
+
147
+ The host drops every other escape and control sequence, including private
148
+ modes, OSC (titles, hyperlinks, clipboard), DCS, APC, PM, and SOS, and every
149
+ C0, C1, and bidirectional formatting character. Text past the right edge is
150
+ clipped, and a line feed on the last row stays on that row: a canvas never
151
+ wraps or scrolls. Wide characters take two cells. Cells in default colours
152
+ take the viewer's theme, and a blank default cell shows the surface under the
153
+ canvas.
154
+
155
+ `hover` is an optional one-line string or an array of styled `TextSpan` values.
156
+ The string must be non-empty after trimming, fit within 512 UTF-8 bytes, and
157
+ contain no C0, C1, DEL, or bidirectional formatting character. Styled hover
158
+ values contain at most 32 spans whose joined text follows the same bounds and
159
+ must be non-empty. Span styling supports `foreground`, `background`, `bold`,
160
+ `italic`, and `underline`; `actionId` is rejected because hover text is
161
+ passive. `Canvas.replace` publishes the canvas metadata, so a plugin can update
162
+ the hover text with another `replace` call while retaining the same canvas
163
+ grid.
164
+
165
+ On the current frontend, live canvases in `machine.before` and
166
+ `machine.after` slots are hover targets. The viewer waits 300 milliseconds
167
+ while the pointer remains over the canvas, then shows the text in a single
168
+ line owned by the host. Leaving the canvas dismisses it immediately. Hover
169
+ does not focus the canvas or change its geometry. The dwell timer and pointer
170
+ state belong to each viewer, so remote viewers can see hover text for a
171
+ machine slot independently.
172
+ Hover overlays for these machine slots use the same 22% sidebar machine colour
173
+ blend as the machine name bar over the viewer's background.
174
+
175
+ `focus(true)` asks for keyboard input and needs `captureInput: true` in the
176
+ canvas spec. While the canvas is shown in a pop-over, column, or plugin pane
177
+ and holds focus, the host sends every key except Escape and
178
+ Control, Alt, or Super chords to `onInput(id, handler)` subscriptions as
179
+ `{ kind: 'key', key, phase, contributionId, entity? }`. `key` is the typed
180
+ character or one of `up`, `down`, `left`, `right`, `enter`, `tab`,
181
+ `backspace`, `delete`, `escape`, `space`, `home`, `end`, `pageup`,
182
+ `pagedown`, or `f1`-`f12`; `phase` is `press` or `repeat`. When the canvas
183
+ stops being shown, for example after Escape, input subscriptions receive
184
+ `{ kind: 'focus', focused: false }` and `onDeactivate(id, handler)`
185
+ subscriptions run. The plugin calls `focus(true)` again to take keys back.
186
+ Sidebar cards and machine slots show only canvases without `captureInput`.
187
+ Local frontends receive canvas cells and send input. Remote viewers receive
188
+ the canvas grids and hover metadata, but remote actions, commands, panel
189
+ visibility changes, and canvas input are read-only until transport support is
190
+ added.
191
+
192
+ ## View nodes
193
+
194
+ Every node has a `type`. Field names use camelCase, and tone, weight,
195
+ alignment, and width keywords are lowercase.
196
+
197
+ | Type | Fields |
198
+ |---|---|
199
+ | `stack` | `gap?`, `children` |
200
+ | `row` | `children`, `align?` |
201
+ | `divider` | `label?` |
202
+ | `text` | `text?` or `spans?`, `tone?`, `weight?`, `mono?` |
203
+ | `badge` | `label`, `tone?` |
204
+ | `dot` | `tone?` |
205
+ | `progress` | `value`, `max`, `tone?`, `label?` |
206
+ | `segments` | `items`: `{ value, max, tone?, label? }` |
207
+ | `card` | `title?`, `tone?`, `children` |
208
+ | `stat` | `label`, `value`, `tone?`, `hint?` |
209
+ | `kv` | `items`: `{ label, value, mono?, copy? }` |
210
+ | `tabs` | `id`, `items`, `filters?`, `action?` |
211
+ | `select` | `id`, `label`, `options`, `filters?`, `action?` |
212
+ | `table` | `id`, `columns`, `rows` |
213
+ | `log` | `lines` |
214
+ | `button` | `label`, `action` |
215
+
216
+ Tones are `ok`, `info`, `warn`, `error`, `muted`, `accent`, `pending`, and
217
+ `bright` (`TONES`). Weights are `normal`, `bold`, and `dim`. Alignments are
218
+ `start`, `center`, and `end`.
219
+
220
+ A table column has an `id`, an optional `label`, a `width` of `'fill'` or a
221
+ cell count, an optional `maxWidth` and `align`, and a `priority`. The host
222
+ drops the columns with the highest `priority` values first on a narrow
223
+ surface. A table row has an `id`, `cells` keyed by column ID, and optional
224
+ `tone`, `note` spans, `tags`, and `action`.
225
+
226
+ Tab items and select options carry an `id`, a `label`, an optional `count`,
227
+ and an optional `tag`. When `filters` names a table in the same view, the
228
+ host shows the rows whose `tags` contain the chosen item's `tag`. An item
229
+ without a `tag` shows every row. The host filters and counts for each viewer
230
+ without a plugin round trip.
231
+
232
+ A view action is `{ actionId, value?, opens? }`. The host sends `actionId`
233
+ and the string `value` to the plugin, so an `onAction(actionId, handler)`
234
+ handler receives `event.value`. One action ID with a per-row value replaces
235
+ one subscription per row. When `opens` names a panel, the host opens that
236
+ panel after the plugin accepts the action. A `tabs` or `select` action value
237
+ defaults to the chosen item ID.
238
+
239
+ A node type that the host does not know draws nothing, so a newer SDK can
240
+ add node types.
241
+
242
+ ### Card views
243
+
244
+ Titles come from the contribution's `title`. The host adds no status dot,
245
+ health text, or machine label to a card title or panel. Authors can include
246
+ indicators in their own title or content. Runtime health remains available in
247
+ plugin management through `ctx.health.set()`.
248
+
249
+ A card shows a short summary. Its view can contain `stack`, `row`, `text`,
250
+ `badge`, `dot`, `progress`, `segments`, `stat`, and `divider` nodes, and it
251
+ is at most 6 lines tall. A `stack` is as tall as its children plus `gap`
252
+ lines between them. A `row` is as tall as its tallest child. Each other
253
+ allowed node is 1 line, and an unknown node is 0 lines. Put `card`, `kv`,
254
+ `tabs`, `select`, `table`, `log`, and `button` nodes in a panel. A card that
255
+ shows a canvas uses the general canvas bounds.
256
+
257
+ ### Bounds
258
+
259
+ `validateView(root)` applies the bounds that the host applies, and
260
+ `replace(content)` calls it for view content. `VIEW_LIMITS` holds the
261
+ numbers.
262
+
263
+ - A tree nests at most 8 nodes deep and holds at most 4,096 nodes. Table
264
+ cells count toward both limits.
265
+ - A table has at most 512 rows and 12 columns.
266
+ - No string contains a C0 or C1 control character, DEL, or a bidirectional
267
+ formatting character.
268
+ - Every number is finite.
269
+ - The serialized tree fits in one 256 KiB frame.
270
+ - An action ID and an action `opens` value are plugin identifiers, and an
271
+ action `value` is at most 512 UTF-8 bytes.
272
+ - An action `opens` value names a panel that the plugin declares.
273
+ - A card view follows the card rules above.
274
+
275
+ `validateView(root, { kind, manifest })` applies the card rules when `kind`
276
+ is `'card'` and the `opens` rule when `manifest` is present.
277
+
278
+ ### View helpers
279
+
280
+ `ui` builds nodes as plain JSON. The helpers are optional; hand-written JSON
281
+ with the same fields is equivalent. Options that are `undefined` are left
282
+ out.
283
+
284
+ ```js
285
+ import { definePlugin, ui } from '@standardagents/code-plugin-sdk'
286
+
287
+ export default definePlugin({
288
+ id: 'builds-monitor',
289
+ activate(ctx) {
290
+ const card = ctx.card('summary')
291
+ card.replace(ui.view(ui.stack([
292
+ ui.stat('Queue', '3', { tone: 'pending' }),
293
+ ui.table('builds', [{ id: 'version', label: 'Version' }], [{
294
+ id: 'b-1',
295
+ cells: { version: ui.text('0.4.12', { weight: 'bold' }) },
296
+ action: ui.action('open-build', { value: 'b-1', opens: 'build-detail' }),
297
+ }]),
298
+ ui.button('Open dashboard', 'open-dashboard'),
299
+ ])))
300
+ ctx.onAction('open-build', event => { /* event.value is 'b-1' */ })
301
+ },
302
+ })
303
+ ```
304
+
305
+ | Helper | Result |
306
+ |---|---|
307
+ | `ui.view(root)` | `{ kind: 'view', root }` |
308
+ | `ui.action(actionId, { value, opens })` | an action |
309
+ | `ui.span(text, { tone, weight, mono })` | a span |
310
+ | `ui.stack(children, { gap })`, `ui.row(children, { align })` | a layout node |
311
+ | `ui.card(children, { title, tone })` | a card node |
312
+ | `ui.divider(label)` | a divider |
313
+ | `ui.text(textOrSpans, { tone, weight, mono })` | a text node; an array sets `spans` |
314
+ | `ui.badge(label, tone)`, `ui.dot(tone)` | a badge or dot |
315
+ | `ui.progress(value, max, { tone, label })`, `ui.segments(items)` | a meter |
316
+ | `ui.stat(label, value, { tone, hint })`, `ui.kv(items)` | a value display |
317
+ | `ui.tabs(id, items, { filters, action })` | a tab strip |
318
+ | `ui.select(id, label, options, { filters, action })` | a drop-down |
319
+ | `ui.table(id, columns, rows)` | a table |
320
+ | `ui.log(lines)` | a log |
321
+ | `ui.button(label, action)` | a button; `action` is an action or an action ID |
322
+
323
+ `packages/plugin-sdk/test/fixtures/view-builds-monitor.json` holds a complete
324
+ view that matches the protocol's round-trip test.
325
+
326
+ ### Pane header and footer slots
327
+
328
+ A `slot` on `pane.header` or `pane.footer` contributes rows inside the pane
329
+ frame, above or below its terminal body. Declarations use `width: "full"`
330
+ (the default) or `width: "half"`. Contributions stack in ascending plugin
331
+ order, then plugin ID and contribution ID. Full contributions occupy their
332
+ own rows. Consecutive half contributions share a row, with the odd column
333
+ assigned to the left. An unpaired half leaves the right half empty.
334
+ Multiline half contributions share a block as tall as the taller contribution.
335
+
336
+ Rows, styled text, and non-interactive canvas cells render in these slots.
337
+ Each slot is bounded to 32 rows. Small panes clip slot rows while preserving
338
+ terminal space. Slot height participates in the same geometry used for terminal
339
+ painting, pointer input, graphics clipping, and controller-owned resizing.
340
+ Remote viewers receive the same contributions. Missing panes, stale producer
341
+ snapshots, and mismatched pane generations withdraw their rows.
342
+
343
+ Pane-slot actions, input capture, hover popovers, and persisted slot collapse
344
+ remain unconnected. They are separate from the implemented row rendering.
345
+
346
+ `ctx.context.get()` exposes `projects`, `panes`, and `complete` for the owning
347
+ machine. Project records contain `{ entity, name, path }`; pane records contain
348
+ `{ entity, projectId, name, cwd }`. Entities include machine ID and a decimal
349
+ string generation. The pane `cwd` is its configured project launch directory.
350
+ Live shell-directory observations are a separate future capability. Each list
351
+ has at most 128 records, and their combined JSON is bounded to 32 KiB. `complete`
352
+ is false when a local path could not be decoded or a bound omitted records.
353
+ Commands, launch environment, prompts, and encrypted payloads are excluded.
354
+ Plugins must perform filesystem work on their owning machine and clear
355
+ contributions when their target disappears or changes generation.
356
+
357
+ ### Current frontend coverage
358
+
359
+ Sections, plugin cards, machine slots, stacked pane header/footer slots, canvas surfaces, host-rendered views,
360
+ global command palette entries, and the three
361
+ panel presentations are implemented. A popover follows the content width and height within the
362
+ modal area. Its title is the contribution title and its body starts beneath it. A column attaches to the sidebar and resizes the workspace. A
363
+ plugin pane has a tab and focus, drag, close, and reveal behavior; its width
364
+ follows content and it does not create a tmux pane or resize a terminal.
365
+
366
+ Project slot anchors, standalone badges attached to hierarchy rows,
367
+ menu registrations, key registrations, link registrations,
368
+ and entity-scoped command palette entries have protocol and composition support
369
+ but do not have connected production frontend surfaces. Badge nodes inside a
370
+ view tree are rendered. The `overlay`
371
+ contribution kind has protocol validation, while general workspace overlay
372
+ placement and input remain unfinished. Use a supported panel presentation for
373
+ current interactive canvases.
374
+
375
+ ## Actions and subscriptions
376
+
377
+ Commands, menus, keys, and links carry stable action IDs. Actions receive a
378
+ selection and handler context with an abort signal. Link handlers return a
379
+ boolean that controls matching-handler traversal.
380
+
381
+ Global command registrations appear in the command palette. Menu, key, and
382
+ link registrations are accepted and stored by the SDK and daemon, while their
383
+ frontend menus, global key dispatch, and link list are still planned. An
384
+ entity-scoped command also has no current palette surface. `url.open` is the
385
+ supported host operation for opening a URL.
386
+
387
+ Event, hook, input, select, resize, activate, deactivate, and visibility
388
+ subscriptions use bounded conditions. Schedules allow `always`,
389
+ `section-visible`, `slot-visible`, or `panel-open` conditions. One schedule
390
+ invocation runs at a time.
391
+
392
+ ## Typed requests
393
+
394
+ The operation map covers pane and project lifecycle, notifications, URL and
395
+ fetch access, secrets, configuration, account state, context, popovers, canvas
396
+ write and focus, subscription changes, health, and webhook acknowledgements.
397
+ Each operation has a typed input and output. `RequestOptions` accepts an abort
398
+ signal and a timeout.
399
+
400
+ `ctx.state` provides account-backed `get(key)`, `set(key, value)`, and
401
+ `keys()` methods. State is isolated by the plugin ID. `keys()` takes no input
402
+ besides optional `RequestOptions` and returns a sorted `string[]` containing
403
+ the plugin's key names. The result is empty when no keys exist, is limited to
404
+ 256 keys, and has no prefix or pagination parameters. A key written by one
405
+ machine is visible to the other machines signed into the same account, so a
406
+ plugin can use distinct machine-specific key names without maintaining a
407
+ shared index. Key names are 1 to 128 bytes. The first character is ASCII
408
+ alphanumeric or `_`; later characters are ASCII alphanumeric, `_`, `.`, `:`,
409
+ or `-`. Values remain limited to 64 KiB of JSON. The host applies the same
410
+ account authority and revocation checks to all three methods.
411
+
412
+ The public SDK applies the same capability checks and request bounds to every
413
+ plugin. Plugin IDs do not grant fetch, pane, project, or secret exceptions.
414
+ The current native host connects fetch, secrets, configuration, account state,
415
+ context, URL opening, subscription changes, health, canvas writes, and canvas
416
+ focus. Pane/project lifecycle requests, notifications, `popover.open`, and
417
+ webhook acknowledgements have public types but currently return an unsupported
418
+ operation error through the production context. Declaring a capability does
419
+ not supply an unfinished host adapter.
420
+
421
+ The shared fetch policy currently permits HTTPS GET requests to
422
+ `builds.standardcode.ai` on port 443 under `/admin/api/` or `/builds/`. Requests
423
+ have no body, user-info, or fragment. Allowed request headers are authorization,
424
+ range, accept, and cache-control. The response body limit is 16 MiB. These
425
+ bounds apply to every fetch-authorized plugin. Plugin code retains normal
426
+ Node.js access to files, networking, and subprocesses under the user's identity;
427
+ capabilities govern SDK/host operations and do not provide OS sandboxing.
428
+
429
+ ## Build context
430
+
431
+ `context.get` returns a `PluginContextInfo`:
432
+
433
+ ```json
434
+ {
435
+ "accountId": "account-id",
436
+ "machineId": "machine-id",
437
+ "projects": [],
438
+ "panes": [],
439
+ "complete": true,
440
+ "build": {
441
+ "version": "0.14.1-branch.3bd7f23c.98a422",
442
+ "commit": "<40 hex digits>",
443
+ "ref": "refs/heads/feat/sidebar-plugins",
444
+ "channel": "branch"
445
+ },
446
+ "fleet": {
447
+ "mode": "follow",
448
+ "npmTag": "branch-feat-sidebar-plugins-3bd7f23c-b-6e049b24f5928544",
449
+ "ref": "refs/heads/feat/sidebar-plugins",
450
+ "version": "0.14.1-branch.3bd7f23c.98a422"
451
+ }
452
+ }
453
+ ```
454
+
455
+ `build` describes the native release that this machine runs. It is `null` for
456
+ a development binary without release metadata.
457
+
458
+ - `channel` is `branch` for branch and issue builds, `canary` for `main`
459
+ builds, `production` for a stable version, and `team` for a legacy team
460
+ release.
461
+ - `ref` is `refs/heads/main` for canary and team builds and
462
+ `refs/tags/vX.Y.Z` for production builds. A branch build reports its sealed
463
+ branch name.
464
+
465
+ `fleet` describes the account build policy. It is `null` when the account has
466
+ no build policy.
467
+
468
+ - `mode` is `follow` for a followed npm tag, `production` for the `latest`
469
+ tag, and `pin` for an exact version.
470
+ - `npmTag` is the followed tag, or `null` for a pin.
471
+ - `version` is the pinned version, or the target of the current rollout, or
472
+ the active account version. It is `null` when none is known.
473
+ - `ref` is `refs/heads/main` for `canary`. A branch or issue tag has a `ref`
474
+ only when it names the branch of the running build. The daemon does not
475
+ read the network to resolve other branch names, so `ref` is `null` for them.
476
+
477
+ Older daemons return only `accountId` and `machineId`. Treat a missing
478
+ `build` or `fleet` as `null`.
479
+
480
+ A plugin with the `events` capability can subscribe with
481
+ `onEvent('build-context', handler)`. The daemon delivers a `BuildContextEvent`
482
+ when `build` or `fleet` changes. Its `data` holds the new `build` and `fleet`
483
+ values in the shape above. Other account changes do not deliver the event, and
484
+ the daemon does not deliver it when a plugin starts. Read `context.get` at
485
+ activation for the initial values. The daemon pushes each change, so a plugin
486
+ needs no schedule to track the followed channel.
487
+
488
+ Plugins with `events` can subscribe to `onEvent('pane-context', handler)`.
489
+ Its `data` replaces the local `machineId`, `projects`, `panes`, and `complete`
490
+ fields from `context.get`. Only a changed local context produces an event.
491
+ Subscribe before the initial `context.get` request and discard that response
492
+ if an event arrives while it is pending. Pane removal and generation changes
493
+ must withdraw publishers for the old entity. Project launch directories are
494
+ configured paths; shell `cd` changes are outside this event's current contract.
495
+
496
+ A plugin can watch these directories with Node's native filesystem events.
497
+ Debounce bursts with a one-shot timer, retain changes that arrive during a read,
498
+ and close watchers on disposal or target removal. Do not schedule directory
499
+ or Git refresh polling. Watch linked-worktree Git metadata and shared refs as
500
+ well as the working tree. Filesystem notification errors should report degraded
501
+ health; they must not silently switch to polling.
502
+
503
+ ## Hooks and delivery
504
+
505
+ Hook dispatch and durable webhook delivery are planned host integrations.
506
+ The public contract defines hook events with an entity, launch specification,
507
+ ancestry, operation ID, and deadline. A hook returns `proceed` with an optional replacement launch or
508
+ `cancel` with a bounded reason. Webhook events carry a delivery ID for
509
+ acknowledgement and replay-safe handling.
510
+
511
+ ## Testing harness
512
+
513
+ `@standardagents/code-plugin-sdk/testing` provides `createHarness`. A harness drives
514
+ activation, event delivery, visibility, manual time, flushing, and disposal.
515
+ It records traces, surface replacement, subscriptions, and resources without
516
+ starting subprocesses. `harness.surface(id, entity?)` returns the current
517
+ content of a published contribution, including view and canvas content, or
518
+ `undefined` after the contribution clears.
519
+
520
+ ### Canvas theme colors
521
+
522
+ `CanvasSpec.themeColors` maps up to 32 canvas-local ANSI indexes (0–255) to
523
+ recipes. Each recipe has `source` (optional ANSI index 0–15, omitted for the
524
+ default foreground), `mix` (optional second ANSI index mixed equally), and
525
+ `opacity` (0–1, blended against the viewer background). Both foreground and
526
+ background SGR references to that index resolve through the same recipe.
527
+ For example, `{ 16: { source: 4, opacity: 0.72 }, 20: { opacity: 0.4 } }`
528
+ provides muted terminal blue at index 16 and a subtle foreground at index 20.
529
+
530
+ Resolution occurs in the viewer for sidebar, pane, slot, column, and popover
531
+ canvases. Remote producers do not read or bake another machine's colors.
532
+ Recipes remain unchanged across terminal theme updates. Unavailable source
533
+ colors fall back to the named ANSI index or default foreground; unavailable
534
+ background colors leave opacity unapplied. OSC queries remain unsupported
535
+ in plugin canvas streams. This capability is public and identical for all
536
+ plugins.
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ // Authoring tool for plugin packages. The SDK entry never imports this file.
3
+ import { spawnSync } from 'node:child_process'
4
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
5
+ import { join, resolve } from 'node:path'
6
+ import { COLLECTION_FILE, PluginError, checkPackageForPublish, validateCollection } from '../src/index.mjs'
7
+
8
+ const USAGE = `usage: standard-plugin check [dir] [--source npm|git]
9
+ standard-plugin pack [dir]
10
+
11
+ check validates package.json, ${COLLECTION_FILE}, each collection entry, and lockfiles
12
+ pack runs check, creates npm-shrinkwrap.json when dependencies need one, then runs npm pack --dry-run`
13
+ const IGNORED_DIRECTORIES = new Set(['node_modules', '.git'])
14
+ const FILE_LIMIT = 50000
15
+
16
+ function readJson(path) {
17
+ let text
18
+ try { text = readFileSync(path, 'utf8') } catch (error) {
19
+ if (error.code === 'ENOENT') return undefined
20
+ throw error
21
+ }
22
+ return JSON.parse(text)
23
+ }
24
+
25
+ /** Relative POSIX paths of regular files under root, without following links. */
26
+ function listFiles(root) {
27
+ const files = []
28
+ const pending = ['']
29
+ while (pending.length) {
30
+ const directory = pending.pop()
31
+ for (const entry of readdirSync(join(root, directory), { withFileTypes: true })) {
32
+ const path = directory ? `${directory}/${entry.name}` : entry.name
33
+ if (entry.isDirectory()) { if (!IGNORED_DIRECTORIES.has(entry.name)) pending.push(path) }
34
+ else if (entry.isFile()) files.push(path)
35
+ if (files.length > FILE_LIMIT) throw new Error(`More than ${FILE_LIMIT} files under ${root}`)
36
+ }
37
+ }
38
+ return files
39
+ }
40
+
41
+ function tryJson(root, name, problems, location) {
42
+ try { return readJson(join(root, name)) } catch (error) {
43
+ problems.push({ location, code: 'invalid_json', message: `${name}: ${error.message}` })
44
+ return undefined
45
+ }
46
+ }
47
+
48
+ function collect(problems, location, list) {
49
+ for (const problem of list) problems.push({ location, ...problem })
50
+ }
51
+
52
+ /** Checks one source directory. Returns problems with the directory each one belongs to. */
53
+ function checkDirectory(root, { sourceKind = 'npm' } = {}) {
54
+ const problems = []
55
+ const files = listFiles(root)
56
+ const packageJson = tryJson(root, 'package.json', problems, '.')
57
+ const collectionInput = tryJson(root, COLLECTION_FILE, problems, '.')
58
+ if (collectionInput === undefined) {
59
+ if (packageJson === undefined) problems.push({ location: '.', code: 'package_missing', message: `package.json or ${COLLECTION_FILE} is required` })
60
+ else collect(problems, '.', checkPackageForPublish({ packageJson, files, sourceKind }))
61
+ return problems
62
+ }
63
+ let collection
64
+ try { collection = validateCollection(collectionInput) } catch (error) {
65
+ if (!(error instanceof PluginError)) throw error
66
+ problems.push({ location: '.', code: error.code, message: `${COLLECTION_FILE}: ${error.message}` })
67
+ return problems
68
+ }
69
+ if (packageJson !== undefined) collect(problems, '.', checkPackageForPublish({ packageJson, files, sourceKind, requireManifest: false }))
70
+ else if (sourceKind === 'npm') problems.push({ location: '.', code: 'package_missing', message: 'an npm package needs a root package.json' })
71
+ for (const entry of collection.plugins) {
72
+ const entryPackage = tryJson(join(root, entry.path), 'package.json', problems, entry.path)
73
+ if (entryPackage === undefined) {
74
+ if (!problems.some(problem => problem.location === entry.path)) {
75
+ problems.push({ location: entry.path, code: 'package_missing', message: `collection entry ${entry.id} has no package.json` })
76
+ }
77
+ continue
78
+ }
79
+ const prefix = `${entry.path}/`
80
+ const entryFiles = files.filter(file => file.startsWith(prefix)).map(file => file.slice(prefix.length))
81
+ collect(problems, entry.path, checkPackageForPublish({ packageJson: entryPackage, files: entryFiles, sourceKind, expectedId: entry.id }))
82
+ }
83
+ return problems
84
+ }
85
+
86
+ function report(problems) {
87
+ for (const problem of problems) console.log(`${problem.location}: ${problem.code}: ${problem.message}`)
88
+ console.log(problems.length ? `${problems.length} problem${problems.length === 1 ? '' : 's'}` : 'ok')
89
+ }
90
+
91
+ function npm(args, cwd) {
92
+ const result = spawnSync('npm', args, { cwd, stdio: 'inherit', shell: process.platform === 'win32' })
93
+ if (result.error) throw result.error
94
+ return result.status ?? 1
95
+ }
96
+
97
+ function parse(argv) {
98
+ const [command, ...rest] = argv
99
+ const options = { directory: '.', sourceKind: 'npm' }
100
+ for (let index = 0; index < rest.length; index++) {
101
+ const argument = rest[index]
102
+ if (argument === '--source') {
103
+ options.sourceKind = rest[++index]
104
+ if (!['npm', 'git'].includes(options.sourceKind)) throw new Error('--source takes npm or git')
105
+ } else if (argument.startsWith('-')) throw new Error(`unknown option ${argument}`)
106
+ else options.directory = argument
107
+ }
108
+ return { command, ...options, directory: resolve(options.directory) }
109
+ }
110
+
111
+ function check(options) {
112
+ const problems = checkDirectory(options.directory, options)
113
+ report(problems)
114
+ return problems.length ? 1 : 0
115
+ }
116
+
117
+ function pack(options) {
118
+ if (options.sourceKind !== 'npm') throw new Error('pack checks an npm package; --source git does not apply')
119
+ let problems = checkDirectory(options.directory, options)
120
+ const missing = problems.filter(problem => problem.code === 'lockfile_missing')
121
+ for (const problem of missing) {
122
+ const directory = resolve(options.directory, problem.location)
123
+ if (!existsSync(join(directory, 'package-lock.json'))) {
124
+ console.log(`${problem.location}: run npm install first so npm shrinkwrap has a package-lock.json to convert`)
125
+ continue
126
+ }
127
+ console.log(`${problem.location}: creating npm-shrinkwrap.json from package-lock.json`)
128
+ const status = npm(['shrinkwrap'], directory)
129
+ if (status !== 0) return status
130
+ console.log(`${problem.location}: commit npm-shrinkwrap.json; npm publishes it with the package`)
131
+ }
132
+ if (missing.length) problems = checkDirectory(options.directory, options)
133
+ report(problems)
134
+ if (problems.length) return 1
135
+ return npm(['pack', '--dry-run'], options.directory)
136
+ }
137
+
138
+ function main(argv) {
139
+ let options
140
+ try { options = parse(argv) } catch (error) { console.error(error.message); console.error(USAGE); return 2 }
141
+ try {
142
+ if (options.command === 'check') return check(options)
143
+ if (options.command === 'pack') return pack(options)
144
+ } catch (error) { console.error(`standard-plugin: ${error.message}`); return 2 }
145
+ console.error(USAGE)
146
+ return 2
147
+ }
148
+
149
+ process.exitCode = main(process.argv.slice(2))