@intentic/extension-api 1.224.0 → 1.225.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @intentic/extension-api
2
2
 
3
- The one SDK an extension programs against — the extension-author contract for the intentic app.
3
+ The one SDK an extension programs against: the extension-author contract for the intentic app.
4
4
 
5
5
  One of the packages an extension may depend on, with `@intentic/extension-manifest`,
6
6
  `@intentic/extension-ui` and `@intentic/sandbox-contract`. Published to npm; it must stay free of app
@@ -10,22 +10,22 @@ gates extensions.
10
10
  It **does** name `@intentic/sandbox-contract` types, and that is deliberate: `api.sandbox.rpc` is the daemon's
11
11
  own contract as a typed client, which is the whole reason an extension no longer has to build a URL to reach
12
12
  it. The dependency is type-only, so nothing of the contract lands in an extension's runtime. This used to be
13
- forbidden — the contract imported the manifest schema from here, so depending back would have closed a cycle.
13
+ forbidden: the contract imported the manifest schema from here, so depending back would have closed a cycle.
14
14
  `@intentic/extension-manifest` exists to break exactly that, and its README has the reasoning.
15
15
 
16
16
  ## What's here
17
17
 
18
- - **[api.ts](src/api.ts)** — `IntenticApi`, the host surface delivered to `activate(api, context)`. There is
18
+ - **[api.ts](src/api.ts)**: `IntenticApi`, the host surface delivered to `activate(api, context)`. There is
19
19
  no ambient global; everything an extension registers is a `Disposable` pushed onto
20
20
  `context.subscriptions`, so deactivation unwinds it. `api.sandbox.rpc` is the typed daemon client, gated by
21
21
  the manifest's `permissions.sandbox` allowlist exactly as the older `request`/`json` doors are.
22
- - **The manifest schema lives in [@intentic/extension-manifest](../extension-manifest)**, not here — it is what
22
+ - **The manifest schema lives in [@intentic/extension-manifest](../extension-manifest)**, not here: it is what
23
23
  an extension *declares*, and the daemon needs it without needing any of this package. The manifest is the
24
24
  **approval + gating surface**: the install dialog shows exactly the declared contribution points, and the host
25
25
  refuses any runtime registration (view, command, viewer, setting, process…) the approved manifest never
26
26
  declared. Contribution points: `views`, `files`, `viewers`, `documents`, `commands`, `settings`,
27
27
  `processes`, `agent`, `environment`, `capabilities`, `listener`, `automationTemplates`, `bin`, plus the
28
- `permissions.sandbox` route allowlist. That list is not prose to be kept in sync by hand —
28
+ `permissions.sandbox` route allowlist. That list is not prose to be kept in sync by hand:
29
29
  `surface-guard.test.ts` reads it back out of this file and fails when it stops matching the schema.
30
30
  Every one of those points carries its own description, generated out to
31
31
  [an authoring schema](https://intentic.dev/intentic-extension.schema.json); point a manifest's `$schema` at
@@ -33,49 +33,49 @@ forbidden — the contract imported the manifest schema from here, so depending
33
33
  A `listener` owns both halves of its public vocabulary: labelled event types for daemon validation and the
34
34
  source/filter/starter wording a generic automation editor renders. Installing a listener therefore adds a
35
35
  configurable automation source without an app release or a second provider table.
36
- `automationTemplates` is the other half of that bargain: the starting points for a pack's own service —
37
- trigger, prompt, guard, setup instructions — declared by whoever knows the service rather than written into
36
+ `automationTemplates` is the other half of that bargain: the starting points for a pack's own service:
37
+ trigger, prompt, guard, setup instructions: declared by whoever knows the service rather than written into
38
38
  the automations surface. Both fold into one catalogue the daemon serves (`GET /automations/catalog`), which
39
39
  is also what `POST /automations` validates against, so the editor cannot offer a trigger the daemon refuses.
40
- Identity is derived, never declared — `extensionIdOf(manifest) = ${publisher}.${name}`.
41
- - **[facts.ts](src/facts.ts)** — the stable **detection** vocabulary (`RepoFacts`, `CapabilityFacts`) a
40
+ Identity is derived, never declared: `extensionIdOf(manifest) = ${publisher}.${name}`.
41
+ - **[facts.ts](src/facts.ts)**: the stable **detection** vocabulary (`RepoFacts`, `CapabilityFacts`) a
42
42
  view's `detect()` reads to decide when to activate. This is *not* the data plane.
43
- - **[server.ts](src/server.ts)** — `ExtensionServerApi`, the BACKEND half's surface. A manifest `server`
43
+ - **[server.ts](src/server.ts)**: `ExtensionServerApi`, the BACKEND half's surface. A manifest `server`
44
44
  bundle exports `activateServer(api, context)` and runs in the daemon's backend host (one separate
45
45
  supervised process shared by every enabled backend); `api.routes.mount` serves the extension's own
46
46
  `/x/<id>/…` namespace, `api.daemon.request/json` reaches the daemon's routes under the manifest's
47
- `permissions.daemon` allowlist, and workspace files are plain `node:fs` under `api.workspaceRoot` — full
47
+ `permissions.daemon` allowlist, and workspace files are plain `node:fs` under `api.workspaceRoot`: full
48
48
  trust, so paths rather than a file service. The extension's own namespace needs no `permissions.sandbox`
49
49
  entry on the UI side: its backend is its own.
50
50
 
51
51
  Three surfaces, at three different grains, and the grain is what picks one. A **view** activates per *repo*
52
52
  off the facts (`rail`, `directory`, `sandbox`). A **viewer** takes over a *file extension*. A **document**
53
- answers per *directory* — `detect(path)` marks the rows it can explain in the Workspace tree, and the host
53
+ answers per *directory*: `detect(path)` marks the rows it can explain in the Workspace tree, and the host
54
54
  opens the provider's component as a tab beside the code. A monorepo is one repo with fifty-five documented
55
55
  packages, which is exactly the case a per-repo `detect()` cannot express. An offer that is EVIDENCE about the
56
56
  directory rather than an affordance every directory of its kind has says so (`evidence: true`), and the tree
57
- keeps its icon on the row instead of revealing it on hover — the difference between a reader seeing which
57
+ keeps its icon on the row instead of revealing it on hover: the difference between a reader seeing which
58
58
  packages have a page and a reader having to go looking for one.
59
- - **[scope.ts](src/scope.ts)** — `sandboxRef` and `sandboxScopeGuard`: how an extension keeps state that
59
+ - **[scope.ts](src/scope.ts)**, `sandboxRef` and `sandboxScopeGuard`: how an extension keeps state that
60
60
  belongs to ONE sandbox. See "Where state lives" below; this is the rule most easily got wrong, because
61
61
  getting it wrong looks fine until somebody switches sandbox.
62
- - **[background.ts](src/background.ts)** — `sandboxPoll` and `sandboxLedger`: the work an extension does while
62
+ - **[background.ts](src/background.ts)**, `sandboxPoll` and `sandboxLedger`: the work an extension does while
63
63
  none of it is on screen. A tile that badges has to be filled by something, and what has already been seen has
64
64
  to be written down somewhere; both were hand-written in six extensions before they were here.
65
- - **[stream.ts](src/stream.ts)**, **[version.ts](src/version.ts)** — SSE/ndjson helpers and the host API
65
+ - **[stream.ts](src/stream.ts)**, **[version.ts](src/version.ts)**: SSE/ndjson helpers and the host API
66
66
  version (`engines.intentic` is checked against it before activation).
67
67
 
68
68
  Version 2 makes listener contributions self-describing (`events` + `automation`); version 1 listeners only
69
- declared bare event ids and cannot describe a generic editor. Version 2.1 adds the backend half — the
70
- manifest `server` bundle and `permissions.daemon` — additively: a 2.0 manifest is a 2.1 manifest that ships
69
+ declared bare event ids and cannot describe a generic editor. Version 2.1 adds the backend half: the
70
+ manifest `server` bundle and `permissions.daemon`, additively: a 2.0 manifest is a 2.1 manifest that ships
71
71
  no backend.
72
72
 
73
73
  ## The data plane
74
74
 
75
- An extension talks to the daemon over `api.sandbox.request/json(path)` — an authenticated transport (auth is
75
+ An extension talks to the daemon over `api.sandbox.request/json(path)`: an authenticated transport (auth is
76
76
  injected host-side; the bundle never sees a token). **Its reach is not unrestricted:** every path is matched
77
77
  against the extension's manifest `permissions.sandbox` allowlist and an undeclared route throws. Responses
78
- are `sandbox-contract` schemas, parsed at the call site (`Schema.parse(await api.sandbox.json(path))`) — the
78
+ are `sandbox-contract` schemas, parsed at the call site (`Schema.parse(await api.sandbox.json(path))`): the
79
79
  in-repo, compiled-together design means a wire change is a compiler error fixed atomically, so there is no
80
80
  separate "stable data API" to promote. `facts.ts` stays the stable surface only for *detection*.
81
81
 
@@ -84,20 +84,20 @@ separate "stable data API" to promote. `facts.ts` stays the stable surface only
84
84
  Three tiers, and the tier decides what happens when the user points the browser at a **different sandbox**.
85
85
  Everything an extension holds is about one workspace, so a switch has to leave nothing of the last one behind.
86
86
 
87
- - **Cached reads** — `useQuery` in a view, or `api.sandbox.fetch(query)` from outside one. Key them with
87
+ - **Cached reads**: `useQuery` in a view, or `api.sandbox.fetch(query)` from outside one. Key them with
88
88
  `api.sandbox.key(...)` and the switch is handled by construction: the key carries the active sandbox id, so
89
89
  the next box is a different cache entry. Use the *same* key for a view's query and for the badge poll that
90
90
  warms it, and the poll's answer becomes the view's first paint.
91
- - **State inside a mounted component** — an ordinary `ref` in a `.vue` file. Nothing to do; it dies with the
91
+ - **State inside a mounted component**: an ordinary `ref` in a `.vue` file. Nothing to do; it dies with the
92
92
  component.
93
- - **Module state owned by `activate()`** — the badge counts, presence maps and poll results that must survive
93
+ - **Module state owned by `activate()`**: the badge counts, presence maps and poll results that must survive
94
94
  the view being unmounted, because a badge you only see after opening the view is pointless. Declare it with
95
95
  `sandboxRef(() => initial)` and the host empties it on every switch. There is no subscription to remember
96
96
  and no teardown to write; `dispose` is there for state that owns an object URL or anything else the garbage
97
97
  collector will not take back.
98
98
 
99
99
  For anything asynchronous in that third tier, take a `sandboxScopeGuard()` **before** the await and ask it
100
- **after** — a poll issued against the last sandbox otherwise resolves a moment later and writes its answer
100
+ **after**: a poll issued against the last sandbox otherwise resolves a moment later and writes its answer
101
101
  into the new one, which is the same wrong badge with a harder repro. It matters twice over for a call that
102
102
  WRITES: acknowledging what a badge has shown, in the wrong workspace's tree, is bookkeeping no later poll
103
103
  corrects.
@@ -117,16 +117,16 @@ const { state: unseen, start } = sandboxPoll<readonly Finding[]>({
117
117
  ```
118
118
 
119
119
  `start()` returns the `Disposable` to push onto `context.subscriptions`; `refresh()` reads off-cycle for the
120
- moments that should not wait out the interval. The five rules a hand-written version has to remember — never
120
+ moments that should not wait out the interval. The five rules a hand-written version has to remember: never
121
121
  reject, skip an unreachable daemon, discard an answer that outlived its sandbox, keep the last good value on
122
- failure, stop the clock on disposal — are the poll's, not yours. Pass `immediate: false` if there is nothing
122
+ failure, stop the clock on disposal, are the poll's, not yours. Pass `immediate: false` if there is nothing
123
123
  worth asking until something else tells you what to ask about, and read `previous` in `read` if a round
124
124
  accumulates onto what you already hold rather than replacing it.
125
125
 
126
126
  What the tile SAYS stays yours: `badge()` is the judgement each surface exists to make, and no two of them
127
127
  agree about tone or wording.
128
128
 
129
- `sandboxLedger(host, path)` is the other half — the JSON file recording what the owner has already seen, as
129
+ `sandboxLedger(host, path)` is the other half: the JSON file recording what the owner has already seen, as
130
130
  `key → mark`, where the mark is what makes an entry stale. Compare marks (a chore's evidence digest, a story's
131
131
  verdict) and the same key with new evidence is news again; ignore them and it is a plain presence ledger. It
132
132
  reads a missing or mangled file as "nothing acknowledged", writes nothing when nothing moved, and holds the
@@ -134,7 +134,7 @@ scope guard across its own read-then-write so an acknowledgement cannot land in
134
134
 
135
135
  This is not advice. `sandboxScope.guard.test.ts` in the app walks each extension's UI entry through its own
136
136
  imports and refuses module-level `ref`/`shallowRef`/`reactive`, any reassignable module binding, and any
137
- repeating clock in what it reaches — because the failure it prevents was found in six extensions at once: a
137
+ repeating clock in what it reaches, because the failure it prevents was found in six extensions at once: a
138
138
  rail tile reading `21` under a workspace that had two.
139
139
 
140
140
  ## Authoring an extension
@@ -145,16 +145,16 @@ The five UI extensions under [`_extensions/`](../../_extensions) are the working
145
145
 
146
146
  ## Key files
147
147
 
148
- - [src/api.ts](src/api.ts) — the handle an extension is given; the centre of this package.
149
- - [src/facts.ts](src/facts.ts) — the public facts a view's `detect()` answers from.
150
- - [src/engines.ts](src/engines.ts) — how `engines.intentic` is matched against the version below, for the host
148
+ - [src/api.ts](src/api.ts): the handle an extension is given; the centre of this package.
149
+ - [src/facts.ts](src/facts.ts): the public facts a view's `detect()` answers from.
150
+ - [src/engines.ts](src/engines.ts): how `engines.intentic` is matched against the version below, for the host
151
151
  and the daemon alike.
152
- - [src/route.ts](src/route.ts) — the query rules a view with internal navigation uses.
153
- - [src/scope.ts](src/scope.ts) — module state that belongs to one sandbox, and the guard for work in flight
152
+ - [src/route.ts](src/route.ts): the query rules a view with internal navigation uses.
153
+ - [src/scope.ts](src/scope.ts): module state that belongs to one sandbox, and the guard for work in flight
154
154
  across a switch.
155
- - [src/version.ts](src/version.ts) and [src/surface.json](src/surface.json) — the protocol version, and what
155
+ - [src/version.ts](src/version.ts) and [src/surface.json](src/surface.json): the protocol version, and what
156
156
  each version of it promised.
157
157
 
158
- The manifest schema and the `permissions.sandbox` matcher are **not here** — they moved to
158
+ The manifest schema and the `permissions.sandbox` matcher are **not here**: they moved to
159
159
  [@intentic/extension-manifest](../extension-manifest), which exists so the daemon can read a manifest without
160
160
  depending on the browser-facing API.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@intentic/extension-api",
3
- "version": "1.224.0",
4
- "description": "The versioned public API intentic extensions compile against — manifest schema, detection facts and the host API",
3
+ "version": "1.225.0",
4
+ "description": "The versioned public API intentic extensions compile against, manifest schema, detection facts and the host API",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "repository": {
@@ -44,7 +44,7 @@
44
44
  "dependencies": {
45
45
  "@orpc/contract": "1.14.13",
46
46
  "tslib": "2.8.1",
47
- "@intentic/sandbox-contract": "1.224.0"
47
+ "@intentic/sandbox-contract": "1.225.0"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "vue": "3"
package/src/api.ts CHANGED
@@ -488,14 +488,14 @@ export interface IntenticApi {
488
488
  };
489
489
  // Navigate the shell to an app path (e.g. "/capabilities", "/ext/<view>/<key>").
490
490
  readonly navigate: (path: string) => void;
491
- /* THE SAME PATH AS A BROWSER ADDRESS — what a view puts in an `<a href>` so the thing it draws is a real
491
+ /* THE SAME PATH AS A BROWSER ADDRESS: what a view puts in an `<a href>` so the thing it draws is a real
492
492
  * link and not a <button> that happens to move the shell.
493
493
  *
494
494
  * Every row and card in this app that goes somewhere has a URL behind it, and a view that only calls
495
495
  * `navigate` throws all of it away: nothing under the pointer in the status bar, nothing in the browser's
496
496
  * own right-click menu, nothing to copy, and Ctrl/⌘-click navigating the tab the user is reading instead
497
497
  * of opening a second one. So a navigational row renders as `<a :href="api.href(path)">` and calls
498
- * `navigate` from its click handler — guarded with `browserOwnsClick` (@intentic/extension-ui) so a
498
+ * `navigate` from its click handler: guarded with `browserOwnsClick` (@intentic/extension-ui) so a
499
499
  * modified click is left to the browser. */
500
500
  readonly href: (path: string) => string;
501
501
  /* THE URL AS A VIEW'S STATE, so what a reader is looking at can be linked to.
package/src/stream.ts CHANGED
@@ -23,7 +23,7 @@ async function* sseFrames(body: ReadableStream<Uint8Array>): AsyncGenerator<stri
23
23
  const result = await Promise.race([reader.read(), idle]);
24
24
  clearTimeout(timer!);
25
25
  if (result === "idle") {
26
- return; // daemon went silent past the heartbeat window — end rather than hang
26
+ return; // daemon went silent past the heartbeat window: end rather than hang
27
27
  }
28
28
  const { done, value } = result;
29
29
  if (done) {
package/src/version.ts CHANGED
@@ -53,7 +53,7 @@
53
53
  // unrelated update queued behind it. The symptom is a window that stops answering, blamed on whichever
54
54
  // component the loop was noticed in, so the fix has to be a box nothing observes rather than a rule to
55
55
  // remember. Additive: `sandboxRef` is unchanged, and both are emptied on a switch by the same door.
56
- // 2.9.0 adds `api.href` — the same app path the host would navigate to, as a browser address. A view that can
56
+ // 2.9.0 adds `api.href`: the same app path the host would navigate to, as a browser address. A view that can
57
57
  // only call `navigate` has to draw every destination it offers as a <button>, and a button is not a link: no
58
58
  // address under the pointer, nothing in the browser's own right-click menu, nothing to copy, and Ctrl/⌘-click
59
59
  // moving the tab the reader is in instead of opening a second one. Six views across three packs had each drawn