@agentproto/app-kit 1.4.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { join, relative, isAbsolute, dirname, resolve, basename } from 'path';
1
+ import { join, relative, isAbsolute, dirname, resolve, posix, basename } from 'path';
2
2
  import { defineWorkspace } from '@agentproto/workspace';
3
- import { mkdir, writeFile, copyFile, cp, readFile, mkdtemp, readdir, rm, stat } from 'fs/promises';
3
+ import { mkdir, writeFile, copyFile, cp, readFile, mkdtemp, stat, readdir, rm } from 'fs/promises';
4
4
  import matter2 from 'gray-matter';
5
5
  import { parseAgentManifest, agentFromManifest } from '@agentproto/agent/manifest';
6
6
  import { loadWorkflowHandle } from '@agentproto/workflow-loader';
@@ -62,7 +62,8 @@ async function emitApp(app, dir) {
62
62
  ...app.ui.tools !== void 0 ? { tools: app.ui.tools } : {},
63
63
  ...app.ui.port !== void 0 ? { port: app.ui.port } : {},
64
64
  ...app.ui.csp !== void 0 ? { csp: app.ui.csp } : {},
65
- ...app.ui.build !== void 0 ? { build: app.ui.build } : {}
65
+ ...app.ui.build !== void 0 ? { build: app.ui.build } : {},
66
+ ...app.ui.extensions !== void 0 ? { extensions: app.ui.extensions } : {}
66
67
  };
67
68
  }
68
69
  let artifactPath;
@@ -142,13 +143,28 @@ async function emitApp(app, dir) {
142
143
  }
143
144
  function requiresFrontmatter(app) {
144
145
  const r = app.requirements;
146
+ const apps = appsFrontmatter(app);
147
+ const hasAppDetail = (app.appRequirements ?? []).some(
148
+ (e) => e.version !== void 0 || e.workflows !== void 0 && e.workflows.length > 0
149
+ );
145
150
  if (r && (r.browser || r.fs || r.gpu || r.secrets.length > 0)) {
146
151
  return {
147
- requires: { browser: r.browser, fs: r.fs, gpu: r.gpu, secrets: r.secrets, apps: r.apps }
152
+ requires: { browser: r.browser, fs: r.fs, gpu: r.gpu, secrets: r.secrets, apps }
148
153
  };
149
154
  }
155
+ if (hasAppDetail) {
156
+ return { requires: { apps } };
157
+ }
150
158
  return app.requires !== void 0 ? { requires: app.requires } : {};
151
159
  }
160
+ function appsFrontmatter(app) {
161
+ const entries = app.appRequirements ?? app.requires ?? [];
162
+ return entries.map((e) => {
163
+ if (typeof e === "string") return e;
164
+ if (e.version === void 0 && (e.workflows === void 0 || e.workflows.length === 0)) return e.id;
165
+ return e;
166
+ });
167
+ }
152
168
  function toManifest(handle, body) {
153
169
  const data = JSON.parse(JSON.stringify(handle));
154
170
  return matter2.stringify(`
@@ -156,6 +172,52 @@ ${body}
156
172
  `, data);
157
173
  }
158
174
 
175
+ // src/version-range.ts
176
+ var VERSION_RE = /^(\d+)\.(\d+)\.(\d+)(?:[-+][\w.\-]+)?$/;
177
+ function parseVersion(v) {
178
+ const m = VERSION_RE.exec(v);
179
+ if (!m) {
180
+ throw new Error(`invalid version "${v}" \u2014 expected major.minor.patch (e.g. "1.2.3").`);
181
+ }
182
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
183
+ }
184
+ function compare(a, b) {
185
+ return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
186
+ }
187
+ function parseVersionRange(range) {
188
+ let m = /^\^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(range);
189
+ if (m !== null) {
190
+ return {
191
+ kind: "caret",
192
+ version: [Number(m[1]), m[2] !== void 0 ? Number(m[2]) : 0, m[3] !== void 0 ? Number(m[3]) : 0]
193
+ };
194
+ }
195
+ m = /^~(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(range);
196
+ if (m !== null) {
197
+ return {
198
+ kind: "tilde",
199
+ version: [Number(m[1]), m[2] !== void 0 ? Number(m[2]) : 0, m[3] !== void 0 ? Number(m[3]) : 0],
200
+ minorGiven: m[2] !== void 0
201
+ };
202
+ }
203
+ if (VERSION_RE.test(range)) return { kind: "exact", version: parseVersion(range) };
204
+ throw new Error(
205
+ `invalid version range "${range}" \u2014 expected an exact version ("1.2.3"), a caret ("^1", "^1.2", "^1.2.3"), or a tilde ("~1", "~1.2", "~1.2.3").`
206
+ );
207
+ }
208
+ function versionSatisfies(version, range) {
209
+ const v = parseVersion(version);
210
+ const r = parseVersionRange(range);
211
+ if (r.kind === "exact") return compare(v, r.version) === 0;
212
+ if (compare(v, r.version) < 0) return false;
213
+ if (r.kind === "caret") {
214
+ if (r.version[0] > 0) return v[0] === r.version[0];
215
+ if (r.version[1] > 0) return v[0] === 0 && v[1] === r.version[1];
216
+ return v[0] === 0 && v[1] === 0 && v[2] === r.version[2];
217
+ }
218
+ return v[0] === r.version[0] && (!r.minorGiven || v[1] === r.version[1]);
219
+ }
220
+
159
221
  // src/manifest-fields.ts
160
222
  var APP_PLACEMENTS = ["local", "box", "any", "split"];
161
223
  function isRecord(v) {
@@ -173,6 +235,52 @@ function bool(v, key, fail) {
173
235
  if (typeof v !== "boolean") throw fail(`'${key}' must be a boolean.`);
174
236
  return v;
175
237
  }
238
+ function appRequirementEntry(entry, key, fail) {
239
+ if (typeof entry === "string") {
240
+ if (entry.trim() === "") throw fail(`'${key}' entries must be non-empty strings or objects.`);
241
+ return { id: entry };
242
+ }
243
+ if (!isRecord(entry)) {
244
+ throw fail(`'${key}' entries must be non-empty strings or objects, got ${JSON.stringify(entry)}.`);
245
+ }
246
+ const id = entry.id;
247
+ if (typeof id !== "string" || id.trim() === "") {
248
+ throw fail(`'${key}.id' must be a non-empty string.`);
249
+ }
250
+ const version = entry.version;
251
+ if (version !== void 0) {
252
+ if (typeof version !== "string" || version.trim() === "") {
253
+ throw fail(`'${key}.version' must be a non-empty string.`);
254
+ }
255
+ try {
256
+ parseVersionRange(version);
257
+ } catch (err) {
258
+ throw fail(`'${key}.version' ${err instanceof Error ? err.message : String(err)}`);
259
+ }
260
+ }
261
+ const workflows = entry.workflows;
262
+ if (workflows !== void 0) {
263
+ if (!Array.isArray(workflows) || !workflows.every((w) => typeof w === "string" && w.trim() !== "")) {
264
+ throw fail(`'${key}.workflows' must be an array of non-empty strings.`);
265
+ }
266
+ }
267
+ const extra = Object.keys(entry).filter((k) => k !== "id" && k !== "version" && k !== "workflows");
268
+ if (extra.length > 0) {
269
+ throw fail(`'${key}.${extra[0]}' is not a supported requires.apps entry field (expected id, version, workflows).`);
270
+ }
271
+ return {
272
+ id,
273
+ ...version !== void 0 ? { version } : {},
274
+ ...workflows !== void 0 ? { workflows: [...workflows] } : {}
275
+ };
276
+ }
277
+ function appRequirementList(v, key, fail) {
278
+ if (v === void 0) return [];
279
+ if (!Array.isArray(v)) {
280
+ throw fail(`'${key}' must be an array of non-empty strings or { id, version?, workflows? } objects.`);
281
+ }
282
+ return v.map((e, i) => appRequirementEntry(e, `${key}[${i}]`, fail));
283
+ }
176
284
  function normalizeManifestFields(input, declared, fail) {
177
285
  let placement = "any";
178
286
  if (input.placement !== void 0) {
@@ -185,6 +293,7 @@ function normalizeManifestFields(input, declared, fail) {
185
293
  }
186
294
  let requires;
187
295
  let requirements = { browser: false, fs: false, gpu: false, secrets: [], apps: [] };
296
+ let appRequirements = [];
188
297
  const r = input.requires;
189
298
  if (r !== void 0) {
190
299
  if (Array.isArray(r)) {
@@ -193,16 +302,18 @@ function normalizeManifestFields(input, declared, fail) {
193
302
  }
194
303
  requires = [...r];
195
304
  requirements = { ...requirements, apps: requires };
305
+ appRequirements = appRequirementList(r, "requires", fail);
196
306
  } else if (isRecord(r)) {
197
- const apps = stringList(r.apps, "requires.apps", fail);
307
+ const apps = appRequirementList(r.apps, "requires.apps", fail);
198
308
  requirements = {
199
309
  browser: bool(r.browser, "requires.browser", fail),
200
310
  fs: bool(r.fs, "requires.fs", fail),
201
311
  gpu: bool(r.gpu, "requires.gpu", fail),
202
312
  secrets: stringList(r.secrets, "requires.secrets", fail),
203
- apps
313
+ apps: apps.map((e) => e.id)
204
314
  };
205
- if (apps.length > 0) requires = apps;
315
+ appRequirements = apps;
316
+ if (apps.length > 0) requires = apps.map((e) => e.id);
206
317
  } else {
207
318
  throw fail("'requires' must be an array of strings or an object { browser, fs, gpu, secrets, apps }.");
208
319
  }
@@ -233,7 +344,7 @@ function normalizeManifestFields(input, declared, fail) {
233
344
  if (!isRecord(input.accepts)) throw fail("'accepts' must be an object { tasks }.");
234
345
  accepts = { tasks: bool(input.accepts.tasks, "accepts.tasks", fail) };
235
346
  }
236
- return { placement, requires, requirements, exposes, accepts };
347
+ return { placement, requires, requirements, appRequirements, exposes, accepts };
237
348
  }
238
349
 
239
350
  // src/define-app.ts
@@ -301,13 +412,28 @@ function defineApp(def) {
301
412
  secrets: Object.freeze([...fields.requirements.secrets]),
302
413
  apps: Object.freeze([...fields.requirements.apps])
303
414
  });
415
+ const appRequirements = Object.freeze(
416
+ fields.appRequirements.map((e) => Object.freeze({ ...e, ...e.workflows !== void 0 ? { workflows: Object.freeze([...e.workflows]) } : {} }))
417
+ );
304
418
  const placement = fields.placement;
305
419
  const exposes = Object.freeze({
306
420
  agents: Object.freeze([...fields.exposes.agents]),
307
421
  workflows: Object.freeze([...fields.exposes.workflows])
308
422
  });
309
423
  const accepts = Object.freeze({ ...fields.accepts });
310
- const ui = def.ui ? Object.freeze({ ...def.ui }) : void 0;
424
+ const uiExtensions = def.ui === void 0 || def.ui.extensions === void 0 ? void 0 : Object.freeze({
425
+ openai: freezeDeep(
426
+ normalizeOpenAIUiExtension(
427
+ validateOpenAIExtensionsNamespace(def.ui.extensions),
428
+ def.ui.tools,
429
+ (msg) => new AppDefinitionError(msg)
430
+ )
431
+ )
432
+ });
433
+ const ui = def.ui ? Object.freeze({
434
+ ...def.ui,
435
+ ...uiExtensions !== void 0 ? { extensions: uiExtensions } : {}
436
+ }) : void 0;
311
437
  const artifact = def.artifact ? Object.freeze({ ...def.artifact }) : void 0;
312
438
  const skill = def.skill ? Object.freeze({ ...def.skill }) : void 0;
313
439
  const artifacts = def.artifacts ? Object.freeze(def.artifacts.map((a) => Object.freeze({ ...a }))) : void 0;
@@ -337,6 +463,7 @@ function defineApp(def) {
337
463
  ...description !== void 0 ? { description } : {},
338
464
  ...requires !== void 0 ? { requires } : {},
339
465
  requirements,
466
+ appRequirements,
340
467
  placement,
341
468
  exposes,
342
469
  accepts,
@@ -380,6 +507,7 @@ function defineApp(def) {
380
507
  ...description !== void 0 ? { description } : {},
381
508
  ...requires !== void 0 ? { requires } : {},
382
509
  requirements,
510
+ appRequirements,
383
511
  placement,
384
512
  exposes,
385
513
  accepts,
@@ -478,6 +606,240 @@ function validateUniqueIds(items, kind) {
478
606
  seen.add(item.id);
479
607
  }
480
608
  }
609
+ function validateOpenAIExtensionsNamespace(raw) {
610
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
611
+ throw new AppDefinitionError("`ui.extensions` must be an object.");
612
+ }
613
+ for (const key of Object.keys(raw)) {
614
+ if (key !== "openai") {
615
+ throw new AppDefinitionError(
616
+ `\`ui.extensions.${key}\` is not supported \u2014 \`extensions\` accepts only \`openai\` (v1).`
617
+ );
618
+ }
619
+ }
620
+ return raw.openai;
621
+ }
622
+ var OPENAI_EXTENSION_KEYS = /* @__PURE__ */ new Set(["entrypoints", "icons", "display", "mentions"]);
623
+ var OPENAI_CUT_KEY_MESSAGE = "is not a supported OpenAI extension field in v1 (cut would be silently half-advertised)";
624
+ var DOT_EXTENSION_RE = /^\.[a-z0-9][a-z0-9._+-]{0,31}$/;
625
+ var OPENAI_MODES = ["inline", "fullscreen"];
626
+ function normalizeOpenAIUiExtension(raw, tools, fail) {
627
+ if (raw === void 0) return void 0;
628
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
629
+ throw fail("`ui.extensions.openai` must be an object.");
630
+ }
631
+ for (const key of Object.keys(raw)) {
632
+ if (!OPENAI_EXTENSION_KEYS.has(key)) {
633
+ throw fail(`\`ui.extensions.openai.${key}\` ${OPENAI_CUT_KEY_MESSAGE}.`);
634
+ }
635
+ }
636
+ const entrypoints = normalizeOpenAIEntrypoints(raw.entrypoints, fail);
637
+ const icons = normalizeOpenAIIcons(raw.icons, fail);
638
+ const display = normalizeOpenAIDisplay(raw.display, fail);
639
+ const mentions = normalizeOpenAIMentions(raw.mentions, tools, fail);
640
+ return {
641
+ ...entrypoints !== void 0 ? { entrypoints } : {},
642
+ ...icons !== void 0 ? { icons } : {},
643
+ ...display !== void 0 ? { display } : {},
644
+ ...mentions !== void 0 ? { mentions } : {}
645
+ };
646
+ }
647
+ function normalizeOpenAIEntrypoints(raw, fail) {
648
+ if (raw === void 0) return void 0;
649
+ if (!Array.isArray(raw)) {
650
+ throw fail("`ui.extensions.openai.entrypoints` must be an array.");
651
+ }
652
+ if (raw.length < 1 || raw.length > 3) {
653
+ throw fail(
654
+ `\`ui.extensions.openai.entrypoints\` must contain 1..3 entries, got ${raw.length}.`
655
+ );
656
+ }
657
+ const seenTypes = /* @__PURE__ */ new Set();
658
+ const out = [];
659
+ for (let i = 0; i < raw.length; i++) {
660
+ const path = `ui.extensions.openai.entrypoints[${i}]`;
661
+ const ep = raw[i];
662
+ if (typeof ep !== "object" || ep === null || Array.isArray(ep)) {
663
+ throw fail(`\`${path}\` must be an object.`);
664
+ }
665
+ const keys = Object.keys(ep);
666
+ const invalidKeys = keys.filter((k) => k !== "type" && k !== "extensions");
667
+ if (invalidKeys.length > 0) {
668
+ throw fail(
669
+ `\`${path}.${invalidKeys[0]}\` is not a supported \`${ep.type}\` entrypoint field.`
670
+ );
671
+ }
672
+ const type = ep.type;
673
+ if (type !== "global" && type !== "thread" && type !== "file") {
674
+ throw fail(`\`${path}.type\` must be "global", "thread" or "file", got ${JSON.stringify(type)}.`);
675
+ }
676
+ if (seenTypes.has(type)) {
677
+ throw fail(`\`ui.extensions.openai.entrypoints\` declares \`${type}\` more than once.`);
678
+ }
679
+ seenTypes.add(type);
680
+ if (type === "file") {
681
+ out.push({ type, extensions: normalizeDotExtensions(ep.extensions, path, fail) });
682
+ } else {
683
+ out.push({ type });
684
+ }
685
+ }
686
+ return out;
687
+ }
688
+ function normalizeDotExtensions(raw, path, fail) {
689
+ if (!Array.isArray(raw)) {
690
+ throw fail(`\`${path}.extensions\` must be an array of dot-prefixed strings.`);
691
+ }
692
+ if (raw.length < 1 || raw.length > 32) {
693
+ throw fail(`\`${path}.extensions\` must contain 1..32 entries, got ${raw.length}.`);
694
+ }
695
+ const seen = /* @__PURE__ */ new Set();
696
+ const out = [];
697
+ for (let i = 0; i < raw.length; i++) {
698
+ const ext = raw[i];
699
+ if (typeof ext !== "string" || ext.trim() === "") {
700
+ throw fail(`\`${path}.extensions[${i}]\` must be a non-empty string.`);
701
+ }
702
+ const normalized = ext.toLowerCase();
703
+ if (seen.has(normalized)) {
704
+ throw fail(`\`${path}.extensions\` declares '${normalized}' more than once.`);
705
+ }
706
+ seen.add(normalized);
707
+ if (!DOT_EXTENSION_RE.test(normalized)) {
708
+ throw fail(
709
+ `\`${path}.extensions[${i}]\` ('${ext}') must be a dot-prefixed lowercase suffix matching /^\\.[a-z0-9][a-z0-9._+-]{0,31}$/, e.g. '.md'.`
710
+ );
711
+ }
712
+ out.push(normalized);
713
+ }
714
+ return out;
715
+ }
716
+ function normalizeOpenAIIcons(raw, fail) {
717
+ if (raw === void 0) return void 0;
718
+ if (!Array.isArray(raw)) throw fail("`ui.extensions.openai.icons` must be an array.");
719
+ if (raw.length < 1) {
720
+ throw fail("`ui.extensions.openai.icons` must contain at least one icon when present.");
721
+ }
722
+ return raw.map((icon, i) => {
723
+ const path = `ui.extensions.openai.icons[${i}]`;
724
+ if (typeof icon !== "object" || icon === null || Array.isArray(icon)) {
725
+ throw fail(`\`${path}\` must be an object.`);
726
+ }
727
+ const keys = Object.keys(icon);
728
+ for (const key of keys) {
729
+ if (key !== "src" && key !== "mimeType" && key !== "sizes" && key !== "theme") {
730
+ throw fail(`\`${path}.${key}\` is not a supported icon field.`);
731
+ }
732
+ }
733
+ const src = icon.src;
734
+ if (typeof src !== "string" || src.trim() === "") {
735
+ throw fail(`\`${path}.src\` must be a non-empty string.`);
736
+ }
737
+ if (!(ICON_SRC_HTTPS_RE.test(src) || hasValidIconSrcDataUrl(src))) {
738
+ throw fail(
739
+ `\`${path}.src\` must be an HTTPS URL or a 'data:image/\u2026;base64,\u2026' URL (inline SVG data allowed), got '${src.slice(0, 64)}'.`
740
+ );
741
+ }
742
+ const mimeType = icon.mimeType;
743
+ if (mimeType !== void 0 && (typeof mimeType !== "string" || mimeType.trim() === "")) {
744
+ throw fail(`\`${path}.mimeType\` must be a non-empty string when present.`);
745
+ }
746
+ const sizes = icon.sizes;
747
+ if (sizes !== void 0 && (!Array.isArray(sizes) || sizes.length === 0 || !sizes.every((s) => typeof s === "string" && s.trim() !== ""))) {
748
+ throw fail(`\`${path}.sizes\` must be an array of non-empty strings when present.`);
749
+ }
750
+ const theme = icon.theme;
751
+ if (theme !== void 0 && theme !== "light" && theme !== "dark") {
752
+ throw fail(`\`${path}.theme\` must be "light" or "dark" when present, got ${JSON.stringify(theme)}.`);
753
+ }
754
+ return {
755
+ src,
756
+ ...mimeType !== void 0 ? { mimeType } : {},
757
+ ...sizes !== void 0 ? { sizes: Object.freeze([...sizes]) } : {},
758
+ ...theme !== void 0 ? { theme } : {}
759
+ };
760
+ });
761
+ }
762
+ var ICON_SRC_HTTPS_RE = /^https:\/\/\S+$/;
763
+ function hasValidIconSrcDataUrl(src) {
764
+ const comma = src.indexOf(",");
765
+ if (comma < 0) return false;
766
+ const header = src.slice(0, comma);
767
+ const payload = src.slice(comma + 1);
768
+ if (!/^data:image\/[a-z0-9.+-]+(?:;charset=[\w-]+)?(?:;base64|;utf8)?$/i.test(header)) return false;
769
+ if (/;base64$/i.test(header)) return /^[A-Za-z0-9+/=\r\n]+$/.test(payload);
770
+ return payload.trim() !== "";
771
+ }
772
+ function normalizeOpenAIDisplay(raw, fail) {
773
+ if (raw === void 0) return void 0;
774
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
775
+ throw fail("`ui.extensions.openai.display` must be an object.");
776
+ }
777
+ const availableModes = raw.availableModes;
778
+ if (availableModes !== void 0) {
779
+ if (!Array.isArray(availableModes) || availableModes.length === 0) {
780
+ throw fail("`ui.extensions.openai.display.availableModes` must be a non-empty array when present.");
781
+ }
782
+ const seen = /* @__PURE__ */ new Set();
783
+ for (const mode of availableModes) {
784
+ if (mode !== "inline" && mode !== "fullscreen") {
785
+ throw fail(
786
+ `\`ui.extensions.openai.display.availableModes\` supports only "inline" and "fullscreen", got ${JSON.stringify(mode)}.`
787
+ );
788
+ }
789
+ if (seen.has(mode)) {
790
+ throw fail(`\`ui.extensions.openai.display.availableModes\` declares '${mode}' more than once.`);
791
+ }
792
+ seen.add(mode);
793
+ }
794
+ }
795
+ const preferredMode = raw.preferredMode;
796
+ if (preferredMode !== void 0 && !OPENAI_MODES.includes(preferredMode)) {
797
+ throw fail(
798
+ `\`ui.extensions.openai.display.preferredMode\` must be "inline" or "fullscreen", got ${JSON.stringify(preferredMode)}.`
799
+ );
800
+ }
801
+ if (availableModes !== void 0 && preferredMode !== void 0 && !availableModes.includes(preferredMode)) {
802
+ throw fail(
803
+ `\`ui.extensions.openai.display.preferredMode\` ('${preferredMode}') must occur in \`ui.extensions.openai.display.availableModes\` when both are declared.`
804
+ );
805
+ }
806
+ return {
807
+ ...availableModes !== void 0 ? { availableModes: Object.freeze([...availableModes]) } : {},
808
+ ...preferredMode !== void 0 ? { preferredMode } : {}
809
+ };
810
+ }
811
+ function normalizeOpenAIMentions(raw, tools, fail) {
812
+ if (raw === void 0) return void 0;
813
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
814
+ throw fail("`ui.extensions.openai.mentions` must be an object.");
815
+ }
816
+ for (const key of Object.keys(raw)) {
817
+ if (key !== "searchTool") {
818
+ throw fail(`\`ui.extensions.openai.mentions.${key}\` is not a supported mentions field.`);
819
+ }
820
+ }
821
+ const searchTool = raw.searchTool;
822
+ if (typeof searchTool !== "string" || searchTool.trim() === "") {
823
+ throw fail("`ui.extensions.openai.mentions.searchTool` must be a non-empty string.");
824
+ }
825
+ if (tools === void 0 || !tools.includes(searchTool)) {
826
+ throw fail(
827
+ `\`ui.extensions.openai.mentions.searchTool\` '${searchTool}' must be declared in this same ui block's \`tools\`${tools ? ` (${tools.join(", ")})` : " \u2014 the ui declares none"}.`
828
+ );
829
+ }
830
+ return { searchTool };
831
+ }
832
+ function freezeDeep(value) {
833
+ if (Array.isArray(value)) return Object.freeze(value.map(freezeDeep));
834
+ if (typeof value === "object" && value !== null) {
835
+ const out = {};
836
+ for (const [key, v] of Object.entries(value)) {
837
+ out[key] = freezeDeep(v);
838
+ }
839
+ return Object.freeze(out);
840
+ }
841
+ return value;
842
+ }
481
843
 
482
844
  // src/errors.ts
483
845
  var AppLoadError = class extends Error {
@@ -842,7 +1204,13 @@ async function loadAppHandle(dir) {
842
1204
  ...fm.ui.tools !== void 0 ? { tools: fm.ui.tools } : {},
843
1205
  ...fm.ui.port !== void 0 ? { port: fm.ui.port } : {},
844
1206
  ...fm.ui.csp !== void 0 ? { csp: fm.ui.csp } : {},
845
- ...build !== void 0 ? { build } : {}
1207
+ ...build !== void 0 ? { build } : {},
1208
+ // Pass through UNFILTERED — `defineApp`'s
1209
+ // `validateOpenAIExtensionsNamespace` rejects any sibling vendor key
1210
+ // (a typo, a future vendor block, hand-authored drift) with the same
1211
+ // diagnostic the TS-authoring path throws. Pre-filtering to `openai`
1212
+ // here would silently drop those instead of failing loudly.
1213
+ ...fm.ui.extensions !== void 0 ? { extensions: fm.ui.extensions } : {}
846
1214
  };
847
1215
  }
848
1216
  return defineApp({
@@ -974,8 +1342,92 @@ function extractMeta(front) {
974
1342
  ...ui !== void 0 ? { ui } : {}
975
1343
  };
976
1344
  }
1345
+ var RELEASE_DEFAULT_EXCLUDE = [
1346
+ "ui/**",
1347
+ "docs/**",
1348
+ "data/**",
1349
+ "scripts/**",
1350
+ // Root-level only: `**/dev/**` would also drop a shipped agent named
1351
+ // `dev` (`.agentproto/agents/dev/`); `ui/dev/` is covered by `ui/**`.
1352
+ "dev/**",
1353
+ "**/*.log",
1354
+ "**/*.map",
1355
+ "**/.DS_Store",
1356
+ "**/.env",
1357
+ "**/.env.*"
1358
+ ];
1359
+ function globToRegExp(glob) {
1360
+ const g = glob.replace(/^\.\//, "");
1361
+ let re = "";
1362
+ for (let i = 0; i < g.length; i++) {
1363
+ const c = g[i];
1364
+ if (c === "*") {
1365
+ if (g[i + 1] === "*") {
1366
+ i++;
1367
+ if (g[i + 1] === "/") {
1368
+ i++;
1369
+ re += "(?:.*/)?";
1370
+ } else {
1371
+ re += ".*";
1372
+ }
1373
+ } else {
1374
+ re += "[^/]*";
1375
+ }
1376
+ } else if ("\\^$+?.()|{}[]".includes(c)) {
1377
+ re += "\\" + c;
1378
+ } else {
1379
+ re += c;
1380
+ }
1381
+ }
1382
+ return new RegExp(`^${re}$`);
1383
+ }
1384
+ function toPosix(p) {
1385
+ return p.replace(/\\/g, "/");
1386
+ }
1387
+ function matchesAny(path, globs) {
1388
+ const p = toPosix(path);
1389
+ return globs.some((re) => re.test(p));
1390
+ }
1391
+ function readPackageRules(front) {
1392
+ const raw = front.package;
1393
+ if (raw === void 0 || raw === null) return {};
1394
+ if (typeof raw !== "object" || Array.isArray(raw)) {
1395
+ throw new AgentAppPackError("invalid-package", "APP.md `package` must be an object.");
1396
+ }
1397
+ const r = raw;
1398
+ const list = (key) => {
1399
+ const v = r[key];
1400
+ if (v === void 0) return void 0;
1401
+ if (!Array.isArray(v) || v.some((s) => typeof s !== "string" || s.trim() === "")) {
1402
+ throw new AgentAppPackError(
1403
+ "invalid-package",
1404
+ `APP.md \`package.${key}\` must be an array of non-empty glob strings.`
1405
+ );
1406
+ }
1407
+ return v;
1408
+ };
1409
+ if (r.stripBuild !== void 0 && typeof r.stripBuild !== "boolean") {
1410
+ throw new AgentAppPackError("invalid-package", "APP.md `package.stripBuild` must be a boolean.");
1411
+ }
1412
+ const include = list("include");
1413
+ const exclude = list("exclude");
1414
+ return {
1415
+ ...include !== void 0 ? { include } : {},
1416
+ ...exclude !== void 0 ? { exclude } : {},
1417
+ ...typeof r.stripBuild === "boolean" ? { stripBuild: r.stripBuild } : {}
1418
+ };
1419
+ }
1420
+ function uiRelPath(front) {
1421
+ const ui = front.ui;
1422
+ const raw = typeof ui === "string" ? ui : ui && typeof ui === "object" && !Array.isArray(ui) && typeof ui.path === "string" ? ui.path : void 0;
1423
+ if (raw === void 0 || raw === "" || isAbsolute(raw)) return void 0;
1424
+ const rel = posix.normalize(toPosix(raw)).replace(/^\.\//, "");
1425
+ if (rel.startsWith("../") || rel === "..") return void 0;
1426
+ return rel;
1427
+ }
977
1428
  async function packApp(input) {
978
1429
  const appDirAbs = resolve(input.appDir);
1430
+ const release = input.release === true;
979
1431
  const appMdPath = join(appDirAbs, ".agentproto", "APP.md");
980
1432
  if (!await pathExists(appMdPath)) {
981
1433
  throw new AgentAppPackError(
@@ -984,49 +1436,82 @@ async function packApp(input) {
984
1436
  );
985
1437
  }
986
1438
  const raw = await readFile(appMdPath, "utf8");
987
- const front = matter2(raw).data;
1439
+ const parsedAppMd = matter2(raw);
1440
+ const front = parsedAppMd.data;
988
1441
  const meta = extractMeta(front);
1442
+ const rules = readPackageRules(front);
989
1443
  const outAbs = input.out !== void 0 ? resolve(input.out) : resolve(process.cwd(), `${safeId(meta.id)}-${meta.version}.agentapp`);
990
1444
  await mkdir(dirname(outAbs), { recursive: true });
991
- const files = (await collectFiles(appDirAbs)).filter((f) => f.path !== "manifest.json").filter((f) => join(appDirAbs, f.path) !== outAbs).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
992
- const fileCount = files.length;
993
- const totalSize = files.reduce((sum, f) => sum + f.size, 0);
994
- const sha256 = await aggregateSha256(
995
- appDirAbs,
996
- files.map((f) => f.path)
997
- );
998
- const manifest = {
999
- format: AGENTAPP_FORMAT,
1000
- id: meta.id,
1001
- ...meta.name !== void 0 ? { name: meta.name } : {},
1002
- version: meta.version,
1003
- ...meta.description !== void 0 ? { description: meta.description } : {},
1004
- agents: meta.agents,
1005
- workflows: meta.workflows,
1006
- ...meta.ui !== void 0 ? { ui: meta.ui } : {},
1007
- files: files.map((f) => f.path),
1008
- fileCount,
1009
- totalSize,
1010
- sha256,
1011
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1012
- agentprotoVersion: ">=0.1.0"
1013
- };
1445
+ const appMdRel = ".agentproto/APP.md";
1446
+ const uiRel = uiRelPath(front);
1447
+ const uiAssetsPrefix = uiRel !== void 0 ? `${posix.dirname(uiRel)}/assets/`.replace(/^\.\//, "") : void 0;
1448
+ const includeRes = rules.include?.map(globToRegExp);
1449
+ const excludeRes = [...release ? RELEASE_DEFAULT_EXCLUDE : [], ...rules.exclude ?? []].map(globToRegExp);
1450
+ const alwaysShip = (p) => p === appMdRel || p === uiRel || uiAssetsPrefix !== void 0 && p.startsWith(uiAssetsPrefix);
1451
+ const selected = (await collectFiles(appDirAbs)).filter((f) => f.path !== "manifest.json").filter((f) => join(appDirAbs, f.path) !== outAbs).filter((f) => {
1452
+ const p = toPosix(f.path);
1453
+ if (alwaysShip(p)) return true;
1454
+ if (includeRes !== void 0 && !matchesAny(p, includeRes)) return false;
1455
+ return !matchesAny(p, excludeRes);
1456
+ }).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
1457
+ if (release && uiRel !== void 0 && !selected.some((f) => toPosix(f.path) === uiRel)) {
1458
+ throw new AgentAppPackError(
1459
+ "missing-ui",
1460
+ `ui.path "${uiRel}" does not exist in ${appDirAbs} \u2014 build the UI first (\`agentproto app build <appDir>\`, or \`agentproto app pack --release\`, which runs \`ui.build\`).`
1461
+ );
1462
+ }
1463
+ const stripBuild = rules.stripBuild ?? release;
1014
1464
  const staging = await mkdtemp(join(tmpdir(), "agentapp-"));
1015
1465
  try {
1016
- await cp(appDirAbs, staging, {
1017
- recursive: true,
1018
- filter: (source) => !SKIP_DIR_NAMES.has(basename(source))
1019
- });
1466
+ for (const f of selected) {
1467
+ const dest = join(staging, f.path);
1468
+ await mkdir(dirname(dest), { recursive: true });
1469
+ await copyFile(join(appDirAbs, f.path), dest);
1470
+ }
1471
+ if (stripBuild) {
1472
+ const ui = front.ui;
1473
+ if (ui && typeof ui === "object" && !Array.isArray(ui) && "build" in ui) {
1474
+ const data = structuredClone(front);
1475
+ delete data.ui.build;
1476
+ await writeFile(join(staging, appMdRel), matter2.stringify(parsedAppMd.content, data), "utf8");
1477
+ }
1478
+ }
1479
+ const files = [];
1480
+ for (const f of selected) {
1481
+ files.push({ path: f.path, size: (await stat(join(staging, f.path))).size });
1482
+ }
1483
+ const fileCount = files.length;
1484
+ const totalSize = files.reduce((sum, f) => sum + f.size, 0);
1485
+ const sha256 = await aggregateSha256(
1486
+ staging,
1487
+ files.map((f) => f.path)
1488
+ );
1489
+ const manifest = {
1490
+ format: AGENTAPP_FORMAT,
1491
+ id: meta.id,
1492
+ ...meta.name !== void 0 ? { name: meta.name } : {},
1493
+ version: meta.version,
1494
+ ...meta.description !== void 0 ? { description: meta.description } : {},
1495
+ agents: meta.agents,
1496
+ workflows: meta.workflows,
1497
+ ...meta.ui !== void 0 ? { ui: meta.ui } : {},
1498
+ files: files.map((f) => f.path),
1499
+ fileCount,
1500
+ totalSize,
1501
+ sha256,
1502
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1503
+ agentprotoVersion: ">=0.1.0"
1504
+ };
1020
1505
  await writeFile(join(staging, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", "utf8");
1021
1506
  const entries = (await readdir(staging)).sort();
1022
1507
  const packCode = await runTar(["-czf", outAbs, ...entries], { cwd: staging });
1023
1508
  if (packCode !== 0) {
1024
1509
  throw new AgentAppPackError("tar-failed", `tar failed with exit code ${packCode}`);
1025
1510
  }
1511
+ return { file: outAbs, manifest };
1026
1512
  } finally {
1027
1513
  await rm(staging, { recursive: true, force: true });
1028
1514
  }
1029
- return { file: outAbs, manifest };
1030
1515
  }
1031
1516
  function isUnsafeBundlePath(p) {
1032
1517
  if (p === "" || isAbsolute(p)) return true;
@@ -1090,6 +1575,6 @@ async function unpackApp(input) {
1090
1575
  }
1091
1576
  }
1092
1577
 
1093
- export { AGENTAPP_FORMAT, AgentAppPackError, AppDefinitionError, AppLoadError, aggregateSha256, collectFiles, defineApp, emitApp, isManifest, loadAppBundledTools, loadAppHandle, packApp, peekAppUi, refKey, resolveAppUIRoot, safeId, stripOwner, unpackApp };
1578
+ export { AGENTAPP_FORMAT, AgentAppPackError, AppDefinitionError, AppLoadError, RELEASE_DEFAULT_EXCLUDE, aggregateSha256, collectFiles, defineApp, emitApp, globToRegExp, isManifest, loadAppBundledTools, loadAppHandle, packApp, peekAppUi, readPackageRules, refKey, resolveAppUIRoot, safeId, stripOwner, unpackApp, versionSatisfies };
1094
1579
  //# sourceMappingURL=index.mjs.map
1095
1580
  //# sourceMappingURL=index.mjs.map