@ikenga/contract 0.5.0 → 0.6.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.
Files changed (48) hide show
  1. package/dist/browser.d.ts +624 -0
  2. package/dist/browser.d.ts.map +1 -0
  3. package/dist/browser.js +264 -0
  4. package/dist/browser.js.map +1 -0
  5. package/dist/engine/acp.d.ts +271 -0
  6. package/dist/engine/acp.d.ts.map +1 -0
  7. package/dist/engine/acp.js +13 -0
  8. package/dist/engine/acp.js.map +1 -0
  9. package/dist/{engine.d.ts → engine/adapter.d.ts} +60 -243
  10. package/dist/engine/adapter.d.ts.map +1 -0
  11. package/dist/{engine.js → engine/adapter.js} +14 -6
  12. package/dist/engine/adapter.js.map +1 -0
  13. package/dist/engine/errors.d.ts +17 -0
  14. package/dist/engine/errors.d.ts.map +1 -0
  15. package/dist/engine/errors.js +19 -0
  16. package/dist/engine/errors.js.map +1 -0
  17. package/dist/engine/index.d.ts +12 -0
  18. package/dist/engine/index.d.ts.map +1 -0
  19. package/dist/engine/index.js +12 -0
  20. package/dist/engine/index.js.map +1 -0
  21. package/dist/index.d.ts +2 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +2 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/manifest.d.ts +147 -0
  26. package/dist/manifest.d.ts.map +1 -1
  27. package/dist/manifest.js +32 -1
  28. package/dist/manifest.js.map +1 -1
  29. package/dist/registry.d.ts +216 -0
  30. package/dist/registry.d.ts.map +1 -1
  31. package/dist/registry.js +23 -0
  32. package/dist/registry.js.map +1 -1
  33. package/dist/rpc.d.ts +1 -1
  34. package/dist/rpc.d.ts.map +1 -1
  35. package/package.json +7 -3
  36. package/src/browser.test.ts +350 -0
  37. package/src/browser.ts +364 -0
  38. package/src/{engine.ts → engine/acp.ts} +49 -198
  39. package/src/engine/adapter.ts +243 -0
  40. package/src/{engine.test.ts → engine/engine.test.ts} +33 -2
  41. package/src/engine/errors.ts +20 -0
  42. package/src/engine/index.ts +12 -0
  43. package/src/index.ts +2 -1
  44. package/src/manifest.ts +35 -1
  45. package/src/registry.ts +25 -0
  46. package/src/rpc.ts +1 -1
  47. package/dist/engine.d.ts.map +0 -1
  48. package/dist/engine.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikenga/contract",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Shared contract for the Ikenga pkg system: manifest schema, RPC types, Engine interface, capability scopes.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -24,8 +24,8 @@
24
24
  "import": "./dist/rpc.js"
25
25
  },
26
26
  "./engine": {
27
- "types": "./dist/engine.d.ts",
28
- "import": "./dist/engine.js"
27
+ "types": "./dist/engine/index.d.ts",
28
+ "import": "./dist/engine/index.js"
29
29
  },
30
30
  "./scopes": {
31
31
  "types": "./dist/scopes.d.ts",
@@ -42,6 +42,10 @@
42
42
  "./registry": {
43
43
  "types": "./dist/registry.d.ts",
44
44
  "import": "./dist/registry.js"
45
+ },
46
+ "./browser": {
47
+ "types": "./dist/browser.d.ts",
48
+ "import": "./dist/browser.js"
45
49
  }
46
50
  },
47
51
  "files": [
@@ -0,0 +1,350 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import {
5
+ BROWSER_PANE_ID_PATTERN,
6
+ BROWSER_REF_PATTERN,
7
+ BROWSER_WAIT_FOR_KINDS,
8
+ BrowserClickInputSchema,
9
+ BrowserEvalInputSchema,
10
+ BrowserFillInputSchema,
11
+ BrowserGotoInputSchema,
12
+ BrowserListResultSchema,
13
+ BrowserOpenInputSchema,
14
+ BrowserOpenResultSchema,
15
+ BrowserPaneIdSchema,
16
+ BrowserPressKeyInputSchema,
17
+ BrowserReadTextInputSchema,
18
+ BrowserRefSchema,
19
+ BrowserScreenshotInputSchema,
20
+ BrowserSelectInputSchema,
21
+ BrowserSessionSchema,
22
+ BrowserSessionCreateInputSchema,
23
+ BrowserSessionDeleteInputSchema,
24
+ BrowserSessionListResultSchema,
25
+ BrowserSnapshotSchema,
26
+ BrowserWaitForInputSchema,
27
+ BrowserWaitForResultSchema,
28
+ } from './browser.js';
29
+
30
+ // ---------- Brand patterns ----------
31
+
32
+ test('BrowserPaneId accepts b1, b42; rejects b0, b, B1, banana', () => {
33
+ for (const ok of ['b1', 'b42', 'b9999']) {
34
+ assert.equal(BROWSER_PANE_ID_PATTERN.test(ok), true, `expected ${ok} ok`);
35
+ assert.equal(BrowserPaneIdSchema.safeParse(ok).success, true);
36
+ }
37
+ for (const bad of ['b0', 'b', 'B1', 'banana', 'b01', 'b 1', '']) {
38
+ assert.equal(BROWSER_PANE_ID_PATTERN.test(bad), false, `expected ${bad} bad`);
39
+ assert.equal(BrowserPaneIdSchema.safeParse(bad).success, false);
40
+ }
41
+ });
42
+
43
+ test('BrowserRef accepts e0, e12; rejects E0, e, eX', () => {
44
+ for (const ok of ['e0', 'e1', 'e123']) {
45
+ assert.equal(BROWSER_REF_PATTERN.test(ok), true);
46
+ assert.equal(BrowserRefSchema.safeParse(ok).success, true);
47
+ }
48
+ for (const bad of ['E0', 'e', 'eX', 'e 1', '0', '']) {
49
+ assert.equal(BrowserRefSchema.safeParse(bad).success, false);
50
+ }
51
+ });
52
+
53
+ // ---------- open / list / nav ----------
54
+
55
+ test('BrowserOpenInput: url required + must be URL; partition optional', () => {
56
+ assert.equal(
57
+ BrowserOpenInputSchema.safeParse({ url: 'https://example.com' }).success,
58
+ true,
59
+ );
60
+ assert.equal(
61
+ BrowserOpenInputSchema.safeParse({ url: 'not-a-url' }).success,
62
+ false,
63
+ );
64
+ assert.equal(BrowserOpenInputSchema.safeParse({}).success, false);
65
+ assert.equal(
66
+ BrowserOpenInputSchema.safeParse({
67
+ url: 'https://example.com',
68
+ partition: 'work',
69
+ rect: { x: 0, y: 0, w: 800, h: 600 },
70
+ }).success,
71
+ true,
72
+ );
73
+ });
74
+
75
+ test('BrowserOpenInput: rect dimensions must be positive', () => {
76
+ const bad = BrowserOpenInputSchema.safeParse({
77
+ url: 'https://example.com',
78
+ rect: { x: 0, y: 0, w: 0, h: 600 },
79
+ });
80
+ assert.equal(bad.success, false);
81
+ });
82
+
83
+ test('BrowserOpenResult round-trips', () => {
84
+ const parsed = BrowserOpenResultSchema.parse({
85
+ id: 'b1',
86
+ url: 'https://example.com',
87
+ partition: 'default',
88
+ });
89
+ assert.equal(parsed.id, 'b1');
90
+ assert.equal(parsed.partition, 'default');
91
+ });
92
+
93
+ test('BrowserListResult parses an empty list', () => {
94
+ const parsed = BrowserListResultSchema.parse({ panes: [] });
95
+ assert.deepEqual(parsed.panes, []);
96
+ });
97
+
98
+ test('BrowserListResult parses populated panes', () => {
99
+ const parsed = BrowserListResultSchema.parse({
100
+ panes: [
101
+ { id: 'b1', url: 'https://example.com', partition: 'default', focused: true },
102
+ { id: 'b2', url: 'https://other.test', partition: 'work', focused: false },
103
+ ],
104
+ });
105
+ assert.equal(parsed.panes.length, 2);
106
+ assert.equal(parsed.panes[0]!.focused, true);
107
+ });
108
+
109
+ test('BrowserGotoInput: url must validate', () => {
110
+ assert.equal(
111
+ BrowserGotoInputSchema.safeParse({ id: 'b1', url: 'https://example.com/x' }).success,
112
+ true,
113
+ );
114
+ assert.equal(
115
+ BrowserGotoInputSchema.safeParse({ id: 'b1', url: 'not-a-url' }).success,
116
+ false,
117
+ );
118
+ });
119
+
120
+ // ---------- snapshot ----------
121
+
122
+ test('BrowserSnapshot accepts a realistic minimal payload', () => {
123
+ const parsed = BrowserSnapshotSchema.parse({
124
+ id: 'b1',
125
+ url: 'https://example.com/',
126
+ title: 'Example Domain',
127
+ text: 'document\n heading "Example Domain" [ref=e1]',
128
+ nodes: [
129
+ { ref: 'e0', role: 'document', children: ['e1'] },
130
+ { ref: 'e1', role: 'heading', name: 'Example Domain' },
131
+ ],
132
+ snapshotId: 0,
133
+ });
134
+ assert.equal(parsed.nodes.length, 2);
135
+ assert.equal(parsed.snapshotId, 0);
136
+ });
137
+
138
+ test('BrowserSnapshot rejects bad ref shapes inside nodes', () => {
139
+ const bad = BrowserSnapshotSchema.safeParse({
140
+ id: 'b1',
141
+ url: 'https://example.com/',
142
+ title: 'x',
143
+ text: 'x',
144
+ nodes: [{ ref: 'BAD', role: 'document' }],
145
+ snapshotId: 0,
146
+ });
147
+ assert.equal(bad.success, false);
148
+ });
149
+
150
+ // ---------- interaction ----------
151
+
152
+ test('BrowserClickInput accepts ref-only, selector-only, text-only forms', () => {
153
+ for (const variant of [
154
+ { id: 'b1', ref: 'e2' },
155
+ { id: 'b1', selector: 'button.primary' },
156
+ { id: 'b1', text: 'Sign in' },
157
+ ]) {
158
+ assert.equal(BrowserClickInputSchema.safeParse(variant).success, true);
159
+ }
160
+ });
161
+
162
+ test('BrowserFillInput requires text', () => {
163
+ assert.equal(
164
+ BrowserFillInputSchema.safeParse({ id: 'b1', ref: 'e3', text: 'hello' }).success,
165
+ true,
166
+ );
167
+ assert.equal(
168
+ BrowserFillInputSchema.safeParse({ id: 'b1', ref: 'e3' }).success,
169
+ false,
170
+ );
171
+ });
172
+
173
+ test('BrowserSelectInput requires value', () => {
174
+ assert.equal(
175
+ BrowserSelectInputSchema.safeParse({ id: 'b1', ref: 'e3', value: 'usd' }).success,
176
+ true,
177
+ );
178
+ assert.equal(
179
+ BrowserSelectInputSchema.safeParse({ id: 'b1', ref: 'e3' }).success,
180
+ false,
181
+ );
182
+ });
183
+
184
+ test('BrowserPressKeyInput requires combo', () => {
185
+ assert.equal(
186
+ BrowserPressKeyInputSchema.safeParse({ id: 'b1', combo: 'Enter' }).success,
187
+ true,
188
+ );
189
+ assert.equal(BrowserPressKeyInputSchema.safeParse({ id: 'b1' }).success, false);
190
+ });
191
+
192
+ // ---------- wait_for ----------
193
+
194
+ test('BrowserWaitForInput enforces kind enum', () => {
195
+ for (const kind of BROWSER_WAIT_FOR_KINDS) {
196
+ assert.equal(
197
+ BrowserWaitForInputSchema.safeParse({ id: 'b1', kind, value: 'x' }).success,
198
+ true,
199
+ );
200
+ }
201
+ assert.equal(
202
+ BrowserWaitForInputSchema.safeParse({ id: 'b1', kind: 'bogus' }).success,
203
+ false,
204
+ );
205
+ });
206
+
207
+ test('BrowserWaitForInput clamps timeout_ms range', () => {
208
+ assert.equal(
209
+ BrowserWaitForInputSchema.safeParse({
210
+ id: 'b1',
211
+ kind: 'idle',
212
+ timeout_ms: 50,
213
+ }).success,
214
+ false,
215
+ );
216
+ assert.equal(
217
+ BrowserWaitForInputSchema.safeParse({
218
+ id: 'b1',
219
+ kind: 'idle',
220
+ timeout_ms: 60_001,
221
+ }).success,
222
+ false,
223
+ );
224
+ assert.equal(
225
+ BrowserWaitForInputSchema.safeParse({
226
+ id: 'b1',
227
+ kind: 'idle',
228
+ timeout_ms: 5000,
229
+ }).success,
230
+ true,
231
+ );
232
+ });
233
+
234
+ test('BrowserWaitForResult shape', () => {
235
+ const parsed = BrowserWaitForResultSchema.parse({
236
+ satisfied: true,
237
+ elapsed_ms: 123,
238
+ });
239
+ assert.equal(parsed.satisfied, true);
240
+ assert.equal(parsed.elapsed_ms, 123);
241
+ });
242
+
243
+ // ---------- misc ----------
244
+
245
+ test('BrowserReadTextInput requires both id and ref', () => {
246
+ assert.equal(
247
+ BrowserReadTextInputSchema.safeParse({ id: 'b1', ref: 'e1' }).success,
248
+ true,
249
+ );
250
+ assert.equal(BrowserReadTextInputSchema.safeParse({ id: 'b1' }).success, false);
251
+ assert.equal(BrowserReadTextInputSchema.safeParse({ ref: 'e1' }).success, false);
252
+ });
253
+
254
+ test('BrowserScreenshotInput accepts optional out_path', () => {
255
+ assert.equal(BrowserScreenshotInputSchema.safeParse({ id: 'b1' }).success, true);
256
+ assert.equal(
257
+ BrowserScreenshotInputSchema.safeParse({ id: 'b1', out_path: '/tmp/x.png' })
258
+ .success,
259
+ true,
260
+ );
261
+ });
262
+
263
+ test('BrowserEvalInput requires script string', () => {
264
+ assert.equal(
265
+ BrowserEvalInputSchema.safeParse({ id: 'b1', script: '1+1' }).success,
266
+ true,
267
+ );
268
+ assert.equal(BrowserEvalInputSchema.safeParse({ id: 'b1' }).success, false);
269
+ });
270
+
271
+ test('BrowserSession accepts unix-ms timestamps; last_used_at is optional', () => {
272
+ const ok = BrowserSessionSchema.safeParse({
273
+ pkg_id: 'com.ikenga.mcp-browser',
274
+ name: 'spotify-main',
275
+ partition: 'spotify',
276
+ created_at: 1_778_000_000_000,
277
+ });
278
+ assert.equal(ok.success, true);
279
+
280
+ const ok2 = BrowserSessionSchema.safeParse({
281
+ pkg_id: 'com.ikenga.mcp-browser',
282
+ name: 'spotify-main',
283
+ partition: 'spotify',
284
+ created_at: 1_778_000_000_000,
285
+ last_used_at: 1_778_000_100_000,
286
+ });
287
+ assert.equal(ok2.success, true);
288
+
289
+ const ok3 = BrowserSessionSchema.safeParse({
290
+ pkg_id: 'com.ikenga.mcp-browser',
291
+ name: 'spotify-main',
292
+ partition: 'spotify',
293
+ created_at: 1_778_000_000_000,
294
+ last_used_at: null,
295
+ });
296
+ assert.equal(ok3.success, true);
297
+
298
+ const bad = BrowserSessionSchema.safeParse({
299
+ pkg_id: 'com.ikenga.mcp-browser',
300
+ name: 'spotify-main',
301
+ partition: 'spotify',
302
+ created_at: 'yesterday',
303
+ });
304
+ assert.equal(bad.success, false);
305
+ });
306
+
307
+ test('BrowserSessionCreateInput: name required, partition optional', () => {
308
+ assert.equal(
309
+ BrowserSessionCreateInputSchema.safeParse({ name: 'spotify' }).success,
310
+ true,
311
+ );
312
+ assert.equal(
313
+ BrowserSessionCreateInputSchema.safeParse({
314
+ name: 'spotify',
315
+ partition: 'spotify-prod',
316
+ }).success,
317
+ true,
318
+ );
319
+ assert.equal(BrowserSessionCreateInputSchema.safeParse({}).success, false);
320
+ assert.equal(
321
+ BrowserSessionCreateInputSchema.safeParse({ name: '' }).success,
322
+ false,
323
+ );
324
+ assert.equal(
325
+ BrowserSessionDeleteInputSchema.safeParse({ name: 'spotify' }).success,
326
+ true,
327
+ );
328
+ assert.equal(
329
+ BrowserSessionListResultSchema.safeParse({ sessions: [] }).success,
330
+ true,
331
+ );
332
+ });
333
+
334
+ test('BrowserOpenInput rejects setting both partition and session', () => {
335
+ assert.equal(
336
+ BrowserOpenInputSchema.safeParse({
337
+ url: 'https://example.com',
338
+ partition: 'p1',
339
+ session: 's1',
340
+ }).success,
341
+ false,
342
+ );
343
+ assert.equal(
344
+ BrowserOpenInputSchema.safeParse({
345
+ url: 'https://example.com',
346
+ session: 's1',
347
+ }).success,
348
+ true,
349
+ );
350
+ });