@neurosquad/card-sdk 1.0.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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +431 -0
  3. package/dist/card-sdk.js +5266 -0
  4. package/dist/cli.js +2838 -0
  5. package/dist/react.js +257 -0
  6. package/dist/testing.js +1150 -0
  7. package/dist/types/client/base64.d.ts +7 -0
  8. package/dist/types/client/card.d.ts +653 -0
  9. package/dist/types/client/channel.d.ts +49 -0
  10. package/dist/types/client/coalesce.d.ts +14 -0
  11. package/dist/types/client/connect.d.ts +39 -0
  12. package/dist/types/client/errors.d.ts +46 -0
  13. package/dist/types/client/helpers.d.ts +25 -0
  14. package/dist/types/client/net.d.ts +111 -0
  15. package/dist/types/client/theme.d.ts +31 -0
  16. package/dist/types/client/toolResult.d.ts +16 -0
  17. package/dist/types/contract/api.d.ts +812 -0
  18. package/dist/types/contract/index.d.ts +11 -0
  19. package/dist/types/contract/jsonSchema.d.ts +88 -0
  20. package/dist/types/contract/localized.d.ts +10 -0
  21. package/dist/types/contract/manifest.d.ts +179 -0
  22. package/dist/types/contract/network.d.ts +48 -0
  23. package/dist/types/contract/permissions.d.ts +180 -0
  24. package/dist/types/contract/ports.d.ts +237 -0
  25. package/dist/types/contract/protocol.d.ts +74 -0
  26. package/dist/types/contract/source.d.ts +111 -0
  27. package/dist/types/contract/theme.d.ts +21 -0
  28. package/dist/types/contract/version.d.ts +126 -0
  29. package/dist/types/i18n.d.ts +50 -0
  30. package/dist/types/index.d.ts +28 -0
  31. package/dist/types/react/index.d.ts +132 -0
  32. package/dist/types/testing/index.d.ts +7 -0
  33. package/dist/types/testing/mockHost.d.ts +315 -0
  34. package/dist/types/version.d.ts +2 -0
  35. package/dist/ui.css +581 -0
  36. package/package.json +77 -0
  37. package/schema/neurosquad-card.v1.json +434 -0
  38. package/templates/react/README.md +34 -0
  39. package/templates/react/_gitignore +10 -0
  40. package/templates/react/icon.png +0 -0
  41. package/templates/react/index.html +12 -0
  42. package/templates/react/neurosquad-card.json +94 -0
  43. package/templates/react/package.json +25 -0
  44. package/templates/react/src/App.tsx +131 -0
  45. package/templates/react/src/i18n.ts +61 -0
  46. package/templates/react/src/main.tsx +77 -0
  47. package/templates/react/src/styles.css +42 -0
  48. package/templates/react/tsconfig.json +18 -0
  49. package/templates/react/vite.config.ts +19 -0
  50. package/templates/vanilla/README.md +37 -0
  51. package/templates/vanilla/_gitignore +6 -0
  52. package/templates/vanilla/icon.png +0 -0
  53. package/templates/vanilla/index.html +41 -0
  54. package/templates/vanilla/main.js +256 -0
  55. package/templates/vanilla/neurosquad-card.json +94 -0
  56. package/templates/vanilla/style.css +41 -0
package/dist/cli.js ADDED
@@ -0,0 +1,2838 @@
1
+ #!/usr/bin/env node
2
+ /*! @neurosquad/card-sdk 1.0.0 | MIT */
3
+
4
+ // src/version.ts
5
+ var SDK_VERSION = "1.0.0";
6
+
7
+ // src/cli/create.ts
8
+ import {
9
+ copyFileSync,
10
+ existsSync as existsSync2,
11
+ mkdirSync,
12
+ readdirSync,
13
+ readFileSync as readFileSync2,
14
+ statSync,
15
+ writeFileSync
16
+ } from "node:fs";
17
+ import { basename, join as join2, relative, resolve } from "node:path";
18
+
19
+ // src/contract/version.ts
20
+ var CARD_PROTOCOL_VERSION = 1;
21
+ var MANIFEST_FILE = "neurosquad-card.json";
22
+ var MANIFEST_VERSION = 1;
23
+ var RESERVED_PATH_PREFIX = "__ns/";
24
+ var KiB = 1024;
25
+ var MiB = 1024 * KiB;
26
+ var LIMITS = {
27
+ // --- wire ---------------------------------------------------------------
28
+ /** Serialized size of one message (JSON length in UTF-16 code units, as measured by JSON.stringify). */
29
+ messageBytes: 1 * MiB,
30
+ /** Responses of fs.read / net.fetch may be this large. */
31
+ largeMessageBytes: 12 * MiB,
32
+ inFlightRequests: 64,
33
+ /** Token bucket per instance: refill per second / bucket size. */
34
+ requestsPerSecond: 200,
35
+ requestBurst: 400,
36
+ valueDepth: 64,
37
+ valueNodes: 2e5,
38
+ // --- package ------------------------------------------------------------
39
+ tarballBytes: 50 * MiB,
40
+ unpackedBytes: 100 * MiB,
41
+ fileBytes: 20 * MiB,
42
+ files: 5e3,
43
+ pathDepth: 20,
44
+ pathChars: 240,
45
+ manifestBytes: 256 * KiB,
46
+ iconBytes: 128 * KiB,
47
+ // --- storage ------------------------------------------------------------
48
+ storageKeyChars: 256,
49
+ storageValueBytes: 1 * MiB,
50
+ storageKeys: 1e4,
51
+ instanceStorageBytes: 5 * MiB,
52
+ packageStorageBytes: 20 * MiB,
53
+ // --- manifest content ---------------------------------------------------
54
+ settings: 40,
55
+ portsPerDirection: 16,
56
+ toolsPerPackage: 32,
57
+ permissionHosts: 32,
58
+ schemaNodes: 500,
59
+ schemaDepth: 16,
60
+ /** `enum` options per schema node, and their serialized size (MC-2: comparisons cost main CPU). */
61
+ schemaEnumOptions: 256,
62
+ schemaEnumBytes: 16 * KiB,
63
+ /** Serialized size of one `const` value in a card schema. */
64
+ schemaConstBytes: 4 * KiB,
65
+ menuItems: 12,
66
+ // --- network ------------------------------------------------------------
67
+ netRequestBodyBytes: 5 * MiB,
68
+ netResponseBytes: 10 * MiB,
69
+ netConcurrent: 6,
70
+ netPerMinute: 120,
71
+ netRedirects: 5,
72
+ netTimeoutMs: 3e4,
73
+ netMaxTimeoutMs: 12e4,
74
+ netStreams: 4,
75
+ // --- files --------------------------------------------------------------
76
+ fsReadBytes: 10 * MiB,
77
+ fsWriteBytes: 10 * MiB,
78
+ fsListEntries: 5e3,
79
+ fsWatchers: 20,
80
+ // --- agents / terminals -------------------------------------------------
81
+ promptChars: 2e4,
82
+ promptsPerMinute: 6,
83
+ terminalWriteChars: 2e4,
84
+ terminalRunTimeoutMs: 12e4,
85
+ screenLines: 500,
86
+ outputFlushMs: 100,
87
+ outputFlushBytes: 64 * KiB,
88
+ // --- tools --------------------------------------------------------------
89
+ toolResultBytes: 1 * MiB,
90
+ toolTimeoutMs: 3e4,
91
+ toolMaxTimeoutMs: 12e4,
92
+ // --- ports --------------------------------------------------------------
93
+ portMessagesPerSecond: 20,
94
+ retainedValueBytes: 256 * KiB,
95
+ portRequestTimeoutMs: 3e4,
96
+ // --- canvas / ui --------------------------------------------------------
97
+ attentionIntervalMs: 1e4,
98
+ focusIntervalMs: 5e3,
99
+ toastIntervalMs: 2e3,
100
+ clipboardIntervalMs: 1e3,
101
+ clipboardChars: 1 * MiB,
102
+ spawnsPerInstance: 4,
103
+ resizeIntervalMs: 500,
104
+ titleChars: 120,
105
+ /** card.setStatus / setBadge / setOverview per minute (the SDK client coalesces to stay under it). */
106
+ chromeUpdatesPerMinute: 120,
107
+ statusChars: 80,
108
+ overviewChars: 160,
109
+ toastChars: 280,
110
+ confirmChars: 1e3,
111
+ logLineChars: 2e3,
112
+ logLinesPerSecond: 50,
113
+ // --- frames / lifecycle -------------------------------------------------
114
+ /** Card frames alive at once across the app; beyond it the least recently seen offscreen/hidden ones are suspended. */
115
+ liveFrames: 24,
116
+ /** Of those, how many may be kept alive while hidden thanks to the `background` permission. */
117
+ backgroundFrames: 8,
118
+ /** A card in a hidden workspace is suspended after this long (unless `background`). */
119
+ hiddenSuspendMs: 6e4,
120
+ /** Time a card gets between `lifecycle.suspend` and its frame being unloaded. */
121
+ suspendGraceMs: 1e3,
122
+ /** Single-use document token lifetime. */
123
+ frameTokenTtlMs: 3e4,
124
+ /** From frame attach to `host.ready`; after that the card shows "not responding". */
125
+ readyTimeoutMs: 1e4,
126
+ heartbeatMs: 5e3,
127
+ unresponsiveMs: 15e3,
128
+ /** Mounting at most this many card frames per animation frame (canvas perf). */
129
+ frameMountsPerFrame: 2
130
+ };
131
+
132
+ // src/contract/jsonSchema.ts
133
+ var CARD_STRING_FORMATS = ["uri", "date-time", "date", "email", "color", "uuid"];
134
+ var ANNOTATIONS = /* @__PURE__ */ new Set([
135
+ "$id",
136
+ "$schema",
137
+ "$comment",
138
+ "title",
139
+ "description",
140
+ "default",
141
+ "examples",
142
+ "deprecated",
143
+ "readOnly",
144
+ "writeOnly"
145
+ ]);
146
+ var KEYWORDS = /* @__PURE__ */ new Set([
147
+ "$ref",
148
+ "$defs",
149
+ "type",
150
+ "enum",
151
+ "const",
152
+ "properties",
153
+ "required",
154
+ "additionalProperties",
155
+ "minProperties",
156
+ "maxProperties",
157
+ "items",
158
+ "minItems",
159
+ "maxItems",
160
+ "uniqueItems",
161
+ "minLength",
162
+ "maxLength",
163
+ "pattern",
164
+ "format",
165
+ "minimum",
166
+ "maximum",
167
+ "exclusiveMinimum",
168
+ "exclusiveMaximum",
169
+ "multipleOf",
170
+ "anyOf",
171
+ "oneOf"
172
+ ]);
173
+ var TYPES = /* @__PURE__ */ new Set([
174
+ "string",
175
+ "number",
176
+ "integer",
177
+ "boolean",
178
+ "object",
179
+ "array",
180
+ "null"
181
+ ]);
182
+ function isPlainObject(value) {
183
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
184
+ const proto = Object.getPrototypeOf(value);
185
+ return proto === Object.prototype || proto === null;
186
+ }
187
+ function typeOf(value) {
188
+ if (value === null) return "null";
189
+ if (Array.isArray(value)) return "array";
190
+ switch (typeof value) {
191
+ case "string":
192
+ return "string";
193
+ case "boolean":
194
+ return "boolean";
195
+ case "number":
196
+ return Number.isFinite(value) ? Number.isInteger(value) ? "integer" : "number" : "invalid";
197
+ case "object":
198
+ return isPlainObject(value) ? "object" : "invalid";
199
+ default:
200
+ return "invalid";
201
+ }
202
+ }
203
+ function stableStringify(value) {
204
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
205
+ if (isPlainObject(value)) {
206
+ const keys = Object.keys(value).sort();
207
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
208
+ }
209
+ return JSON.stringify(value) ?? "null";
210
+ }
211
+ function codePointLength(text) {
212
+ let count = 0;
213
+ for (let i = 0; i < text.length; i++) {
214
+ const code = text.charCodeAt(i);
215
+ if (code >= 55296 && code <= 56319 && i + 1 < text.length) i++;
216
+ count++;
217
+ }
218
+ return count;
219
+ }
220
+ function pointer(path, key) {
221
+ return `${path}/${String(key).replace(/~/g, "~0").replace(/\//g, "~1")}`;
222
+ }
223
+ function checkJsonValue(value, limits = {}) {
224
+ const maxDepth = limits.maxDepth ?? 64;
225
+ let nodes = limits.maxNodes ?? 2e5;
226
+ const walk = (current, path, depth) => {
227
+ if (--nodes < 0) return { path, keyword: "json", message: "value has too many parts" };
228
+ if (depth > maxDepth) return { path, keyword: "json", message: "value is nested too deeply" };
229
+ const kind = typeOf(current);
230
+ if (kind === "invalid") {
231
+ return {
232
+ path,
233
+ keyword: "json",
234
+ message: "not a JSON value (NaN, Infinity, undefined, function or class instance)"
235
+ };
236
+ }
237
+ if (kind === "array") {
238
+ const list = current;
239
+ for (let i = 0; i < list.length; i++) {
240
+ const issue = walk(list[i], pointer(path, i), depth + 1);
241
+ if (issue) return issue;
242
+ }
243
+ } else if (kind === "object") {
244
+ for (const [key, child] of Object.entries(current)) {
245
+ const issue = walk(child, pointer(path, key), depth + 1);
246
+ if (issue) return issue;
247
+ }
248
+ }
249
+ return null;
250
+ };
251
+ return walk(value, "", 0);
252
+ }
253
+ var FORMAT_CHECKS = {
254
+ uri: (text) => /^[a-zA-Z][a-zA-Z0-9+.-]*:[^\s]*$/.test(text) && text.length <= 8192,
255
+ "date-time": (text) => /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?(Z|[+-]\d{2}:\d{2})$/.test(text) && !Number.isNaN(Date.parse(text)),
256
+ date: (text) => /^\d{4}-\d{2}-\d{2}$/.test(text) && !Number.isNaN(Date.parse(`${text}T00:00:00Z`)),
257
+ email: (text) => /^[^\s@]{1,64}@[^\s@]{1,255}$/.test(text),
258
+ color: (text) => /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(text),
259
+ uuid: (text) => /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(text)
260
+ };
261
+ var regexCache = /* @__PURE__ */ new Map();
262
+ function compiled(pattern) {
263
+ if (!regexCache.has(pattern)) {
264
+ let regex = null;
265
+ try {
266
+ regex = new RegExp(pattern, "u");
267
+ } catch {
268
+ regex = null;
269
+ }
270
+ if (regexCache.size > 500) regexCache.clear();
271
+ regexCache.set(pattern, regex);
272
+ }
273
+ return regexCache.get(pattern) ?? null;
274
+ }
275
+ var Budget = class {
276
+ constructor(left) {
277
+ this.left = left;
278
+ }
279
+ };
280
+ var CHARS_PER_UNIT = 64;
281
+ function chargedStringify(value, budget) {
282
+ const text = stableStringify(value);
283
+ budget.left -= Math.ceil(text.length / CHARS_PER_UNIT);
284
+ return text;
285
+ }
286
+ var enumCache = /* @__PURE__ */ new WeakMap();
287
+ var constCache = /* @__PURE__ */ new WeakMap();
288
+ function enumOptions(schema, budget) {
289
+ let options = enumCache.get(schema);
290
+ if (!options) {
291
+ options = new Set((schema.enum ?? []).map((option) => chargedStringify(option, budget)));
292
+ enumCache.set(schema, options);
293
+ }
294
+ return options;
295
+ }
296
+ function constText(schema, budget) {
297
+ let text = constCache.get(schema);
298
+ if (text === void 0) {
299
+ text = chargedStringify(schema.const, budget);
300
+ constCache.set(schema, text);
301
+ }
302
+ return text;
303
+ }
304
+ function resolveRef(root, ref) {
305
+ if (ref === "#") return root;
306
+ const match = /^#\/\$defs\/([^/]+)$/.exec(ref);
307
+ if (!match) return null;
308
+ const name = match[1].replace(/~1/g, "/").replace(/~0/g, "~");
309
+ const defs = root.$defs;
310
+ if (!isPlainObject(defs) || !Object.prototype.hasOwnProperty.call(defs, name)) return null;
311
+ const found = defs[name];
312
+ return isPlainObject(found) ? found : null;
313
+ }
314
+ function validateJson(schema, value, options = {}) {
315
+ const issues = [];
316
+ const maxIssues = options.maxIssues ?? 20;
317
+ const budget = new Budget(options.budget ?? 2e5);
318
+ const base = checkJsonValue(value, { maxDepth: options.maxDepth ?? 64 });
319
+ if (base) return [base];
320
+ validateNode(schema, schema, value, "", issues, maxIssues, budget, 0);
321
+ return issues.slice(0, maxIssues);
322
+ }
323
+ function push(issues, max, issue) {
324
+ if (issues.length < max) issues.push(issue);
325
+ }
326
+ function validateNode(root, schema, value, path, issues, maxIssues, budget, refDepth) {
327
+ if (issues.length >= maxIssues) return;
328
+ if (--budget.left < 0) {
329
+ push(issues, maxIssues, { path, keyword: "budget", message: "schema too complex to check" });
330
+ return;
331
+ }
332
+ if (schema.$ref !== void 0) {
333
+ const target = resolveRef(root, schema.$ref);
334
+ if (!target || refDepth > 32) {
335
+ push(issues, maxIssues, {
336
+ path,
337
+ keyword: "$ref",
338
+ message: `unresolvable reference ${schema.$ref}`
339
+ });
340
+ return;
341
+ }
342
+ validateNode(root, target, value, path, issues, maxIssues, budget, refDepth + 1);
343
+ if (issues.length >= maxIssues) return;
344
+ }
345
+ const actual = typeOf(value);
346
+ if (schema.type !== void 0) {
347
+ const allowed = Array.isArray(schema.type) ? schema.type : [schema.type];
348
+ const ok = allowed.some(
349
+ (type) => type === actual || type === "number" && actual === "integer"
350
+ );
351
+ if (!ok) {
352
+ push(issues, maxIssues, {
353
+ path,
354
+ keyword: "type",
355
+ message: `expected ${allowed.join(" or ")}, got ${actual}`
356
+ });
357
+ return;
358
+ }
359
+ }
360
+ let valueText;
361
+ const serialized = () => valueText ??= chargedStringify(value, budget);
362
+ if (schema.const !== void 0 && constText(schema, budget) !== serialized()) {
363
+ push(issues, maxIssues, {
364
+ path,
365
+ keyword: "const",
366
+ message: `must be ${JSON.stringify(schema.const)}`.slice(0, 300)
367
+ });
368
+ }
369
+ if (schema.enum !== void 0 && !enumOptions(schema, budget).has(serialized())) {
370
+ const shown = schema.enum.slice(0, 8).map((option) => JSON.stringify(option)).join(", ");
371
+ push(issues, maxIssues, { path, keyword: "enum", message: `must be one of ${shown}` });
372
+ }
373
+ if (actual === "string") {
374
+ const text = value;
375
+ if (schema.maxLength !== void 0 && text.length > schema.maxLength * 2) {
376
+ push(issues, maxIssues, {
377
+ path,
378
+ keyword: "maxLength",
379
+ message: `longer than ${schema.maxLength}`
380
+ });
381
+ } else {
382
+ const length = schema.minLength !== void 0 || schema.maxLength !== void 0 ? codePointLength(text) : 0;
383
+ if (schema.minLength !== void 0 && length < schema.minLength) {
384
+ push(issues, maxIssues, {
385
+ path,
386
+ keyword: "minLength",
387
+ message: `shorter than ${schema.minLength}`
388
+ });
389
+ }
390
+ if (schema.maxLength !== void 0 && length > schema.maxLength) {
391
+ push(issues, maxIssues, {
392
+ path,
393
+ keyword: "maxLength",
394
+ message: `longer than ${schema.maxLength}`
395
+ });
396
+ }
397
+ }
398
+ if (schema.pattern !== void 0) {
399
+ const regex = compiled(schema.pattern);
400
+ if (!regex || !regex.test(text)) {
401
+ push(issues, maxIssues, {
402
+ path,
403
+ keyword: "pattern",
404
+ message: `does not match ${schema.pattern}`
405
+ });
406
+ }
407
+ }
408
+ if (schema.format !== void 0) {
409
+ const check = FORMAT_CHECKS[schema.format];
410
+ if (check && !check(text)) {
411
+ push(issues, maxIssues, {
412
+ path,
413
+ keyword: "format",
414
+ message: `not a valid ${schema.format}`
415
+ });
416
+ }
417
+ }
418
+ }
419
+ if (actual === "number" || actual === "integer") {
420
+ const number = value;
421
+ if (schema.minimum !== void 0 && number < schema.minimum) {
422
+ push(issues, maxIssues, { path, keyword: "minimum", message: `less than ${schema.minimum}` });
423
+ }
424
+ if (schema.maximum !== void 0 && number > schema.maximum) {
425
+ push(issues, maxIssues, {
426
+ path,
427
+ keyword: "maximum",
428
+ message: `greater than ${schema.maximum}`
429
+ });
430
+ }
431
+ if (schema.exclusiveMinimum !== void 0 && number <= schema.exclusiveMinimum) {
432
+ push(issues, maxIssues, {
433
+ path,
434
+ keyword: "exclusiveMinimum",
435
+ message: `must be greater than ${schema.exclusiveMinimum}`
436
+ });
437
+ }
438
+ if (schema.exclusiveMaximum !== void 0 && number >= schema.exclusiveMaximum) {
439
+ push(issues, maxIssues, {
440
+ path,
441
+ keyword: "exclusiveMaximum",
442
+ message: `must be less than ${schema.exclusiveMaximum}`
443
+ });
444
+ }
445
+ if (schema.multipleOf !== void 0 && schema.multipleOf > 0) {
446
+ const quotient = number / schema.multipleOf;
447
+ if (Math.abs(quotient - Math.round(quotient)) > 1e-9) {
448
+ push(issues, maxIssues, {
449
+ path,
450
+ keyword: "multipleOf",
451
+ message: `not a multiple of ${schema.multipleOf}`
452
+ });
453
+ }
454
+ }
455
+ }
456
+ if (actual === "array") {
457
+ const list = value;
458
+ if (schema.minItems !== void 0 && list.length < schema.minItems) {
459
+ push(issues, maxIssues, {
460
+ path,
461
+ keyword: "minItems",
462
+ message: `fewer than ${schema.minItems} items`
463
+ });
464
+ }
465
+ if (schema.maxItems !== void 0 && list.length > schema.maxItems) {
466
+ push(issues, maxIssues, {
467
+ path,
468
+ keyword: "maxItems",
469
+ message: `more than ${schema.maxItems} items`
470
+ });
471
+ return;
472
+ }
473
+ if (schema.uniqueItems) {
474
+ const seen = /* @__PURE__ */ new Set();
475
+ for (const item of list) {
476
+ const key = chargedStringify(item, budget);
477
+ if (seen.has(key)) {
478
+ push(issues, maxIssues, { path, keyword: "uniqueItems", message: "items must be unique" });
479
+ break;
480
+ }
481
+ seen.add(key);
482
+ }
483
+ }
484
+ if (schema.items !== void 0) {
485
+ for (let i = 0; i < list.length && issues.length < maxIssues; i++) {
486
+ validateNode(root, schema.items, list[i], pointer(path, i), issues, maxIssues, budget, 0);
487
+ }
488
+ }
489
+ }
490
+ if (actual === "object") {
491
+ const object = value;
492
+ const keys = Object.keys(object);
493
+ if (schema.minProperties !== void 0 && keys.length < schema.minProperties) {
494
+ push(issues, maxIssues, {
495
+ path,
496
+ keyword: "minProperties",
497
+ message: `fewer than ${schema.minProperties} properties`
498
+ });
499
+ }
500
+ if (schema.maxProperties !== void 0 && keys.length > schema.maxProperties) {
501
+ push(issues, maxIssues, {
502
+ path,
503
+ keyword: "maxProperties",
504
+ message: `more than ${schema.maxProperties} properties`
505
+ });
506
+ return;
507
+ }
508
+ for (const key of schema.required ?? []) {
509
+ if (!Object.prototype.hasOwnProperty.call(object, key)) {
510
+ push(issues, maxIssues, {
511
+ path: pointer(path, key),
512
+ keyword: "required",
513
+ message: "is required"
514
+ });
515
+ }
516
+ }
517
+ const properties = schema.properties ?? {};
518
+ for (const key of keys) {
519
+ if (issues.length >= maxIssues) return;
520
+ const child = Object.prototype.hasOwnProperty.call(properties, key) ? properties[key] : void 0;
521
+ if (child) {
522
+ validateNode(root, child, object[key], pointer(path, key), issues, maxIssues, budget, 0);
523
+ } else if (schema.additionalProperties === false) {
524
+ push(issues, maxIssues, {
525
+ path: pointer(path, key),
526
+ keyword: "additionalProperties",
527
+ message: "is not allowed"
528
+ });
529
+ } else if (typeof schema.additionalProperties === "object") {
530
+ validateNode(
531
+ root,
532
+ schema.additionalProperties,
533
+ object[key],
534
+ pointer(path, key),
535
+ issues,
536
+ maxIssues,
537
+ budget,
538
+ 0
539
+ );
540
+ }
541
+ }
542
+ }
543
+ if (schema.anyOf !== void 0) {
544
+ const ok = schema.anyOf.some((option) => {
545
+ const inner = [];
546
+ validateNode(root, option, value, path, inner, 1, budget, refDepth);
547
+ return inner.length === 0;
548
+ });
549
+ if (!ok)
550
+ push(issues, maxIssues, {
551
+ path,
552
+ keyword: "anyOf",
553
+ message: "matches none of the allowed shapes"
554
+ });
555
+ }
556
+ if (schema.oneOf !== void 0) {
557
+ let matches = 0;
558
+ for (const option of schema.oneOf) {
559
+ const inner = [];
560
+ validateNode(root, option, value, path, inner, 1, budget, refDepth);
561
+ if (inner.length === 0) matches++;
562
+ if (matches > 1) break;
563
+ }
564
+ if (matches !== 1) {
565
+ push(issues, maxIssues, {
566
+ path,
567
+ keyword: "oneOf",
568
+ message: matches === 0 ? "matches none of the allowed shapes" : "matches more than one allowed shape"
569
+ });
570
+ }
571
+ }
572
+ }
573
+ function checkCardSchema(schema, options = {}) {
574
+ const issues = [];
575
+ const maxDepth = options.maxDepth ?? 16;
576
+ let nodes = options.maxNodes ?? 500;
577
+ const root = schema;
578
+ const visit = (node, path, depth) => {
579
+ if (issues.length >= 20) return;
580
+ if (--nodes < 0) {
581
+ if (nodes === -1) issues.push({ path, keyword: "size", message: "schema has too many parts" });
582
+ return;
583
+ }
584
+ if (depth > maxDepth) {
585
+ issues.push({ path, keyword: "depth", message: "schema is nested too deeply" });
586
+ return;
587
+ }
588
+ if (!isPlainObject(node)) {
589
+ issues.push({ path, keyword: "schema", message: "a schema must be an object" });
590
+ return;
591
+ }
592
+ for (const [key, child] of Object.entries(node)) {
593
+ const at = pointer(path, key);
594
+ if (ANNOTATIONS.has(key)) {
595
+ if ((key === "title" || key === "description" || key === "$comment") && typeof child !== "string") {
596
+ issues.push({ path: at, keyword: key, message: "must be a string" });
597
+ }
598
+ continue;
599
+ }
600
+ if (!KEYWORDS.has(key)) {
601
+ issues.push({ path: at, keyword: key, message: `unsupported keyword "${key}"` });
602
+ continue;
603
+ }
604
+ switch (key) {
605
+ case "$ref":
606
+ if (typeof child !== "string" || !resolveRef(root, child)) {
607
+ issues.push({
608
+ path: at,
609
+ keyword: key,
610
+ message: 'must be "#" or "#/$defs/<name>" and resolve'
611
+ });
612
+ }
613
+ break;
614
+ case "$defs":
615
+ case "properties":
616
+ if (!isPlainObject(child)) {
617
+ issues.push({ path: at, keyword: key, message: "must be an object of schemas" });
618
+ } else {
619
+ for (const [name, sub] of Object.entries(child))
620
+ visit(sub, pointer(at, name), depth + 1);
621
+ }
622
+ break;
623
+ case "items":
624
+ visit(child, at, depth + 1);
625
+ break;
626
+ case "additionalProperties":
627
+ if (typeof child !== "boolean") visit(child, at, depth + 1);
628
+ break;
629
+ case "anyOf":
630
+ case "oneOf":
631
+ if (!Array.isArray(child) || child.length === 0 || child.length > 16) {
632
+ issues.push({ path: at, keyword: key, message: "must be a list of 1\u201316 schemas" });
633
+ } else {
634
+ child.forEach((sub, i) => visit(sub, pointer(at, i), depth + 1));
635
+ }
636
+ break;
637
+ case "type": {
638
+ const list = Array.isArray(child) ? child : [child];
639
+ if (list.length === 0 || list.some((type) => typeof type !== "string" || !TYPES.has(type))) {
640
+ issues.push({ path: at, keyword: key, message: "unknown type" });
641
+ }
642
+ break;
643
+ }
644
+ case "enum":
645
+ if (!Array.isArray(child) || child.length === 0 || child.length > LIMITS.schemaEnumOptions || checkJsonValue(child)) {
646
+ issues.push({
647
+ path: at,
648
+ keyword: key,
649
+ message: `must be a list of 1\u2013${LIMITS.schemaEnumOptions} JSON values`
650
+ });
651
+ } else if (JSON.stringify(child).length > LIMITS.schemaEnumBytes) {
652
+ issues.push({
653
+ path: at,
654
+ keyword: key,
655
+ message: `options must total at most ${LIMITS.schemaEnumBytes / 1024} KB`
656
+ });
657
+ }
658
+ break;
659
+ case "const":
660
+ if (checkJsonValue(child))
661
+ issues.push({ path: at, keyword: key, message: "must be a JSON value" });
662
+ else if (JSON.stringify(child).length > LIMITS.schemaConstBytes) {
663
+ issues.push({
664
+ path: at,
665
+ keyword: key,
666
+ message: `must be at most ${LIMITS.schemaConstBytes / 1024} KB`
667
+ });
668
+ }
669
+ break;
670
+ case "required":
671
+ if (!Array.isArray(child) || child.some((name) => typeof name !== "string")) {
672
+ issues.push({ path: at, keyword: key, message: "must be a list of property names" });
673
+ }
674
+ break;
675
+ case "uniqueItems":
676
+ if (typeof child !== "boolean")
677
+ issues.push({ path: at, keyword: key, message: "must be true or false" });
678
+ break;
679
+ case "pattern":
680
+ if (!options.allowPattern) {
681
+ issues.push({
682
+ path: at,
683
+ keyword: key,
684
+ message: '"pattern" is not allowed in card schemas (use enum, format or maxLength)'
685
+ });
686
+ } else if (typeof child !== "string" || child.length > 256 || !compiled(child)) {
687
+ issues.push({
688
+ path: at,
689
+ keyword: key,
690
+ message: "must be a valid regular expression of at most 256 characters"
691
+ });
692
+ }
693
+ break;
694
+ case "format":
695
+ if (typeof child !== "string" || !CARD_STRING_FORMATS.includes(child)) {
696
+ issues.push({
697
+ path: at,
698
+ keyword: key,
699
+ message: `format must be one of ${CARD_STRING_FORMATS.join(", ")}`
700
+ });
701
+ }
702
+ break;
703
+ default:
704
+ if (typeof child !== "number" || !Number.isFinite(child)) {
705
+ issues.push({ path: at, keyword: key, message: "must be a finite number" });
706
+ } else if ([
707
+ "minLength",
708
+ "maxLength",
709
+ "minItems",
710
+ "maxItems",
711
+ "minProperties",
712
+ "maxProperties"
713
+ ].includes(key) && (child < 0 || !Number.isInteger(child))) {
714
+ issues.push({ path: at, keyword: key, message: "must be a non-negative integer" });
715
+ } else if (key === "multipleOf" && child <= 0) {
716
+ issues.push({ path: at, keyword: key, message: "must be greater than 0" });
717
+ }
718
+ }
719
+ }
720
+ };
721
+ visit(schema, "", 0);
722
+ return issues;
723
+ }
724
+
725
+ // src/contract/localized.ts
726
+ function resolveLocalized(text, language) {
727
+ if (text === void 0) return "";
728
+ if (typeof text === "string") return text;
729
+ return text[language] ?? text.en;
730
+ }
731
+ function localizedVariants(text) {
732
+ return typeof text === "string" ? [text] : Object.values(text).filter((v) => typeof v === "string");
733
+ }
734
+
735
+ // src/contract/network.ts
736
+ var LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
737
+ function validHostname(host) {
738
+ if (host.length === 0 || host.length > 253) return false;
739
+ const labels = host.split(".");
740
+ if (labels.length < 2) return false;
741
+ if (!labels.every((label) => LABEL_RE.test(label))) return false;
742
+ return !/^[0-9]+$/.test(labels[labels.length - 1]);
743
+ }
744
+ function parseHostPattern(pattern) {
745
+ if (typeof pattern !== "string") return { ok: false, error: "not a string" };
746
+ const text = pattern.trim();
747
+ if (text !== text.toLowerCase()) return { ok: false, error: "must be lowercase" };
748
+ if (text.includes("/") || text.includes("@") || text.includes("[")) {
749
+ return { ok: false, error: "a host, not a URL (no scheme, path or IP literal)" };
750
+ }
751
+ let host = text;
752
+ let port2;
753
+ const colon = host.lastIndexOf(":");
754
+ if (colon !== -1) {
755
+ const digits = host.slice(colon + 1);
756
+ if (!/^[0-9]{1,5}$/.test(digits)) return { ok: false, error: "invalid port" };
757
+ port2 = Number(digits);
758
+ if (port2 < 1 || port2 > 65535) return { ok: false, error: "invalid port" };
759
+ host = host.slice(0, colon);
760
+ }
761
+ let wildcard = false;
762
+ if (host.startsWith("*.")) {
763
+ wildcard = true;
764
+ host = host.slice(2);
765
+ if (host.split(".").length < 2)
766
+ return { ok: false, error: "a wildcard needs at least two labels after it (*.example.com)" };
767
+ }
768
+ if (host.includes("*")) return { ok: false, error: 'only a leading "*." wildcard is allowed' };
769
+ if (host === "localhost" || host.endsWith(".localhost")) {
770
+ return { ok: false, error: "use the network.local permission for this computer" };
771
+ }
772
+ if (!validHostname(host)) return { ok: false, error: "invalid host name" };
773
+ return {
774
+ ok: true,
775
+ value: { wildcard, host, ...port2 !== void 0 && port2 !== 443 ? { port: port2 } : {} }
776
+ };
777
+ }
778
+ function formatHostPattern(pattern) {
779
+ return `${pattern.wildcard ? "*." : ""}${pattern.host}${pattern.port ? `:${pattern.port}` : ""}`;
780
+ }
781
+
782
+ // src/contract/permissions.ts
783
+ var PERMISSION_IDS = [
784
+ "agents.read",
785
+ "agents.output",
786
+ "agents.prompt",
787
+ "terminals.write",
788
+ "cards.connected",
789
+ "network",
790
+ "network.local",
791
+ "fs.read",
792
+ "fs.write",
793
+ "clipboard.write",
794
+ "canvas.spawn",
795
+ "background",
796
+ "usage.read"
797
+ ];
798
+ function isPermissionId(value) {
799
+ return typeof value === "string" && PERMISSION_IDS.includes(value);
800
+ }
801
+ var PERMISSION_INFO = {
802
+ "agents.read": { risk: "low", implies: [], needsHosts: false },
803
+ "agents.output": { risk: "high", implies: ["agents.read"], needsHosts: false },
804
+ "agents.prompt": { risk: "high", implies: ["agents.read"], needsHosts: false },
805
+ "terminals.write": { risk: "high", implies: ["agents.read"], needsHosts: false },
806
+ "cards.connected": { risk: "medium", implies: [], needsHosts: false },
807
+ network: { risk: "medium", implies: [], needsHosts: true },
808
+ "network.local": { risk: "high", implies: [], needsHosts: false },
809
+ "fs.read": { risk: "high", implies: [], needsHosts: false },
810
+ "fs.write": { risk: "high", implies: ["fs.read"], needsHosts: false },
811
+ "clipboard.write": { risk: "low", implies: [], needsHosts: false },
812
+ "canvas.spawn": { risk: "low", implies: [], needsHosts: false },
813
+ background: { risk: "low", implies: [], needsHosts: false },
814
+ "usage.read": { risk: "low", implies: [], needsHosts: false }
815
+ };
816
+ var PERMISSION_TEXT_EN = {
817
+ "agents.read": {
818
+ title: "See the agents on this canvas",
819
+ body: "Their names, which tool they run, and whether they are working, waiting for you or finished."
820
+ },
821
+ "agents.output": {
822
+ title: "Read what agents and terminals connected to it print",
823
+ body: "Everything on the screens of agent and terminal cards you connect to it with an arrow, including anything secret shown there."
824
+ },
825
+ "agents.prompt": {
826
+ title: "Give instructions to agents connected to it",
827
+ body: "Type and send prompts to AI agents you connect to it with an arrow. An agent can edit files and run commands, so this card can make it do anything the agent can do."
828
+ },
829
+ "terminals.write": {
830
+ title: "Run commands in terminals connected to it",
831
+ body: "Type and run commands in terminal cards you connect to it with an arrow \u2014 anything you could run yourself."
832
+ },
833
+ "cards.connected": {
834
+ title: "Read and change cards connected to it",
835
+ body: "Notes, checklists, task boards and stickies you connect to it with an arrow."
836
+ },
837
+ network: {
838
+ title: "Connect to {{hosts}}",
839
+ body: "Send and receive data from these internet addresses only. Anything the card can see may be sent there."
840
+ },
841
+ "network.local": {
842
+ title: "Connect to servers on this computer",
843
+ body: "Reach programs listening on localhost, such as dev servers and local databases."
844
+ },
845
+ "fs.read": {
846
+ title: "Read files in the workspace folder",
847
+ body: "Any file in this workspace\u2019s project folder, including secrets stored in files like .env."
848
+ },
849
+ "fs.write": {
850
+ title: "Change files in the workspace folder",
851
+ body: "Create, overwrite and move files to the trash in this workspace\u2019s project folder. Changed project files can run code the next time you build or run the project."
852
+ },
853
+ "clipboard.write": {
854
+ title: "Copy to your clipboard",
855
+ body: "Replace what is on your clipboard with text from the card."
856
+ },
857
+ "canvas.spawn": {
858
+ title: "Add cards next to itself",
859
+ body: `Place up to ${LIMITS.spawnsPerInstance} new cards beside it on the canvas.`
860
+ },
861
+ background: {
862
+ title: "Keep running when you are not looking",
863
+ body: "Stay active while its workspace is hidden. Uses more memory and battery."
864
+ },
865
+ "usage.read": {
866
+ title: "See token usage and costs",
867
+ body: "How many tokens the agents on this workspace used and what they cost."
868
+ }
869
+ };
870
+ function normalizePermissions(entries) {
871
+ const issues = [];
872
+ const byId = /* @__PURE__ */ new Map();
873
+ entries.forEach((entry, index) => {
874
+ const raw = typeof entry === "string" ? { id: entry } : entry;
875
+ if (!isPermissionId(raw.id)) {
876
+ issues.push({ index, message: `unknown permission "${String(raw.id)}"` });
877
+ return;
878
+ }
879
+ const info = PERMISSION_INFO[raw.id];
880
+ const hosts = [];
881
+ if (raw.hosts !== void 0) {
882
+ if (!info.needsHosts) issues.push({ index, message: `"${raw.id}" does not take hosts` });
883
+ for (const host of raw.hosts) {
884
+ const parsed = parseHostPattern(host);
885
+ if (!parsed.ok) issues.push({ index, message: `host "${host}": ${parsed.error}` });
886
+ else hosts.push(formatHostPattern(parsed.value));
887
+ }
888
+ }
889
+ if (info.needsHosts && hosts.length === 0 && (raw.hosts === void 0 || raw.hosts.length === 0)) {
890
+ issues.push({ index, message: `"${raw.id}" needs a non-empty "hosts" list` });
891
+ }
892
+ const existing = byId.get(raw.id);
893
+ const merged = {
894
+ id: raw.id,
895
+ optional: (existing ? existing.optional : true) && raw.optional === true,
896
+ ...existing?.reason ?? raw.reason ? { reason: existing?.reason ?? raw.reason } : {}
897
+ };
898
+ if (info.needsHosts) {
899
+ const all = [.../* @__PURE__ */ new Set([...existing?.hosts ?? [], ...hosts])].sort();
900
+ if (all.length > LIMITS.permissionHosts)
901
+ issues.push({ index, message: `at most ${LIMITS.permissionHosts} hosts` });
902
+ merged.hosts = all;
903
+ }
904
+ byId.set(raw.id, merged);
905
+ });
906
+ const permissions = PERMISSION_IDS.flatMap((id) => {
907
+ const found = byId.get(id);
908
+ return found ? [found] : [];
909
+ });
910
+ return { permissions, issues };
911
+ }
912
+
913
+ // src/contract/ports.ts
914
+ var CUSTOM_PORT_TYPE_RE = /^[a-z0-9][a-z0-9-]{0,47}\/[a-z0-9][a-z0-9-]{0,47}$/;
915
+ var PORT_ID_RE = /^[a-z][a-z0-9-]{0,31}$/;
916
+ var TASK_STATUS = ["pending", "active", "done", "error"];
917
+ var TASK_SCHEMA = {
918
+ type: "object",
919
+ required: ["text"],
920
+ additionalProperties: false,
921
+ properties: {
922
+ id: { type: "string", maxLength: 128 },
923
+ text: { type: "string", minLength: 1, maxLength: 500 },
924
+ status: { enum: TASK_STATUS },
925
+ column: { type: "string", maxLength: 64 },
926
+ assignee: { type: "string", maxLength: 128 }
927
+ }
928
+ };
929
+ var WELL_KNOWN_PORT_SCHEMAS = {
930
+ /** Anything JSON. As an input: accepts every output type unchanged. */
931
+ "ns:any": {},
932
+ "ns:text": { type: "string", maxLength: 1e6 },
933
+ "ns:markdown": { type: "string", maxLength: 1e6 },
934
+ "ns:number": { type: "number" },
935
+ "ns:boolean": { type: "boolean" },
936
+ "ns:json": { type: ["object", "array"] },
937
+ "ns:url": { type: "string", format: "uri", maxLength: 8192 },
938
+ /** Base64 image, ≤ ~700 KB decoded (message limit). */
939
+ "ns:image": {
940
+ type: "object",
941
+ required: ["mimeType", "data"],
942
+ additionalProperties: false,
943
+ properties: {
944
+ mimeType: { enum: ["image/png", "image/jpeg", "image/webp", "image/gif"] },
945
+ data: { type: "string", maxLength: 1e6 },
946
+ alt: { type: "string", maxLength: 500 }
947
+ }
948
+ },
949
+ /** A file in the workspace folder (relative path). Receiving one grants no file access. */
950
+ "ns:file-ref": {
951
+ type: "object",
952
+ required: ["path"],
953
+ additionalProperties: false,
954
+ properties: {
955
+ path: { type: "string", minLength: 1, maxLength: 1024 },
956
+ line: { type: "integer", minimum: 1 }
957
+ }
958
+ },
959
+ "ns:task": TASK_SCHEMA,
960
+ "ns:tasks": { type: "array", maxItems: 200, items: TASK_SCHEMA },
961
+ /** Change one existing task; `ref` is its id or its exact text. */
962
+ "ns:task-patch": {
963
+ type: "object",
964
+ required: ["ref"],
965
+ additionalProperties: false,
966
+ properties: {
967
+ ref: { type: "string", minLength: 1, maxLength: 500 },
968
+ text: { type: "string", minLength: 1, maxLength: 500 },
969
+ status: { enum: TASK_STATUS },
970
+ column: { type: "string", maxLength: 64 }
971
+ }
972
+ },
973
+ "ns:table": {
974
+ type: "object",
975
+ required: ["columns", "rows"],
976
+ additionalProperties: false,
977
+ properties: {
978
+ columns: { type: "array", maxItems: 64, items: { type: "string", maxLength: 200 } },
979
+ rows: {
980
+ type: "array",
981
+ maxItems: 5e3,
982
+ items: {
983
+ type: "array",
984
+ maxItems: 64,
985
+ items: { type: ["string", "number", "boolean", "null"] }
986
+ }
987
+ }
988
+ }
989
+ },
990
+ "ns:event": {
991
+ type: "object",
992
+ required: ["type"],
993
+ additionalProperties: false,
994
+ properties: {
995
+ type: { type: "string", minLength: 1, maxLength: 64 },
996
+ data: {},
997
+ at: { type: "number" }
998
+ }
999
+ },
1000
+ /** A bare signal ("go", "refresh"). */
1001
+ "ns:trigger": { type: ["object", "null"], maxProperties: 0 }
1002
+ };
1003
+ var WELL_KNOWN_PORT_TYPES = Object.keys(WELL_KNOWN_PORT_SCHEMAS);
1004
+ function isWellKnownPortType(type) {
1005
+ return Object.prototype.hasOwnProperty.call(WELL_KNOWN_PORT_SCHEMAS, type);
1006
+ }
1007
+ function isValidPortType(type) {
1008
+ return isWellKnownPortType(type) || CUSTOM_PORT_TYPE_RE.test(type);
1009
+ }
1010
+ function port(id, type, permission, label, description, extra = {}) {
1011
+ return {
1012
+ id,
1013
+ type,
1014
+ permission,
1015
+ label,
1016
+ description,
1017
+ mode: "stream",
1018
+ retain: extra.retain ?? false,
1019
+ default: extra.default ?? false
1020
+ };
1021
+ }
1022
+ var BUILTIN_CARD_PORTS = {
1023
+ note: {
1024
+ inputs: [
1025
+ port(
1026
+ "append",
1027
+ "ns:markdown",
1028
+ "cards.connected",
1029
+ "Append",
1030
+ "Adds a paragraph at the end of the note.",
1031
+ { default: true }
1032
+ ),
1033
+ port("replace", "ns:markdown", "cards.connected", "Replace", "Replaces the whole note.")
1034
+ ],
1035
+ outputs: [
1036
+ port(
1037
+ "text",
1038
+ "ns:markdown",
1039
+ "cards.connected",
1040
+ "Text",
1041
+ "The note\u2019s Markdown text, whenever it changes.",
1042
+ { retain: true }
1043
+ )
1044
+ ]
1045
+ },
1046
+ todo: {
1047
+ inputs: [
1048
+ port("add", "ns:tasks", "cards.connected", "Add items", "Adds items to the checklist.", {
1049
+ default: true
1050
+ }),
1051
+ port(
1052
+ "update",
1053
+ "ns:task-patch",
1054
+ "cards.connected",
1055
+ "Update item",
1056
+ "Changes one item\u2019s text or status."
1057
+ )
1058
+ ],
1059
+ outputs: [
1060
+ port(
1061
+ "items",
1062
+ "ns:tasks",
1063
+ "cards.connected",
1064
+ "Items",
1065
+ "All items, whenever the list changes.",
1066
+ { retain: true }
1067
+ )
1068
+ ]
1069
+ },
1070
+ kanban: {
1071
+ inputs: [
1072
+ port("add", "ns:tasks", "cards.connected", "Add tasks", "Adds tasks to the board.", {
1073
+ default: true
1074
+ }),
1075
+ port("update", "ns:task-patch", "cards.connected", "Update task", "Moves or edits one task.")
1076
+ ],
1077
+ outputs: [
1078
+ port(
1079
+ "tasks",
1080
+ "ns:tasks",
1081
+ "cards.connected",
1082
+ "Tasks",
1083
+ "All tasks with their columns, whenever the board changes.",
1084
+ { retain: true }
1085
+ )
1086
+ ]
1087
+ },
1088
+ sticky: {
1089
+ inputs: [
1090
+ port("title", "ns:text", "cards.connected", "Title", "Sets the sticky\u2019s title.", {
1091
+ default: true
1092
+ })
1093
+ ],
1094
+ outputs: [
1095
+ port("title", "ns:text", "cards.connected", "Title", "The sticky\u2019s title.", { retain: true })
1096
+ ]
1097
+ },
1098
+ agent: {
1099
+ inputs: [
1100
+ port(
1101
+ "prompt",
1102
+ "ns:text",
1103
+ "agents.prompt",
1104
+ "Prompt",
1105
+ "Sends a prompt to the agent (queued while it works).",
1106
+ { default: true }
1107
+ )
1108
+ ],
1109
+ outputs: [
1110
+ port(
1111
+ "status",
1112
+ "ns:event",
1113
+ "agents.read",
1114
+ "Status",
1115
+ 'working / needs-input / finished / idle / exited, as `{ type: "status", data: { status } }`.',
1116
+ { retain: true }
1117
+ ),
1118
+ port(
1119
+ "reply",
1120
+ "ns:markdown",
1121
+ "agents.output",
1122
+ "Last reply",
1123
+ "The agent\u2019s final message when a turn ends (Claude Code only).",
1124
+ { retain: true }
1125
+ )
1126
+ ]
1127
+ },
1128
+ terminal: {
1129
+ inputs: [
1130
+ port(
1131
+ "command",
1132
+ "ns:text",
1133
+ "terminals.write",
1134
+ "Command",
1135
+ "Runs one command in the terminal.",
1136
+ { default: true }
1137
+ )
1138
+ ],
1139
+ outputs: [
1140
+ port(
1141
+ "exit",
1142
+ "ns:event",
1143
+ "agents.output",
1144
+ "Command finished",
1145
+ '`{ type: "exit", data: { command, exitCode } }` after each command.'
1146
+ )
1147
+ ]
1148
+ }
1149
+ };
1150
+
1151
+ // src/contract/source.ts
1152
+ var WINDOWS_RESERVED = /^(con|prn|aux|nul|com[0-9¹²³]|lpt[0-9¹²³]|conin\$|conout\$|clock\$) *(\..*)?$/i;
1153
+ var CONTROL = /[\u0000-\u001f\u007f]/;
1154
+ function checkSegments(segments) {
1155
+ for (const segment of segments) {
1156
+ if (segment === "" || segment === "." || segment === "..") return 'empty, "." or ".." segment';
1157
+ if (CONTROL.test(segment)) return "control character";
1158
+ if (/[<>:"|?*]/.test(segment)) return 'character not allowed in file names (<>:"|?*)';
1159
+ if (/[. ]$/.test(segment)) return "segment ends with a dot or space";
1160
+ if (WINDOWS_RESERVED.test(segment)) return `reserved device name "${segment}"`;
1161
+ }
1162
+ return null;
1163
+ }
1164
+ function validatePackagePath(path) {
1165
+ if (typeof path !== "string" || path.length === 0) return { ok: false, error: "empty path" };
1166
+ if (path.length > LIMITS.pathChars) return { ok: false, error: "path too long" };
1167
+ if (path.includes("\\")) return { ok: false, error: "backslash in path" };
1168
+ if (path.startsWith("/")) return { ok: false, error: "absolute path" };
1169
+ const trimmed = path.endsWith("/") ? path.slice(0, -1) : path;
1170
+ const segments = trimmed.split("/");
1171
+ if (segments.length > LIMITS.pathDepth) return { ok: false, error: "path too deep" };
1172
+ const problem = checkSegments(segments);
1173
+ if (problem) return { ok: false, error: problem };
1174
+ const normalized = segments.join("/");
1175
+ const lower = normalized.toLowerCase();
1176
+ if (lower === RESERVED_PATH_PREFIX.slice(0, -1) || lower.startsWith(RESERVED_PATH_PREFIX)) {
1177
+ return { ok: false, error: `paths under ${RESERVED_PATH_PREFIX} are reserved` };
1178
+ }
1179
+ return { ok: true, value: normalized };
1180
+ }
1181
+ function treeHashInput(entries) {
1182
+ return [...entries].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0).map((entry) => `${entry.sha256.toLowerCase()} ${entry.path}
1183
+ `).join("");
1184
+ }
1185
+
1186
+ // src/contract/manifest.ts
1187
+ var MANIFEST_SCHEMA_ID = "https://neurosquad.ai/schemas/neurosquad-card.v1.json";
1188
+ var CARD_NAME_RE = /^[a-z][a-z0-9-]{1,47}$/;
1189
+ var TOOL_NAME_RE = /^[a-z][a-z0-9_]{0,39}$/;
1190
+ var SETTING_KEY_RE = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
1191
+ var SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
1192
+ var SIZE_BOUNDS = { minW: 200, minH: 120, maxW: 2400, maxH: 1800 };
1193
+ var DEFAULT_CARD_SIZE = { w: 420, h: 320 };
1194
+ var SETTING_TYPES = [
1195
+ "string",
1196
+ "text",
1197
+ "number",
1198
+ "boolean",
1199
+ "select",
1200
+ "secret",
1201
+ "color"
1202
+ ];
1203
+ var localized = (maxLength) => ({
1204
+ anyOf: [
1205
+ { type: "string", minLength: 1, maxLength },
1206
+ {
1207
+ type: "object",
1208
+ required: ["en"],
1209
+ additionalProperties: false,
1210
+ properties: {
1211
+ en: { type: "string", minLength: 1, maxLength },
1212
+ ru: { type: "string", minLength: 1, maxLength },
1213
+ zh: { type: "string", minLength: 1, maxLength }
1214
+ }
1215
+ }
1216
+ ]
1217
+ });
1218
+ var size = {
1219
+ type: "object",
1220
+ required: ["w", "h"],
1221
+ additionalProperties: false,
1222
+ properties: {
1223
+ w: { type: "integer", minimum: SIZE_BOUNDS.minW, maximum: SIZE_BOUNDS.maxW },
1224
+ h: { type: "integer", minimum: SIZE_BOUNDS.minH, maximum: SIZE_BOUNDS.maxH }
1225
+ }
1226
+ };
1227
+ var portDeclaration = {
1228
+ type: "object",
1229
+ required: ["id", "label", "type"],
1230
+ additionalProperties: false,
1231
+ properties: {
1232
+ id: { type: "string", pattern: PORT_ID_RE.source },
1233
+ label: { $ref: "#/$defs/localized80" },
1234
+ description: { $ref: "#/$defs/localized500" },
1235
+ type: { type: "string", minLength: 1, maxLength: 97 },
1236
+ schema: { type: "object" },
1237
+ mode: { enum: ["stream", "request"] },
1238
+ response: {
1239
+ type: "object",
1240
+ required: ["type"],
1241
+ additionalProperties: false,
1242
+ properties: {
1243
+ type: { type: "string", minLength: 1, maxLength: 97 },
1244
+ schema: { type: "object" }
1245
+ }
1246
+ },
1247
+ retain: { type: "boolean" },
1248
+ default: { type: "boolean" }
1249
+ }
1250
+ };
1251
+ var MANIFEST_SCHEMA = {
1252
+ $schema: "https://json-schema.org/draft/2020-12/schema",
1253
+ $id: MANIFEST_SCHEMA_ID,
1254
+ title: "NeuroSquad custom card manifest",
1255
+ description: "neurosquad-card.json at the root of a NeuroSquad custom card package.",
1256
+ type: "object",
1257
+ required: ["manifestVersion", "name", "displayName", "version", "protocol"],
1258
+ additionalProperties: false,
1259
+ $defs: {
1260
+ localized80: localized(80),
1261
+ localized300: localized(300),
1262
+ localized500: localized(500),
1263
+ size,
1264
+ port: portDeclaration
1265
+ },
1266
+ properties: {
1267
+ $schema: { type: "string", maxLength: 500 },
1268
+ manifestVersion: { const: MANIFEST_VERSION },
1269
+ name: { type: "string", pattern: CARD_NAME_RE.source },
1270
+ displayName: { $ref: "#/$defs/localized80" },
1271
+ version: { type: "string", maxLength: 64, pattern: SEMVER_RE.source },
1272
+ description: { $ref: "#/$defs/localized300" },
1273
+ author: {
1274
+ type: "object",
1275
+ required: ["name"],
1276
+ additionalProperties: false,
1277
+ properties: {
1278
+ name: { type: "string", minLength: 1, maxLength: 100 },
1279
+ url: { type: "string", format: "uri", maxLength: 500, pattern: "^https://" }
1280
+ }
1281
+ },
1282
+ license: { type: "string", minLength: 1, maxLength: 100 },
1283
+ homepage: { type: "string", format: "uri", maxLength: 500, pattern: "^https://" },
1284
+ icon: { type: "string", minLength: 1, maxLength: LIMITS.pathChars },
1285
+ protocol: { type: "integer", minimum: 1, maximum: 1e3 },
1286
+ minAppVersion: { type: "string", maxLength: 64, pattern: SEMVER_RE.source },
1287
+ entry: { type: "string", minLength: 1, maxLength: LIMITS.pathChars },
1288
+ card: {
1289
+ type: "object",
1290
+ additionalProperties: false,
1291
+ properties: {
1292
+ defaultSize: { $ref: "#/$defs/size" },
1293
+ minSize: { $ref: "#/$defs/size" },
1294
+ maxSize: { $ref: "#/$defs/size" }
1295
+ }
1296
+ },
1297
+ permissions: {
1298
+ type: "array",
1299
+ maxItems: 32,
1300
+ items: {
1301
+ anyOf: [
1302
+ { type: "string", minLength: 1, maxLength: 64 },
1303
+ {
1304
+ type: "object",
1305
+ required: ["id"],
1306
+ additionalProperties: false,
1307
+ properties: {
1308
+ id: { type: "string", minLength: 1, maxLength: 64 },
1309
+ hosts: {
1310
+ type: "array",
1311
+ maxItems: LIMITS.permissionHosts,
1312
+ items: { type: "string", minLength: 1, maxLength: 260 }
1313
+ },
1314
+ reason: { $ref: "#/$defs/localized300" },
1315
+ optional: { type: "boolean" }
1316
+ }
1317
+ }
1318
+ ]
1319
+ }
1320
+ },
1321
+ settings: {
1322
+ type: "array",
1323
+ maxItems: LIMITS.settings,
1324
+ items: {
1325
+ type: "object",
1326
+ required: ["key", "type", "label"],
1327
+ additionalProperties: false,
1328
+ properties: {
1329
+ key: { type: "string", pattern: SETTING_KEY_RE.source },
1330
+ type: { enum: SETTING_TYPES },
1331
+ label: { $ref: "#/$defs/localized80" },
1332
+ description: { $ref: "#/$defs/localized300" },
1333
+ default: { type: ["string", "number", "boolean"] },
1334
+ required: { type: "boolean" },
1335
+ scope: { enum: ["instance", "package"] },
1336
+ placeholder: { $ref: "#/$defs/localized80" },
1337
+ maxLength: { type: "integer", minimum: 1, maximum: 1e5 },
1338
+ min: { type: "number" },
1339
+ max: { type: "number" },
1340
+ step: { type: "number", exclusiveMinimum: 0 },
1341
+ options: {
1342
+ type: "array",
1343
+ minItems: 1,
1344
+ maxItems: 50,
1345
+ items: {
1346
+ type: "object",
1347
+ required: ["value", "label"],
1348
+ additionalProperties: false,
1349
+ properties: {
1350
+ value: { type: "string", maxLength: 200 },
1351
+ label: { $ref: "#/$defs/localized80" }
1352
+ }
1353
+ }
1354
+ }
1355
+ }
1356
+ }
1357
+ },
1358
+ ports: {
1359
+ type: "object",
1360
+ additionalProperties: false,
1361
+ properties: {
1362
+ inputs: {
1363
+ type: "array",
1364
+ maxItems: LIMITS.portsPerDirection,
1365
+ items: { $ref: "#/$defs/port" }
1366
+ },
1367
+ outputs: {
1368
+ type: "array",
1369
+ maxItems: LIMITS.portsPerDirection,
1370
+ items: { $ref: "#/$defs/port" }
1371
+ }
1372
+ }
1373
+ },
1374
+ tools: {
1375
+ type: "array",
1376
+ maxItems: LIMITS.toolsPerPackage,
1377
+ items: {
1378
+ type: "object",
1379
+ required: ["name", "description", "inputSchema"],
1380
+ additionalProperties: false,
1381
+ properties: {
1382
+ name: { type: "string", pattern: TOOL_NAME_RE.source },
1383
+ title: { type: "string", minLength: 1, maxLength: 80 },
1384
+ description: { type: "string", minLength: 1, maxLength: 2e3 },
1385
+ inputSchema: { type: "object" },
1386
+ readOnly: { type: "boolean" },
1387
+ timeoutMs: { type: "integer", minimum: 1e3, maximum: LIMITS.toolMaxTimeoutMs }
1388
+ }
1389
+ }
1390
+ },
1391
+ keywords: {
1392
+ type: "array",
1393
+ maxItems: 20,
1394
+ uniqueItems: true,
1395
+ items: { type: "string", minLength: 1, maxLength: 40 }
1396
+ }
1397
+ }
1398
+ };
1399
+ var RESERVED_NAMES = /* @__PURE__ */ new Set([
1400
+ "neurosquad",
1401
+ "neurosquad-card",
1402
+ "custom",
1403
+ "canvas",
1404
+ "browser",
1405
+ "terminal",
1406
+ "agent",
1407
+ "note",
1408
+ "todo",
1409
+ "kanban",
1410
+ "host",
1411
+ "system",
1412
+ "telegram",
1413
+ "image",
1414
+ "reference",
1415
+ "ports",
1416
+ "web-watch",
1417
+ "watch",
1418
+ "timer",
1419
+ "sticky",
1420
+ "standup",
1421
+ "squad",
1422
+ "mcp",
1423
+ "skill",
1424
+ "official"
1425
+ ]);
1426
+ var BUILTIN_MCP_TOOL_NAMES = [
1427
+ "agent_read",
1428
+ "agent_send",
1429
+ "agent_wait",
1430
+ "browser_click",
1431
+ "browser_history",
1432
+ "browser_navigate",
1433
+ "browser_press_key",
1434
+ "browser_screenshot",
1435
+ "browser_snapshot",
1436
+ "browser_type",
1437
+ "canvas_remove_card",
1438
+ "canvas_spawn_card",
1439
+ "kanban_add",
1440
+ "kanban_move",
1441
+ "kanban_read",
1442
+ "kanban_remove",
1443
+ "kanban_write",
1444
+ "neurosquad_connections",
1445
+ "note_append",
1446
+ "note_read",
1447
+ "note_write",
1448
+ "ports_list",
1449
+ "reference_get",
1450
+ "reference_list",
1451
+ "skill_load",
1452
+ "skill_read_file",
1453
+ "squad_status",
1454
+ "telegram_read",
1455
+ "telegram_send",
1456
+ "terminal_read",
1457
+ "terminal_run",
1458
+ "terminal_send_keys",
1459
+ "todo_add",
1460
+ "todo_read",
1461
+ "todo_remove",
1462
+ "todo_set",
1463
+ "todo_write",
1464
+ "watch_check_now",
1465
+ "watch_status"
1466
+ ];
1467
+ var UNSAFE_TEXT_RE = /[\p{Cc}\p{Cf}\u2028\u2029]/u;
1468
+ var UNSAFE_MULTILINE_TEXT_RE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f\p{Cf}\u2028\u2029]/u;
1469
+ function isSafeDisplayText(text, multiline = false) {
1470
+ return !(multiline ? UNSAFE_MULTILINE_TEXT_RE : UNSAFE_TEXT_RE).test(text);
1471
+ }
1472
+ function impersonationIssues(manifest) {
1473
+ const issues = [];
1474
+ const flag2 = (path, text) => {
1475
+ const folded = text.normalize("NFKC").toLowerCase().replace(/[^\p{L}\p{N}]/gu, "");
1476
+ if (/neurosquad|official|verified/.test(folded)) {
1477
+ issues.push({
1478
+ path,
1479
+ message: "only official cards may call themselves NeuroSquad, official or verified",
1480
+ code: "semantic"
1481
+ });
1482
+ }
1483
+ };
1484
+ for (const text of localizedVariants(manifest.displayName)) flag2("/displayName", text);
1485
+ if (manifest.author) flag2("/author/name", manifest.author.name);
1486
+ return issues;
1487
+ }
1488
+ function displayStrings(manifest) {
1489
+ const out = [];
1490
+ const add = (path, text, multiline = false) => {
1491
+ if (text === void 0) return;
1492
+ if (typeof text === "string") {
1493
+ out.push({ path, text, multiline });
1494
+ return;
1495
+ }
1496
+ for (const [lang, value] of Object.entries(text)) {
1497
+ if (typeof value === "string") out.push({ path: `${path}/${lang}`, text: value, multiline });
1498
+ }
1499
+ };
1500
+ add("/displayName", manifest.displayName);
1501
+ add("/description", manifest.description, true);
1502
+ add("/author/name", manifest.author?.name);
1503
+ add("/license", manifest.license);
1504
+ (manifest.keywords ?? []).forEach((keyword, i) => add(`/keywords/${i}`, keyword));
1505
+ (manifest.permissions ?? []).forEach((permission, i) => {
1506
+ if (typeof permission === "object") add(`/permissions/${i}/reason`, permission.reason, true);
1507
+ });
1508
+ (manifest.settings ?? []).forEach((field, i) => {
1509
+ add(`/settings/${i}/label`, field.label);
1510
+ add(`/settings/${i}/description`, field.description, true);
1511
+ add(`/settings/${i}/placeholder`, field.placeholder);
1512
+ (field.options ?? []).forEach(
1513
+ (option, j) => add(`/settings/${i}/options/${j}/label`, option.label)
1514
+ );
1515
+ });
1516
+ for (const direction of ["inputs", "outputs"]) {
1517
+ ;
1518
+ (manifest.ports?.[direction] ?? []).forEach((port2, i) => {
1519
+ add(`/ports/${direction}/${i}/label`, port2.label);
1520
+ add(`/ports/${direction}/${i}/description`, port2.description, true);
1521
+ });
1522
+ }
1523
+ ;
1524
+ (manifest.tools ?? []).forEach((tool, i) => {
1525
+ add(`/tools/${i}/title`, tool.title);
1526
+ add(`/tools/${i}/description`, tool.description, true);
1527
+ });
1528
+ return out;
1529
+ }
1530
+ function semverCompare(a, b) {
1531
+ const pa = a.split(/[-+]/)[0].split(".").map(Number);
1532
+ const pb = b.split(/[-+]/)[0].split(".").map(Number);
1533
+ for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pa[i] - pb[i];
1534
+ return 0;
1535
+ }
1536
+ function parseManifestText(text, appVersion) {
1537
+ if (text.length > LIMITS.manifestBytes) {
1538
+ return {
1539
+ ok: false,
1540
+ issues: [{ path: "", message: "manifest is larger than 256 KB", code: "json" }]
1541
+ };
1542
+ }
1543
+ let raw;
1544
+ try {
1545
+ raw = JSON.parse(text.replace(/^\uFEFF/, ""));
1546
+ } catch (error) {
1547
+ return {
1548
+ ok: false,
1549
+ issues: [{ path: "", message: `not valid JSON: ${error.message}`, code: "json" }]
1550
+ };
1551
+ }
1552
+ return validateManifest(raw, appVersion);
1553
+ }
1554
+ function validateManifest(raw, appVersion) {
1555
+ const schemaIssues = validateJson(MANIFEST_SCHEMA, raw, { maxIssues: 50 });
1556
+ if (schemaIssues.length > 0) {
1557
+ return {
1558
+ ok: false,
1559
+ issues: schemaIssues.map((issue) => ({
1560
+ path: issue.path,
1561
+ message: issue.message,
1562
+ code: "schema"
1563
+ }))
1564
+ };
1565
+ }
1566
+ const manifest = raw;
1567
+ const issues = [];
1568
+ const warnings = [];
1569
+ const semantic = (path, message) => void issues.push({ path, message, code: "semantic" });
1570
+ if (manifest.protocol > CARD_PROTOCOL_VERSION) {
1571
+ issues.push({
1572
+ path: "/protocol",
1573
+ message: `built for card protocol ${manifest.protocol}; this app speaks ${CARD_PROTOCOL_VERSION} \u2014 update NeuroSquad`,
1574
+ code: "protocol"
1575
+ });
1576
+ }
1577
+ if (appVersion && manifest.minAppVersion && semverCompare(appVersion, manifest.minAppVersion) < 0) {
1578
+ issues.push({
1579
+ path: "/minAppVersion",
1580
+ message: `needs NeuroSquad ${manifest.minAppVersion} or newer`,
1581
+ code: "protocol"
1582
+ });
1583
+ }
1584
+ if (RESERVED_NAMES.has(manifest.name)) semantic("/name", `"${manifest.name}" is reserved`);
1585
+ for (const { path, text, multiline } of displayStrings(manifest)) {
1586
+ if (!isSafeDisplayText(text, multiline))
1587
+ semantic(path, "control, bidi and invisible (zero-width, tag) characters are not allowed");
1588
+ }
1589
+ const entry = validatePackagePath(manifest.entry ?? "index.html");
1590
+ if (!entry.ok) semantic("/entry", entry.error);
1591
+ else if (!/\.html?$/i.test(entry.value)) semantic("/entry", "entry must be an .html file");
1592
+ if (manifest.icon !== void 0) {
1593
+ const icon = validatePackagePath(manifest.icon);
1594
+ if (!icon.ok) semantic("/icon", icon.error);
1595
+ else if (!/\.(png|webp)$/i.test(icon.value))
1596
+ semantic("/icon", "icon must be a .png or .webp file");
1597
+ }
1598
+ const card = {
1599
+ defaultSize: manifest.card?.defaultSize ?? DEFAULT_CARD_SIZE,
1600
+ minSize: manifest.card?.minSize ?? { w: SIZE_BOUNDS.minW, h: SIZE_BOUNDS.minH },
1601
+ maxSize: manifest.card?.maxSize ?? { w: SIZE_BOUNDS.maxW, h: SIZE_BOUNDS.maxH }
1602
+ };
1603
+ if (card.minSize.w > card.defaultSize.w || card.minSize.h > card.defaultSize.h || card.defaultSize.w > card.maxSize.w || card.defaultSize.h > card.maxSize.h) {
1604
+ semantic("/card", "sizes must satisfy minSize \u2264 defaultSize \u2264 maxSize");
1605
+ }
1606
+ const { permissions, issues: permissionIssues } = normalizePermissions(manifest.permissions ?? []);
1607
+ for (const issue of permissionIssues) semantic(`/permissions/${issue.index}`, issue.message);
1608
+ const settingKeys = /* @__PURE__ */ new Set();
1609
+ (manifest.settings ?? []).forEach((field, i) => {
1610
+ const at = `/settings/${i}`;
1611
+ if (settingKeys.has(field.key)) semantic(`${at}/key`, `duplicate setting "${field.key}"`);
1612
+ settingKeys.add(field.key);
1613
+ if (field.type === "secret" && field.default !== void 0)
1614
+ semantic(`${at}/default`, "a secret cannot have a default");
1615
+ if (field.type === "select" && !field.options)
1616
+ semantic(`${at}/options`, "a select needs options");
1617
+ if (field.type !== "select" && field.options)
1618
+ semantic(`${at}/options`, "only a select has options");
1619
+ if (field.default !== void 0) {
1620
+ const expected = field.type === "number" ? "number" : field.type === "boolean" ? "boolean" : "string";
1621
+ if (typeof field.default !== expected)
1622
+ semantic(`${at}/default`, `default must be a ${expected}`);
1623
+ if (field.type === "select" && field.options && !field.options.some((o) => o.value === field.default)) {
1624
+ semantic(`${at}/default`, "default must be one of the options");
1625
+ }
1626
+ if (field.type === "color" && (typeof field.default !== "string" || !/^#[0-9a-fA-F]{6}$/.test(field.default))) {
1627
+ semantic(`${at}/default`, "a color default is #rrggbb");
1628
+ }
1629
+ if (field.type === "number" && typeof field.default === "number") {
1630
+ if (field.min !== void 0 && field.default < field.min || field.max !== void 0 && field.default > field.max) {
1631
+ semantic(`${at}/default`, "default is outside min/max");
1632
+ }
1633
+ }
1634
+ }
1635
+ if (field.min !== void 0 && field.max !== void 0 && field.min > field.max)
1636
+ semantic(at, "min is greater than max");
1637
+ });
1638
+ const checkPorts = (list, direction) => {
1639
+ const ids = /* @__PURE__ */ new Set();
1640
+ let defaults = 0;
1641
+ (list ?? []).forEach((port2, i) => {
1642
+ const at = `/ports/${direction}/${i}`;
1643
+ if (ids.has(port2.id)) semantic(`${at}/id`, `duplicate port "${port2.id}"`);
1644
+ ids.add(port2.id);
1645
+ if (!isValidPortType(port2.type)) {
1646
+ semantic(`${at}/type`, 'type must be an ns:* type or "<package-name>/<type-name>"');
1647
+ } else if (!isWellKnownPortType(port2.type)) {
1648
+ if (!port2.type.startsWith(`${manifest.name}/`)) {
1649
+ warnings.push({
1650
+ path: `${at}/type`,
1651
+ message: `custom type of another package ("${port2.type}") \u2014 fine when you mean to interoperate`,
1652
+ code: "semantic"
1653
+ });
1654
+ }
1655
+ if (!port2.schema) semantic(`${at}/schema`, "a custom type needs a schema");
1656
+ }
1657
+ if (port2.schema) {
1658
+ for (const issue of checkCardSchema(port2.schema, {
1659
+ maxNodes: LIMITS.schemaNodes,
1660
+ maxDepth: LIMITS.schemaDepth
1661
+ })) {
1662
+ semantic(`${at}/schema${issue.path}`, issue.message);
1663
+ }
1664
+ }
1665
+ if (direction === "outputs") {
1666
+ if (port2.mode !== void 0 || port2.response !== void 0 || port2.default !== void 0) {
1667
+ semantic(at, "mode, response and default are for inputs");
1668
+ }
1669
+ } else {
1670
+ if (port2.retain !== void 0) semantic(`${at}/retain`, "retain is for outputs");
1671
+ if (port2.default) defaults++;
1672
+ if (port2.mode === "request") {
1673
+ if (!port2.response) semantic(`${at}/response`, "a request input needs a response type");
1674
+ else if (!isValidPortType(port2.response.type))
1675
+ semantic(`${at}/response/type`, "invalid response type");
1676
+ if (port2.response?.schema) {
1677
+ for (const issue of checkCardSchema(port2.response.schema, {
1678
+ maxNodes: LIMITS.schemaNodes,
1679
+ maxDepth: LIMITS.schemaDepth
1680
+ })) {
1681
+ semantic(`${at}/response/schema${issue.path}`, issue.message);
1682
+ }
1683
+ }
1684
+ } else if (port2.response) {
1685
+ semantic(`${at}/response`, "only a request input has a response");
1686
+ }
1687
+ }
1688
+ for (const text of [port2.label, ...port2.description ? [port2.description] : []]) {
1689
+ if (localizedVariants(text).some((variant) => variant.trim().length === 0))
1690
+ semantic(at, "empty label or description");
1691
+ }
1692
+ });
1693
+ if (defaults > 1) semantic(`/ports/${direction}`, "at most one default input");
1694
+ };
1695
+ checkPorts(manifest.ports?.inputs, "inputs");
1696
+ checkPorts(manifest.ports?.outputs, "outputs");
1697
+ const toolNames = /* @__PURE__ */ new Set();
1698
+ (manifest.tools ?? []).forEach((tool, i) => {
1699
+ const at = `/tools/${i}`;
1700
+ if (toolNames.has(tool.name)) semantic(`${at}/name`, `duplicate tool "${tool.name}"`);
1701
+ toolNames.add(tool.name);
1702
+ const exposed = mcpToolName(manifest.name, tool.name);
1703
+ if (BUILTIN_MCP_TOOL_NAMES.includes(exposed))
1704
+ semantic(`${at}/name`, `"${exposed}" is a built-in NeuroSquad tool`);
1705
+ else if (exposed.includes("__"))
1706
+ semantic(`${at}/name`, `"${exposed}" contains "__" (reserved for installed MCP servers)`);
1707
+ if (tool.inputSchema.type !== "object")
1708
+ semantic(`${at}/inputSchema/type`, 'inputSchema must have type "object"');
1709
+ if (tool.inputSchema.properties && Object.prototype.hasOwnProperty.call(tool.inputSchema.properties, "card")) {
1710
+ semantic(
1711
+ `${at}/inputSchema/properties/card`,
1712
+ '"card" is reserved (the host adds it to pick among several cards)'
1713
+ );
1714
+ }
1715
+ for (const issue of checkCardSchema(tool.inputSchema, {
1716
+ maxNodes: LIMITS.schemaNodes,
1717
+ maxDepth: LIMITS.schemaDepth
1718
+ })) {
1719
+ semantic(`${at}/inputSchema${issue.path}`, issue.message);
1720
+ }
1721
+ });
1722
+ if (issues.length > 0) return { ok: false, issues };
1723
+ const normalized = {
1724
+ ...manifest,
1725
+ entry: entry.ok ? entry.value : "index.html",
1726
+ card,
1727
+ permissions,
1728
+ settings: manifest.settings ?? [],
1729
+ ports: { inputs: manifest.ports?.inputs ?? [], outputs: manifest.ports?.outputs ?? [] },
1730
+ tools: manifest.tools ?? []
1731
+ };
1732
+ return { ok: true, manifest: normalized, warnings };
1733
+ }
1734
+ function mcpToolName(packageName, tool) {
1735
+ return `${packageName.replace(/-/g, "_")}_${tool}`.slice(0, 64);
1736
+ }
1737
+
1738
+ // src/cli/util.ts
1739
+ import { existsSync, readFileSync } from "node:fs";
1740
+ import { dirname, join } from "node:path";
1741
+ import { fileURLToPath } from "node:url";
1742
+ var useColor = process.stdout.isTTY === true && !("NO_COLOR" in process.env) && process.env["TERM"] !== "dumb";
1743
+ var paint = (code) => (text) => useColor ? `\x1B[${code}m${text}\x1B[0m` : text;
1744
+ var c = {
1745
+ bold: paint(1),
1746
+ dim: paint(2),
1747
+ red: paint(31),
1748
+ green: paint(32),
1749
+ yellow: paint(33),
1750
+ blue: paint(34),
1751
+ magenta: paint(35),
1752
+ cyan: paint(36)
1753
+ };
1754
+ var CliError = class extends Error {
1755
+ constructor(message, exitCode = 1) {
1756
+ super(message);
1757
+ this.exitCode = exitCode;
1758
+ }
1759
+ };
1760
+ var cachedRoot = null;
1761
+ function packageRoot() {
1762
+ if (cachedRoot) return cachedRoot;
1763
+ let dir = dirname(fileURLToPath(import.meta.url));
1764
+ for (let i = 0; i < 6; i++) {
1765
+ const manifest = join(dir, "package.json");
1766
+ if (existsSync(manifest)) {
1767
+ try {
1768
+ if (JSON.parse(readFileSync(manifest, "utf-8")).name === "@neurosquad/card-sdk") {
1769
+ cachedRoot = dir;
1770
+ return dir;
1771
+ }
1772
+ } catch {
1773
+ }
1774
+ }
1775
+ dir = dirname(dir);
1776
+ }
1777
+ throw new CliError("cannot find the @neurosquad/card-sdk package folder");
1778
+ }
1779
+ function formatBytes(bytes) {
1780
+ if (bytes < 1024) return `${bytes} B`;
1781
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1782
+ return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
1783
+ }
1784
+ function parseArgs(argv, booleans) {
1785
+ const positionals = [];
1786
+ const flags = /* @__PURE__ */ new Map();
1787
+ for (let i = 0; i < argv.length; i++) {
1788
+ const arg = argv[i];
1789
+ if (arg === "--") {
1790
+ positionals.push(...argv.slice(i + 1));
1791
+ break;
1792
+ }
1793
+ if (arg.startsWith("--")) {
1794
+ const eq = arg.indexOf("=");
1795
+ const name = eq === -1 ? arg.slice(2) : arg.slice(2, eq);
1796
+ if (eq !== -1) flags.set(name, arg.slice(eq + 1));
1797
+ else if (booleans.has(name) || i + 1 >= argv.length || argv[i + 1].startsWith("-"))
1798
+ flags.set(name, true);
1799
+ else flags.set(name, argv[++i]);
1800
+ } else if (arg.startsWith("-") && arg.length === 2) {
1801
+ flags.set(arg.slice(1), true);
1802
+ } else positionals.push(arg);
1803
+ }
1804
+ return { positionals, flags };
1805
+ }
1806
+ function flag(args, name) {
1807
+ const value = args.flags.get(name);
1808
+ return typeof value === "string" ? value : void 0;
1809
+ }
1810
+ function safeTerminalText(text) {
1811
+ return String(text).replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "").replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)?/g, "").replace(/\u001b[\s\S]?/g, "").replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, " ").replace(new RegExp("\\p{Cf}", "gu"), "");
1812
+ }
1813
+ function isSecretLookingPath(path) {
1814
+ const name = (path.split("/").pop() ?? path).toLowerCase();
1815
+ if (name.startsWith(".env")) return !/^\.env\.(example|sample|template|dist)$/.test(name);
1816
+ if (/\.(pem|key|p12|pfx)$/.test(name)) return true;
1817
+ if (/^id_(rsa|ed25519|ecdsa|dsa)/.test(name)) return true;
1818
+ return [
1819
+ ".npmrc",
1820
+ ".yarnrc",
1821
+ ".yarnrc.yml",
1822
+ ".git-credentials",
1823
+ ".netrc",
1824
+ "_netrc",
1825
+ ".pypirc"
1826
+ ].includes(name);
1827
+ }
1828
+ function isSecretLookingFile(path, read) {
1829
+ if (!isSecretLookingPath(path)) return false;
1830
+ const name = (path.split("/").pop() ?? path).toLowerCase();
1831
+ if (![".npmrc", ".yarnrc", ".yarnrc.yml"].includes(name)) return true;
1832
+ try {
1833
+ return /(_auth|authtoken|_password|password|token|npmAuthIdent)/i.test(read());
1834
+ } catch {
1835
+ return true;
1836
+ }
1837
+ }
1838
+ var GIT_NULL = process.platform === "win32" ? "NUL" : "/dev/null";
1839
+ function hardenedGit(args, env = process.env) {
1840
+ const clean = {};
1841
+ for (const [key, value] of Object.entries(env)) {
1842
+ if (!/^GIT_/i.test(key)) clean[key] = value;
1843
+ }
1844
+ return {
1845
+ args: [
1846
+ "-c",
1847
+ "core.fsmonitor=false",
1848
+ "-c",
1849
+ `core.hooksPath=${GIT_NULL}`,
1850
+ "-c",
1851
+ "core.untrackedCache=false",
1852
+ "-c",
1853
+ "protocol.allow=never",
1854
+ "--no-optional-locks",
1855
+ ...args
1856
+ ],
1857
+ env: {
1858
+ ...clean,
1859
+ GIT_CONFIG_NOSYSTEM: "1",
1860
+ GIT_CONFIG_GLOBAL: GIT_NULL,
1861
+ GIT_OPTIONAL_LOCKS: "0",
1862
+ GIT_TERMINAL_PROMPT: "0"
1863
+ }
1864
+ };
1865
+ }
1866
+
1867
+ // src/cli/create.ts
1868
+ var TEMPLATES = ["vanilla", "react"];
1869
+ var TEXT_EXTENSIONS = /\.(json|html|js|mjs|ts|tsx|css|md|txt)$|^_gitignore$/;
1870
+ function slugify(name) {
1871
+ const slug = name.toLowerCase().normalize("NFKD").replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^[^a-z]+/, "").replace(/-+$/, "").slice(0, 48).replace(/-+$/, "");
1872
+ return CARD_NAME_RE.test(slug) ? slug : "my-card";
1873
+ }
1874
+ function titleFromName(name) {
1875
+ const words = name.split("-").filter(Boolean).join(" ");
1876
+ return words.charAt(0).toUpperCase() + words.slice(1);
1877
+ }
1878
+ function copyTemplate(from, to, replace, created) {
1879
+ mkdirSync(to, { recursive: true });
1880
+ for (const entry of readdirSync(from, { withFileTypes: true })) {
1881
+ const source = join2(from, entry.name);
1882
+ const targetName = entry.name === "_gitignore" ? ".gitignore" : entry.name;
1883
+ const target = join2(to, targetName);
1884
+ if (entry.isDirectory()) copyTemplate(source, target, replace, created);
1885
+ else {
1886
+ if (TEXT_EXTENSIONS.test(entry.name))
1887
+ writeFileSync(target, replace(readFileSync2(source, "utf-8")));
1888
+ else copyFileSync(source, target);
1889
+ created.push(target);
1890
+ }
1891
+ }
1892
+ }
1893
+ function createCard(dirArg, options = {}) {
1894
+ const template = options.template ?? "vanilla";
1895
+ if (!TEMPLATES.includes(template)) {
1896
+ throw new CliError(`unknown template "${template}" \u2014 use one of: ${TEMPLATES.join(", ")}`);
1897
+ }
1898
+ const dir = resolve(dirArg);
1899
+ if (existsSync2(dir)) {
1900
+ if (!statSync(dir).isDirectory()) throw new CliError(`${dir} exists and is not a folder`);
1901
+ if (readdirSync(dir).length > 0 && !options.force) {
1902
+ throw new CliError(`${dir} is not empty \u2014 pick a new folder or pass --force`);
1903
+ }
1904
+ }
1905
+ const name = options.name ?? slugify(basename(dir));
1906
+ if (!CARD_NAME_RE.test(name)) {
1907
+ throw new CliError(
1908
+ `"${name}" is not a valid card name: lowercase letters, digits and dashes, starting with a letter (2\u201348 chars)`
1909
+ );
1910
+ }
1911
+ const displayName = options.displayName ?? titleFromName(name);
1912
+ const root = packageRoot();
1913
+ const templateDir = join2(root, "templates", template);
1914
+ if (!existsSync2(templateDir)) throw new CliError(`template folder missing: ${templateDir}`);
1915
+ const values = {
1916
+ __CARD_NAME__: name,
1917
+ __CARD_DISPLAY_NAME__: displayName.replace(/["\\]/g, ""),
1918
+ __SDK_VERSION__: SDK_VERSION
1919
+ };
1920
+ const replace = (text) => text.replace(/__CARD_NAME__|__CARD_DISPLAY_NAME__|__SDK_VERSION__/g, (key) => values[key]);
1921
+ const files = [];
1922
+ copyTemplate(templateDir, dir, replace, files);
1923
+ if (template === "vanilla") {
1924
+ const vendor = join2(dir, "vendor");
1925
+ mkdirSync(vendor, { recursive: true });
1926
+ const assets = [
1927
+ [join2(root, "dist", "card-sdk.js"), "card-sdk.js"],
1928
+ [join2(root, "dist", "testing.js"), "testing.js"],
1929
+ [join2(root, "dist", "ui.css"), "ui.css"],
1930
+ [join2(root, "schema", "neurosquad-card.v1.json"), "neurosquad-card.schema.json"]
1931
+ ];
1932
+ for (const [source, target] of assets) {
1933
+ if (!existsSync2(source))
1934
+ throw new CliError(`missing ${source} \u2014 build the SDK first (npm run build)`);
1935
+ copyFileSync(source, join2(vendor, target));
1936
+ files.push(join2(vendor, target));
1937
+ }
1938
+ }
1939
+ const manifest = JSON.parse(readFileSync2(join2(dir, MANIFEST_FILE), "utf-8"));
1940
+ const check = validateManifest(manifest);
1941
+ if (!check.ok) {
1942
+ throw new CliError(
1943
+ `the new manifest is invalid: ${check.issues.map((i) => `${i.path} ${i.message}`).join("; ")} \u2014 try --name`
1944
+ );
1945
+ }
1946
+ return { dir, files, name };
1947
+ }
1948
+ function runCreate(dirArg, options) {
1949
+ if (!dirArg)
1950
+ throw new CliError("usage: neurosquad-card create <folder> [--template vanilla|react]");
1951
+ const { dir, files, name } = createCard(dirArg, options);
1952
+ const shown = relative(process.cwd(), dir) || ".";
1953
+ console.log(
1954
+ c.green(`\u2714 created ${c.bold(name)} in ${shown}`) + c.dim(` (${files.length} files, ${options.template ?? "vanilla"} template)`)
1955
+ );
1956
+ console.log("");
1957
+ console.log("Next:");
1958
+ console.log(` cd ${shown}`);
1959
+ if (options.template === "react") {
1960
+ console.log(" npm install");
1961
+ console.log(` npm run dev ${c.dim("# preview in the browser with a mock host")}`);
1962
+ console.log(` npm run build ${c.dim("# \u2192 dist/, which the app loads (commit it)")}`);
1963
+ }
1964
+ const cli = options.template === "react" ? "npx neurosquad-card" : "npx @neurosquad/card-sdk";
1965
+ console.log(
1966
+ ` ${cli} dev ${c.dim("# live in NeuroSquad (Settings \u2192 Custom cards \u2192 Developer mode)")}`
1967
+ );
1968
+ console.log(` ${cli} validate`);
1969
+ console.log("");
1970
+ console.log(c.dim("Share it: push the folder to GitHub; people install it with owner/repo."));
1971
+ return 0;
1972
+ }
1973
+
1974
+ // src/cli/dev.ts
1975
+ import { spawn } from "node:child_process";
1976
+ import { existsSync as existsSync4, readFileSync as readFileSync4, realpathSync as realpathSync2 } from "node:fs";
1977
+ import { homedir } from "node:os";
1978
+ import { join as join4, resolve as resolve3 } from "node:path";
1979
+
1980
+ // src/cli/inspect.ts
1981
+ import { spawnSync } from "node:child_process";
1982
+ import { existsSync as existsSync3, lstatSync, readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync } from "node:fs";
1983
+ import { isAbsolute, join as join3, relative as relative2, sep } from "node:path";
1984
+
1985
+ // src/cli/images.ts
1986
+ var PNG_MAGIC = [137, 80, 78, 71, 13, 10, 26, 10];
1987
+ function readImageInfo(bytes) {
1988
+ if (bytes.length >= 24 && PNG_MAGIC.every((b, i) => bytes[i] === b)) {
1989
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1990
+ if (String.fromCharCode(...bytes.subarray(12, 16)) !== "IHDR") return null;
1991
+ return { format: "png", width: view.getUint32(16), height: view.getUint32(20) };
1992
+ }
1993
+ const ascii = (start, end) => String.fromCharCode(...bytes.subarray(start, end));
1994
+ if (bytes.length >= 30 && ascii(0, 4) === "RIFF" && ascii(8, 12) === "WEBP") {
1995
+ const chunk = ascii(12, 16);
1996
+ const u24 = (at) => bytes[at] | bytes[at + 1] << 8 | bytes[at + 2] << 16;
1997
+ if (chunk === "VP8X") return { format: "webp", width: u24(24) + 1, height: u24(27) + 1 };
1998
+ if (chunk === "VP8 ") {
1999
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2000
+ return {
2001
+ format: "webp",
2002
+ width: view.getUint16(26, true) & 16383,
2003
+ height: view.getUint16(28, true) & 16383
2004
+ };
2005
+ }
2006
+ if (chunk === "VP8L" && bytes[20] === 47) {
2007
+ const b = (i) => bytes[21 + i];
2008
+ const width = 1 + ((b(1) & 63) << 8 | b(0));
2009
+ const height = 1 + ((b(3) & 15) << 10 | b(2) << 2 | (b(1) & 192) >> 6);
2010
+ return { format: "webp", width, height };
2011
+ }
2012
+ return null;
2013
+ }
2014
+ return null;
2015
+ }
2016
+
2017
+ // src/cli/inspect.ts
2018
+ var ALWAYS_SKIPPED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
2019
+ function fromManifestIssue(issue) {
2020
+ return { kind: "manifest", where: issue.path || "/", message: issue.message };
2021
+ }
2022
+ function listCandidates(dir) {
2023
+ const hardened = hardenedGit([
2024
+ "ls-files",
2025
+ "-z",
2026
+ "--cached",
2027
+ "--others",
2028
+ "--exclude-standard",
2029
+ "--",
2030
+ "."
2031
+ ]);
2032
+ const git = spawnSync("git", hardened.args, {
2033
+ cwd: dir,
2034
+ encoding: "utf-8",
2035
+ windowsHide: true,
2036
+ env: hardened.env
2037
+ });
2038
+ if (git.status === 0 && typeof git.stdout === "string") {
2039
+ const paths2 = git.stdout.split("\0").filter((path) => path.length > 0).filter((path) => !path.split("/").some((part) => ALWAYS_SKIPPED_DIRS.has(part)));
2040
+ return { paths: [...new Set(paths2)], source: "git" };
2041
+ }
2042
+ const paths = [];
2043
+ const walk = (current, depth) => {
2044
+ if (depth > LIMITS.pathDepth + 1 || paths.length > LIMITS.files * 2) return;
2045
+ for (const entry of readdirSync2(current, { withFileTypes: true })) {
2046
+ const abs = join3(current, entry.name);
2047
+ if (entry.isDirectory()) {
2048
+ if (!ALWAYS_SKIPPED_DIRS.has(entry.name)) walk(abs, depth + 1);
2049
+ } else paths.push(relative2(dir, abs).split(sep).join("/"));
2050
+ }
2051
+ };
2052
+ walk(dir, 0);
2053
+ return { paths, source: "walk" };
2054
+ }
2055
+ function lintEntryHtml(html, entry, warnings) {
2056
+ const warn = (message) => void warnings.push({ kind: "file", where: entry, message });
2057
+ const scripts = [...html.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script>/gi)];
2058
+ if (scripts.some(([, attrs, body]) => !/\bsrc\s*=/.test(attrs) && body.trim().length > 0)) {
2059
+ warn("inline <script> is blocked by the card CSP \u2014 move the code into a .js file");
2060
+ }
2061
+ if (/\son[a-z]+\s*=\s*["']/i.test(html)) {
2062
+ warn('inline event handlers (onclick="\u2026") are blocked by the card CSP \u2014 use addEventListener');
2063
+ }
2064
+ const external = [...html.matchAll(/\b(?:src|href)\s*=\s*["'](https?:)?\/\/[^"']+["']/gi)].filter(
2065
+ (match) => !/<a\b[^>]*$/i.test(html.slice(Math.max(0, match.index - 200), match.index))
2066
+ );
2067
+ if (external.length > 0) {
2068
+ warn(
2069
+ "external scripts, styles, fonts or images are blocked \u2014 bundle them into the package (data: and blob: images work; use card.net.fetch for data)"
2070
+ );
2071
+ }
2072
+ }
2073
+ function inspectPackage(dir, options = {}) {
2074
+ const report = {
2075
+ dir,
2076
+ manifest: null,
2077
+ errors: [],
2078
+ warnings: [],
2079
+ files: [],
2080
+ totalBytes: 0,
2081
+ fileSource: "walk",
2082
+ secretFiles: []
2083
+ };
2084
+ const error = (kind, where, message) => void report.errors.push({ kind, where, message });
2085
+ const warn = (kind, where, message) => void report.warnings.push({ kind, where, message });
2086
+ if (!existsSync3(dir) || !lstatSync(dir).isDirectory()) {
2087
+ error("package", dir, "not a folder");
2088
+ return report;
2089
+ }
2090
+ const manifestPath = join3(dir, MANIFEST_FILE);
2091
+ if (!existsSync3(manifestPath)) {
2092
+ error("package", MANIFEST_FILE, `${MANIFEST_FILE} is missing at the package root`);
2093
+ } else {
2094
+ const result = parseManifestText(readFileSync3(manifestPath, "utf-8"), options.appVersion);
2095
+ if (result.ok) {
2096
+ report.manifest = result.manifest;
2097
+ report.warnings.push(...result.warnings.map(fromManifestIssue));
2098
+ for (const issue of impersonationIssues(result.manifest)) {
2099
+ report.warnings.push({
2100
+ ...fromManifestIssue(issue),
2101
+ message: `${issue.message} \u2014 the app refuses this unless the card comes from the official organization`
2102
+ });
2103
+ }
2104
+ } else report.errors.push(...result.issues.map(fromManifestIssue));
2105
+ }
2106
+ const { paths, source } = listCandidates(dir);
2107
+ report.fileSource = source;
2108
+ const lower = /* @__PURE__ */ new Map();
2109
+ let realDir;
2110
+ try {
2111
+ realDir = realpathSync.native(dir);
2112
+ } catch {
2113
+ realDir = dir;
2114
+ }
2115
+ for (const path of paths.sort()) {
2116
+ const abs = join3(dir, ...path.split("/"));
2117
+ let stat;
2118
+ try {
2119
+ stat = lstatSync(abs);
2120
+ } catch {
2121
+ continue;
2122
+ }
2123
+ if (stat.isSymbolicLink()) {
2124
+ warn("file", path, "symbolic link \u2014 the installer skips links; copy the file instead");
2125
+ continue;
2126
+ }
2127
+ let real;
2128
+ try {
2129
+ real = realpathSync.native(abs);
2130
+ } catch {
2131
+ continue;
2132
+ }
2133
+ const inside = relative2(realDir, real);
2134
+ if (inside === "" || inside.startsWith("..") || isAbsolute(inside) || inside.split(sep).join("/").toLowerCase() !== path.toLowerCase()) {
2135
+ warn(
2136
+ "file",
2137
+ path,
2138
+ "reached through a symbolic link or junction \u2014 skipped (it would be read from outside the card folder)"
2139
+ );
2140
+ continue;
2141
+ }
2142
+ if (isSecretLookingFile(path, () => readFileSync3(abs, "utf-8"))) {
2143
+ report.secretFiles.push(path);
2144
+ warn(
2145
+ "file",
2146
+ path,
2147
+ "looks like a credential file \u2014 `pack` refuses it (add it to .gitignore, or pass --allow-secret-files)"
2148
+ );
2149
+ }
2150
+ if (!stat.isFile()) continue;
2151
+ const checked = validatePackagePath(path);
2152
+ if (!checked.ok) {
2153
+ error("file", path, `the installer refuses this path: ${checked.error}`);
2154
+ continue;
2155
+ }
2156
+ if (stat.size > LIMITS.fileBytes) {
2157
+ error("file", path, `larger than ${LIMITS.fileBytes / 1024 / 1024} MB`);
2158
+ continue;
2159
+ }
2160
+ if (!path.includes("/") && path.endsWith(".tgz")) {
2161
+ warn(
2162
+ "file",
2163
+ path,
2164
+ "a packed archive inside the card folder would be installed too \u2014 delete or .gitignore it"
2165
+ );
2166
+ }
2167
+ const clash = lower.get(path.toLowerCase());
2168
+ if (clash)
2169
+ warn("file", path, `differs from "${clash}" only by case \u2014 breaks on Windows and macOS`);
2170
+ lower.set(path.toLowerCase(), path);
2171
+ report.files.push({ path: checked.value, abs, size: stat.size });
2172
+ report.totalBytes += stat.size;
2173
+ }
2174
+ if (report.files.length > LIMITS.files)
2175
+ error("package", "", `more than ${LIMITS.files} files (${report.files.length})`);
2176
+ if (report.totalBytes > LIMITS.unpackedBytes) {
2177
+ error("package", "", `more than ${LIMITS.unpackedBytes / 1024 / 1024} MB of files`);
2178
+ }
2179
+ const manifest = report.manifest;
2180
+ if (manifest) {
2181
+ const byPath = new Map(report.files.map((file) => [file.path, file]));
2182
+ const entry = byPath.get(manifest.entry);
2183
+ if (!entry) {
2184
+ const built = existsSync3(join3(dir, ...manifest.entry.split("/")));
2185
+ error(
2186
+ "manifest",
2187
+ "/entry",
2188
+ built ? `"${manifest.entry}" exists but would not be in the package (ignored by git?) \u2014 commit your build output` : `"${manifest.entry}" does not exist${manifest.entry.includes("/") ? " \u2014 run your build first (npm run build)" : ""}`
2189
+ );
2190
+ } else {
2191
+ lintEntryHtml(readFileSync3(entry.abs, "utf-8"), entry.path, report.warnings);
2192
+ }
2193
+ if (manifest.icon !== void 0) {
2194
+ const icon = byPath.get(manifest.icon);
2195
+ if (!icon) error("manifest", "/icon", `"${manifest.icon}" does not exist in the package`);
2196
+ else if (icon.size > LIMITS.iconBytes) {
2197
+ error("manifest", "/icon", `icon is larger than ${LIMITS.iconBytes / 1024} KB`);
2198
+ } else {
2199
+ const info = readImageInfo(readFileSync3(icon.abs));
2200
+ if (!info)
2201
+ error("manifest", "/icon", "icon is not a PNG or WebP image (checked by its bytes)");
2202
+ else if (info.width !== info.height) {
2203
+ error("manifest", "/icon", `icon must be square (it is ${info.width}\xD7${info.height})`);
2204
+ } else if (info.width < 64) {
2205
+ warn(
2206
+ "manifest",
2207
+ "/icon",
2208
+ `icon is ${info.width}\xD7${info.height}; 128\xD7128 or larger looks sharper`
2209
+ );
2210
+ }
2211
+ }
2212
+ } else warn("manifest", "/icon", "no icon \u2014 the card shows a generic puzzle piece");
2213
+ if (!manifest.license)
2214
+ warn("manifest", "/license", "no license \u2014 say how others may use your card");
2215
+ if (!manifest.description)
2216
+ warn("manifest", "/description", "no description \u2014 shown in the install dialog");
2217
+ }
2218
+ return report;
2219
+ }
2220
+
2221
+ // src/cli/validate.ts
2222
+ import { resolve as resolve2 } from "node:path";
2223
+ var RISK_ORDER = ["high", "medium", "low"];
2224
+ function consentSummary(manifest) {
2225
+ const lines = [];
2226
+ const t = safeTerminalText;
2227
+ const required = manifest.permissions.filter((p) => !p.optional);
2228
+ const optional = manifest.permissions.filter((p) => p.optional);
2229
+ const describe = (id, hosts) => PERMISSION_TEXT_EN[id].title.replace("{{hosts}}", t((hosts ?? []).join(", ")));
2230
+ const riskLabel = (risk) => risk === "high" ? c.red("high ") : risk === "medium" ? c.yellow("medium") : c.green("low ");
2231
+ lines.push(
2232
+ `${c.bold(t(resolveLocalized(manifest.displayName, "en")))} ${c.dim(`(${manifest.name} ${t(manifest.version)})`)}`
2233
+ );
2234
+ if (manifest.author) lines.push(` by ${t(manifest.author.name)} ${c.dim("(self-declared)")}`);
2235
+ lines.push(c.dim(" Community code, not made or checked by NeuroSquad."));
2236
+ if (required.length === 0) lines.push(" Asks for no permissions.");
2237
+ else {
2238
+ lines.push(" It will be able to:");
2239
+ for (const risk of RISK_ORDER) {
2240
+ for (const permission of required.filter((p) => PERMISSION_INFO[p.id].risk === risk)) {
2241
+ lines.push(` ${riskLabel(risk)} ${describe(permission.id, permission.hosts)}`);
2242
+ lines.push(` ${c.dim(PERMISSION_TEXT_EN[permission.id].body)}`);
2243
+ if (permission.reason)
2244
+ lines.push(` ${c.cyan(`Why: ${t(resolveLocalized(permission.reason, "en"))}`)}`);
2245
+ }
2246
+ }
2247
+ }
2248
+ if (optional.length > 0) {
2249
+ lines.push(" May ask later for:");
2250
+ for (const permission of optional)
2251
+ lines.push(` - ${describe(permission.id, permission.hosts)}`);
2252
+ }
2253
+ if (manifest.tools.length > 0) {
2254
+ lines.push(
2255
+ ` Tools for connected agents: ${manifest.tools.map((tool) => mcpToolName(manifest.name, tool.name)).join(", ")}`
2256
+ );
2257
+ }
2258
+ const { inputs, outputs } = manifest.ports;
2259
+ if (inputs.length + outputs.length > 0) {
2260
+ lines.push(
2261
+ ` Ports: ${inputs.length} in (${inputs.map((p) => t(`${p.id}:${p.type}`)).join(", ") || "\u2014"}), ${outputs.length} out (${outputs.map((p) => t(`${p.id}:${p.type}`)).join(", ") || "\u2014"})`
2262
+ );
2263
+ }
2264
+ return lines;
2265
+ }
2266
+ function formatIssue(issue) {
2267
+ const where = issue.kind === "manifest" ? `neurosquad-card.json ${issue.where}` : issue.kind === "file" ? issue.where : issue.where || "package";
2268
+ return `${c.bold(safeTerminalText(where))}: ${safeTerminalText(issue.message)}`;
2269
+ }
2270
+ function printReport(report, options = {}) {
2271
+ for (const issue of report.errors) console.log(`${c.red("\u2716 error")} ${formatIssue(issue)}`);
2272
+ for (const issue of report.warnings) console.log(`${c.yellow("\u25B2 warning")} ${formatIssue(issue)}`);
2273
+ if (report.manifest && options.consent !== false) {
2274
+ console.log("");
2275
+ console.log(c.dim("Install dialog preview:"));
2276
+ for (const line of consentSummary(report.manifest)) console.log(line);
2277
+ }
2278
+ console.log("");
2279
+ console.log(
2280
+ c.dim(
2281
+ `${report.files.length} files, ${formatBytes(report.totalBytes)} (file list from ${report.fileSource === "git" ? "git: tracked + not ignored" : "the folder"})`
2282
+ )
2283
+ );
2284
+ const ok = report.errors.length === 0;
2285
+ console.log(
2286
+ ok ? c.green(
2287
+ `\u2714 valid${report.warnings.length ? ` with ${report.warnings.length} warning(s)` : ""}`
2288
+ ) : c.red(`\u2716 ${report.errors.length} error(s) \u2014 NeuroSquad would refuse to install this card`)
2289
+ );
2290
+ return ok;
2291
+ }
2292
+ function reportJson(report) {
2293
+ return {
2294
+ ok: report.errors.length === 0,
2295
+ dir: report.dir,
2296
+ name: report.manifest?.name ?? null,
2297
+ version: report.manifest?.version ?? null,
2298
+ errors: report.errors,
2299
+ warnings: report.warnings,
2300
+ files: report.files.map(({ path, size: size2 }) => ({ path, size: size2 })),
2301
+ totalBytes: report.totalBytes
2302
+ };
2303
+ }
2304
+ function runValidate(dirArg, options) {
2305
+ const dir = resolve2(dirArg ?? ".");
2306
+ const report = inspectPackage(dir, options.appVersion ? { appVersion: options.appVersion } : {});
2307
+ if (options.json) {
2308
+ console.log(JSON.stringify(reportJson(report), null, 2));
2309
+ return report.errors.length === 0 ? 0 : 1;
2310
+ }
2311
+ console.log(c.dim(`Validating ${safeTerminalText(dir)}`));
2312
+ return printReport(report) ? 0 : 1;
2313
+ }
2314
+
2315
+ // src/cli/dev.ts
2316
+ function defaultUserDataDir() {
2317
+ const fromEnv = process.env["NEUROSQUAD_USER_DATA_DIR"];
2318
+ if (fromEnv) return fromEnv;
2319
+ if (process.platform === "win32") {
2320
+ return join4(process.env["APPDATA"] ?? join4(homedir(), "AppData", "Roaming"), "NeuroSquad");
2321
+ }
2322
+ if (process.platform === "darwin")
2323
+ return join4(homedir(), "Library", "Application Support", "NeuroSquad");
2324
+ return join4(process.env["XDG_CONFIG_HOME"] ?? join4(homedir(), ".config"), "NeuroSquad");
2325
+ }
2326
+ function readHandshake(userDataDir) {
2327
+ const file = join4(userDataDir, "card-dev.json");
2328
+ if (!existsSync4(file)) {
2329
+ throw new CliError(
2330
+ `NeuroSquad developer mode is off (no ${file}).
2331
+ Turn it on in NeuroSquad: Settings \u2192 Custom cards \u2192 Developer mode.
2332
+ Using another profile? Pass --user-data-dir <folder>.`
2333
+ );
2334
+ }
2335
+ let parsed;
2336
+ try {
2337
+ parsed = JSON.parse(readFileSync4(file, "utf-8"));
2338
+ } catch {
2339
+ throw new CliError(`${file} is not valid JSON \u2014 toggle developer mode off and on`);
2340
+ }
2341
+ if (typeof parsed.port !== "number" || typeof parsed.token !== "string" || !/^[0-9a-f]{16,}$/i.test(parsed.token)) {
2342
+ throw new CliError(`${file} is incomplete \u2014 toggle developer mode off and on`);
2343
+ }
2344
+ if (typeof parsed.pid === "number" && !processAlive(parsed.pid)) {
2345
+ throw new CliError(
2346
+ `NeuroSquad (pid ${parsed.pid}) is not running \u2014 start the app with developer mode on`
2347
+ );
2348
+ }
2349
+ return {
2350
+ port: parsed.port,
2351
+ token: parsed.token,
2352
+ pid: parsed.pid ?? 0,
2353
+ appVersion: parsed.appVersion ?? ""
2354
+ };
2355
+ }
2356
+ function processAlive(pid) {
2357
+ try {
2358
+ process.kill(pid, 0);
2359
+ return true;
2360
+ } catch (error) {
2361
+ return error.code === "EPERM";
2362
+ }
2363
+ }
2364
+ var DevLinkClient = class {
2365
+ base;
2366
+ constructor(handshake, host = "127.0.0.1") {
2367
+ this.base = `http://${host}:${handshake.port}/carddev/${handshake.token}`;
2368
+ }
2369
+ async json(path, init) {
2370
+ let response;
2371
+ try {
2372
+ response = await fetch(`${this.base}${path}`, init);
2373
+ } catch (error) {
2374
+ throw new CliError(
2375
+ `cannot reach NeuroSquad's dev link server (${error.message}) \u2014 is the app running with developer mode on?`
2376
+ );
2377
+ }
2378
+ const text = await response.text();
2379
+ if (!response.ok) {
2380
+ throw new CliError(
2381
+ `the app refused ${path} (${response.status}${text ? `: ${text.slice(0, 300)}` : ""})`
2382
+ );
2383
+ }
2384
+ return text ? JSON.parse(text) : null;
2385
+ }
2386
+ /** Asks the app to link a folder; the user confirms in the app. */
2387
+ link(folder) {
2388
+ return this.json("/link", {
2389
+ method: "POST",
2390
+ headers: { "content-type": "application/json" },
2391
+ body: JSON.stringify({ folder })
2392
+ });
2393
+ }
2394
+ status() {
2395
+ return this.json("/status");
2396
+ }
2397
+ reload(packageId) {
2398
+ return this.json("/reload", {
2399
+ method: "POST",
2400
+ headers: { "content-type": "application/json" },
2401
+ body: JSON.stringify({ packageId })
2402
+ });
2403
+ }
2404
+ /** Streams log lines (SSE) until the signal aborts or the app closes the stream. */
2405
+ async logs(packageId, onLine, signal) {
2406
+ const response = await fetch(`${this.base}/logs?packageId=${encodeURIComponent(packageId)}`, {
2407
+ headers: { accept: "text/event-stream" },
2408
+ signal
2409
+ });
2410
+ if (!response.ok || !response.body) throw new Error(`log stream refused (${response.status})`);
2411
+ const reader = response.body.getReader();
2412
+ const decoder = new TextDecoder();
2413
+ let buffer = "";
2414
+ for (; ; ) {
2415
+ const { value, done } = await reader.read();
2416
+ if (done) return;
2417
+ buffer += decoder.decode(value, { stream: true });
2418
+ let index;
2419
+ while ((index = buffer.indexOf("\n\n")) !== -1) {
2420
+ const event = buffer.slice(0, index);
2421
+ buffer = buffer.slice(index + 2);
2422
+ const data = event.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
2423
+ if (!data) continue;
2424
+ try {
2425
+ onLine(JSON.parse(data));
2426
+ } catch {
2427
+ }
2428
+ }
2429
+ }
2430
+ }
2431
+ };
2432
+ function sameFolder(a, b) {
2433
+ const norm = (path) => {
2434
+ const clean = path.replace(/\\/g, "/").replace(/\/+$/, "");
2435
+ return process.platform === "linux" ? clean : clean.toLowerCase();
2436
+ };
2437
+ return norm(a) === norm(b);
2438
+ }
2439
+ async function waitForLink(client, folder, options = {}) {
2440
+ const deadline = Date.now() + (options.timeoutMs ?? 5 * 6e4);
2441
+ for (; ; ) {
2442
+ const status = await client.status();
2443
+ const found = status.linked.find((entry) => sameFolder(entry.folder, folder));
2444
+ if (found) return found;
2445
+ if (Date.now() > deadline)
2446
+ throw new CliError("the link was not confirmed in the app in time \u2014 run the command again");
2447
+ await new Promise((r) => setTimeout(r, options.intervalMs ?? 1e3));
2448
+ }
2449
+ }
2450
+ function formatLog(line) {
2451
+ const time = new Date(line.at).toLocaleTimeString();
2452
+ const level = line.level === "error" ? c.red("error") : line.level === "warn" ? c.yellow("warn ") : line.level === "info" ? c.cyan("info ") : c.dim("debug");
2453
+ return `${c.dim(time)} ${level} ${c.dim(safeTerminalText(String(line.instanceId).slice(0, 8)))} ${safeTerminalText(line.message)}`;
2454
+ }
2455
+ function buildCommand(dir) {
2456
+ const file = join4(dir, "package.json");
2457
+ if (!existsSync4(file)) return null;
2458
+ try {
2459
+ const scripts = JSON.parse(readFileSync4(file, "utf-8")).scripts ?? {};
2460
+ if (scripts["watch"]) return ["run", "watch"];
2461
+ if (scripts["build"]) return ["run", "build", "--", "--watch"];
2462
+ } catch {
2463
+ }
2464
+ return null;
2465
+ }
2466
+ async function runDev(dirArg, options) {
2467
+ const dir = realpathSync2(resolve3(dirArg ?? "."));
2468
+ const report = inspectPackage(dir);
2469
+ if (!report.manifest) {
2470
+ printReport(report, { consent: false });
2471
+ throw new CliError("fix the manifest before linking");
2472
+ }
2473
+ if (report.errors.length > 0) {
2474
+ printReport(report, { consent: false });
2475
+ console.log(
2476
+ c.yellow("Linking anyway \u2014 the card will show these problems until they are fixed.")
2477
+ );
2478
+ }
2479
+ const children = [];
2480
+ const command = options.build === false ? null : buildCommand(dir);
2481
+ if (command) {
2482
+ console.log(c.dim(`$ npm ${command.join(" ")}`));
2483
+ const child = spawn(`npm ${command.join(" ")}`, {
2484
+ cwd: dir,
2485
+ stdio: ["ignore", "inherit", "inherit"],
2486
+ shell: true
2487
+ });
2488
+ children.push(child);
2489
+ }
2490
+ const handshake = readHandshake(options.userDataDir ?? defaultUserDataDir());
2491
+ const client = new DevLinkClient(handshake);
2492
+ const already = (await client.status()).linked.find((entry) => sameFolder(entry.folder, dir));
2493
+ let linked = already;
2494
+ if (!linked) {
2495
+ await client.link(dir);
2496
+ console.log(c.bold("Confirm the link in NeuroSquad") + c.dim(` \u2014 ${dir}`));
2497
+ linked = await waitForLink(client, dir);
2498
+ }
2499
+ console.log(
2500
+ c.green(`\u2714 linked as ${safeTerminalText(linked.packageId)}`) + c.dim(` (revision ${linked.revision})`)
2501
+ );
2502
+ console.log(c.dim("Saving a file reloads the card. Add it from the canvas: + \u2192 Custom card\u2026"));
2503
+ if (process.stdin.isTTY) console.log(c.dim("Press r to reload the card, q to quit."));
2504
+ const controller = new AbortController();
2505
+ const stop = () => {
2506
+ controller.abort();
2507
+ for (const child of children) child.kill();
2508
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
2509
+ process.stdin.pause();
2510
+ };
2511
+ process.once("SIGINT", () => {
2512
+ stop();
2513
+ process.exit(0);
2514
+ });
2515
+ const packageId = linked.packageId;
2516
+ if (process.stdin.isTTY) {
2517
+ process.stdin.setRawMode(true);
2518
+ process.stdin.resume();
2519
+ process.stdin.on("data", (data) => {
2520
+ const key = data.toString();
2521
+ if (key === "q" || key === "") {
2522
+ stop();
2523
+ process.exit(0);
2524
+ }
2525
+ if (key === "r") {
2526
+ client.reload(packageId).then(
2527
+ (result) => console.log(c.dim(`reloaded (revision ${result.revision})`)),
2528
+ (error) => console.log(c.red(safeTerminalText(error.message)))
2529
+ );
2530
+ }
2531
+ });
2532
+ }
2533
+ if (options.logs === false) {
2534
+ await new Promise(
2535
+ (resolveWait) => controller.signal.addEventListener("abort", () => resolveWait())
2536
+ );
2537
+ return 0;
2538
+ }
2539
+ let delay = 1e3;
2540
+ while (!controller.signal.aborted) {
2541
+ try {
2542
+ await client.logs(packageId, (line) => console.log(formatLog(line)), controller.signal);
2543
+ delay = 1e3;
2544
+ } catch (error) {
2545
+ if (controller.signal.aborted) break;
2546
+ console.log(c.dim(`log stream: ${safeTerminalText(error.message)}; retrying\u2026`));
2547
+ }
2548
+ await new Promise((r) => setTimeout(r, delay));
2549
+ delay = Math.min(delay * 2, 15e3);
2550
+ }
2551
+ return 0;
2552
+ }
2553
+
2554
+ // src/cli/pack.ts
2555
+ import { createHash } from "node:crypto";
2556
+ import { readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
2557
+ import { resolve as resolve4 } from "node:path";
2558
+
2559
+ // src/cli/tar.ts
2560
+ import { gunzipSync, gzipSync } from "node:zlib";
2561
+ var BLOCK = 512;
2562
+ var FIXED_MTIME = 1577836800;
2563
+ function octal(value, length) {
2564
+ return value.toString(8).padStart(length - 1, "0") + "\0";
2565
+ }
2566
+ function header(name, size2, type, prefix = "") {
2567
+ const block = new Uint8Array(BLOCK);
2568
+ const encoder = new TextEncoder();
2569
+ const write = (text, offset, length) => {
2570
+ block.set(encoder.encode(text).subarray(0, length), offset);
2571
+ };
2572
+ write(name, 0, 100);
2573
+ write(octal(type === "5" ? 493 : 420, 8), 100, 8);
2574
+ write(octal(0, 8), 108, 8);
2575
+ write(octal(0, 8), 116, 8);
2576
+ write(octal(size2, 12), 124, 12);
2577
+ write(octal(FIXED_MTIME, 12), 136, 12);
2578
+ write(" ", 148, 8);
2579
+ write(type, 156, 1);
2580
+ write("ustar\0", 257, 6);
2581
+ write("00", 263, 2);
2582
+ write(prefix, 345, 155);
2583
+ let sum = 0;
2584
+ for (const byte of block) sum += byte;
2585
+ write(octal(sum, 7) + " ", 148, 8);
2586
+ return block;
2587
+ }
2588
+ function padded(data) {
2589
+ const rest = data.length % BLOCK;
2590
+ return rest === 0 ? [data] : [data, new Uint8Array(BLOCK - rest)];
2591
+ }
2592
+ function splitUstar(path) {
2593
+ const bytes = new TextEncoder().encode(path);
2594
+ if (bytes.length <= 100 && bytes.length === path.length) return { name: path, prefix: "" };
2595
+ if (bytes.length !== path.length) return null;
2596
+ for (let i = path.length - 1; i > 0; i--) {
2597
+ if (path[i] !== "/") continue;
2598
+ const prefix = path.slice(0, i);
2599
+ const name = path.slice(i + 1);
2600
+ if (prefix.length <= 155 && name.length <= 100 && name.length > 0) return { name, prefix };
2601
+ }
2602
+ return null;
2603
+ }
2604
+ function paxRecord(key, value) {
2605
+ const body = ` ${key}=${value}
2606
+ `;
2607
+ let length = body.length + 1;
2608
+ while (`${length}${body}`.length !== length) length = `${length}${body}`.length;
2609
+ return `${length}${body}`;
2610
+ }
2611
+ function createTarGz(files, root) {
2612
+ const parts = [];
2613
+ const sorted = [...files].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
2614
+ parts.push(header(`${root}/`, 0, "5"));
2615
+ for (const file of sorted) {
2616
+ const full = `${root}/${file.path}`;
2617
+ const split = splitUstar(full);
2618
+ if (split) parts.push(header(split.name, file.data.length, "0", split.prefix));
2619
+ else {
2620
+ const pax = new TextEncoder().encode(paxRecord("path", full));
2621
+ parts.push(header("PaxHeader", pax.length, "x"), ...padded(pax));
2622
+ parts.push(header(full.slice(0, 99), file.data.length, "0"));
2623
+ }
2624
+ parts.push(...padded(file.data));
2625
+ }
2626
+ parts.push(new Uint8Array(BLOCK * 2));
2627
+ const total = parts.reduce((n, part) => n + part.length, 0);
2628
+ const tar = new Uint8Array(total);
2629
+ let offset = 0;
2630
+ for (const part of parts) {
2631
+ tar.set(part, offset);
2632
+ offset += part.length;
2633
+ }
2634
+ return new Uint8Array(gzipSync(tar, { level: 9 }));
2635
+ }
2636
+
2637
+ // src/cli/pack.ts
2638
+ function packPackage(dir, options = {}) {
2639
+ const report = inspectPackage(dir, options.appVersion ? { appVersion: options.appVersion } : {});
2640
+ if (report.errors.length > 0 || !report.manifest) {
2641
+ const error = new CliError(
2642
+ "the package has errors \u2014 fix them first (see `neurosquad-card validate`)"
2643
+ );
2644
+ error.report = report;
2645
+ throw error;
2646
+ }
2647
+ if (report.secretFiles.length > 0 && !options.allowSecretFiles) {
2648
+ throw new CliError(
2649
+ `refusing to pack files that look like credentials: ${report.secretFiles.map(safeTerminalText).join(", ")} \u2014 add them to .gitignore, or pass --allow-secret-files if they are meant to ship`
2650
+ );
2651
+ }
2652
+ const out = resolve4(options.out ?? `${report.manifest.name}-${report.manifest.version}.tgz`);
2653
+ const entries = report.files.filter((file) => resolve4(file.abs) !== out).map((file) => {
2654
+ const data = readFileSync5(file.abs);
2655
+ return {
2656
+ path: file.path,
2657
+ size: data.length,
2658
+ data: new Uint8Array(data),
2659
+ sha256: createHash("sha256").update(data).digest("hex")
2660
+ };
2661
+ });
2662
+ const treeHash = createHash("sha256").update(treeHashInput(entries), "utf8").digest("hex");
2663
+ const archive = createTarGz(entries, report.manifest.name);
2664
+ if (archive.length > LIMITS.tarballBytes) {
2665
+ throw new CliError(
2666
+ `the archive is ${formatBytes(archive.length)}; the app accepts at most ${formatBytes(LIMITS.tarballBytes)}`
2667
+ );
2668
+ }
2669
+ let written = null;
2670
+ if (!options.dryRun) {
2671
+ try {
2672
+ writeFileSync2(out, archive);
2673
+ written = out;
2674
+ } catch (error) {
2675
+ rmSync(out, { force: true });
2676
+ throw new CliError(`could not write ${out}: ${error.message}`);
2677
+ }
2678
+ }
2679
+ return {
2680
+ report,
2681
+ treeHash,
2682
+ out: written,
2683
+ archiveBytes: archive.length,
2684
+ files: entries.map(({ path, size: size2, sha256 }) => ({ path, size: size2, sha256 }))
2685
+ };
2686
+ }
2687
+ function runPack(dirArg, options) {
2688
+ const dir = resolve4(dirArg ?? ".");
2689
+ let result;
2690
+ try {
2691
+ result = packPackage(dir, options);
2692
+ } catch (error) {
2693
+ const report = error.report;
2694
+ if (report) {
2695
+ if (options.json) console.log(JSON.stringify(reportJson(report), null, 2));
2696
+ else printReport(report, { consent: false });
2697
+ }
2698
+ throw error;
2699
+ }
2700
+ if (options.json) {
2701
+ console.log(
2702
+ JSON.stringify(
2703
+ {
2704
+ ...reportJson(result.report),
2705
+ treeHash: result.treeHash,
2706
+ out: result.out,
2707
+ archiveBytes: result.archiveBytes,
2708
+ files: result.files
2709
+ },
2710
+ null,
2711
+ 2
2712
+ )
2713
+ );
2714
+ return 0;
2715
+ }
2716
+ for (const warning of result.report.warnings)
2717
+ console.log(
2718
+ `${c.yellow("\u25B2 warning")} ${safeTerminalText(warning.where)}: ${safeTerminalText(warning.message)}`
2719
+ );
2720
+ const width = Math.max(...result.files.map((file) => file.path.length), 4);
2721
+ console.log(c.dim(`${"file".padEnd(width)} size`));
2722
+ for (const file of result.files)
2723
+ console.log(`${safeTerminalText(file.path).padEnd(width)} ${c.dim(formatBytes(file.size))}`);
2724
+ console.log("");
2725
+ console.log(
2726
+ `${result.files.length} files, ${formatBytes(result.report.totalBytes)} unpacked, ${formatBytes(result.archiveBytes)} packed`
2727
+ );
2728
+ console.log(`tree hash ${c.bold(result.treeHash)}`);
2729
+ console.log(
2730
+ result.out ? c.green(`\u2714 wrote ${result.out}`) : c.green("\u2714 dry run: the installer would accept this package")
2731
+ );
2732
+ return 0;
2733
+ }
2734
+
2735
+ // src/cli/index.ts
2736
+ var HELP = `${c.bold("neurosquad-card")} ${c.dim(SDK_VERSION)} \u2014 build custom cards for NeuroSquad
2737
+
2738
+ ${c.bold("Usage")}
2739
+ neurosquad-card create <folder> [--template vanilla|react] [--name <slug>] [--display-name <text>] [--force]
2740
+ New card from a template. vanilla: no build step. react: React + Vite.
2741
+ neurosquad-card dev [folder] [--user-data-dir <dir>] [--no-build] [--no-logs]
2742
+ Link the folder into the running app (developer mode on), hot reload on save,
2743
+ stream the card's log. Runs "npm run watch" (or "build -- --watch") when present:
2744
+ that runs the folder's own scripts, so only use dev on folders you trust (or --no-build).
2745
+ neurosquad-card validate [folder] [--json] [--app-version <x.y.z>]
2746
+ Check the manifest and files with the app's own rules; preview the install dialog.
2747
+ neurosquad-card pack [folder] [--out <file.tgz>] [--dry-run] [--json] [--allow-secret-files]
2748
+ Apply the installer's archive rules, print files and the tree hash, write a .tgz.
2749
+ Refuses files that look like credentials (.env, *.pem, *.key, id_rsa, .npmrc\u2026).
2750
+
2751
+ ${c.bold("Options")}
2752
+ -h, --help this help
2753
+ -v, --version the SDK version
2754
+
2755
+ Without installing: npx @neurosquad/card-sdk <command>`;
2756
+ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
2757
+ "force",
2758
+ "json",
2759
+ "dry-run",
2760
+ "allow-secret-files",
2761
+ "no-build",
2762
+ "no-logs",
2763
+ "help",
2764
+ "version",
2765
+ "h",
2766
+ "v"
2767
+ ]);
2768
+ async function main(argv) {
2769
+ const [command, ...rest] = argv;
2770
+ const args = parseArgs(rest, BOOLEAN_FLAGS);
2771
+ if (!command || command === "help" || command === "--help" || command === "-h") {
2772
+ console.log(HELP);
2773
+ return 0;
2774
+ }
2775
+ if (command === "--version" || command === "-v" || command === "version") {
2776
+ console.log(SDK_VERSION);
2777
+ return 0;
2778
+ }
2779
+ if (args.flags.has("help") || args.flags.has("h")) {
2780
+ console.log(HELP);
2781
+ return 0;
2782
+ }
2783
+ const dir = args.positionals[0];
2784
+ switch (command) {
2785
+ case "create": {
2786
+ const template = flag(args, "template") ?? "vanilla";
2787
+ if (!TEMPLATES.includes(template)) {
2788
+ throw new CliError(`unknown template "${template}" \u2014 use one of: ${TEMPLATES.join(", ")}`);
2789
+ }
2790
+ return runCreate(dir, {
2791
+ template,
2792
+ ...flag(args, "name") ? { name: flag(args, "name") } : {},
2793
+ ...flag(args, "display-name") ? { displayName: flag(args, "display-name") } : {},
2794
+ force: args.flags.has("force")
2795
+ });
2796
+ }
2797
+ case "validate":
2798
+ return runValidate(dir, {
2799
+ json: args.flags.has("json"),
2800
+ ...flag(args, "app-version") ? { appVersion: flag(args, "app-version") } : {}
2801
+ });
2802
+ case "pack":
2803
+ return runPack(dir, {
2804
+ json: args.flags.has("json"),
2805
+ dryRun: args.flags.has("dry-run"),
2806
+ allowSecretFiles: args.flags.has("allow-secret-files"),
2807
+ ...flag(args, "out") ? { out: flag(args, "out") } : {},
2808
+ ...flag(args, "app-version") ? { appVersion: flag(args, "app-version") } : {}
2809
+ });
2810
+ case "dev":
2811
+ return runDev(dir, {
2812
+ ...flag(args, "user-data-dir") ? { userDataDir: flag(args, "user-data-dir") } : {},
2813
+ build: !args.flags.has("no-build"),
2814
+ logs: !args.flags.has("no-logs")
2815
+ });
2816
+ default:
2817
+ throw new CliError(`unknown command "${command}" \u2014 run neurosquad-card --help`);
2818
+ }
2819
+ }
2820
+ function run() {
2821
+ main(process.argv.slice(2)).then(
2822
+ (code) => {
2823
+ process.exitCode = code;
2824
+ },
2825
+ (error) => {
2826
+ if (error instanceof CliError) {
2827
+ console.error(c.red(`\u2716 ${error.message}`));
2828
+ process.exitCode = error.exitCode;
2829
+ } else {
2830
+ console.error(error);
2831
+ process.exitCode = 1;
2832
+ }
2833
+ }
2834
+ );
2835
+ }
2836
+
2837
+ // src/cli/bin.ts
2838
+ run();