@intentic/extension-api 1.224.0 → 1.226.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 +35 -35
- package/package.json +3 -3
- package/src/api.ts +2 -2
- package/src/stream.ts +1 -1
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @intentic/extension-api
|
|
2
2
|
|
|
3
|
-
The one SDK an extension programs against
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
41
|
-
- **[facts.ts](src/facts.ts)
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
|
70
|
-
manifest `server` bundle and `permissions.daemon
|
|
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)
|
|
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))`)
|
|
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
|
|
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
|
|
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()
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
149
|
-
- [src/facts.ts](src/facts.ts)
|
|
150
|
-
- [src/engines.ts](src/engines.ts)
|
|
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)
|
|
153
|
-
- [src/scope.ts](src/scope.ts)
|
|
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)
|
|
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
|
|
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.
|
|
4
|
-
"description": "The versioned public API intentic extensions compile against
|
|
3
|
+
"version": "1.226.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.
|
|
47
|
+
"@intentic/sandbox-contract": "1.226.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
|
|
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
|
|
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
|
|
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
|
|
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
|