@intentic/extension-manifest 1.312.0 → 1.314.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 +9 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +5 -0
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +38 -3
- package/dist/manifest.js.map +1 -1
- package/dist/meaning.d.ts +21 -0
- package/dist/meaning.d.ts.map +1 -0
- package/dist/meaning.js +127 -0
- package/dist/meaning.js.map +1 -0
- package/dist/points/agent.d.ts +1 -1
- package/dist/points/agent.d.ts.map +1 -1
- package/dist/points/agent.js +3 -2
- package/dist/points/agent.js.map +1 -1
- package/dist/points/bin.d.ts.map +1 -1
- package/dist/points/bin.js +2 -1
- package/dist/points/bin.js.map +1 -1
- package/dist/points/capabilities.d.ts +3 -0
- package/dist/points/capabilities.d.ts.map +1 -1
- package/dist/points/capabilities.js +51 -10
- package/dist/points/capabilities.js.map +1 -1
- package/dist/points/commands.d.ts.map +1 -1
- package/dist/points/commands.js +4 -2
- package/dist/points/commands.js.map +1 -1
- package/dist/points/documents.d.ts.map +1 -1
- package/dist/points/documents.js +2 -1
- package/dist/points/documents.js.map +1 -1
- package/dist/points/environment.d.ts.map +1 -1
- package/dist/points/environment.js +2 -1
- package/dist/points/environment.js.map +1 -1
- package/dist/points/files.d.ts.map +1 -1
- package/dist/points/files.js +2 -1
- package/dist/points/files.js.map +1 -1
- package/dist/points/index.d.ts +15 -1
- package/dist/points/index.d.ts.map +1 -1
- package/dist/points/index.js +3 -0
- package/dist/points/index.js.map +1 -1
- package/dist/points/listener.d.ts.map +1 -1
- package/dist/points/listener.js +2 -1
- package/dist/points/listener.js.map +1 -1
- package/dist/points/processes.d.ts.map +1 -1
- package/dist/points/processes.js +2 -1
- package/dist/points/processes.js.map +1 -1
- package/dist/points/settings.d.ts.map +1 -1
- package/dist/points/settings.js +2 -1
- package/dist/points/settings.js.map +1 -1
- package/dist/points/tools.d.ts +18 -0
- package/dist/points/tools.d.ts.map +1 -0
- package/dist/points/tools.js +31 -0
- package/dist/points/tools.js.map +1 -0
- package/dist/points/viewers.d.ts.map +1 -1
- package/dist/points/viewers.js +2 -1
- package/dist/points/viewers.js.map +1 -1
- package/dist/points/views.d.ts.map +1 -1
- package/dist/points/views.js +4 -2
- package/dist/points/views.js.map +1 -1
- package/dist/powers-diff.d.ts +1 -1
- package/dist/powers-diff.d.ts.map +1 -1
- package/dist/powers-diff.js +6 -56
- package/dist/powers-diff.js.map +1 -1
- package/dist/tool-servers.d.ts +19 -0
- package/dist/tool-servers.d.ts.map +1 -0
- package/dist/tool-servers.js +21 -0
- package/dist/tool-servers.js.map +1 -0
- package/intentic-extension.schema.json +120 -8
- package/package.json +2 -2
- package/src/index.ts +2 -0
- package/src/manifest.ts +40 -3
- package/src/meaning.ts +189 -0
- package/src/points/agent.ts +5 -3
- package/src/points/bin.ts +2 -1
- package/src/points/capabilities.ts +68 -20
- package/src/points/commands.ts +4 -2
- package/src/points/documents.ts +2 -1
- package/src/points/environment.ts +2 -1
- package/src/points/files.ts +2 -1
- package/src/points/index.ts +3 -0
- package/src/points/listener.ts +2 -1
- package/src/points/processes.ts +2 -1
- package/src/points/settings.ts +2 -1
- package/src/points/tools.ts +48 -0
- package/src/points/viewers.ts +2 -1
- package/src/points/views.ts +4 -2
- package/src/powers-diff.ts +9 -60
- package/src/tool-servers.ts +50 -0
|
@@ -54,12 +54,20 @@
|
|
|
54
54
|
"entry": {
|
|
55
55
|
"description": "Repo-relative path of your prebuilt single-file ESM bundle, built with `vue` and `@intentic/extension-api` as externals. Absent ⇒ an extension with no UI.",
|
|
56
56
|
"type": "string",
|
|
57
|
-
"minLength": 1
|
|
57
|
+
"minLength": 1,
|
|
58
|
+
"power": {
|
|
59
|
+
"key": "entry",
|
|
60
|
+
"sentence": "runs a UI bundle in your browser"
|
|
61
|
+
}
|
|
58
62
|
},
|
|
59
63
|
"server": {
|
|
60
64
|
"description": "Repo-relative path of your prebuilt single-file node ESM server bundle, exporting `activateServer`. Served under your own route namespace, which the daemon proxies. Nothing is provided at runtime but node builtins, so bundle everything else in. Absent ⇒ no backend.",
|
|
61
65
|
"type": "string",
|
|
62
|
-
"minLength": 1
|
|
66
|
+
"minLength": 1,
|
|
67
|
+
"power": {
|
|
68
|
+
"key": "server",
|
|
69
|
+
"sentence": "runs a backend bundle inside the daemon's extension host"
|
|
70
|
+
}
|
|
63
71
|
},
|
|
64
72
|
"permissions": {
|
|
65
73
|
"description": "How far this extension may reach into the daemon, as \"<METHOD> <path-glob>\" entries where `*` matches one path segment: e.g. \"GET /panels\", \"POST /panels/*/start\". The install dialog shows these, the host refuses anything undeclared, and the usage ledger records which were actually earned.",
|
|
@@ -69,14 +77,22 @@
|
|
|
69
77
|
"description": "Daemon routes your UI half may call. Your own backend namespace needs no entry: its backend is your own code.",
|
|
70
78
|
"type": "array",
|
|
71
79
|
"items": {
|
|
72
|
-
"type": "string"
|
|
80
|
+
"type": "string",
|
|
81
|
+
"power": {
|
|
82
|
+
"key": "sandbox:${value}",
|
|
83
|
+
"sentence": "its UI calls the sandbox route ${value}"
|
|
84
|
+
}
|
|
73
85
|
}
|
|
74
86
|
},
|
|
75
87
|
"daemon": {
|
|
76
88
|
"description": "Daemon routes your SERVER half may call. Separate from `sandbox` because the two halves run as different principals: the UI as the owner's session, the backend as a minted per-extension token, so a grant to one must never quietly widen the other.",
|
|
77
89
|
"type": "array",
|
|
78
90
|
"items": {
|
|
79
|
-
"type": "string"
|
|
91
|
+
"type": "string",
|
|
92
|
+
"power": {
|
|
93
|
+
"key": "daemon:${value}",
|
|
94
|
+
"sentence": "its backend calls the daemon route ${value}"
|
|
95
|
+
}
|
|
80
96
|
}
|
|
81
97
|
}
|
|
82
98
|
},
|
|
@@ -110,6 +126,10 @@
|
|
|
110
126
|
},
|
|
111
127
|
"badge": {
|
|
112
128
|
"description": "Allow this view to say something on its tile: a count, a glyph, or that work is running there. Declared because a badge interrupts from every other screen in the app; leave it out and any badge the extension registers is dropped.",
|
|
129
|
+
"power": {
|
|
130
|
+
"key": "view-badge:${id}",
|
|
131
|
+
"sentence": "may badge the \"${label}\" tile from any screen"
|
|
132
|
+
},
|
|
113
133
|
"type": "boolean"
|
|
114
134
|
}
|
|
115
135
|
},
|
|
@@ -118,6 +138,10 @@
|
|
|
118
138
|
"label",
|
|
119
139
|
"surface"
|
|
120
140
|
],
|
|
141
|
+
"power": {
|
|
142
|
+
"key": "view:${id}",
|
|
143
|
+
"sentence": "a ${surface} view \"${label}\""
|
|
144
|
+
},
|
|
121
145
|
"additionalProperties": false
|
|
122
146
|
},
|
|
123
147
|
"description": "Sidebar elements this extension may register at runtime. Each entry reserves an id and a surface; the extension supplies the component with api.views.register, and the host refuses any registration this list does not cover."
|
|
@@ -146,6 +170,10 @@
|
|
|
146
170
|
"path",
|
|
147
171
|
"invalidates"
|
|
148
172
|
],
|
|
173
|
+
"power": {
|
|
174
|
+
"key": "files:${path}",
|
|
175
|
+
"sentence": "is told when ${path} changes"
|
|
176
|
+
},
|
|
149
177
|
"additionalProperties": false
|
|
150
178
|
},
|
|
151
179
|
"description": "Which workspace files back your views, so the daemon's file watcher can tell the browser they went stale instead of you polling for it. The agent edits the workspace out of band from every HTTP route, and this push is the only thing that can notice."
|
|
@@ -192,6 +220,10 @@
|
|
|
192
220
|
"extensions",
|
|
193
221
|
"fetch"
|
|
194
222
|
],
|
|
223
|
+
"power": {
|
|
224
|
+
"key": "viewer:${id}",
|
|
225
|
+
"sentence": "${edit?opens and edits:opens}${compare? and compares:} .${extensions|, .} files (${fetch})"
|
|
226
|
+
},
|
|
195
227
|
"additionalProperties": false
|
|
196
228
|
},
|
|
197
229
|
"description": "File formats this extension can render. The host resolves an opened file to your viewer by its extension, fetches the content, and renders your component with it: you keep none of the fetch lifecycle and none of the daemon credentials."
|
|
@@ -215,6 +247,10 @@
|
|
|
215
247
|
"id",
|
|
216
248
|
"label"
|
|
217
249
|
],
|
|
250
|
+
"power": {
|
|
251
|
+
"key": "document:${id}",
|
|
252
|
+
"sentence": "marks workspace directories (\"${label}\")"
|
|
253
|
+
},
|
|
218
254
|
"additionalProperties": false
|
|
219
255
|
},
|
|
220
256
|
"description": "Per-directory documents this extension can offer. Your provider marks the rows in the Workspace tree it has something to say about, and the host opens your component as a tab."
|
|
@@ -244,6 +280,10 @@
|
|
|
244
280
|
},
|
|
245
281
|
"keybinding": {
|
|
246
282
|
"description": "A global keyboard shortcut, e.g. \"Mod+Shift+K\" — `Mod` is ⌘ on Apple and Ctrl elsewhere. Declared here because a global shortcut is consequential: the owner approves it at install, and the host binds only what was approved.",
|
|
283
|
+
"power": {
|
|
284
|
+
"key": "keybinding:${command}",
|
|
285
|
+
"sentence": "the global shortcut ${keybinding} (\"${title}\")"
|
|
286
|
+
},
|
|
247
287
|
"type": "string",
|
|
248
288
|
"pattern": "^\\S+$"
|
|
249
289
|
},
|
|
@@ -256,6 +296,10 @@
|
|
|
256
296
|
"command",
|
|
257
297
|
"title"
|
|
258
298
|
],
|
|
299
|
+
"power": {
|
|
300
|
+
"key": "command:${command}",
|
|
301
|
+
"sentence": "a palette command \"${title}\""
|
|
302
|
+
},
|
|
259
303
|
"additionalProperties": false
|
|
260
304
|
},
|
|
261
305
|
"description": "Commands this extension may register handlers for, surfaced in the command palette. Title, icon and shortcut all come from here rather than from the registration call, because this is what the owner approved at install."
|
|
@@ -307,6 +351,10 @@
|
|
|
307
351
|
},
|
|
308
352
|
"env": {
|
|
309
353
|
"description": "Inject the stored value into the agent's shell environment under this name, every turn. How a credential you hold reaches the agent's command-line tools.",
|
|
354
|
+
"power": {
|
|
355
|
+
"key": "setting-env:${key}",
|
|
356
|
+
"sentence": "puts the \"${key}\" setting into the agent's environment as ${env}"
|
|
357
|
+
},
|
|
310
358
|
"type": "string",
|
|
311
359
|
"pattern": "^[A-Z][A-Z0-9_]*$"
|
|
312
360
|
}
|
|
@@ -355,6 +403,11 @@
|
|
|
355
403
|
"name",
|
|
356
404
|
"command"
|
|
357
405
|
],
|
|
406
|
+
"effect": "process",
|
|
407
|
+
"power": {
|
|
408
|
+
"key": "process:${name}",
|
|
409
|
+
"sentence": "a background process \"${name}\"${autoStart? (starts on boot):}"
|
|
410
|
+
},
|
|
358
411
|
"additionalProperties": false
|
|
359
412
|
},
|
|
360
413
|
"description": "Long-lived background processes the daemon runs for this extension: a gateway holding a connection the daemon must not, a dev server. Managed the same way panel dev servers are, and startable and stoppable from the Extensions tab."
|
|
@@ -367,7 +420,11 @@
|
|
|
367
420
|
"type": "string"
|
|
368
421
|
}
|
|
369
422
|
},
|
|
370
|
-
"
|
|
423
|
+
"power": {
|
|
424
|
+
"key": "agent",
|
|
425
|
+
"sentence": "contributes skills, agents and hooks to the agent's turns"
|
|
426
|
+
},
|
|
427
|
+
"description": "Declare that this checkout is also a Claude Code plugin, so Claude Code turns pick up its skills, agents, hooks and commands. Only Claude Code reads it: give the agent tools with `contributes.tools`, which every runtime gets, and put a skill every runtime should read in a capability card's `skill`. MCP servers in the plugin's `.mcp.json` are deprecated, reach Claude Code alone, and are warned about at load.",
|
|
371
428
|
"additionalProperties": false
|
|
372
429
|
},
|
|
373
430
|
"environment": {
|
|
@@ -382,6 +439,11 @@
|
|
|
382
439
|
"required": [
|
|
383
440
|
"fragment"
|
|
384
441
|
],
|
|
442
|
+
"effect": "image",
|
|
443
|
+
"power": {
|
|
444
|
+
"key": "environment",
|
|
445
|
+
"sentence": "bakes an environment fragment into the sandbox image"
|
|
446
|
+
},
|
|
385
447
|
"description": "A Dockerfile fragment baked into the sandbox image so your tools are actually installed at runtime: a whisper binary, a psql client. The owner approves the composed overlay and rebuilds out of band, so this does not take effect immediately.",
|
|
386
448
|
"additionalProperties": false
|
|
387
449
|
},
|
|
@@ -573,11 +635,13 @@
|
|
|
573
635
|
},
|
|
574
636
|
"fragment": {
|
|
575
637
|
"description": "A Dockerfile fragment holding the client binary this tool needs (psql, mysql, whisper).",
|
|
638
|
+
"effect": "image",
|
|
576
639
|
"type": "string",
|
|
577
640
|
"minLength": 1
|
|
578
641
|
},
|
|
579
642
|
"pack": {
|
|
580
643
|
"description": "A sandbox feature pack name (whisper, llamacpp, browser, …) supplying this tool. Preferred over `fragment`: an image that already bakes the pack needs no rebuild, and there is no copy to drift.",
|
|
644
|
+
"effect": "image",
|
|
581
645
|
"type": "string",
|
|
582
646
|
"minLength": 1
|
|
583
647
|
},
|
|
@@ -624,9 +688,15 @@
|
|
|
624
688
|
"additionalProperties": false
|
|
625
689
|
},
|
|
626
690
|
"mcp": {
|
|
627
|
-
"description": "A path in this extension's backend (`server`) answering MCP over Streamable HTTP
|
|
691
|
+
"description": "Deprecated: declare `contributes.tools` with `perCard` naming this card instead, and serve the tools with `api.tools.serve`. A path in this extension's backend (`server`) answering MCP over Streamable HTTP; every turn granted a card of this kind gets it as a server named by the card's id, each request arriving at `<path>/<card id>`.",
|
|
692
|
+
"effect": "mcp",
|
|
693
|
+
"mintsServer": true,
|
|
694
|
+
"power": {
|
|
695
|
+
"key": "capability-tools:${id}",
|
|
696
|
+
"sentence": "serves MCP tools to the agent for each \"${catalog.name}\" card"
|
|
697
|
+
},
|
|
628
698
|
"type": "string",
|
|
629
|
-
"pattern": "^[a-z0-9][a-z0-9
|
|
699
|
+
"pattern": "^[a-z0-9][a-z0-9-]*(?:\\/[a-z0-9][a-z0-9-]*)*$"
|
|
630
700
|
}
|
|
631
701
|
},
|
|
632
702
|
"required": [
|
|
@@ -1007,6 +1077,7 @@
|
|
|
1007
1077
|
"kind",
|
|
1008
1078
|
"skill"
|
|
1009
1079
|
],
|
|
1080
|
+
"mintsServer": true,
|
|
1010
1081
|
"additionalProperties": false
|
|
1011
1082
|
},
|
|
1012
1083
|
{
|
|
@@ -1192,6 +1263,7 @@
|
|
|
1192
1263
|
"kind",
|
|
1193
1264
|
"skill"
|
|
1194
1265
|
],
|
|
1266
|
+
"mintsServer": true,
|
|
1195
1267
|
"additionalProperties": false
|
|
1196
1268
|
},
|
|
1197
1269
|
{
|
|
@@ -1368,7 +1440,11 @@
|
|
|
1368
1440
|
],
|
|
1369
1441
|
"additionalProperties": false
|
|
1370
1442
|
}
|
|
1371
|
-
]
|
|
1443
|
+
],
|
|
1444
|
+
"power": {
|
|
1445
|
+
"key": "capability:${id}",
|
|
1446
|
+
"sentence": "a ${kind} capability card \"${catalog.name}\""
|
|
1447
|
+
}
|
|
1372
1448
|
},
|
|
1373
1449
|
"description": "Capability cards this pack adds to the \"+\" grid: a connected CLI tool, a site the agent acts on as the owner through the shared browser, an operating system pack, a browser family the owner connects their own copy of, or a preset over a core kind. The card and its form are data here; the machinery that acts on them is core, which is why a card may only name one of these five kinds."
|
|
1374
1450
|
},
|
|
@@ -1531,6 +1607,10 @@
|
|
|
1531
1607
|
"events",
|
|
1532
1608
|
"automation"
|
|
1533
1609
|
],
|
|
1610
|
+
"power": {
|
|
1611
|
+
"key": "listener:${provider}",
|
|
1612
|
+
"sentence": "a realtime listener provider \"${provider}\""
|
|
1613
|
+
},
|
|
1534
1614
|
"description": "A realtime event source this extension supplies, so automations can trigger on it. One declaration feeds both halves: the daemon accepts these event types and serves this provider's control surface, and the automation editor derives its source picker, filters and starter prompt from it, so a newly installed listener is configurable without a matching app release.",
|
|
1535
1615
|
"additionalProperties": false
|
|
1536
1616
|
},
|
|
@@ -1656,7 +1736,39 @@
|
|
|
1656
1736
|
"bin": {
|
|
1657
1737
|
"type": "string",
|
|
1658
1738
|
"minLength": 1,
|
|
1739
|
+
"power": {
|
|
1740
|
+
"key": "bin",
|
|
1741
|
+
"sentence": "puts its shipped tools on the agent's PATH"
|
|
1742
|
+
},
|
|
1659
1743
|
"description": "A checkout-relative directory of executables the daemon puts on the agent's PATH every turn, how you ship the agent a command-line tool. The files are the approved code themselves: they ride the pinned checkout, and the daemon only adds the directory to PATH."
|
|
1744
|
+
},
|
|
1745
|
+
"tools": {
|
|
1746
|
+
"type": "object",
|
|
1747
|
+
"properties": {
|
|
1748
|
+
"perCard": {
|
|
1749
|
+
"description": "The id of one of this extension's `cli` capability cards. Every turn granted a card of that kind gets one server named by the card's id, handed that card's settings (secrets included) with each call. Absent ⇒ one server for the extension, named by its `name`, in every turn while the extension is enabled.",
|
|
1750
|
+
"type": "string",
|
|
1751
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
1752
|
+
},
|
|
1753
|
+
"process": {
|
|
1754
|
+
"description": "A process from `contributes.processes`, declared with `port: \"auto\"`, that answers MCP over Streamable HTTP at `path` on its port. Absent ⇒ your `server` bundle serves the tools.",
|
|
1755
|
+
"type": "string",
|
|
1756
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
1757
|
+
},
|
|
1758
|
+
"path": {
|
|
1759
|
+
"description": "Where the MCP endpoint answers, without a leading or trailing slash: on the process's port, or in your backend's own namespace when your `server` bundle speaks MCP itself. Absent with no `process` ⇒ the host serves what `api.tools.serve` returns, which is what you want: the host owns the transport, the deadlines and the card lookup. With `perCard`, a request arrives at `<path>/<card id>`.",
|
|
1760
|
+
"type": "string",
|
|
1761
|
+
"pattern": "^[a-z0-9][a-z0-9-]*(?:\\/[a-z0-9][a-z0-9-]*)*$"
|
|
1762
|
+
}
|
|
1763
|
+
},
|
|
1764
|
+
"effect": "mcp",
|
|
1765
|
+
"mintsServer": true,
|
|
1766
|
+
"power": {
|
|
1767
|
+
"key": "tools${perCard?-${perCard}:}",
|
|
1768
|
+
"sentence": "gives the agent MCP tools${perCard?, one server for each \"${perCard}\" card:}"
|
|
1769
|
+
},
|
|
1770
|
+
"description": "Tools for the agent, as an MCP server the daemon mounts into every turn and every runtime (Claude Code, Codex, Cursor, ACP agents). Serve them from your `server` bundle with `api.tools.serve((card) => [...])`, or from a declared process's port. Replaces an agent plugin's `.mcp.json`, which only Claude Code read.",
|
|
1771
|
+
"additionalProperties": false
|
|
1660
1772
|
}
|
|
1661
1773
|
},
|
|
1662
1774
|
"additionalProperties": false
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentic/extension-manifest",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.314.0",
|
|
4
4
|
"description": "What an intentic extension DECLARES, the intentic-extension.json schema and the sandbox-route allowlist rule",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@intentic/base": "1.
|
|
35
|
+
"@intentic/base": "1.314.0",
|
|
36
36
|
"tslib": "2.8.1",
|
|
37
37
|
"zod": "4.6.5"
|
|
38
38
|
},
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,8 @@ export * from "./bundle.js";
|
|
|
2
2
|
export * from "./contribution-point.js";
|
|
3
3
|
export * from "./json-schema.js";
|
|
4
4
|
export * from "./manifest.js";
|
|
5
|
+
export * from "./meaning.js";
|
|
5
6
|
export * from "./permissions.js";
|
|
6
7
|
export * from "./points/index.js";
|
|
7
8
|
export * from "./powers-diff.js";
|
|
9
|
+
export * from "./tool-servers.js";
|
package/src/manifest.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { contributesSchema } from "./points/index.js";
|
|
|
6
6
|
// dialog renders, and the host refuses any runtime registration whose id it didn't declare here. This file is the
|
|
7
7
|
// envelope only; contributions are assembled from points/.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
const ManifestShape = z.object({
|
|
10
10
|
// Declared so it survives the parse instead of being silently stripped; nothing at runtime reads it.
|
|
11
11
|
$schema: z.string().optional().describe("The authoring schema, for editor completion and validation. Nothing at runtime reads it."),
|
|
12
12
|
publisher: z.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
@@ -32,6 +32,7 @@ export const ExtensionManifestSchema = z.object({
|
|
|
32
32
|
.string()
|
|
33
33
|
.min(1)
|
|
34
34
|
.refine((value) => !value.split("/").includes(".."), { message: "entry must stay inside the checkout" })
|
|
35
|
+
.meta({ power: { key: "entry", sentence: "runs a UI bundle in your browser" } })
|
|
35
36
|
.optional()
|
|
36
37
|
.describe(
|
|
37
38
|
"Repo-relative path of your prebuilt single-file ESM bundle, built with `vue` and `@intentic/extension-api` as externals. Absent ⇒ an extension with no UI.",
|
|
@@ -41,6 +42,7 @@ export const ExtensionManifestSchema = z.object({
|
|
|
41
42
|
.string()
|
|
42
43
|
.min(1)
|
|
43
44
|
.refine((value) => !value.split("/").includes(".."), { message: "server must stay inside the checkout" })
|
|
45
|
+
.meta({ power: { key: "server", sentence: "runs a backend bundle inside the daemon's extension host" } })
|
|
44
46
|
.optional()
|
|
45
47
|
.describe(
|
|
46
48
|
"Repo-relative path of your prebuilt single-file node ESM server bundle, exporting `activateServer`. Served under your own route namespace, which the daemon proxies. Nothing is provided at runtime but node builtins, so bundle everything else in. Absent ⇒ no backend.",
|
|
@@ -52,11 +54,11 @@ export const ExtensionManifestSchema = z.object({
|
|
|
52
54
|
permissions: z
|
|
53
55
|
.object({
|
|
54
56
|
sandbox: z
|
|
55
|
-
.array(z.string())
|
|
57
|
+
.array(z.string().meta({ power: { key: "sandbox:${value}", sentence: "its UI calls the sandbox route ${value}" } }))
|
|
56
58
|
.optional()
|
|
57
59
|
.describe("Daemon routes your UI half may call. Your own backend namespace needs no entry: its backend is your own code."),
|
|
58
60
|
daemon: z
|
|
59
|
-
.array(z.string())
|
|
61
|
+
.array(z.string().meta({ power: { key: "daemon:${value}", sentence: "its backend calls the daemon route ${value}" } }))
|
|
60
62
|
.optional()
|
|
61
63
|
.describe(
|
|
62
64
|
"Daemon routes your SERVER half may call. Separate from `sandbox` because the two halves run as different principals: the UI as the owner's session, the backend as a minted per-extension token, so a grant to one must never quietly widen the other.",
|
|
@@ -68,6 +70,41 @@ export const ExtensionManifestSchema = z.object({
|
|
|
68
70
|
),
|
|
69
71
|
contributes: contributesSchema.optional(),
|
|
70
72
|
});
|
|
73
|
+
|
|
74
|
+
// What each field means taken together, which no one field's schema can say: tools need something to serve them.
|
|
75
|
+
const servedTools = (manifest: z.infer<typeof ManifestShape>, ctx: z.RefinementCtx): void => {
|
|
76
|
+
const contributes = manifest.contributes;
|
|
77
|
+
const tools = contributes?.tools;
|
|
78
|
+
const cards = contributes?.capabilities ?? [];
|
|
79
|
+
for (const card of cards) {
|
|
80
|
+
if (card.kind === "cli" && card.mcp !== undefined && manifest.server === undefined) {
|
|
81
|
+
ctx.addIssue({ code: "custom", path: ["contributes", "capabilities"], message: `card "${card.id}" declares \`mcp\`, which only a \`server\` bundle can answer` });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (tools === undefined) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (tools.process !== undefined) {
|
|
88
|
+
const process = contributes?.processes?.find((entry) => entry.name === tools.process);
|
|
89
|
+
if (process?.port !== "auto") {
|
|
90
|
+
ctx.addIssue({
|
|
91
|
+
code: "custom",
|
|
92
|
+
path: ["contributes", "tools", "process"],
|
|
93
|
+
message: `tools.process must name a process in contributes.processes declared with port: "auto"`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (tools.path === undefined) {
|
|
97
|
+
ctx.addIssue({ code: "custom", path: ["contributes", "tools", "path"], message: "tools served by a process need the path its MCP endpoint answers at" });
|
|
98
|
+
}
|
|
99
|
+
} else if (manifest.server === undefined) {
|
|
100
|
+
ctx.addIssue({ code: "custom", path: ["contributes", "tools"], message: "tools need a `server` bundle to serve them, or a `process` that does" });
|
|
101
|
+
}
|
|
102
|
+
if (tools.perCard !== undefined && !cards.some((card) => card.kind === "cli" && card.id === tools.perCard)) {
|
|
103
|
+
ctx.addIssue({ code: "custom", path: ["contributes", "tools", "perCard"], message: `tools.perCard must name one of this extension's cli cards` });
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const ExtensionManifestSchema = ManifestShape.superRefine(servedTools);
|
|
71
108
|
export type ExtensionManifest = z.infer<typeof ExtensionManifestSchema>;
|
|
72
109
|
|
|
73
110
|
// The extension's identity everywhere (capability entries, /ext routes, settings namespaces); derived, never declared,
|
package/src/meaning.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// What a manifest field MEANS to the host, declared on the field itself with `.meta()`, so the schema is the one place
|
|
4
|
+
// that says it: the powers an update re-asks for (powers-diff.ts), what adding a card does to the sandbox (the capability
|
|
5
|
+
// catalog's effects) and which capability kinds register an `mcp__<id>__` server (the contract's reserved names) are all
|
|
6
|
+
// read from here, never re-listed beside the schema.
|
|
7
|
+
|
|
8
|
+
// A template is plain text with `${path}` holes filled from the entry the field belongs to: `${id}`, `${catalog.name}`,
|
|
9
|
+
// and `${value}` for a scalar in a list. `${path|sep}` joins a list with `sep` (", " by default), `${path?yes:no}` picks
|
|
10
|
+
// by whether the value is set and not false.
|
|
11
|
+
export interface PowerMeaning {
|
|
12
|
+
// The stable key an owner's approval is pinned to: an update re-asks when a key appears.
|
|
13
|
+
readonly key: string;
|
|
14
|
+
// The plain sentence shown for it.
|
|
15
|
+
readonly sentence: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// The capability-catalog effect a field adds when it is present on a card (`@intentic/capability-catalog`, effects.ts).
|
|
19
|
+
export type EffectMeaning = "mcp" | "image" | "process";
|
|
20
|
+
|
|
21
|
+
export interface FieldMeaning {
|
|
22
|
+
// Present (and not `false`) grants this power.
|
|
23
|
+
readonly power?: PowerMeaning;
|
|
24
|
+
readonly effect?: EffectMeaning;
|
|
25
|
+
// A card of this kind, or with this field set, registers an `mcp__<card id>__` server for the turn.
|
|
26
|
+
readonly mintsServer?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The meaning a schema carries, gathered through its wrappers (optional, default, pipe): `.meta()` may sit on any of
|
|
30
|
+
// them, and a `.describe()` clone inherits its parent's.
|
|
31
|
+
const readMeta = (schema: z.ZodType): FieldMeaning => {
|
|
32
|
+
const { power, effect, mintsServer } = (schema.meta() ?? {}) as FieldMeaning;
|
|
33
|
+
return { ...(power === undefined ? {} : { power }), ...(effect === undefined ? {} : { effect }), ...(mintsServer === undefined ? {} : { mintsServer }) };
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const WRAPPERS = new Set(["optional", "nullable", "default", "prefault", "readonly", "nonoptional", "catch"]);
|
|
37
|
+
|
|
38
|
+
interface Unwrapped {
|
|
39
|
+
readonly schema: z.ZodType;
|
|
40
|
+
readonly meaning: FieldMeaning;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The schema under its wrappers, with every meaning met on the way down merged (the outermost wins a clash).
|
|
44
|
+
export const unwrapMeaning = (schema: z.ZodType): Unwrapped => {
|
|
45
|
+
let current = schema;
|
|
46
|
+
// Outermost first, so the merge below lets it win.
|
|
47
|
+
const met: FieldMeaning[] = [];
|
|
48
|
+
for (;;) {
|
|
49
|
+
met.unshift(readMeta(current));
|
|
50
|
+
const def = current._zod.def as unknown as { type: string; innerType?: z.ZodType; in?: z.ZodType };
|
|
51
|
+
if (WRAPPERS.has(def.type) && def.innerType !== undefined) {
|
|
52
|
+
current = def.innerType;
|
|
53
|
+
} else if (def.type === "pipe" && def.in !== undefined) {
|
|
54
|
+
current = def.in;
|
|
55
|
+
} else {
|
|
56
|
+
return { schema: current, meaning: Object.assign({}, ...met) as FieldMeaning };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
62
|
+
|
|
63
|
+
const lookup = (context: Record<string, unknown>, path: string): unknown =>
|
|
64
|
+
path.split(".").reduce<unknown>((at, part) => (isRecord(at) ? at[part] : undefined), context);
|
|
65
|
+
|
|
66
|
+
const present = (value: unknown): boolean => value !== undefined && value !== null && value !== false && value !== "";
|
|
67
|
+
|
|
68
|
+
// The template's holes, outermost only, so a branch of `${path?yes:no}` may hold holes of its own.
|
|
69
|
+
const holesOf = (template: string): { readonly start: number; readonly end: number; readonly body: string }[] => {
|
|
70
|
+
const holes: { start: number; end: number; body: string }[] = [];
|
|
71
|
+
for (let at = template.indexOf("${"); at !== -1; at = template.indexOf("${", at)) {
|
|
72
|
+
let depth = 0;
|
|
73
|
+
let close = -1;
|
|
74
|
+
for (let i = at + 1; i < template.length; i++) {
|
|
75
|
+
if (template[i] === "{") {
|
|
76
|
+
depth++;
|
|
77
|
+
} else if (template[i] === "}" && --depth === 0) {
|
|
78
|
+
close = i;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (close === -1) {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
holes.push({ start: at, end: close + 1, body: template.slice(at + 2, close) });
|
|
86
|
+
at = close + 1;
|
|
87
|
+
}
|
|
88
|
+
return holes;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Fills a template's holes from `context`; see PowerMeaning for the three forms.
|
|
92
|
+
export const fillTemplate = (template: string, context: Record<string, unknown>): string => {
|
|
93
|
+
let filled = "";
|
|
94
|
+
let from = 0;
|
|
95
|
+
for (const hole of holesOf(template)) {
|
|
96
|
+
filled += template.slice(from, hole.start);
|
|
97
|
+
from = hole.end;
|
|
98
|
+
const choice = /^([\w.]+)\?([\s\S]*)$/u.exec(hole.body);
|
|
99
|
+
if (choice !== null) {
|
|
100
|
+
// The branch separator is the first `:` outside a nested hole.
|
|
101
|
+
const branches = choice[2] ?? "";
|
|
102
|
+
let depth = 0;
|
|
103
|
+
let split = -1;
|
|
104
|
+
for (let i = 0; i < branches.length; i++) {
|
|
105
|
+
const char = branches[i];
|
|
106
|
+
depth += char === "{" ? 1 : char === "}" ? -1 : 0;
|
|
107
|
+
if (char === ":" && depth === 0) {
|
|
108
|
+
split = i;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const [yes, no] = split === -1 ? [branches, ""] : [branches.slice(0, split), branches.slice(split + 1)];
|
|
113
|
+
filled += fillTemplate(present(lookup(context, choice[1] ?? "")) ? yes : no, context);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const [path = "", separator = ", "] = hole.body.split("|");
|
|
117
|
+
const value = lookup(context, path);
|
|
118
|
+
filled += Array.isArray(value) ? value.map(String).join(separator) : value === undefined || value === null ? "" : String(value);
|
|
119
|
+
}
|
|
120
|
+
return filled + template.slice(from);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Which member of a union a value is: the discriminated union's own key when it has one, else the first that parses.
|
|
124
|
+
const optionFor = (options: readonly z.ZodType[], value: unknown, discriminator: string | undefined): z.ZodType | undefined => {
|
|
125
|
+
if (discriminator !== undefined && isRecord(value)) {
|
|
126
|
+
for (const option of options) {
|
|
127
|
+
const shape = (unwrapMeaning(option).schema as unknown as { shape?: Record<string, z.ZodType> }).shape;
|
|
128
|
+
const literal = shape?.[discriminator]?._zod.def as { values?: readonly unknown[] } | undefined;
|
|
129
|
+
if (literal?.values?.includes(value[discriminator]) === true) {
|
|
130
|
+
return option;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return options.find((option) => option.safeParse(value).success);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Walks a value alongside its schema, calling `visit` for each node carrying a meaning, with the value there and the
|
|
138
|
+
// entry it belongs to (the object itself for an object, the enclosing one for a field, `{ value }` over it for a scalar
|
|
139
|
+
// in a list).
|
|
140
|
+
export const walkMeaning = (
|
|
141
|
+
schema: z.ZodType,
|
|
142
|
+
value: unknown,
|
|
143
|
+
visit: (meaning: FieldMeaning, context: Record<string, unknown>) => void,
|
|
144
|
+
parent: Record<string, unknown> = {},
|
|
145
|
+
): void => {
|
|
146
|
+
if (!present(value)) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const { schema: inner, meaning } = unwrapMeaning(schema);
|
|
150
|
+
const context = isRecord(value) ? value : { ...parent, value };
|
|
151
|
+
if (Object.keys(meaning).length > 0) {
|
|
152
|
+
visit(meaning, context);
|
|
153
|
+
}
|
|
154
|
+
const def = inner._zod.def as unknown as {
|
|
155
|
+
type: string;
|
|
156
|
+
shape?: Record<string, z.ZodType>;
|
|
157
|
+
element?: z.ZodType;
|
|
158
|
+
options?: readonly z.ZodType[];
|
|
159
|
+
discriminator?: string;
|
|
160
|
+
};
|
|
161
|
+
if (def.type === "object" && def.shape !== undefined && isRecord(value)) {
|
|
162
|
+
for (const [key, field] of Object.entries(def.shape)) {
|
|
163
|
+
walkMeaning(field, value[key], visit, value);
|
|
164
|
+
}
|
|
165
|
+
} else if (def.type === "array" && def.element !== undefined && Array.isArray(value)) {
|
|
166
|
+
for (const element of value) {
|
|
167
|
+
walkMeaning(def.element, element, visit, parent);
|
|
168
|
+
}
|
|
169
|
+
} else if (def.type === "union" && def.options !== undefined) {
|
|
170
|
+
const option = optionFor(def.options, value, def.discriminator);
|
|
171
|
+
if (option !== undefined) {
|
|
172
|
+
walkMeaning(option, value, visit, parent);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// Every field of an object schema (or a union of them) that carries a meaning, by key, without a value: what a schema
|
|
178
|
+
// declares, as opposed to what one manifest uses.
|
|
179
|
+
export const fieldMeanings = (schema: z.ZodType): Map<string, FieldMeaning> => {
|
|
180
|
+
const meanings = new Map<string, FieldMeaning>();
|
|
181
|
+
const shape = (unwrapMeaning(schema).schema as unknown as { shape?: Record<string, z.ZodType> }).shape ?? {};
|
|
182
|
+
for (const [key, field] of Object.entries(shape)) {
|
|
183
|
+
const { meaning } = unwrapMeaning(field);
|
|
184
|
+
if (Object.keys(meaning).length > 0) {
|
|
185
|
+
meanings.set(key, meaning);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return meanings;
|
|
189
|
+
};
|
package/src/points/agent.ts
CHANGED
|
@@ -3,15 +3,17 @@ import type { ContributionPoint } from "../contribution-point.js";
|
|
|
3
3
|
|
|
4
4
|
// "This checkout is ALSO a Claude Code plugin": the daemon hands the directory to the Agent SDK's plugin
|
|
5
5
|
// loader, which reads skills/agents/hooks/commands/.mcp.json each turn, the daemon never parses plugin
|
|
6
|
-
// internals.
|
|
6
|
+
// internals. Only Claude Code turns read it, so tools belong in `contributes.tools` and a skill every runtime should
|
|
7
|
+
// read in a capability card's `skill`; a `.mcp.json` here is deprecated and warned about at load.
|
|
7
8
|
export const AgentContributionSchema = z.object({
|
|
8
9
|
path: z.string().optional().describe("Relative to the extension checkout. Absent ⇒ the checkout root."),
|
|
9
|
-
})
|
|
10
|
+
})
|
|
11
|
+
.meta({ power: { key: "agent", sentence: "contributes skills, agents and hooks to the agent's turns" } });
|
|
10
12
|
export type AgentContribution = z.infer<typeof AgentContributionSchema>;
|
|
11
13
|
|
|
12
14
|
export const agentPoint = {
|
|
13
15
|
name: "agent",
|
|
14
16
|
description:
|
|
15
|
-
"Declare that this checkout is also a Claude Code plugin, so
|
|
17
|
+
"Declare that this checkout is also a Claude Code plugin, so Claude Code turns pick up its skills, agents, hooks and commands. Only Claude Code reads it: give the agent tools with `contributes.tools`, which every runtime gets, and put a skill every runtime should read in a capability card's `skill`. MCP servers in the plugin's `.mcp.json` are deprecated, reach Claude Code alone, and are warned about at load.",
|
|
16
18
|
schema: AgentContributionSchema,
|
|
17
19
|
} as const satisfies ContributionPoint;
|
package/src/points/bin.ts
CHANGED
|
@@ -11,5 +11,6 @@ export const binPoint = {
|
|
|
11
11
|
schema: z
|
|
12
12
|
.string()
|
|
13
13
|
.min(1)
|
|
14
|
-
.refine((value) => !value.split("/").includes(".."), { message: "bin must stay inside the checkout" })
|
|
14
|
+
.refine((value) => !value.split("/").includes(".."), { message: "bin must stay inside the checkout" })
|
|
15
|
+
.meta({ power: { key: "bin", sentence: "puts its shipped tools on the agent's PATH" } }),
|
|
15
16
|
} as const satisfies ContributionPoint;
|