@nimbalyst/extension-sdk 0.1.5 → 0.2.1

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 (73) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/__tests__/createTextCollabContentAdapter.test.d.ts +2 -0
  3. package/dist/__tests__/createTextCollabContentAdapter.test.d.ts.map +1 -0
  4. package/dist/__tests__/createTextCollabContentAdapter.test.js +60 -0
  5. package/dist/__tests__/hasSeedableContent.test.d.ts +2 -0
  6. package/dist/__tests__/hasSeedableContent.test.d.ts.map +1 -0
  7. package/dist/__tests__/hasSeedableContent.test.js +25 -0
  8. package/dist/__tests__/manifestValidation.agentProviders.test.d.ts +2 -0
  9. package/dist/__tests__/manifestValidation.agentProviders.test.d.ts.map +1 -0
  10. package/dist/__tests__/manifestValidation.agentProviders.test.js +44 -0
  11. package/dist/agents/AgentProtocol.d.ts +227 -0
  12. package/dist/agents/AgentProtocol.d.ts.map +1 -0
  13. package/dist/agents/AgentProtocol.js +23 -0
  14. package/dist/agents/AgentProtocolHost.d.ts +190 -0
  15. package/dist/agents/AgentProtocolHost.d.ts.map +1 -0
  16. package/dist/agents/AgentProtocolHost.js +42 -0
  17. package/dist/agents/index.d.ts +26 -0
  18. package/dist/agents/index.d.ts.map +1 -0
  19. package/dist/agents/index.js +24 -0
  20. package/dist/collab/createTextCollabContentAdapter.d.ts +26 -0
  21. package/dist/collab/createTextCollabContentAdapter.d.ts.map +1 -0
  22. package/dist/collab/createTextCollabContentAdapter.js +79 -0
  23. package/dist/createReadOnlyHost.d.ts +5 -0
  24. package/dist/createReadOnlyHost.d.ts.map +1 -1
  25. package/dist/createReadOnlyHost.js +4 -0
  26. package/dist/externals.d.ts +3 -3
  27. package/dist/externals.d.ts.map +1 -1
  28. package/dist/externals.js +7 -0
  29. package/dist/index.d.ts +4 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +3 -0
  32. package/dist/manifestValidation.d.ts +118 -0
  33. package/dist/manifestValidation.d.ts.map +1 -0
  34. package/dist/manifestValidation.js +475 -0
  35. package/dist/types/collab.d.ts +138 -0
  36. package/dist/types/collab.d.ts.map +1 -0
  37. package/dist/types/collab.js +1 -0
  38. package/dist/types/editor.d.ts +198 -0
  39. package/dist/types/editor.d.ts.map +1 -1
  40. package/dist/types/editors.d.ts +4 -0
  41. package/dist/types/editors.d.ts.map +1 -1
  42. package/dist/types/extension.d.ts +438 -5
  43. package/dist/types/extension.d.ts.map +1 -1
  44. package/dist/types/extension.js +11 -1
  45. package/dist/types/index.d.ts +3 -0
  46. package/dist/types/index.d.ts.map +1 -1
  47. package/dist/types/index.js +3 -0
  48. package/dist/types/panel.d.ts +50 -0
  49. package/dist/types/panel.d.ts.map +1 -1
  50. package/dist/types/permissions.d.ts +117 -0
  51. package/dist/types/permissions.d.ts.map +1 -0
  52. package/dist/types/permissions.js +33 -0
  53. package/dist/types/theme.d.ts +67 -0
  54. package/dist/types/theme.d.ts.map +1 -1
  55. package/dist/types/theme.js +7 -1
  56. package/dist/types/trackerImporter.d.ts +147 -0
  57. package/dist/types/trackerImporter.d.ts.map +1 -0
  58. package/dist/types/trackerImporter.js +33 -0
  59. package/dist/useCollaborativeEditor.d.ts +165 -0
  60. package/dist/useCollaborativeEditor.d.ts.map +1 -0
  61. package/dist/useCollaborativeEditor.js +253 -0
  62. package/dist/useEditorLifecycle.d.ts.map +1 -1
  63. package/dist/useEditorLifecycle.js +9 -5
  64. package/dist/validate.browser.d.ts +5 -0
  65. package/dist/validate.browser.d.ts.map +1 -0
  66. package/dist/validate.browser.js +27 -0
  67. package/dist/validate.d.ts +2 -8
  68. package/dist/validate.d.ts.map +1 -1
  69. package/dist/validate.js +117 -0
  70. package/dist/validationTypes.d.ts +9 -0
  71. package/dist/validationTypes.d.ts.map +1 -0
  72. package/dist/validationTypes.js +1 -0
  73. package/package.json +16 -3
@@ -0,0 +1,475 @@
1
+ /**
2
+ * Pure (no Node/Electron deps) validation helpers for manifest fields.
3
+ *
4
+ * Runs at both build time (via validate.ts, which reads manifest.json from
5
+ * disk) and runtime (when the host loads an extension and needs to refuse
6
+ * invalid contributions). Keep this file dependency-free so it can be bundled
7
+ * into either context without dragging in fs/path.
8
+ */
9
+ import { MAX_BACKEND_MODULES_PER_EXTENSION } from './types/extension.js';
10
+ /**
11
+ * Manifest validation rejects extensions declaring more than this many AI
12
+ * agent providers. Chosen by symmetry with
13
+ * {@link MAX_BACKEND_MODULES_PER_EXTENSION}: an extension that ships more than
14
+ * a handful of agent providers is almost certainly mis-modeled (split it into
15
+ * multiple extensions, or surface them as models on one provider). The cap
16
+ * also keeps the settings UI -- which lists each provider as its own row --
17
+ * from running away.
18
+ */
19
+ export const MAX_AGENT_PROVIDERS_PER_EXTENSION = 4;
20
+ const AGENT_PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
21
+ /**
22
+ * Mirrors the host's permission registry. We duplicate the list here so
23
+ * extensions don't have to depend on Electron internals to validate locally.
24
+ * Adding a new id requires updating both this list AND
25
+ * `packages/electron/src/main/extensions/permissionRegistry.ts`.
26
+ */
27
+ const KNOWN_PERMISSION_IDS = [
28
+ 'workspace-files',
29
+ 'nimbalyst-database-read',
30
+ 'nimbalyst-database-write',
31
+ 'secrets-read',
32
+ 'mcp-server-register',
33
+ ];
34
+ /**
35
+ * Permission ids that used to be in the catalog but never enforced anything
36
+ * meaningful at the backend boundary (ambient Node capabilities). Manifests
37
+ * that still reference them are accepted -- the validator silently drops the
38
+ * id from the effective list -- so we don't break older extensions during
39
+ * the catalog cleanup. Authors get a non-fatal warning issue back.
40
+ */
41
+ const DEPRECATED_PERMISSION_IDS = [
42
+ 'spawn-process',
43
+ 'network-loopback',
44
+ 'network-internet',
45
+ 'filesystem',
46
+ ];
47
+ const KNOWN_RUNTIMES = [
48
+ 'utility-process',
49
+ 'worker-thread',
50
+ ];
51
+ const BACKEND_MODULE_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
52
+ /**
53
+ * Validate the `contributions.backendModules` array on a manifest.
54
+ * Returns the list of problems found - empty means valid.
55
+ *
56
+ * Callers decide whether to treat issues as fatal (host loader: refuse the
57
+ * extension) or as warnings (build-time validator: print and continue with
58
+ * non-zero exit).
59
+ */
60
+ export function validateBackendModules(backendModules) {
61
+ if (backendModules === undefined) {
62
+ return [];
63
+ }
64
+ if (!Array.isArray(backendModules)) {
65
+ return [{ message: 'contributions.backendModules must be an array' }];
66
+ }
67
+ const issues = [];
68
+ if (backendModules.length > MAX_BACKEND_MODULES_PER_EXTENSION) {
69
+ issues.push({
70
+ message: `contributions.backendModules declares ${backendModules.length} modules; ` +
71
+ `the maximum is ${MAX_BACKEND_MODULES_PER_EXTENSION}. ` +
72
+ 'Consolidate modules to keep the consent prompt manageable.',
73
+ });
74
+ }
75
+ const seenIds = new Set();
76
+ for (let i = 0; i < backendModules.length; i += 1) {
77
+ const raw = backendModules[i];
78
+ if (!raw || typeof raw !== 'object') {
79
+ issues.push({ message: `backendModules[${i}] must be an object` });
80
+ continue;
81
+ }
82
+ const module = raw;
83
+ const moduleLabel = typeof module.id === 'string' ? module.id : `index ${i}`;
84
+ if (typeof module.id !== 'string' || !BACKEND_MODULE_ID_PATTERN.test(module.id)) {
85
+ issues.push({
86
+ moduleId: typeof module.id === 'string' ? module.id : undefined,
87
+ message: `backendModules[${moduleLabel}].id must be a lowercase string matching ` +
88
+ `${BACKEND_MODULE_ID_PATTERN.source} (got ${JSON.stringify(module.id)})`,
89
+ });
90
+ }
91
+ else if (seenIds.has(module.id)) {
92
+ issues.push({
93
+ moduleId: module.id,
94
+ message: `backendModules contains duplicate id "${module.id}"`,
95
+ });
96
+ }
97
+ else {
98
+ seenIds.add(module.id);
99
+ }
100
+ if (typeof module.entry !== 'string' || module.entry.length === 0) {
101
+ issues.push({
102
+ moduleId: typeof module.id === 'string' ? module.id : undefined,
103
+ message: `backendModules[${moduleLabel}].entry must be a non-empty relative path string`,
104
+ });
105
+ }
106
+ else if (module.entry.startsWith('/') || module.entry.includes('..')) {
107
+ // Refuse absolute paths and parent-directory traversal so a module
108
+ // can't escape its extension root. The host resolves entry relative
109
+ // to the extension directory; only safe relative paths belong here.
110
+ issues.push({
111
+ moduleId: typeof module.id === 'string' ? module.id : undefined,
112
+ message: `backendModules[${moduleLabel}].entry must be a relative path within the extension root ` +
113
+ `(no leading "/", no ".." segments)`,
114
+ });
115
+ }
116
+ if (!KNOWN_RUNTIMES.includes(module.runtime)) {
117
+ issues.push({
118
+ moduleId: typeof module.id === 'string' ? module.id : undefined,
119
+ message: `backendModules[${moduleLabel}].runtime must be one of: ${KNOWN_RUNTIMES.join(', ')} ` +
120
+ `(got ${JSON.stringify(module.runtime)})`,
121
+ });
122
+ }
123
+ if (module.permissions !== undefined && !Array.isArray(module.permissions)) {
124
+ issues.push({
125
+ moduleId: typeof module.id === 'string' ? module.id : undefined,
126
+ message: `backendModules[${moduleLabel}].permissions must be an array of ` +
127
+ `host-brokered capability ids (or omitted). The implicit "run native ` +
128
+ `code" grant is conferred by enabling the module itself, not by an ` +
129
+ `entry in this array.`,
130
+ });
131
+ }
132
+ else if (Array.isArray(module.permissions)) {
133
+ const uniquePermissions = new Set();
134
+ for (const permission of module.permissions) {
135
+ if (typeof permission !== 'string') {
136
+ issues.push({
137
+ moduleId: typeof module.id === 'string' ? module.id : undefined,
138
+ message: `backendModules[${moduleLabel}].permissions contains non-string entry`,
139
+ });
140
+ continue;
141
+ }
142
+ if (DEPRECATED_PERMISSION_IDS.includes(permission)) {
143
+ // Older manifests still ship these. They never meaningfully gated
144
+ // anything inside the backend runtime (ambient Node access), and
145
+ // the catalog cleanup removed them. Warn the author and silently
146
+ // drop the id from the effective list.
147
+ issues.push({
148
+ severity: 'warning',
149
+ moduleId: typeof module.id === 'string' ? module.id : undefined,
150
+ message: `backendModules[${moduleLabel}].permissions includes deprecated id "${permission}". ` +
151
+ `This id is unenforceable inside a Node backend (the module can require child_process/fs/net ` +
152
+ `directly) and has been removed from the catalog. Granting the module is itself the consent ` +
153
+ `to run native code; drop this id from your manifest.`,
154
+ });
155
+ continue;
156
+ }
157
+ if (!KNOWN_PERMISSION_IDS.includes(permission)) {
158
+ issues.push({
159
+ moduleId: typeof module.id === 'string' ? module.id : undefined,
160
+ message: `backendModules[${moduleLabel}].permissions contains unknown id "${permission}". ` +
161
+ `Valid ids: ${KNOWN_PERMISSION_IDS.join(', ')}`,
162
+ });
163
+ }
164
+ if (uniquePermissions.has(permission)) {
165
+ issues.push({
166
+ moduleId: typeof module.id === 'string' ? module.id : undefined,
167
+ message: `backendModules[${moduleLabel}].permissions contains duplicate "${permission}"`,
168
+ });
169
+ }
170
+ uniquePermissions.add(permission);
171
+ }
172
+ }
173
+ const enablement = module.enablement;
174
+ if (!enablement || typeof enablement !== 'object') {
175
+ issues.push({
176
+ moduleId: typeof module.id === 'string' ? module.id : undefined,
177
+ message: `backendModules[${moduleLabel}].enablement is required`,
178
+ });
179
+ }
180
+ else {
181
+ if (enablement.default !== 'disabled') {
182
+ issues.push({
183
+ moduleId: typeof module.id === 'string' ? module.id : undefined,
184
+ message: `backendModules[${moduleLabel}].enablement.default must be "disabled". ` +
185
+ 'Privileged capabilities are always opt-in.',
186
+ });
187
+ }
188
+ if (enablement.promptOn !== 'firstUse') {
189
+ issues.push({
190
+ moduleId: typeof module.id === 'string' ? module.id : undefined,
191
+ message: `backendModules[${moduleLabel}].enablement.promptOn must be "firstUse"`,
192
+ });
193
+ }
194
+ if (typeof enablement.purpose !== 'string' || enablement.purpose.trim().length === 0) {
195
+ issues.push({
196
+ moduleId: typeof module.id === 'string' ? module.id : undefined,
197
+ message: `backendModules[${moduleLabel}].enablement.purpose must be a non-empty string. ` +
198
+ 'This is shown verbatim in the consent prompt - write it from the user\'s perspective.',
199
+ });
200
+ }
201
+ else if (enablement.purpose.length > 280) {
202
+ issues.push({
203
+ moduleId: typeof module.id === 'string' ? module.id : undefined,
204
+ message: `backendModules[${moduleLabel}].enablement.purpose is too long (${enablement.purpose.length} chars). ` +
205
+ 'Keep it under 280 characters; long copy doesn\'t fit the consent prompt.',
206
+ });
207
+ }
208
+ }
209
+ }
210
+ return issues;
211
+ }
212
+ /**
213
+ * Convenience wrapper that throws if any fatal issues are found. Warnings
214
+ * (currently: deprecated permission ids) are surfaced via the issues array
215
+ * but never throw, so an extension that still references `spawn-process`
216
+ * keeps loading -- the host just drops the id when computing effective
217
+ * permissions.
218
+ *
219
+ * Use this in main-process load paths where invalid manifests should refuse
220
+ * the extension outright.
221
+ */
222
+ export function assertBackendModulesValid(extensionId, backendModules) {
223
+ const issues = validateBackendModules(backendModules);
224
+ const fatal = issues.filter((i) => i.severity !== 'warning');
225
+ if (fatal.length === 0) {
226
+ return;
227
+ }
228
+ const lines = fatal.map((i) => i.moduleId ? ` - [${i.moduleId}] ${i.message}` : ` - ${i.message}`);
229
+ throw new Error(`Extension ${extensionId} has invalid backendModules declarations:\n${lines.join('\n')}`);
230
+ }
231
+ /**
232
+ * Filter a raw `permissions` array on a backend-module contribution down to
233
+ * the ids the host actually understands. Deprecated catalog ids
234
+ * (`spawn-process`, `network-loopback`, `network-internet`, `filesystem`)
235
+ * are dropped silently -- they never gated anything inside the backend
236
+ * runtime, so the host treats them as no-ops. Unknown ids are also dropped;
237
+ * `validateBackendModules` raises a separate error for those, so the loader
238
+ * will refuse the module before this is consulted in earnest.
239
+ */
240
+ export function effectiveModulePermissions(raw) {
241
+ if (!Array.isArray(raw))
242
+ return [];
243
+ const out = [];
244
+ const seen = new Set();
245
+ for (const entry of raw) {
246
+ if (typeof entry !== 'string')
247
+ continue;
248
+ if (seen.has(entry))
249
+ continue;
250
+ seen.add(entry);
251
+ if (KNOWN_PERMISSION_IDS.includes(entry)) {
252
+ out.push(entry);
253
+ }
254
+ }
255
+ return out;
256
+ }
257
+ /**
258
+ * Extract the set of declared backend-module ids from a manifest's
259
+ * `contributions.backendModules` array. Used by
260
+ * {@link validateAgentProviders} so the `backendModuleId` cross-check does
261
+ * not need the caller to pre-compute the set.
262
+ *
263
+ * Returns an empty set if `backendModules` is missing or malformed. Exported
264
+ * so build tooling can reuse the same extraction logic when checking entry
265
+ * files against the declared modules.
266
+ */
267
+ export function extractBackendModuleIds(backendModules) {
268
+ const ids = new Set();
269
+ if (!Array.isArray(backendModules))
270
+ return ids;
271
+ for (const raw of backendModules) {
272
+ if (!raw || typeof raw !== 'object')
273
+ continue;
274
+ const id = raw.id;
275
+ if (typeof id === 'string' && id.length > 0) {
276
+ ids.add(id);
277
+ }
278
+ }
279
+ return ids;
280
+ }
281
+ /**
282
+ * Validate the `contributions.aiAgentProviders` array on a manifest.
283
+ *
284
+ * AI agent providers are surfaced in Nimbalyst's chat / agent UI as selectable
285
+ * providers (alongside built-in Claude Code, Claude, OpenAI, etc.). Each
286
+ * provider is implemented by a backend module that runs outside the renderer
287
+ * (so it can drive a CLI, hold long-lived processes, or speak a custom
288
+ * protocol). The contribution shape, in practice:
289
+ *
290
+ * {
291
+ * id: string, // unique within the extension
292
+ * displayName: string, // shown in the provider dropdown
293
+ * backendModuleId: string, // must match a declared backendModule
294
+ * models?: Array<{ id, name, ... }> // models the provider advertises
295
+ * toolFileLinks?: { [toolName]: ... } // optional UI hint table
296
+ * }
297
+ *
298
+ * This validator checks the manifest-side wiring; it does not introspect the
299
+ * bundle. See {@link validateExtensionBundle} (Node-only) for the bundle-side
300
+ * check that the referenced backend module actually exports an
301
+ * `activate`/`createAgentProvider` function.
302
+ *
303
+ * Returns the list of problems found - empty means valid.
304
+ */
305
+ export function validateAgentProviders(aiAgentProviders, backendModules) {
306
+ if (aiAgentProviders === undefined) {
307
+ return [];
308
+ }
309
+ if (!Array.isArray(aiAgentProviders)) {
310
+ return [
311
+ { message: 'contributions.aiAgentProviders must be an array' },
312
+ ];
313
+ }
314
+ const issues = [];
315
+ if (aiAgentProviders.length > MAX_AGENT_PROVIDERS_PER_EXTENSION) {
316
+ issues.push({
317
+ message: `contributions.aiAgentProviders declares ${aiAgentProviders.length} providers; ` +
318
+ `the maximum is ${MAX_AGENT_PROVIDERS_PER_EXTENSION}. ` +
319
+ 'Split into multiple extensions, or surface variants as models on a single provider.',
320
+ });
321
+ }
322
+ const knownBackendModuleIds = extractBackendModuleIds(backendModules);
323
+ const seenProviderIds = new Set();
324
+ for (let i = 0; i < aiAgentProviders.length; i += 1) {
325
+ const raw = aiAgentProviders[i];
326
+ if (!raw || typeof raw !== 'object') {
327
+ issues.push({
328
+ message: `aiAgentProviders[${i}] must be an object`,
329
+ });
330
+ continue;
331
+ }
332
+ const provider = raw;
333
+ const providerLabel = typeof provider.id === 'string' && provider.id.length > 0
334
+ ? provider.id
335
+ : `index ${i}`;
336
+ // (b) id presence + shape + uniqueness within the extension.
337
+ if (typeof provider.id !== 'string' ||
338
+ !AGENT_PROVIDER_ID_PATTERN.test(provider.id)) {
339
+ issues.push({
340
+ providerId: typeof provider.id === 'string' ? provider.id : undefined,
341
+ message: `aiAgentProviders[${providerLabel}].id must be a lowercase string matching ` +
342
+ `${AGENT_PROVIDER_ID_PATTERN.source} (got ${JSON.stringify(provider.id)})`,
343
+ });
344
+ }
345
+ else if (seenProviderIds.has(provider.id)) {
346
+ issues.push({
347
+ providerId: provider.id,
348
+ message: `aiAgentProviders contains duplicate id "${provider.id}"`,
349
+ });
350
+ }
351
+ else {
352
+ seenProviderIds.add(provider.id);
353
+ }
354
+ // displayName: non-empty string. Surfaced verbatim in the provider
355
+ // dropdown, so it has to be readable; not currently length-capped because
356
+ // we have no signal yet on what the UI tolerates. Matches the
357
+ // AiAgentProviderContribution type, which names this field `displayName`.
358
+ if (typeof provider.displayName !== 'string' || provider.displayName.trim().length === 0) {
359
+ issues.push({
360
+ providerId: typeof provider.id === 'string' ? provider.id : undefined,
361
+ message: `aiAgentProviders[${providerLabel}].displayName must be a non-empty string ` +
362
+ '(shown verbatim in the provider dropdown).',
363
+ });
364
+ }
365
+ // (a) backendModuleId must reference a real backendModules[*].id.
366
+ if (typeof provider.backendModuleId !== 'string' ||
367
+ provider.backendModuleId.length === 0) {
368
+ issues.push({
369
+ providerId: typeof provider.id === 'string' ? provider.id : undefined,
370
+ message: `aiAgentProviders[${providerLabel}].backendModuleId must be a non-empty string ` +
371
+ 'referencing a backendModules[*].id declared on the same manifest.',
372
+ });
373
+ }
374
+ else if (!knownBackendModuleIds.has(provider.backendModuleId)) {
375
+ issues.push({
376
+ providerId: typeof provider.id === 'string' ? provider.id : undefined,
377
+ message: `aiAgentProviders[${providerLabel}].backendModuleId "${provider.backendModuleId}" ` +
378
+ 'does not match any contributions.backendModules[*].id. ' +
379
+ 'Agent providers must be implemented by a declared backend module on the same extension.',
380
+ });
381
+ }
382
+ // (c) models, if present, must be an array of { id, name }. These are
383
+ // the only required fields on the AiAgentProviderModel type; the host
384
+ // derives the provider from the contribution id, so models carry no
385
+ // per-entry provider field.
386
+ if (provider.models !== undefined) {
387
+ if (!Array.isArray(provider.models)) {
388
+ issues.push({
389
+ providerId: typeof provider.id === 'string' ? provider.id : undefined,
390
+ message: `aiAgentProviders[${providerLabel}].models must be an array (or omitted).`,
391
+ });
392
+ }
393
+ else {
394
+ const seenModelIds = new Set();
395
+ for (let m = 0; m < provider.models.length; m += 1) {
396
+ const rawModel = provider.models[m];
397
+ const modelLabel = `models[${m}]`;
398
+ if (!rawModel || typeof rawModel !== 'object') {
399
+ issues.push({
400
+ providerId: typeof provider.id === 'string' ? provider.id : undefined,
401
+ message: `aiAgentProviders[${providerLabel}].${modelLabel} must be an object`,
402
+ });
403
+ continue;
404
+ }
405
+ const model = rawModel;
406
+ if (typeof model.id !== 'string' ||
407
+ model.id.trim().length === 0) {
408
+ issues.push({
409
+ providerId: typeof provider.id === 'string' ? provider.id : undefined,
410
+ message: `aiAgentProviders[${providerLabel}].${modelLabel}.id must be a non-empty string`,
411
+ });
412
+ }
413
+ else if (seenModelIds.has(model.id)) {
414
+ issues.push({
415
+ providerId: typeof provider.id === 'string' ? provider.id : undefined,
416
+ message: `aiAgentProviders[${providerLabel}].models contains duplicate id "${model.id}"`,
417
+ });
418
+ }
419
+ else {
420
+ seenModelIds.add(model.id);
421
+ }
422
+ if (typeof model.name !== 'string' ||
423
+ model.name.trim().length === 0) {
424
+ issues.push({
425
+ providerId: typeof provider.id === 'string' ? provider.id : undefined,
426
+ message: `aiAgentProviders[${providerLabel}].${modelLabel}.name must be a non-empty string`,
427
+ });
428
+ }
429
+ }
430
+ }
431
+ }
432
+ // (d) toolFileLinks, if present, must be a plain object whose keys are
433
+ // non-empty strings (tool names). We deliberately do NOT validate the
434
+ // values: the host-side schema for the link payload is still in flux,
435
+ // and rejecting unknown value shapes here would force every shape change
436
+ // through this SDK file. Keys are the only contract this validator
437
+ // pins.
438
+ if (provider.toolFileLinks !== undefined) {
439
+ if (typeof provider.toolFileLinks !== 'object' ||
440
+ provider.toolFileLinks === null ||
441
+ Array.isArray(provider.toolFileLinks)) {
442
+ issues.push({
443
+ providerId: typeof provider.id === 'string' ? provider.id : undefined,
444
+ message: `aiAgentProviders[${providerLabel}].toolFileLinks must be a plain object ` +
445
+ 'mapping tool names to link descriptors (or omitted).',
446
+ });
447
+ }
448
+ else {
449
+ for (const key of Object.keys(provider.toolFileLinks)) {
450
+ if (typeof key !== 'string' || key.length === 0) {
451
+ issues.push({
452
+ providerId: typeof provider.id === 'string' ? provider.id : undefined,
453
+ message: `aiAgentProviders[${providerLabel}].toolFileLinks contains an empty/invalid tool name key`,
454
+ });
455
+ }
456
+ }
457
+ }
458
+ }
459
+ }
460
+ return issues;
461
+ }
462
+ /**
463
+ * Convenience wrapper that throws if any fatal aiAgentProviders issues are
464
+ * found. Mirrors {@link assertBackendModulesValid}. Use this in main-process
465
+ * load paths where invalid manifests should refuse the extension outright.
466
+ */
467
+ export function assertAgentProvidersValid(extensionId, aiAgentProviders, backendModules) {
468
+ const issues = validateAgentProviders(aiAgentProviders, backendModules);
469
+ const fatal = issues.filter((i) => i.severity !== 'warning');
470
+ if (fatal.length === 0) {
471
+ return;
472
+ }
473
+ const lines = fatal.map((i) => i.providerId ? ` - [${i.providerId}] ${i.message}` : ` - ${i.message}`);
474
+ throw new Error(`Extension ${extensionId} has invalid aiAgentProviders declarations:\n${lines.join('\n')}`);
475
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * CollabCodec
3
+ *
4
+ * Per-extension Y.Doc content contract: the single, PURE thing an editor
5
+ * defines for collaboration. Pure functions `file bytes <-> Y.Doc shape` (no
6
+ * React, no host imports) so the same code seeds a live editor AND runs
7
+ * headlessly (Share-to-Team without the editor open, re-upload from local
8
+ * origin, plain-text projection for search/AI). It lets host features
9
+ * (re-upload, history, export, AI editing, search indexing, comments, backup,
10
+ * restore) operate on any extension's collaborative document without knowing
11
+ * its internal layout.
12
+ *
13
+ * Extensions register a codec via
14
+ * `context.services.collab.registerContentAdapter(...)` from their
15
+ * `activate()` function, and pass the SAME codec to
16
+ * `useCollaborativeEditor(host, { codec, bind })`. `isEmpty` / `seedFromFile`
17
+ * / `exportToFile` are therefore defined exactly ONCE, on the codec -- the
18
+ * live seed and the headless seed are provably the same code.
19
+ *
20
+ * Codecs run only client-side (main process, renderer, extensions). The collab
21
+ * Worker stays codec-agnostic and treats Y.Doc state as opaque ciphertext. The
22
+ * renderer registry is authoritative; the main-process registry is an optional
23
+ * cache (in-repo statics + text descriptors) and its absence degrades to
24
+ * client seeding, never a hard error.
25
+ *
26
+ * NOTE: This is the canonical definition. The host-internal
27
+ * `@nimbalyst/collab-adapters` registry re-imports this type --
28
+ * do not fork the interface there.
29
+ *
30
+ * @see CollabContentAdapter -- the former name, kept as a deprecated alias.
31
+ */
32
+ import type { Doc } from 'yjs';
33
+ export type CollabContentFileSource = string | Uint8Array;
34
+ /**
35
+ * Serializable description of a CollabContentAdapter that lets the HOST
36
+ * replicate the adapter in another process (e.g. the Electron main process)
37
+ * WITHOUT loading the extension's code or doing a dynamic import. Only adapters
38
+ * built from a known host factory carry one. Today the single kind is `text`
39
+ * (a one-`Y.Text` document, produced by `createTextCollabContentAdapter`).
40
+ */
41
+ export interface TextCollabAdapterDescriptor {
42
+ kind: 'text';
43
+ documentType: string;
44
+ fileExtensions: string[];
45
+ mimeType?: string;
46
+ textField: string;
47
+ layoutVersion: number;
48
+ }
49
+ export type CollabAdapterDescriptor = TextCollabAdapterDescriptor;
50
+ export interface CollabCodecMigration {
51
+ from: number;
52
+ to: number;
53
+ run(yDoc: Doc): void;
54
+ }
55
+ /** @deprecated Renamed to {@link CollabCodecMigration}. */
56
+ export type CollabContentAdapterMigration = CollabCodecMigration;
57
+ export interface CollabCodec<TStructured = unknown> {
58
+ /** Identifies this codec; matches the shared doc's documentType. */
59
+ documentType: string;
60
+ /** File extensions this adapter is the on-disk codec for. Include
61
+ * the leading dot (e.g. '.md', '.mockup.html'). The first entry is
62
+ * used as the default by save-a-copy / export-to-file flows. */
63
+ fileExtensions: string[];
64
+ /** Optional MIME type used by save dialogs and asset uploads. */
65
+ mimeType?: string;
66
+ /** Layout schema version. Bump when the Y.Doc shape changes; pair
67
+ * with `migrations` to migrate older docs forward before any
68
+ * write op. */
69
+ layoutVersion: number;
70
+ /** Optional migrations from older layout versions. Run by the
71
+ * registry before `applyFromFile` / `applyStructuredPatch` when
72
+ * the Y.Doc's recorded layoutVersion is older than this adapter's
73
+ * layoutVersion. */
74
+ migrations?: CollabCodecMigration[];
75
+ /** True iff the Y.Doc has no extension content yet. Used to gate
76
+ * the initial-share seed flow. Shared by the live seed (the hook)
77
+ * and every headless seed path, so they can never disagree. */
78
+ isEmpty(yDoc: Doc): boolean;
79
+ /** Seed an empty Y.Doc from on-disk file bytes/text. Initial share
80
+ * only -- adapters can assume `isEmpty(yDoc) === true`. */
81
+ seedFromFile(yDoc: Doc, source: CollabContentFileSource): void;
82
+ /** Replace Y.Doc content with the supplied on-disk file content.
83
+ * Must be safe to call on a populated Y.Doc. Default behaviour is
84
+ * wipe-and-reseed inside a single Y.Doc transaction. Adapters
85
+ * that want finer-grained history can override with a
86
+ * diff-and-patch implementation. */
87
+ applyFromFile(yDoc: Doc, source: CollabContentFileSource): void;
88
+ /** Serialize the live Y.Doc back to the on-disk file format. */
89
+ exportToFile(yDoc: Doc): string | Uint8Array;
90
+ /** Plain-text projection for search, AI prompts, diffs, history
91
+ * previews. Lossy is fine; this is not a round-trip channel. */
92
+ toPlainText(yDoc: Doc): string;
93
+ /** Optional: structured projection for AI tool-call edits and
94
+ * comment anchoring. Shape is extension-defined. Paired with
95
+ * `applyStructuredPatch`. */
96
+ toStructured?(yDoc: Doc): TStructured;
97
+ /** Optional: write structured edits back. Paired with
98
+ * `toStructured`. AI-write surface is gated on the presence of
99
+ * both this and `toStructured`. */
100
+ applyStructuredPatch?(yDoc: Doc, patch: unknown): void;
101
+ /** Optional: produce a snapshot for revision history. Defaults to
102
+ * `Y.encodeStateAsUpdateV2(yDoc)`. Override if you need a denser
103
+ * snapshot format. */
104
+ exportRevisionSnapshot?(yDoc: Doc): Uint8Array;
105
+ /** Optional: restore a revision snapshot. Defaults to
106
+ * `Y.applyUpdateV2(yDoc, bytes)`. */
107
+ restoreRevisionSnapshot?(yDoc: Doc, bytes: Uint8Array): void;
108
+ /** Optional: a serializable descriptor the host can ship to another process
109
+ * to rebuild this adapter without the extension's code. Set automatically by
110
+ * host factories like `createTextCollabContentAdapter`; hand-written
111
+ * adapters may leave it undefined (they stay process-local). */
112
+ serializableDescriptor?: CollabAdapterDescriptor;
113
+ }
114
+ /**
115
+ * @deprecated Renamed to {@link CollabCodec}. Same shape -- a codec is a pure
116
+ * `file bytes <-> Y.Doc shape` contract. Kept so existing extensions and the
117
+ * host-internal `@nimbalyst/collab-adapters` registry keep compiling.
118
+ */
119
+ export type CollabContentAdapter<TStructured = unknown> = CollabCodec<TStructured>;
120
+ /**
121
+ * The collab surface on the extension context. Extensions call into
122
+ * this from their `activate()` to register a collab codec for any
123
+ * document type they ship.
124
+ */
125
+ export interface ExtensionCollabService {
126
+ /**
127
+ * Register a CollabCodec for one of the extension's document types.
128
+ * Returns a Disposable that unregisters on deactivation; the host also
129
+ * tracks the registration in `context.subscriptions` automatically.
130
+ *
131
+ * (Named `registerContentAdapter` for backwards compatibility; it accepts a
132
+ * {@link CollabCodec}.)
133
+ */
134
+ registerContentAdapter(codec: CollabCodec): {
135
+ dispose(): void;
136
+ };
137
+ }
138
+ //# sourceMappingURL=collab.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collab.d.ts","sourceRoot":"","sources":["../../src/types/collab.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE/B,MAAM,MAAM,uBAAuB,GAAG,MAAM,GAAG,UAAU,CAAC;AAE1D;;;;;;GAMG;AACH,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,uBAAuB,GAAG,2BAA2B,CAAC;AAElE,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,CAAC;CACtB;AAED,2DAA2D;AAC3D,MAAM,MAAM,6BAA6B,GAAG,oBAAoB,CAAC;AAEjE,MAAM,WAAW,WAAW,CAAC,WAAW,GAAG,OAAO;IAChD,oEAAoE;IACpE,YAAY,EAAE,MAAM,CAAC;IAErB;;qEAEiE;IACjE,cAAc,EAAE,MAAM,EAAE,CAAC;IAEzB,iEAAiE;IACjE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;oBAEgB;IAChB,aAAa,EAAE,MAAM,CAAC;IAEtB;;;yBAGqB;IACrB,UAAU,CAAC,EAAE,oBAAoB,EAAE,CAAC;IAEpC;;oEAEgE;IAChE,OAAO,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC;IAE5B;gEAC4D;IAC5D,YAAY,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,uBAAuB,GAAG,IAAI,CAAC;IAE/D;;;;yCAIqC;IACrC,aAAa,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,uBAAuB,GAAG,IAAI,CAAC;IAEhE,gEAAgE;IAChE,YAAY,CAAC,IAAI,EAAE,GAAG,GAAG,MAAM,GAAG,UAAU,CAAC;IAE7C;qEACiE;IACjE,WAAW,CAAC,IAAI,EAAE,GAAG,GAAG,MAAM,CAAC;IAE/B;;kCAE8B;IAC9B,YAAY,CAAC,CAAC,IAAI,EAAE,GAAG,GAAG,WAAW,CAAC;IAEtC;;wCAEoC;IACpC,oBAAoB,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAEvD;;2BAEuB;IACvB,sBAAsB,CAAC,CAAC,IAAI,EAAE,GAAG,GAAG,UAAU,CAAC;IAE/C;0CACsC;IACtC,uBAAuB,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IAE7D;;;qEAGiE;IACjE,sBAAsB,CAAC,EAAE,uBAAuB,CAAC;CAClD;AAED;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,CAAC,WAAW,GAAG,OAAO,IAAI,WAAW,CAAC,WAAW,CAAC,CAAC;AAEnF;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;;;;OAOG;IACH,sBAAsB,CAAC,KAAK,EAAE,WAAW,GAAG;QAAE,OAAO,IAAI,IAAI,CAAA;KAAE,CAAC;CACjE"}
@@ -0,0 +1 @@
1
+ export {};