@intentic/extension-manifest 1.176.3

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.
Files changed (114) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/dist/bundle.d.ts +4 -0
  4. package/dist/bundle.d.ts.map +1 -0
  5. package/dist/bundle.js +22 -0
  6. package/dist/bundle.js.map +1 -0
  7. package/dist/contribution-point.d.ts +7 -0
  8. package/dist/contribution-point.d.ts.map +1 -0
  9. package/dist/contribution-point.js +2 -0
  10. package/dist/contribution-point.js.map +1 -0
  11. package/dist/index.d.ts +8 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +8 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/json-schema.d.ts +4 -0
  16. package/dist/json-schema.d.ts.map +1 -0
  17. package/dist/json-schema.js +33 -0
  18. package/dist/json-schema.js.map +1 -0
  19. package/dist/manifest.d.ts +300 -0
  20. package/dist/manifest.d.ts.map +1 -0
  21. package/dist/manifest.js +46 -0
  22. package/dist/manifest.js.map +1 -0
  23. package/dist/mark.d.ts +6 -0
  24. package/dist/mark.d.ts.map +1 -0
  25. package/dist/mark.js +12 -0
  26. package/dist/mark.js.map +1 -0
  27. package/dist/permissions.d.ts +2 -0
  28. package/dist/permissions.d.ts.map +1 -0
  29. package/dist/permissions.js +21 -0
  30. package/dist/permissions.js.map +1 -0
  31. package/dist/points/agent.d.ts +13 -0
  32. package/dist/points/agent.d.ts.map +1 -0
  33. package/dist/points/agent.js +10 -0
  34. package/dist/points/agent.js.map +1 -0
  35. package/dist/points/automation-templates.d.ts +67 -0
  36. package/dist/points/automation-templates.d.ts.map +1 -0
  37. package/dist/points/automation-templates.js +47 -0
  38. package/dist/points/automation-templates.js.map +1 -0
  39. package/dist/points/bin.d.ts +7 -0
  40. package/dist/points/bin.d.ts.map +1 -0
  41. package/dist/points/bin.js +10 -0
  42. package/dist/points/bin.js.map +1 -0
  43. package/dist/points/capabilities.d.ts +352 -0
  44. package/dist/points/capabilities.d.ts.map +1 -0
  45. package/dist/points/capabilities.js +131 -0
  46. package/dist/points/capabilities.js.map +1 -0
  47. package/dist/points/commands.d.ts +21 -0
  48. package/dist/points/commands.d.ts.map +1 -0
  49. package/dist/points/commands.js +23 -0
  50. package/dist/points/commands.js.map +1 -0
  51. package/dist/points/documents.d.ts +15 -0
  52. package/dist/points/documents.d.ts.map +1 -0
  53. package/dist/points/documents.js +14 -0
  54. package/dist/points/documents.js.map +1 -0
  55. package/dist/points/environment.d.ts +13 -0
  56. package/dist/points/environment.d.ts.map +1 -0
  57. package/dist/points/environment.js +14 -0
  58. package/dist/points/environment.js.map +1 -0
  59. package/dist/points/files.d.ts +15 -0
  60. package/dist/points/files.d.ts.map +1 -0
  61. package/dist/points/files.js +20 -0
  62. package/dist/points/files.js.map +1 -0
  63. package/dist/points/index.d.ts +609 -0
  64. package/dist/points/index.d.ts.map +1 -0
  65. package/dist/points/index.js +44 -0
  66. package/dist/points/index.js.map +1 -0
  67. package/dist/points/listener.d.ts +51 -0
  68. package/dist/points/listener.d.ts.map +1 -0
  69. package/dist/points/listener.js +44 -0
  70. package/dist/points/listener.js.map +1 -0
  71. package/dist/points/processes.d.ts +23 -0
  72. package/dist/points/processes.d.ts.map +1 -0
  73. package/dist/points/processes.js +15 -0
  74. package/dist/points/processes.js.map +1 -0
  75. package/dist/points/settings.d.ts +37 -0
  76. package/dist/points/settings.d.ts.map +1 -0
  77. package/dist/points/settings.js +24 -0
  78. package/dist/points/settings.js.map +1 -0
  79. package/dist/points/viewers.d.ts +25 -0
  80. package/dist/points/viewers.d.ts.map +1 -0
  81. package/dist/points/viewers.js +17 -0
  82. package/dist/points/viewers.js.map +1 -0
  83. package/dist/points/views.d.ts +27 -0
  84. package/dist/points/views.d.ts.map +1 -0
  85. package/dist/points/views.js +18 -0
  86. package/dist/points/views.js.map +1 -0
  87. package/dist/powers-diff.d.ts +8 -0
  88. package/dist/powers-diff.d.ts.map +1 -0
  89. package/dist/powers-diff.js +77 -0
  90. package/dist/powers-diff.js.map +1 -0
  91. package/intentic-extension.schema.json +1341 -0
  92. package/package.json +49 -0
  93. package/src/bundle.ts +41 -0
  94. package/src/contribution-point.ts +26 -0
  95. package/src/index.ts +7 -0
  96. package/src/json-schema.ts +62 -0
  97. package/src/manifest.ts +104 -0
  98. package/src/mark.ts +34 -0
  99. package/src/permissions.ts +36 -0
  100. package/src/points/agent.ts +17 -0
  101. package/src/points/automation-templates.ts +84 -0
  102. package/src/points/bin.ts +15 -0
  103. package/src/points/capabilities.ts +271 -0
  104. package/src/points/commands.ts +44 -0
  105. package/src/points/documents.ts +31 -0
  106. package/src/points/environment.ts +24 -0
  107. package/src/points/files.ts +54 -0
  108. package/src/points/index.ts +73 -0
  109. package/src/points/listener.ts +81 -0
  110. package/src/points/processes.ts +20 -0
  111. package/src/points/settings.ts +31 -0
  112. package/src/points/viewers.ts +36 -0
  113. package/src/points/views.ts +33 -0
  114. package/src/powers-diff.ts +100 -0
@@ -0,0 +1,271 @@
1
+ import { evaluateWhen, isWhenExpression, parseWhen } from "@intentic/base/when";
2
+ import { z } from "zod";
3
+ import type { ContributionPoint } from "../contribution-point.js";
4
+ import { MARK_FIELDS } from "../mark.js";
5
+
6
+ // A field the "+" install dialog renders for a capability's config form (a slug key, a label, secret/optional
7
+ // flags, an optional select, a `when` gate). Mirrors the platform catalog's field shape so the web can render
8
+ // contributed cards from installed extensions exactly like core capability cards.
9
+ export const CapabilityFieldSchema = z.object({
10
+ key: z.string().regex(/^[a-zA-Z][a-zA-Z0-9]*$/),
11
+ label: z.string().min(1),
12
+ placeholder: z.string().optional(),
13
+ secret: z.boolean().optional().describe("Mask it, and never echo it back."),
14
+ optional: z.boolean().optional(),
15
+ multiline: z.boolean().optional(),
16
+ // An OPT-IN EXTRA rather than a decision — rendered as a switch, carried as the "on"/"off" the config
17
+ // schemas already speak (the vpn's pfs/aggressive precedent). A two-option Segmented can express the same
18
+ // value, and reads wrong for this: it presents a choice the user must make to proceed, sized like the
19
+ // required fields around it. Something the capability works fine without wants the control that is quiet
20
+ // when it is off. A switch always holds one of its two values, so such a field never blocks a submit
21
+ // whatever `optional` says.
22
+ boolean: z
23
+ .boolean()
24
+ .optional()
25
+ .describe(
26
+ 'Render it as a switch, carrying "on"/"off". For an opt-in EXTRA rather than a decision: a two-option picker says the same thing but presents a choice the user must make to proceed, sized like the required fields around it. A switch always holds a value, so a field like this never blocks a submit.',
27
+ ),
28
+ // A line under the control, for what the user cannot see from the label alone (a host requirement, when a
29
+ // value takes effect). The card's `hint` speaks for the whole card; this one is bound to the field it
30
+ // qualifies, which is where a per-option caveat has to sit to be read at all.
31
+ hint: z
32
+ .string()
33
+ .optional()
34
+ .describe(
35
+ "A line under this control, for what the label alone cannot say — a host requirement, when a value takes effect. The card's own `hint` speaks for the whole card; this one is bound to the field it qualifies.",
36
+ ),
37
+ /* This field's value only takes effect after the sandbox is REBUILT — it rides the environment overlay
38
+ * rather than something the daemon can act on now. Rendered as a chip beside the label.
39
+ *
40
+ * The one fact a user needs before touching a control, and the one the form cannot infer: two switches
41
+ * side by side, identical in every visible way, can cost five seconds and five minutes. The docker card
42
+ * has exactly that pair (its GPU option is baked into the image; its engine options are a file dockerd
43
+ * rereads), and without this flag the only way to find out which you pressed is to press it. */
44
+ rebuild: z
45
+ .boolean()
46
+ .optional()
47
+ .describe(
48
+ "This value only takes effect after the sandbox is rebuilt, because it rides the image overlay. Shown as a chip beside the label — two switches side by side, identical in every visible way, can otherwise cost five seconds or five minutes with no way to tell which.",
49
+ ),
50
+ default: z.string().optional(),
51
+ options: z
52
+ .array(z.object({ value: z.string(), label: z.string() }))
53
+ .optional()
54
+ .describe("Turns the field into a select."),
55
+ /* Gates this field on the answers already given — the SSH credential that only applies to the auth mode
56
+ * chosen, the gateway fields that belong to one VPN provider. A `when` condition (@intentic/base/when)
57
+ * evaluated against the form's live values, so it re-reads as the user toggles.
58
+ *
59
+ * Refused at parse when it does not parse. A card is data an extension ships, and a condition nobody can
60
+ * evaluate is not a field that is always shown or always hidden — it is a card whose author believes it
61
+ * asks something it never asks. Failing the manifest names the card; failing at render names nothing. */
62
+ when: z
63
+ .string()
64
+ .refine(isWhenExpression, { message: "not a valid `when` condition" })
65
+ .optional()
66
+ .describe(
67
+ "Only show this field while a condition over the answers already given holds — `auth == 'key'`, `provider in ['ipsec', 'fortinet']`, `!advanced`. Supports `&&`, `||`, `!`, comparisons and `in`.",
68
+ ),
69
+ // A fixed value baked into the config rather than asked for — how a card pins a discriminator
70
+ // (platform="reddit", provider="stripe"). Rendered as nothing; sent as itself.
71
+ value: z
72
+ .string()
73
+ .optional()
74
+ .describe(
75
+ 'A fixed value baked into the config rather than asked for — how a card pins its discriminator (platform="reddit", provider="stripe"). Renders as nothing.',
76
+ ),
77
+ /* This field holds a TOTP seed — the base32 key (or otpauth:// URI) a service shows when enrolling an
78
+ * authenticator app. Declare it WITH `secret: true`: the seed is a durable second factor, so it is never
79
+ * echoed and, unlike an ordinary secret, never enters the agent's environment either — the daemon mints the
80
+ * six-digit codes on demand (`otp <name>` / GET /capabilities/<id>/otp) and only those cross, each dead
81
+ * within its period. A cli entry whose env references a totp field therefore fails to parse (see below). */
82
+ totp: z
83
+ .boolean()
84
+ .optional()
85
+ .describe(
86
+ "This field holds a TOTP seed — the base32 key or otpauth:// URI a service shows when enrolling an authenticator app. Declare it with `secret: true`. Unlike an ordinary secret it never enters the agent's environment: the daemon mints the six-digit codes on demand and only those cross.",
87
+ ),
88
+ });
89
+ export type CapabilityField = z.infer<typeof CapabilityFieldSchema>;
90
+
91
+ /* WHETHER A FIELD IS IN PLAY, given what has been answered so far — and the only place that decides it.
92
+ *
93
+ * Two tiers ask this question about the same card. The web's form asks it to decide what to draw and what to
94
+ * validate; the daemon asks it at install to decide which fields it may demand. They used to answer it with
95
+ * their own copies of the same comparison, in different packages, and the answers only had to diverge once for
96
+ * a card to become unusable in exactly one direction: a field the form never showed, refused at submit for
97
+ * being empty. Nothing in either copy referred to the other, so the divergence would have arrived as a bug
98
+ * report about one card rather than as a broken rule.
99
+ *
100
+ * It lives beside the schema for the reason the description does: this is a fact about the shape, and the two
101
+ * consumers are in different packages. Parsed per call rather than cached — a card has a handful of fields,
102
+ * this runs on a keystroke at worst, and a cache keyed by manifest strings is a map that outlives every
103
+ * extension that ever declared one. */
104
+ export const fieldApplies = (field: CapabilityField, values: Readonly<Record<string, unknown>>): boolean =>
105
+ field.when === undefined || evaluateWhen(parseWhen(field.when), values);
106
+
107
+ // The "+" card an entry renders: how it looks in the grid and how the user gets the credential it asks for.
108
+ // Shared by every arm below, because none of that varies with the kind.
109
+ const CatalogSchema = z.object({
110
+ name: z.string().min(1),
111
+ ...MARK_FIELDS,
112
+ // ONE LINE — aim for 60 characters or fewer. The grid clamps this at two lines and a card sits beside two
113
+ // others in a pane the index column has already taken 16rem out of, so a paragraph here is a paragraph the
114
+ // reader gets truncated. Everything longer belongs in `hint`, which the config form prints in full and the
115
+ // catalog's search reads. Not capped in the schema: an extension published before this rule should still
116
+ // install, and a card that reads badly is a worse outcome than one that fails to load only in theory.
117
+ description: z
118
+ .string()
119
+ .min(1)
120
+ .describe(
121
+ "ONE LINE — aim for 60 characters or fewer. The grid clamps it at two lines in a narrow pane, so a paragraph here is a paragraph the reader gets truncated. Everything longer belongs in `hint`.",
122
+ ),
123
+ category: z.string().min(1),
124
+ // The paragraph. Shown under the add-form, and searched from the catalog — so the words that identify this
125
+ // card to someone hunting for it ("webauthn", "socket mode") belong here even when the tile can't show them.
126
+ hint: z
127
+ .string()
128
+ .optional()
129
+ .describe(
130
+ 'The paragraph, shown under the add form and searched from the catalog — so the words that identify this card to someone hunting for it ("webauthn", "socket mode") belong here even when the tile cannot show them.',
131
+ ),
132
+ // The credential-creation walkthrough the install dialog renders (the platform catalog's guide shape).
133
+ guide: z
134
+ .object({
135
+ url: z.string().optional(),
136
+ urlFromField: z.string().optional(),
137
+ path: z.string().optional(),
138
+ linkLabel: z.string().optional(),
139
+ scopes: z.string().optional(),
140
+ steps: z.array(z.string()).optional(),
141
+ })
142
+ .optional()
143
+ .describe("The walkthrough the install dialog renders for getting the credential this card asks for."),
144
+ });
145
+
146
+ // Every arm carries these: the slug that becomes the card's /capabilities/<id> route AND the discriminator
147
+ // value the daemon's handler resolves (a cli `provider`, a browser/host `platform`), plus the card and its form.
148
+ const contributionBase = {
149
+ id: z.string().regex(/^[a-z0-9][a-z0-9-]*$/),
150
+ catalog: CatalogSchema,
151
+ fields: z.array(CapabilityFieldSchema),
152
+ };
153
+
154
+ /* A CAPABILITY CARD AS DATA. The catalog is the extensible layer; the HANDLERS are core — so an entry names one
155
+ * of the kinds whose daemon-side machinery is fully generic over its data, and the machinery stays put. The
156
+ * kinds NOT listed here are the ones whose card is one-to-one with a handler that owns real privilege (`docker`
157
+ * bakes --privileged, `vpn` bakes NET_ADMIN, `extension` installs extensions, `devops` scaffolds repos): their
158
+ * cards live in the platform catalog because separating card from handler would split one concept in two, and
159
+ * because a manifest that could name them would be a manifest that grants itself privilege. That restriction is
160
+ * this discriminated union, not a comment — a manifest naming any other kind fails to parse.
161
+ *
162
+ * `${id}` in a skill file is substituted with the instance name at apply time (so a host pack's tool names read
163
+ * `mcp__my-laptop__run_command`), and, for `cli`, each `$ENVVAR` becomes its per-instance suffixed name. */
164
+ export const CapabilityContributionSchema = z
165
+ .discriminatedUnion("kind", [
166
+ // A CLI tool the AGENT gets, authenticated: the env vars its shell receives (value templates over the
167
+ // fields — `${field}` substitutes, `${field:uri}` percent-encodes), a SKILL.md cheatsheet, and an optional
168
+ // image fragment holding the client binary (psql, mysql, whisper).
169
+ z.object({
170
+ ...contributionBase,
171
+ kind: z.literal("cli"),
172
+ fields: z.array(CapabilityFieldSchema).min(1),
173
+ env: z
174
+ .record(z.string().regex(/^[A-Z][A-Z0-9_]*$/), z.string())
175
+ .describe(
176
+ "The environment the agent's shell gets, as value templates over the fields — `${field}` substitutes, `${field:uri}` percent-encodes. Each name is suffixed per instance.",
177
+ ),
178
+ skill: z
179
+ .string()
180
+ .min(1)
181
+ .describe("Checkout-relative SKILL.md teaching the agent this tool. `${id}` in it is replaced with the instance name at apply time."),
182
+ fragment: z
183
+ .string()
184
+ .min(1)
185
+ .optional()
186
+ .describe("A Dockerfile fragment holding the client binary this tool needs (psql, mysql, whisper)."),
187
+ }),
188
+ /* A site the agent acts on AS THE OWNER, through the shared logged-in Chromium. `loginUrl` is what the
189
+ * sign-in window opens; the profile it persists is the credential. `homeUrl` is where that same profile
190
+ * opens once it HAS one — the owner's own hands on the connected browser, which a login page is the wrong
191
+ * place to start (signed in, it only redirects). Two fields because for some platforms the login lives on
192
+ * another site entirely (YouTube signs in at accounts.google.com), so one cannot be derived from the other.
193
+ * No `env` and no `fragment`: the browser itself is core (one Chromium install serves every platform),
194
+ * only the identity is per-entry. A card never declares the account's username/password either — those are
195
+ * CORE form fields on every browser card (the catalog appends them; the daemon's add validation accepts
196
+ * them), because which box a login form wants filled is the same fact on every site.
197
+ *
198
+ * BOTH URLs ARE OPTIONAL, so that one card can be the GENERIC one: a site card pins them (Reddit knows
199
+ * where Reddit signs in), and the generic "browser session" card asks for them on its form instead, which
200
+ * is what lets a user connect a site nobody shipped a card for. A card must do one or the other — pin a
201
+ * URL or declare a field that supplies it — and the daemon's apply says so on the form when neither does,
202
+ * because the alternative is a sign-in window that opens on nothing. */
203
+ z.object({
204
+ ...contributionBase,
205
+ kind: z.literal("browser"),
206
+ loginUrl: z
207
+ .url()
208
+ .optional()
209
+ .describe(
210
+ "What the sign-in window opens; the profile it persists IS the credential. Optional so one card can be the generic one that asks for the URL on its form instead — but a card must either pin this or declare a field that supplies it, or the window opens on nothing.",
211
+ ),
212
+ homeUrl: z
213
+ .url()
214
+ .optional()
215
+ .describe(
216
+ "Where that same profile opens once it HAS a session — the owner's own hands on the connected browser. Separate from loginUrl because for some platforms the login lives on another site entirely (YouTube signs in at accounts.google.com).",
217
+ ),
218
+ skill: z.string().min(1).describe("Checkout-relative SKILL.md teaching the agent this site's actions."),
219
+ }),
220
+ // An operating system a connected computer can run — the skill pack that teaches the agent THAT machine's
221
+ // shell. The enrollment, the socket and the scope enforcement are core; only the pack varies.
222
+ z.object({
223
+ ...contributionBase,
224
+ kind: z.literal("host"),
225
+ skill: z.string().min(1).describe("Checkout-relative SKILL.md teaching the agent that machine's shell."),
226
+ }),
227
+ /* A PRESET over a core kind: no payload at all, just a named card whose `fields` carry the defaults. What an
228
+ * ACP agent needs is a command, so "OpenCode" is entirely a name, a logo and a filled-in form — which is
229
+ * exactly what a catalog row is. */
230
+ z.object({ ...contributionBase, kind: z.literal("agent") }),
231
+ ])
232
+ .superRefine((spec, ctx) => {
233
+ // The totp flag's one invariant, enforced where the manifest is parsed rather than trusted to authors: a
234
+ // seed the daemon mints codes from must never ride the env into the agent's shell — that would hand the
235
+ // agent the second factor itself instead of one expiring code at a time.
236
+ if (spec.kind !== "cli") {
237
+ return;
238
+ }
239
+ for (const field of spec.fields.filter((candidate) => candidate.totp === true)) {
240
+ if (Object.values(spec.env).some((template) => template.includes(`\${${field.key}}`) || template.includes(`\${${field.key}:uri}`))) {
241
+ ctx.addIssue({
242
+ code: "custom",
243
+ message: `env must not reference the totp field "${field.key}" — the daemon mints codes from it instead`,
244
+ });
245
+ }
246
+ }
247
+ });
248
+ export type CapabilityContribution = z.infer<typeof CapabilityContributionSchema>;
249
+ // The arms carrying a per-instance SKILL.md — the daemon templates and installs these identically.
250
+ export type SkillContribution = Extract<CapabilityContribution, { skill: string }>;
251
+
252
+ /* The config key a kind's cards PIN to their own id, so a stored capability can be traced back to the card that
253
+ * made it — the daemon resolves the entry's handler data through it, and the web tells one card's instances from
254
+ * another's. `agent` has none on purpose: its cards are presets over one config shape, differing only in their
255
+ * defaults, so every agent instance belongs to every agent card equally.
256
+ *
257
+ * Here, beside the schema, because it is a fact about the contribution shape — the daemon, the catalog and the
258
+ * web all need it, and three copies of it is three chances for a card's instances to go missing. `satisfies`
259
+ * rather than a lookup table so a new arm above is a compile error until this answers for it. */
260
+ const DISCRIMINATOR = { cli: "provider", browser: "platform", host: "platform", agent: undefined } satisfies Record<
261
+ CapabilityContribution["kind"],
262
+ string | undefined
263
+ >;
264
+ export const contributionDiscriminator = (kind: string): string | undefined => DISCRIMINATOR[kind as keyof typeof DISCRIMINATOR];
265
+
266
+ export const capabilitiesPoint = {
267
+ name: "capabilities",
268
+ description:
269
+ '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, 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 four kinds.',
270
+ schema: z.array(CapabilityContributionSchema),
271
+ } as const satisfies ContributionPoint;
@@ -0,0 +1,44 @@
1
+ import { isWhenExpression } from "@intentic/base/when";
2
+ import { z } from "zod";
3
+ import type { ContributionPoint } from "../contribution-point.js";
4
+
5
+ // A command the extension may register a handler for (api.commands.register); surfaced in the command palette.
6
+ export const CommandContributionSchema = z.object({
7
+ command: z.string().regex(/^[a-z0-9][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*)+$/),
8
+ title: z.string().min(1).describe("What the command palette shows. The manifest's value wins over the one passed at registration."),
9
+ icon: z.string().optional().describe("A name from the host's icon set, drawn beside the title."),
10
+ // An optional global keyboard shortcut, in the host's chord notation (`Mod`/`Ctrl`/`Shift`/`Alt` + key, e.g.
11
+ // "Mod+Shift+K"; `Mod` = ⌘ on Apple, Ctrl elsewhere). It is DECLARED here so it rides the install dialog's
12
+ // approval surface — a global shortcut is consequential, so like title/icon the manifest value is authoritative
13
+ // and the host binds only what was approved. Whitespace-free; an unparseable chord simply never fires.
14
+ keybinding: z
15
+ .string()
16
+ .regex(/^\S+$/)
17
+ .optional()
18
+ .describe(
19
+ '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.',
20
+ ),
21
+ /* When the KEYBINDING applies — a condition over the shell's context keys (`tabSurface == 'chat'`,
22
+ * `!editableTarget`; see @intentic/base/when). The palette ignores it: a command is always runnable by
23
+ * name, and what a condition decides is whether the chord is claimed from whatever else would have had it.
24
+ *
25
+ * Declared here because it could not be declared anywhere. The shell's own commands used to gate on a
26
+ * JavaScript predicate, which an extension has no way to ship — so every extension command took its chord
27
+ * globally, from every surface, including terminals where a bare key belongs to the program on the other
28
+ * end. A condition an extension can write is what makes a contributed shortcut safe to grant. */
29
+ when: z
30
+ .string()
31
+ .refine(isWhenExpression, { message: "not a valid `when` condition" })
32
+ .optional()
33
+ .describe(
34
+ "When the shortcut applies, as a condition over the shell's context keys — `tabSurface == 'chat'`, `!editableTarget`. Without one the chord is claimed everywhere, including inside a terminal where a bare key belongs to the program running in it. The command palette ignores this: a command is always runnable by name.",
35
+ ),
36
+ });
37
+ export type CommandContribution = z.infer<typeof CommandContributionSchema>;
38
+
39
+ export const commandsPoint = {
40
+ name: "commands",
41
+ description:
42
+ "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.",
43
+ schema: z.array(CommandContributionSchema),
44
+ } as const satisfies ContributionPoint;
@@ -0,0 +1,31 @@
1
+ import { z } from "zod";
2
+ import type { ContributionPoint } from "../contribution-point.js";
3
+
4
+ /* A per-directory document family the extension may register at runtime (api.documents.register): the provider
5
+ * marks the directory rows it can explain in the Workspace tree, and the host opens its component as a tab —
6
+ * see DocumentProviderRegistration.
7
+ *
8
+ * Only the id and the label are declared, deliberately. The consequential part of a viewer is which FILES it
9
+ * takes over, and of a command its global shortcut — both are decided in the manifest because the owner must see
10
+ * them. A document provider takes nothing over: it adds an icon to rows it has something for, and every one of
11
+ * those rows is evidence the owner can see for themselves. So the manifest gates WHETHER the extension may mark
12
+ * up the tree at all, and the per-row wording stays with the provider, which is the only thing that knows what
13
+ * it found. */
14
+ export const DocumentContributionSchema = z.object({
15
+ id: z.string().regex(/^[a-z0-9][a-z0-9-]*$/),
16
+ // The family's human name, shown in the install dialog beside the extension's other contributions.
17
+ label: z
18
+ .string()
19
+ .min(1)
20
+ .describe(
21
+ "The family's name, shown in the install dialog beside your other contributions. Per-row wording stays with the provider, which is the only thing that knows what it found.",
22
+ ),
23
+ });
24
+ export type DocumentContribution = z.infer<typeof DocumentContributionSchema>;
25
+
26
+ export const documentsPoint = {
27
+ name: "documents",
28
+ description:
29
+ "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.",
30
+ schema: z.array(DocumentContributionSchema),
31
+ } as const satisfies ContributionPoint;
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ import type { ContributionPoint } from "../contribution-point.js";
3
+
4
+ // A Dockerfile fragment baked into the sandbox image overlay so the extension's tools are present at runtime
5
+ // (a whisper binary, a psql client, …). The daemon rejects FROM and privileged `# intentic:runtime` directives
6
+ // from extension fragments (those stay daemon-owned), and the owner approves the composed overlay + rebuilds
7
+ // out-of-band.
8
+ export const EnvironmentContributionSchema = z.object({
9
+ fragment: z
10
+ .string()
11
+ .min(1)
12
+ .refine((value) => !value.split("/").includes(".."), { message: "fragment must stay inside the checkout" })
13
+ .describe(
14
+ "Checkout-relative path to a file holding ONLY RUN and ENV instructions. FROM and privileged directives are rejected — those stay daemon-owned.",
15
+ ),
16
+ });
17
+ export type EnvironmentContribution = z.infer<typeof EnvironmentContributionSchema>;
18
+
19
+ export const environmentPoint = {
20
+ name: "environment",
21
+ description:
22
+ "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.",
23
+ schema: EnvironmentContributionSchema,
24
+ } as const satisfies ContributionPoint;
@@ -0,0 +1,54 @@
1
+ import { z } from "zod";
2
+ import type { ContributionPoint } from "../contribution-point.js";
3
+
4
+ /* WHICH WORKSPACE FILE MAKES THIS EXTENSION'S VIEW STALE — the extension's half of the core's
5
+ * WORKSPACE_STATE_FILES table (@intentic/sandbox-contract), in the same two fields so the browser can union them
6
+ * without translating.
7
+ *
8
+ * An intentic workspace is file-first: the agent edits /work with its own file tools, out of band from every
9
+ * HTTP route, and the daemon's filesystem watcher is the ONLY thing that can tell a browser its view went stale.
10
+ * Before this contribution point existed an extension had no way into that push, so every one of them polled —
11
+ * and the core's table had to hardcode `automations`/`automation-approvals`, query keys owned by an extension,
12
+ * because the extension itself couldn't declare them. Declaring is now the extension's job and unioning is the
13
+ * host's.
14
+ *
15
+ * It rides the manifest rather than a runtime api.workspace.onDidChangeFiles for two reasons: the owner sees at
16
+ * install which of their files an extension reads, and there is nothing imperative left to get wrong — no
17
+ * subscribe, no unsubscribe, no listener that quietly stops firing. */
18
+ export const FileContributionSchema = z.object({
19
+ /* Workspace-root-relative, forward-slash — the space the watcher's changed paths arrive in. Matched by
20
+ * PREFIX, so one entry covers an exact file (`.intentic/automations.json`), a directory (`.intentic/drafts/`
21
+ * — keep the trailing slash so it cannot match a sibling file) or a name family (`.intentic/environment.`).
22
+ * Deliberately not a glob: prefix is the whole matching rule on both sides of this union. */
23
+ path: z
24
+ .string()
25
+ .min(1)
26
+ .refine((value) => !value.startsWith("/") && !value.split("/").includes(".."), {
27
+ message: "path must be workspace-root-relative and stay inside the workspace",
28
+ })
29
+ .describe(
30
+ "Workspace-root-relative, forward-slash, matched by prefix — so one entry covers an exact file (`.intentic/automations.json`), a directory (`.intentic/drafts/`, with the trailing slash so it cannot match a sibling file) or a name family (`.intentic/environment.`). Not a glob.",
31
+ ),
32
+ /* The browser query keys those contents feed — the first element of the extension's own
33
+ * `api.sandbox.key(...)` keys, which is what makes them match (the sandbox id is a SUFFIX). Empty is not
34
+ * allowed: a path that makes nothing stale is a declaration with no effect, and saying so at install beats
35
+ * discovering it as a view that never refreshes.
36
+ *
37
+ * Keep the paths as narrow as the view actually needs. A broad prefix costs every connected browser a
38
+ * refetch per matching write, and a write-heavy path (an index, a transcript, a log) turns that into a
39
+ * request storm — the reason the core table leaves the daemon's own machine state off the push entirely. */
40
+ invalidates: z
41
+ .array(z.string().min(1))
42
+ .min(1)
43
+ .describe(
44
+ "The query keys this path makes stale — the first element of your own api.sandbox.key(...) keys. Keep both this and the path as narrow as the view actually needs: a broad prefix costs every connected browser a refetch on every matching write.",
45
+ ),
46
+ });
47
+ export type FileContribution = z.infer<typeof FileContributionSchema>;
48
+
49
+ export const filesPoint = {
50
+ name: "files",
51
+ description:
52
+ "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.",
53
+ schema: z.array(FileContributionSchema),
54
+ } as const satisfies ContributionPoint;
@@ -0,0 +1,73 @@
1
+ import { z } from "zod";
2
+ import type { ContributionPoint } from "../contribution-point.js";
3
+ import { agentPoint } from "./agent.js";
4
+ import { automationTemplatesPoint } from "./automation-templates.js";
5
+ import { binPoint } from "./bin.js";
6
+ import { capabilitiesPoint } from "./capabilities.js";
7
+ import { commandsPoint } from "./commands.js";
8
+ import { documentsPoint } from "./documents.js";
9
+ import { environmentPoint } from "./environment.js";
10
+ import { filesPoint } from "./files.js";
11
+ import { listenerPoint } from "./listener.js";
12
+ import { processesPoint } from "./processes.js";
13
+ import { settingsPoint } from "./settings.js";
14
+ import { viewersPoint } from "./viewers.js";
15
+ import { viewsPoint } from "./views.js";
16
+
17
+ export * from "./agent.js";
18
+ export * from "./automation-templates.js";
19
+ export * from "./bin.js";
20
+ export * from "./capabilities.js";
21
+ export * from "./commands.js";
22
+ export * from "./documents.js";
23
+ export * from "./environment.js";
24
+ export * from "./files.js";
25
+ export * from "./listener.js";
26
+ export * from "./processes.js";
27
+ export * from "./settings.js";
28
+ export * from "./viewers.js";
29
+ export * from "./views.js";
30
+
31
+ /* EVERYTHING A MANIFEST MAY DECLARE. `contributes` is assembled from this list (manifest.ts), the authoring
32
+ * JSON Schema is generated from it (json-schema.ts), and the SDK's surface guard reads the point names back out
33
+ * of it — so the three cannot disagree about what this build supports.
34
+ *
35
+ * Collected explicitly rather than by a module-load side effect, because two readers need the answer to be the
36
+ * same every time it is asked: the wire contract's lock file, which is a committed document a diff has to be
37
+ * able to guard, and the generated schema, which is committed too. A registry that filled itself as modules
38
+ * happened to load would make both of those depend on import order.
39
+ *
40
+ * Adding a point is a file in this directory and a line here — points.test.ts fails when a file appears without
41
+ * the line, so the pair cannot come apart. */
42
+ export const CONTRIBUTION_POINTS = [
43
+ viewsPoint,
44
+ filesPoint,
45
+ viewersPoint,
46
+ documentsPoint,
47
+ commandsPoint,
48
+ settingsPoint,
49
+ processesPoint,
50
+ agentPoint,
51
+ environmentPoint,
52
+ capabilitiesPoint,
53
+ listenerPoint,
54
+ automationTemplatesPoint,
55
+ binPoint,
56
+ ] as const satisfies readonly ContributionPoint[];
57
+
58
+ // The `contributes` shape those points assemble to: each point's key, its schema, optional. A mapped type
59
+ // rather than a widened record so `manifest.contributes.views` keeps its exact type at every call site — the
60
+ // whole point of the schema being typed at all.
61
+ type ContributesShape = {
62
+ [Point in (typeof CONTRIBUTION_POINTS)[number] as Point["name"]]: z.ZodOptional<Point["schema"]>;
63
+ };
64
+
65
+ /* The `contributes` object, assembled rather than hand-written — which is what makes adding a point a file plus
66
+ * a line above, instead of an edit to a schema thirteen unrelated features share.
67
+ *
68
+ * Each point's description rides `z.describe` onto its own key, so it survives into the generated authoring
69
+ * schema and reaches the author as hover text. The cast is the one place the value side and the type side meet:
70
+ * `Object.fromEntries` can only say `Record<string, …>`, and ContributesShape is that said precisely. */
71
+ export const contributesSchema = z.object(
72
+ Object.fromEntries(CONTRIBUTION_POINTS.map((point) => [point.name, point.schema.describe(point.description).optional()])) as ContributesShape,
73
+ );
@@ -0,0 +1,81 @@
1
+ import { z } from "zod";
2
+ import type { ContributionPoint } from "../contribution-point.js";
3
+
4
+ // One narrowing field the generic automation editor draws for a source — a channel, a branch. `hint` is the
5
+ // sentence under the input, for a filter whose empty case is easy to get wrong.
6
+ const TriggerFieldContributionSchema = z.object({
7
+ label: z.string().min(1),
8
+ placeholder: z.string().min(1),
9
+ hint: z.string().min(1).optional().describe("The sentence under the input, for a filter whose empty case is easy to get wrong."),
10
+ });
11
+
12
+ /* A realtime listener source the extension supplies. This is the ONE catalog both halves consume: the daemon
13
+ * derives its accepted event types from `events` and folds `automation` into the trigger catalogue it serves,
14
+ * while the automation editor derives the source picker, filters and starter from that. Keeping those facts on
15
+ * the provider extension is what lets a newly installed listener become configurable without a matching app
16
+ * release.
17
+ *
18
+ * The daemon serves a provider-scoped control surface — GET /listeners/<provider>/state to reconcile, POST
19
+ * …/dispatch to wake an automation (optionally holding a turn-stream), …/failure + …/status to report. The
20
+ * daemon holds no provider connection itself.
21
+ *
22
+ * WHAT DISPATCHES IT IS OPEN. A gateway process (contributes.processes) is the usual answer and the one the
23
+ * reconcile feed is shaped for — it holds a live connection the daemon must not. But an extension BACKEND can
24
+ * dispatch through the same route by declaring the dispatch path in `permissions.daemon`, which is
25
+ * how an area that learns things on its own schedule (an estate poller noticing a container died) contributes
26
+ * a trigger without running a gateway at all. Declaring this with neither is legal and inert: the source is
27
+ * offered, and nothing ever fires it. */
28
+ export const ListenerContributionSchema = z.object({
29
+ provider: z
30
+ .string()
31
+ .regex(/^[a-z0-9][a-z0-9-]*$/)
32
+ .describe("The slug this source's automation triggers fire on."),
33
+ events: z
34
+ .array(
35
+ z.object({
36
+ type: z.string().regex(/^[a-z0-9][a-z0-9_]*$/),
37
+ label: z.string().min(1),
38
+ }),
39
+ )
40
+ .min(1)
41
+ .refine((events) => new Set(events.map((event) => event.type)).size === events.length, {
42
+ message: "listener event types must be unique",
43
+ })
44
+ .describe("The event types this source can fire, with the wording the automation editor offers them under. The daemon accepts no others."),
45
+ automation: z
46
+ .object({
47
+ label: z.string().min(1),
48
+ // Only sources whose `message` events distinguish addressed messages declare this. Absent means the
49
+ // generic editor offers no mention-only filter rather than inventing provider semantics.
50
+ mentionLabel: z
51
+ .string()
52
+ .min(1)
53
+ .optional()
54
+ .describe(
55
+ "Only for a source whose message events distinguish being addressed. Absent ⇒ the editor offers no mention-only filter, rather than inventing semantics you did not promise.",
56
+ ),
57
+ channel: TriggerFieldContributionSchema.describe("The primary narrowing filter — a channel, a room, a repo."),
58
+ // A SECOND narrowing axis, for a source whose events carry one — a pipeline's git ref, so a trigger can
59
+ // say "the branch that ships" rather than "every agent's every failure". Absent ⇒ the editor offers
60
+ // only the channel filter.
61
+ branchField: TriggerFieldContributionSchema.optional().describe(
62
+ 'A second narrowing axis, for a source whose events carry one — a pipeline\'s git ref, so a trigger can say "the branch that ships" rather than "every agent\'s every failure".',
63
+ ),
64
+ // The provider owns the payload vocabulary, so it also owns the first prompt that explains that payload.
65
+ starterPrompt: z
66
+ .string()
67
+ .min(1)
68
+ .describe(
69
+ "The first prompt a new automation on this source is prefilled with. You own the payload vocabulary, so you own the prompt that explains it.",
70
+ ),
71
+ })
72
+ .describe("How the generic automation editor presents this source: its name, its filters, and the prompt it starts people on."),
73
+ });
74
+ export type ListenerContribution = z.infer<typeof ListenerContributionSchema>;
75
+
76
+ export const listenerPoint = {
77
+ name: "listener",
78
+ description:
79
+ "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.",
80
+ schema: ListenerContributionSchema,
81
+ } as const satisfies ContributionPoint;
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+ import type { ContributionPoint } from "../contribution-point.js";
3
+
4
+ // A long-lived background process the daemon runs for the extension (tmux-managed, like panel dev servers).
5
+ export const ProcessContributionSchema = z.object({
6
+ name: z.string().regex(/^[a-z0-9][a-z0-9-]*$/),
7
+ command: z.string().min(1),
8
+ cwd: z.string().optional().describe("Relative to the extension checkout. Absent ⇒ the checkout root."),
9
+ port: z.literal("auto").optional().describe("Assign a free port and inject it as PORT."),
10
+ preview: z.boolean().optional().describe("Expose the port on a tunnelled preview hostname."),
11
+ autoStart: z.boolean().optional().describe("Launch it on install and on daemon boot, rather than waiting to be started."),
12
+ });
13
+ export type ProcessContribution = z.infer<typeof ProcessContributionSchema>;
14
+
15
+ export const processesPoint = {
16
+ name: "processes",
17
+ description:
18
+ "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.",
19
+ schema: z.array(ProcessContributionSchema),
20
+ } as const satisfies ContributionPoint;
@@ -0,0 +1,31 @@
1
+ import { z } from "zod";
2
+ import type { ContributionPoint } from "../contribution-point.js";
3
+
4
+ // A typed setting descriptor the host renders schema-driven into the Settings page and persists daemon-side.
5
+ export const SettingContributionSchema = z.object({
6
+ key: z.string().regex(/^[a-z0-9][a-zA-Z0-9-]*$/),
7
+ type: z.enum(["boolean", "string", "number", "enum"]).describe("Which control the Settings page draws. `enum` reads its choices from `enum`."),
8
+ title: z.string().min(1),
9
+ description: z.string().optional().describe("The line under the control."),
10
+ default: z.union([z.string(), z.number(), z.boolean()]).optional(),
11
+ enum: z.array(z.string()).optional().describe('The choices, for type "enum". Meaningless otherwise.'),
12
+ secret: z
13
+ .boolean()
14
+ .optional()
15
+ .describe("Mask the value in the UI and strip it from reads — a set secret round-trips as “still set”, never as its value."),
16
+ env: z
17
+ .string()
18
+ .regex(/^[A-Z][A-Z0-9_]*$/)
19
+ .optional()
20
+ .describe(
21
+ "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.",
22
+ ),
23
+ });
24
+ export type SettingContribution = z.infer<typeof SettingContributionSchema>;
25
+
26
+ export const settingsPoint = {
27
+ name: "settings",
28
+ description:
29
+ "Typed settings the host renders into the Settings page for you and persists daemon-side. You never draw the form or store the value; you read it back with api.settings.get.",
30
+ schema: z.array(SettingContributionSchema),
31
+ } as const satisfies ContributionPoint;