@agent-native/core 0.22.44 → 0.23.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/a2a/artifact-response.js +1 -1
- package/dist/a2a/artifact-response.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +12 -4
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/app-skill.d.ts +139 -0
- package/dist/cli/app-skill.d.ts.map +1 -0
- package/dist/cli/app-skill.js +960 -0
- package/dist/cli/app-skill.js.map +1 -0
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +13 -4
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/index.js +24 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/skills.d.ts +39 -0
- package/dist/cli/skills.d.ts.map +1 -0
- package/dist/cli/skills.js +363 -0
- package/dist/cli/skills.js.map +1 -0
- package/dist/cli/templates-meta.d.ts.map +1 -1
- package/dist/cli/templates-meta.js +9 -6
- package/dist/cli/templates-meta.js.map +1 -1
- package/dist/cli/workspace-dev.d.ts.map +1 -1
- package/dist/cli/workspace-dev.js +2 -0
- package/dist/cli/workspace-dev.js.map +1 -1
- package/dist/client/AgentPanel.d.ts +2 -0
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +2 -2
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +9 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +15 -7
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +15 -0
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/use-chat-threads.d.ts +5 -1
- package/dist/client/use-chat-threads.d.ts.map +1 -1
- package/dist/client/use-chat-threads.js +14 -3
- package/dist/client/use-chat-threads.js.map +1 -1
- package/dist/collab/client.d.ts.map +1 -1
- package/dist/collab/client.js +20 -1
- package/dist/collab/client.js.map +1 -1
- package/dist/collab/routes.d.ts.map +1 -1
- package/dist/collab/routes.js +16 -2
- package/dist/collab/routes.js.map +1 -1
- package/dist/collab/storage.d.ts +8 -0
- package/dist/collab/storage.d.ts.map +1 -1
- package/dist/collab/storage.js +55 -7
- package/dist/collab/storage.js.map +1 -1
- package/dist/collab/ydoc-manager.d.ts.map +1 -1
- package/dist/collab/ydoc-manager.js +121 -69
- package/dist/collab/ydoc-manager.js.map +1 -1
- package/dist/deploy/workspace-deploy.js +6 -0
- package/dist/deploy/workspace-deploy.js.map +1 -1
- package/dist/mcp-client/index.d.ts +1 -1
- package/dist/mcp-client/index.d.ts.map +1 -1
- package/dist/mcp-client/index.js +1 -1
- package/dist/mcp-client/index.js.map +1 -1
- package/dist/mcp-client/routes.d.ts +1 -0
- package/dist/mcp-client/routes.d.ts.map +1 -1
- package/dist/mcp-client/routes.js +52 -0
- package/dist/mcp-client/routes.js.map +1 -1
- package/dist/mcp-client/workspace-servers.d.ts +15 -0
- package/dist/mcp-client/workspace-servers.d.ts.map +1 -0
- package/dist/mcp-client/workspace-servers.js +297 -0
- package/dist/mcp-client/workspace-servers.js.map +1 -0
- package/dist/resources/handlers.d.ts.map +1 -1
- package/dist/resources/handlers.js +38 -25
- package/dist/resources/handlers.js.map +1 -1
- package/dist/resources/store.d.ts +11 -3
- package/dist/resources/store.d.ts.map +1 -1
- package/dist/resources/store.js +220 -9
- package/dist/resources/store.js.map +1 -1
- package/dist/scripts/call-agent.js +1 -1
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +21 -6
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-discovery.d.ts.map +1 -1
- package/dist/server/agent-discovery.js +34 -9
- package/dist/server/agent-discovery.js.map +1 -1
- package/dist/server/auth-marketing.d.ts.map +1 -1
- package/dist/server/auth-marketing.js +8 -5
- package/dist/server/auth-marketing.js.map +1 -1
- package/dist/templates/default/AGENTS.md +12 -4
- package/dist/templates/default/DEVELOPING.md +7 -5
- package/dist/templates/workspace-core/AGENTS.md +7 -0
- package/dist/templates/workspace-root/AGENTS.md +6 -0
- package/docs/content/creating-templates.md +14 -9
- package/docs/content/database.md +44 -17
- package/docs/content/deployment.md +15 -7
- package/docs/content/dispatch.md +7 -1
- package/docs/content/embedding-sdk.md +79 -0
- package/docs/content/key-concepts.md +15 -17
- package/docs/content/mcp-clients.md +30 -0
- package/docs/content/multi-app-workspace.md +3 -2
- package/docs/content/multi-tenancy.md +4 -4
- package/docs/content/server.md +10 -7
- package/docs/content/skills-guide.md +75 -0
- package/docs/content/template-analytics.md +1 -1
- package/docs/content/template-assets.md +130 -0
- package/docs/content/template-dispatch.md +3 -2
- package/docs/content/template-slides.md +2 -2
- package/docs/content/workspace-management.md +2 -2
- package/docs/content/workspace.md +11 -9
- package/package.json +1 -1
- package/src/templates/default/AGENTS.md +12 -4
- package/src/templates/default/DEVELOPING.md +7 -5
- package/src/templates/workspace-core/AGENTS.md +7 -0
- package/src/templates/workspace-root/AGENTS.md +6 -0
- package/docs/content/template-images.md +0 -55
|
@@ -0,0 +1,960 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agent-native app-skill` packages an agent-native app as a distributable
|
|
3
|
+
* skill bundle: instructions + MCP connector + embeddable app surfaces.
|
|
4
|
+
*
|
|
5
|
+
* The manifest intentionally contains no user secrets. Hosted installs write
|
|
6
|
+
* URL-only MCP entries; clients that need auth complete OAuth/device setup in
|
|
7
|
+
* the host. Local installs point at a developer-owned app process.
|
|
8
|
+
*/
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { buildHttpMcpEntry } from "./mcp-config-writers.js";
|
|
14
|
+
import { resolveClients, writeConfigs } from "./connect.js";
|
|
15
|
+
const HELP = `agent-native app-skill
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
agent-native app-skill ensure [--manifest <file>] [--hosted|--local] [--client all|codex|claude-code|claude-code-cli|cowork] [--scope user|project] [--name <server>] [--yes] [--dry-run] [--json]
|
|
19
|
+
agent-native app-skill launch [--manifest <file>] [--local|--hosted] [--into <path>] [--client <client>] [--scope user|project] [--name <server>] [--no-register] [--skip-install] [--yes] [--dry-run] [--json]
|
|
20
|
+
agent-native app-skill pack [--manifest <file>] --out <dir> [--json]
|
|
21
|
+
|
|
22
|
+
Commands:
|
|
23
|
+
ensure Register the app skill MCP connector for your local agent clients.
|
|
24
|
+
launch Open the hosted app, or materialize and start a local editable app.
|
|
25
|
+
pack Create marketplace-ready skill, MCP, Codex/Claude plugin, and Vercel skills adapters.
|
|
26
|
+
|
|
27
|
+
Hosted is the default. Use --local when you want editable source, offline work,
|
|
28
|
+
or a privacy-sensitive local app instance.`;
|
|
29
|
+
const APP_SKILL_COMMANDS = new Set(["ensure", "launch", "pack"]);
|
|
30
|
+
const APP_SKILL_SCOPES = new Set(["user", "project"]);
|
|
31
|
+
const SAFE_PACKAGE_EXECUTABLES = new Set(["pnpm", "npm", "bun", "yarn"]);
|
|
32
|
+
const SAFE_ENV_KEYS = [
|
|
33
|
+
"PATH",
|
|
34
|
+
"HOME",
|
|
35
|
+
"USER",
|
|
36
|
+
"SHELL",
|
|
37
|
+
"TMPDIR",
|
|
38
|
+
"TEMP",
|
|
39
|
+
"TMP",
|
|
40
|
+
"TERM",
|
|
41
|
+
"CI",
|
|
42
|
+
"PNPM_HOME",
|
|
43
|
+
"COREPACK_HOME",
|
|
44
|
+
"SystemRoot",
|
|
45
|
+
"ComSpec",
|
|
46
|
+
];
|
|
47
|
+
function isRecord(value) {
|
|
48
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
49
|
+
}
|
|
50
|
+
function stringValue(value) {
|
|
51
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
52
|
+
}
|
|
53
|
+
function stringArray(value) {
|
|
54
|
+
if (!Array.isArray(value))
|
|
55
|
+
return [];
|
|
56
|
+
return value.filter((item) => typeof item === "string");
|
|
57
|
+
}
|
|
58
|
+
function normalizeOriginUrl(value, field) {
|
|
59
|
+
let url;
|
|
60
|
+
try {
|
|
61
|
+
url = new URL(value);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new Error(`${field} must be a valid URL.`);
|
|
65
|
+
}
|
|
66
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
67
|
+
throw new Error(`${field} must use http:// or https://.`);
|
|
68
|
+
}
|
|
69
|
+
return `${url.origin}${url.pathname}`.replace(/\/+$/, "");
|
|
70
|
+
}
|
|
71
|
+
function withMcpPath(baseUrl) {
|
|
72
|
+
return `${baseUrl.replace(/\/+$/, "")}/_agent-native/mcp`;
|
|
73
|
+
}
|
|
74
|
+
function normalizeSkillVisibility(value) {
|
|
75
|
+
if (value === "internal" || value === "exported" || value === "both") {
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
return "both";
|
|
79
|
+
}
|
|
80
|
+
function normalizeHostAdapter(value) {
|
|
81
|
+
if (value === "codex-plugin" ||
|
|
82
|
+
value === "claude-marketplace" ||
|
|
83
|
+
value === "vercel-skills" ||
|
|
84
|
+
value === "plain-skill" ||
|
|
85
|
+
value === "claude-skill" ||
|
|
86
|
+
value === "chatgpt-mcp" ||
|
|
87
|
+
value === "generic-mcp") {
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
function defaultHostAdapters() {
|
|
93
|
+
return [
|
|
94
|
+
"codex-plugin",
|
|
95
|
+
"claude-marketplace",
|
|
96
|
+
"vercel-skills",
|
|
97
|
+
"plain-skill",
|
|
98
|
+
"claude-skill",
|
|
99
|
+
"chatgpt-mcp",
|
|
100
|
+
"generic-mcp",
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
function uniqueAdapters(values) {
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
return values.filter((value) => {
|
|
106
|
+
if (seen.has(value))
|
|
107
|
+
return false;
|
|
108
|
+
seen.add(value);
|
|
109
|
+
return true;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
export function normalizeAppSkillManifest(raw) {
|
|
113
|
+
if (!isRecord(raw))
|
|
114
|
+
throw new Error("App skill manifest must be an object.");
|
|
115
|
+
const schemaVersion = raw.schemaVersion ?? 1;
|
|
116
|
+
if (schemaVersion !== 1) {
|
|
117
|
+
throw new Error(`Unsupported app skill schemaVersion: ${schemaVersion}`);
|
|
118
|
+
}
|
|
119
|
+
const id = stringValue(raw.id);
|
|
120
|
+
if (!id || !/^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$/.test(id)) {
|
|
121
|
+
throw new Error("App skill manifest id must be kebab-case and 2-64 characters.");
|
|
122
|
+
}
|
|
123
|
+
const hostedRaw = isRecord(raw.hosted) ? raw.hosted : {};
|
|
124
|
+
const hostedUrl = normalizeOriginUrl(stringValue(hostedRaw.url) ?? "", "hosted.url");
|
|
125
|
+
const hostedMcpUrl = normalizeOriginUrl(stringValue(hostedRaw.mcpUrl) ?? withMcpPath(hostedUrl), "hosted.mcpUrl");
|
|
126
|
+
const mcpRaw = isRecord(raw.mcp) ? raw.mcp : {};
|
|
127
|
+
const localRaw = isRecord(raw.local) ? raw.local : undefined;
|
|
128
|
+
const commandsRaw = isRecord(localRaw?.commands)
|
|
129
|
+
? localRaw.commands
|
|
130
|
+
: undefined;
|
|
131
|
+
const surfacesRaw = Array.isArray(raw.surfaces) ? raw.surfaces : [];
|
|
132
|
+
const skillsRaw = Array.isArray(raw.skills) ? raw.skills : [];
|
|
133
|
+
const adaptersRaw = Array.isArray(raw.hostAdapters) ? raw.hostAdapters : [];
|
|
134
|
+
const authRaw = isRecord(raw.auth) ? raw.auth : undefined;
|
|
135
|
+
const adapters = uniqueAdapters(adaptersRaw
|
|
136
|
+
.map(normalizeHostAdapter)
|
|
137
|
+
.filter((value) => Boolean(value)));
|
|
138
|
+
return {
|
|
139
|
+
schemaVersion: 1,
|
|
140
|
+
id,
|
|
141
|
+
displayName: stringValue(raw.displayName) ?? id,
|
|
142
|
+
description: stringValue(raw.description) ??
|
|
143
|
+
`Agent-native app-backed skill for ${id}.`,
|
|
144
|
+
hosted: {
|
|
145
|
+
url: hostedUrl,
|
|
146
|
+
mcpUrl: hostedMcpUrl,
|
|
147
|
+
},
|
|
148
|
+
mcp: {
|
|
149
|
+
serverName: stringValue(mcpRaw.serverName) ?? `agent-native-${id}`,
|
|
150
|
+
},
|
|
151
|
+
...(localRaw
|
|
152
|
+
? {
|
|
153
|
+
local: {
|
|
154
|
+
...(stringValue(localRaw.template)
|
|
155
|
+
? { template: stringValue(localRaw.template) }
|
|
156
|
+
: {}),
|
|
157
|
+
...(stringValue(localRaw.sourcePath)
|
|
158
|
+
? { sourcePath: stringValue(localRaw.sourcePath) }
|
|
159
|
+
: {}),
|
|
160
|
+
...(stringValue(localRaw.defaultUrl)
|
|
161
|
+
? {
|
|
162
|
+
defaultUrl: normalizeOriginUrl(stringValue(localRaw.defaultUrl), "local.defaultUrl"),
|
|
163
|
+
}
|
|
164
|
+
: {}),
|
|
165
|
+
commands: {
|
|
166
|
+
install: stringValue(commandsRaw?.install) ?? "pnpm install",
|
|
167
|
+
dev: stringValue(commandsRaw?.dev) ?? "pnpm dev",
|
|
168
|
+
start: stringValue(commandsRaw?.start) ?? "pnpm start",
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
: {}),
|
|
173
|
+
...(authRaw
|
|
174
|
+
? {
|
|
175
|
+
auth: {
|
|
176
|
+
...(authRaw.mode === "oauth" ||
|
|
177
|
+
authRaw.mode === "device" ||
|
|
178
|
+
authRaw.mode === "none"
|
|
179
|
+
? { mode: authRaw.mode }
|
|
180
|
+
: {}),
|
|
181
|
+
...(stringValue(authRaw.setup)
|
|
182
|
+
? { setup: stringValue(authRaw.setup) }
|
|
183
|
+
: {}),
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
: {}),
|
|
187
|
+
surfaces: surfacesRaw.filter(isRecord).map((surface) => ({
|
|
188
|
+
id: stringValue(surface.id) ?? "app",
|
|
189
|
+
...(stringValue(surface.action)
|
|
190
|
+
? { action: stringValue(surface.action) }
|
|
191
|
+
: {}),
|
|
192
|
+
path: stringValue(surface.path) ?? "/",
|
|
193
|
+
mediaTypes: stringArray(surface.mediaTypes),
|
|
194
|
+
...(stringValue(surface.defaultMediaType)
|
|
195
|
+
? { defaultMediaType: stringValue(surface.defaultMediaType) }
|
|
196
|
+
: {}),
|
|
197
|
+
})),
|
|
198
|
+
skills: skillsRaw
|
|
199
|
+
.filter(isRecord)
|
|
200
|
+
.map((skill) => ({
|
|
201
|
+
path: stringValue(skill.path) ?? "",
|
|
202
|
+
visibility: normalizeSkillVisibility(skill.visibility),
|
|
203
|
+
...(stringValue(skill.exportAs)
|
|
204
|
+
? { exportAs: stringValue(skill.exportAs) }
|
|
205
|
+
: {}),
|
|
206
|
+
}))
|
|
207
|
+
.filter((skill) => skill.path),
|
|
208
|
+
hostAdapters: adapters.length ? adapters : defaultHostAdapters(),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
export function findAppSkillManifest(startDir = process.cwd()) {
|
|
212
|
+
let current = path.resolve(startDir);
|
|
213
|
+
while (true) {
|
|
214
|
+
const candidate = path.join(current, "agent-native.app-skill.json");
|
|
215
|
+
if (fs.existsSync(candidate))
|
|
216
|
+
return candidate;
|
|
217
|
+
const parent = path.dirname(current);
|
|
218
|
+
if (parent === current)
|
|
219
|
+
break;
|
|
220
|
+
current = parent;
|
|
221
|
+
}
|
|
222
|
+
throw new Error("Could not find agent-native.app-skill.json. Pass --manifest <file>.");
|
|
223
|
+
}
|
|
224
|
+
export function loadAppSkillManifest(file) {
|
|
225
|
+
const resolved = path.resolve(file ?? findAppSkillManifest());
|
|
226
|
+
const raw = JSON.parse(fs.readFileSync(resolved, "utf-8"));
|
|
227
|
+
return {
|
|
228
|
+
manifest: normalizeAppSkillManifest(raw),
|
|
229
|
+
file: resolved,
|
|
230
|
+
dir: path.dirname(resolved),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
export function exportedSkills(manifest) {
|
|
234
|
+
return manifest.skills.filter((skill) => skill.visibility === "exported" || skill.visibility === "both");
|
|
235
|
+
}
|
|
236
|
+
export function internalSkills(manifest) {
|
|
237
|
+
return manifest.skills.filter((skill) => skill.visibility === "internal" || skill.visibility === "both");
|
|
238
|
+
}
|
|
239
|
+
export function parseAppSkillArgs(argv) {
|
|
240
|
+
const first = argv[0];
|
|
241
|
+
let command = "ensure";
|
|
242
|
+
let args = argv;
|
|
243
|
+
if (first === "help" || first === "--help" || first === "-h") {
|
|
244
|
+
command = "help";
|
|
245
|
+
args = argv.slice(1);
|
|
246
|
+
}
|
|
247
|
+
else if (APP_SKILL_COMMANDS.has(first ?? "")) {
|
|
248
|
+
command = first;
|
|
249
|
+
args = argv.slice(1);
|
|
250
|
+
}
|
|
251
|
+
else if (first && !first.startsWith("-") && !looksLikeManifestArg(first)) {
|
|
252
|
+
throw new Error(`Unknown app-skill command: ${first}`);
|
|
253
|
+
}
|
|
254
|
+
const out = {
|
|
255
|
+
command,
|
|
256
|
+
client: "all",
|
|
257
|
+
scope: "user",
|
|
258
|
+
mode: "hosted",
|
|
259
|
+
dryRun: false,
|
|
260
|
+
skipInstall: false,
|
|
261
|
+
noRegister: false,
|
|
262
|
+
printJson: false,
|
|
263
|
+
yes: false,
|
|
264
|
+
};
|
|
265
|
+
for (let i = 0; i < args.length; i++) {
|
|
266
|
+
const arg = args[i];
|
|
267
|
+
const eat = (flag) => {
|
|
268
|
+
if (arg === flag) {
|
|
269
|
+
const next = args[++i];
|
|
270
|
+
if (!next || next.startsWith("-")) {
|
|
271
|
+
throw new Error(`Missing value for ${flag}.`);
|
|
272
|
+
}
|
|
273
|
+
return next;
|
|
274
|
+
}
|
|
275
|
+
if (arg.startsWith(`${flag}=`)) {
|
|
276
|
+
const value = arg.slice(flag.length + 1);
|
|
277
|
+
if (!value)
|
|
278
|
+
throw new Error(`Missing value for ${flag}.`);
|
|
279
|
+
return value;
|
|
280
|
+
}
|
|
281
|
+
return undefined;
|
|
282
|
+
};
|
|
283
|
+
let value;
|
|
284
|
+
if ((value = eat("--manifest")) !== undefined)
|
|
285
|
+
out.manifest = value;
|
|
286
|
+
else if ((value = eat("--client")) !== undefined)
|
|
287
|
+
out.client = value;
|
|
288
|
+
else if ((value = eat("--scope")) !== undefined)
|
|
289
|
+
out.scope = value;
|
|
290
|
+
else if ((value = eat("--name")) !== undefined)
|
|
291
|
+
out.serverName = value;
|
|
292
|
+
else if ((value = eat("--server-name")) !== undefined)
|
|
293
|
+
out.serverName = value;
|
|
294
|
+
else if ((value = eat("--out")) !== undefined)
|
|
295
|
+
out.out = value;
|
|
296
|
+
else if ((value = eat("--into")) !== undefined)
|
|
297
|
+
out.into = value;
|
|
298
|
+
else if (arg === "--local")
|
|
299
|
+
out.mode = "local";
|
|
300
|
+
else if (arg === "--hosted")
|
|
301
|
+
out.mode = "hosted";
|
|
302
|
+
else if (arg === "--dry-run")
|
|
303
|
+
out.dryRun = true;
|
|
304
|
+
else if (arg === "--skip-install" || arg === "--no-install")
|
|
305
|
+
out.skipInstall = true;
|
|
306
|
+
else if (arg === "--no-register")
|
|
307
|
+
out.noRegister = true;
|
|
308
|
+
else if (arg === "--json")
|
|
309
|
+
out.printJson = true;
|
|
310
|
+
else if (arg === "--yes" || arg === "-y")
|
|
311
|
+
out.yes = true;
|
|
312
|
+
else if (arg.startsWith("-"))
|
|
313
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
314
|
+
else if (!out.manifest)
|
|
315
|
+
out.manifest = arg;
|
|
316
|
+
else
|
|
317
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
318
|
+
}
|
|
319
|
+
if (!APP_SKILL_SCOPES.has(out.scope)) {
|
|
320
|
+
throw new Error("--scope must be either user or project.");
|
|
321
|
+
}
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
function looksLikeManifestArg(value) {
|
|
325
|
+
return (value.endsWith(".json") ||
|
|
326
|
+
value.includes("/") ||
|
|
327
|
+
value.includes("\\") ||
|
|
328
|
+
fs.existsSync(value));
|
|
329
|
+
}
|
|
330
|
+
function assertPathInside(root, target, field) {
|
|
331
|
+
const resolvedRoot = path.resolve(root);
|
|
332
|
+
const resolvedTarget = path.resolve(target);
|
|
333
|
+
const relative = path.relative(resolvedRoot, resolvedTarget);
|
|
334
|
+
if (relative && (relative.startsWith("..") || path.isAbsolute(relative))) {
|
|
335
|
+
throw new Error(`${field} must resolve inside ${resolvedRoot}.`);
|
|
336
|
+
}
|
|
337
|
+
return resolvedTarget;
|
|
338
|
+
}
|
|
339
|
+
function resolveSkillSource(manifestDir, skill) {
|
|
340
|
+
if (path.isAbsolute(skill.path)) {
|
|
341
|
+
throw new Error(`Skill path must be relative: ${skill.path}`);
|
|
342
|
+
}
|
|
343
|
+
return assertPathInside(manifestDir, path.resolve(manifestDir, skill.path), `Skill path ${skill.path}`);
|
|
344
|
+
}
|
|
345
|
+
function skillExportName(skill) {
|
|
346
|
+
if (skill.exportAs) {
|
|
347
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(skill.exportAs)) {
|
|
348
|
+
throw new Error(`Invalid skill export name: ${skill.exportAs}`);
|
|
349
|
+
}
|
|
350
|
+
return skill.exportAs;
|
|
351
|
+
}
|
|
352
|
+
const normalized = skill.path.replace(/\/+$/, "");
|
|
353
|
+
return path.basename(normalized);
|
|
354
|
+
}
|
|
355
|
+
function copyDirFiltered(source, dest) {
|
|
356
|
+
const sourceRoot = path.resolve(source);
|
|
357
|
+
const destRoot = path.resolve(dest);
|
|
358
|
+
const relativeDest = path.relative(sourceRoot, destRoot);
|
|
359
|
+
const outputRootInsideSource = relativeDest &&
|
|
360
|
+
!relativeDest.startsWith("..") &&
|
|
361
|
+
!path.isAbsolute(relativeDest)
|
|
362
|
+
? path.join(sourceRoot, relativeDest.split(path.sep)[0])
|
|
363
|
+
: destRoot;
|
|
364
|
+
fs.cpSync(source, dest, {
|
|
365
|
+
recursive: true,
|
|
366
|
+
filter: (src) => {
|
|
367
|
+
const resolved = path.resolve(src);
|
|
368
|
+
if (resolved === outputRootInsideSource ||
|
|
369
|
+
resolved.startsWith(`${outputRootInsideSource}${path.sep}`)) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
const base = path.basename(src);
|
|
373
|
+
const lower = base.toLowerCase();
|
|
374
|
+
return (base !== "node_modules" &&
|
|
375
|
+
base !== ".git" &&
|
|
376
|
+
base !== "dist" &&
|
|
377
|
+
base !== ".output" &&
|
|
378
|
+
base !== ".react-router" &&
|
|
379
|
+
base !== ".netlify" &&
|
|
380
|
+
base !== "secrets" &&
|
|
381
|
+
base !== "private" &&
|
|
382
|
+
!base.startsWith(".env") &&
|
|
383
|
+
base !== ".dev.vars" &&
|
|
384
|
+
base !== ".npmrc" &&
|
|
385
|
+
!lower.endsWith(".db") &&
|
|
386
|
+
!lower.endsWith(".sqlite") &&
|
|
387
|
+
!lower.endsWith(".sqlite3") &&
|
|
388
|
+
!lower.endsWith(".pem") &&
|
|
389
|
+
!lower.endsWith(".key") &&
|
|
390
|
+
!lower.endsWith(".crt") &&
|
|
391
|
+
!lower.endsWith(".p12"));
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
function copySkill(manifestDir, skill, destRoot) {
|
|
396
|
+
const source = resolveSkillSource(manifestDir, skill);
|
|
397
|
+
const exportName = skillExportName(skill);
|
|
398
|
+
const dest = path.join(destRoot, exportName);
|
|
399
|
+
if (!fs.existsSync(source)) {
|
|
400
|
+
throw new Error(`Exported skill source not found: ${source}`);
|
|
401
|
+
}
|
|
402
|
+
fs.mkdirSync(destRoot, { recursive: true });
|
|
403
|
+
const stat = fs.statSync(source);
|
|
404
|
+
if (stat.isDirectory()) {
|
|
405
|
+
copyDirFiltered(source, dest);
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
409
|
+
fs.copyFileSync(source, path.join(dest, "SKILL.md"));
|
|
410
|
+
}
|
|
411
|
+
rewriteSkillFrontmatterName(path.join(dest, "SKILL.md"), exportName);
|
|
412
|
+
return dest;
|
|
413
|
+
}
|
|
414
|
+
function rewriteSkillFrontmatterName(file, name) {
|
|
415
|
+
if (!fs.existsSync(file))
|
|
416
|
+
return;
|
|
417
|
+
const source = fs.readFileSync(file, "utf-8");
|
|
418
|
+
const lines = source.split("\n");
|
|
419
|
+
if (lines[0]?.trim() !== "---")
|
|
420
|
+
return;
|
|
421
|
+
const end = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
|
|
422
|
+
if (end <= 0)
|
|
423
|
+
return;
|
|
424
|
+
const nameIndex = lines.findIndex((line, index) => index > 0 && index < end && /^name\s*:/.test(line));
|
|
425
|
+
if (nameIndex === -1) {
|
|
426
|
+
lines.splice(1, 0, `name: ${name}`);
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
lines[nameIndex] = `name: ${name}`;
|
|
430
|
+
}
|
|
431
|
+
fs.writeFileSync(file, lines.join("\n"), "utf-8");
|
|
432
|
+
}
|
|
433
|
+
function writeJson(file, value) {
|
|
434
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
435
|
+
fs.writeFileSync(file, JSON.stringify(value, null, 2) + "\n", "utf-8");
|
|
436
|
+
}
|
|
437
|
+
function mcpServerConfig(manifest, serverName) {
|
|
438
|
+
return {
|
|
439
|
+
mcpServers: {
|
|
440
|
+
[serverName ?? manifest.mcp.serverName]: buildHttpMcpEntry(manifest.hosted.mcpUrl),
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function pluginName(manifest) {
|
|
445
|
+
return `agent-native-${manifest.id}`;
|
|
446
|
+
}
|
|
447
|
+
function claudeMarketplaceName() {
|
|
448
|
+
return "agent-native-apps";
|
|
449
|
+
}
|
|
450
|
+
function writeCodexPluginAdapter(manifest, outDir) {
|
|
451
|
+
const name = pluginName(manifest);
|
|
452
|
+
writeJson(path.join(outDir, ".codex-plugin", "plugin.json"), {
|
|
453
|
+
name,
|
|
454
|
+
version: "0.1.0",
|
|
455
|
+
description: manifest.description,
|
|
456
|
+
author: {
|
|
457
|
+
name: "Agent-Native",
|
|
458
|
+
url: "https://agent-native.com",
|
|
459
|
+
},
|
|
460
|
+
homepage: manifest.hosted.url,
|
|
461
|
+
license: "MIT",
|
|
462
|
+
keywords: [
|
|
463
|
+
"agent-native",
|
|
464
|
+
manifest.id,
|
|
465
|
+
"mcp",
|
|
466
|
+
"skills",
|
|
467
|
+
"app-backed-skill",
|
|
468
|
+
],
|
|
469
|
+
skills: "./skills/",
|
|
470
|
+
mcpServers: "./.mcp.json",
|
|
471
|
+
interface: {
|
|
472
|
+
displayName: manifest.displayName,
|
|
473
|
+
shortDescription: manifest.description,
|
|
474
|
+
longDescription: `${manifest.displayName} packages agent instructions, app actions, ` +
|
|
475
|
+
"an MCP connector, and inline UI surfaces as an installable skill.",
|
|
476
|
+
developerName: "Agent-Native",
|
|
477
|
+
category: "Productivity",
|
|
478
|
+
capabilities: ["Interactive", "Read", "Write"],
|
|
479
|
+
websiteURL: manifest.hosted.url,
|
|
480
|
+
defaultPrompt: [
|
|
481
|
+
`Open ${manifest.displayName} where useful`,
|
|
482
|
+
`Use ${manifest.displayName} for app-backed workflows`,
|
|
483
|
+
`Search ${manifest.displayName} and return usable context`,
|
|
484
|
+
],
|
|
485
|
+
brandColor: "#2563EB",
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
writeJson(path.join(outDir, ".mcp.json"), mcpServerConfig(manifest));
|
|
489
|
+
}
|
|
490
|
+
function writeClaudeMarketplaceAdapter(manifest, manifestDir, skills, outDir) {
|
|
491
|
+
const adapterDir = path.join(outDir, "adapters", "claude-marketplace");
|
|
492
|
+
const name = pluginName(manifest);
|
|
493
|
+
const marketplaceName = claudeMarketplaceName();
|
|
494
|
+
const pluginDir = path.join(adapterDir, "plugins", name);
|
|
495
|
+
const skillRoot = path.join(pluginDir, "skills");
|
|
496
|
+
for (const skill of skills)
|
|
497
|
+
copySkill(manifestDir, skill, skillRoot);
|
|
498
|
+
writeJson(path.join(adapterDir, ".claude-plugin", "marketplace.json"), {
|
|
499
|
+
name: marketplaceName,
|
|
500
|
+
description: "Agent-Native app-backed skills that bundle instructions, MCP connectors, and UI surfaces.",
|
|
501
|
+
owner: {
|
|
502
|
+
name: "Agent-Native",
|
|
503
|
+
},
|
|
504
|
+
plugins: [
|
|
505
|
+
{
|
|
506
|
+
name,
|
|
507
|
+
displayName: manifest.displayName,
|
|
508
|
+
description: manifest.description,
|
|
509
|
+
source: `./plugins/${name}`,
|
|
510
|
+
homepage: manifest.hosted.url,
|
|
511
|
+
keywords: [
|
|
512
|
+
"agent-native",
|
|
513
|
+
manifest.id,
|
|
514
|
+
"mcp",
|
|
515
|
+
"skills",
|
|
516
|
+
"app-backed-skill",
|
|
517
|
+
],
|
|
518
|
+
},
|
|
519
|
+
],
|
|
520
|
+
});
|
|
521
|
+
writeJson(path.join(pluginDir, ".claude-plugin", "plugin.json"), {
|
|
522
|
+
name,
|
|
523
|
+
displayName: manifest.displayName,
|
|
524
|
+
description: manifest.description,
|
|
525
|
+
author: {
|
|
526
|
+
name: "Agent-Native",
|
|
527
|
+
url: "https://agent-native.com",
|
|
528
|
+
},
|
|
529
|
+
homepage: manifest.hosted.url,
|
|
530
|
+
repository: "https://github.com/BuilderIO/agent-native",
|
|
531
|
+
license: "MIT",
|
|
532
|
+
keywords: [
|
|
533
|
+
"agent-native",
|
|
534
|
+
manifest.id,
|
|
535
|
+
"mcp",
|
|
536
|
+
"skills",
|
|
537
|
+
"app-backed-skill",
|
|
538
|
+
],
|
|
539
|
+
skills: "./skills/",
|
|
540
|
+
mcpServers: "./.mcp.json",
|
|
541
|
+
});
|
|
542
|
+
writeJson(path.join(pluginDir, ".mcp.json"), mcpServerConfig(manifest));
|
|
543
|
+
fs.writeFileSync(path.join(adapterDir, "README.md"), [
|
|
544
|
+
`# ${manifest.displayName} Claude Code Marketplace`,
|
|
545
|
+
"",
|
|
546
|
+
"Install this local marketplace from Claude Code:",
|
|
547
|
+
"",
|
|
548
|
+
"```text",
|
|
549
|
+
`/plugin marketplace add ./dist/${manifest.id}-skill/adapters/claude-marketplace`,
|
|
550
|
+
`/plugin install ${name}@${marketplaceName}`,
|
|
551
|
+
"/reload-plugins",
|
|
552
|
+
"/mcp",
|
|
553
|
+
"```",
|
|
554
|
+
"",
|
|
555
|
+
"Authenticate the MCP connector from `/mcp` after installation. The plugin ships a URL-only MCP config, so no shared secrets are stored in the marketplace package.",
|
|
556
|
+
"",
|
|
557
|
+
"For a published marketplace repo, point `/plugin marketplace add` at the repository URL instead of the local folder.",
|
|
558
|
+
"",
|
|
559
|
+
].join("\n"), "utf-8");
|
|
560
|
+
}
|
|
561
|
+
function writeMcpAdapter(manifest, outDir, adapter) {
|
|
562
|
+
const dir = path.join(outDir, "adapters", adapter);
|
|
563
|
+
if (adapter === "generic-mcp") {
|
|
564
|
+
writeJson(path.join(dir, "mcp.json"), mcpServerConfig(manifest));
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
writeJson(path.join(dir, "connector.json"), {
|
|
568
|
+
name: manifest.mcp.serverName,
|
|
569
|
+
title: manifest.displayName,
|
|
570
|
+
url: manifest.hosted.mcpUrl,
|
|
571
|
+
auth: manifest.auth?.mode ?? "oauth",
|
|
572
|
+
surfaces: manifest.surfaces,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
function copySkillAdapter(manifestDir, skills, outDir, adapter) {
|
|
576
|
+
const skillRoot = path.join(outDir, "adapters", adapter, "skills");
|
|
577
|
+
for (const skill of skills)
|
|
578
|
+
copySkill(manifestDir, skill, skillRoot);
|
|
579
|
+
}
|
|
580
|
+
function writeVercelSkillsAdapter(manifest, manifestDir, skills, outDir) {
|
|
581
|
+
const adapterDir = path.join(outDir, "adapters", "vercel-skills");
|
|
582
|
+
copySkillAdapter(manifestDir, skills, outDir, "vercel-skills");
|
|
583
|
+
fs.writeFileSync(path.join(adapterDir, "README.md"), [
|
|
584
|
+
`# ${manifest.displayName} Skills`,
|
|
585
|
+
"",
|
|
586
|
+
"Install with the open skills CLI:",
|
|
587
|
+
"",
|
|
588
|
+
"```bash",
|
|
589
|
+
`npx skills add . --skill ${skills.map(skillExportName).join(" --skill ")} -a codex`,
|
|
590
|
+
"```",
|
|
591
|
+
"",
|
|
592
|
+
"Then register the app-backed MCP connector:",
|
|
593
|
+
"",
|
|
594
|
+
"```bash",
|
|
595
|
+
`npx @agent-native/core@latest app-skill ensure --manifest agent-native.app-skill.json --yes`,
|
|
596
|
+
"```",
|
|
597
|
+
"",
|
|
598
|
+
"The skill files contain instructions only. OAuth or device setup happens in the MCP host; secrets are not stored in skills.",
|
|
599
|
+
"",
|
|
600
|
+
].join("\n"), "utf-8");
|
|
601
|
+
writeJson(path.join(adapterDir, "agent-native.app-skill.json"), manifest);
|
|
602
|
+
}
|
|
603
|
+
export function buildAppSkillPack(loaded, outDir) {
|
|
604
|
+
const manifest = loaded.manifest;
|
|
605
|
+
const target = path.resolve(outDir);
|
|
606
|
+
const skills = exportedSkills(manifest);
|
|
607
|
+
if (skills.length === 0) {
|
|
608
|
+
throw new Error("Manifest has no exported or both-visibility skills.");
|
|
609
|
+
}
|
|
610
|
+
fs.mkdirSync(target, { recursive: true });
|
|
611
|
+
const appSource = resolveLocalSourceDir(loaded);
|
|
612
|
+
const packedManifest = appSource
|
|
613
|
+
? {
|
|
614
|
+
...manifest,
|
|
615
|
+
local: {
|
|
616
|
+
...(manifest.local ?? {}),
|
|
617
|
+
sourcePath: "./app",
|
|
618
|
+
commands: manifest.local?.commands ?? {
|
|
619
|
+
install: "pnpm install",
|
|
620
|
+
dev: "pnpm dev",
|
|
621
|
+
start: "pnpm start",
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
}
|
|
625
|
+
: manifest;
|
|
626
|
+
writeJson(path.join(target, "agent-native.app-skill.json"), packedManifest);
|
|
627
|
+
if (appSource) {
|
|
628
|
+
copyDirFiltered(appSource, path.join(target, "app"));
|
|
629
|
+
}
|
|
630
|
+
const exportedSkillNames = skills.map(skillExportName);
|
|
631
|
+
for (const skill of skills)
|
|
632
|
+
copySkill(loaded.dir, skill, path.join(target, "skills"));
|
|
633
|
+
const files = [
|
|
634
|
+
path.join(target, "agent-native.app-skill.json"),
|
|
635
|
+
...(appSource ? [path.join(target, "app")] : []),
|
|
636
|
+
...exportedSkillNames.map((name) => path.join(target, "skills", name, "SKILL.md")),
|
|
637
|
+
];
|
|
638
|
+
if (manifest.hostAdapters.includes("codex-plugin")) {
|
|
639
|
+
writeCodexPluginAdapter(manifest, target);
|
|
640
|
+
files.push(path.join(target, ".codex-plugin", "plugin.json"), path.join(target, ".mcp.json"));
|
|
641
|
+
}
|
|
642
|
+
if (manifest.hostAdapters.includes("claude-marketplace")) {
|
|
643
|
+
writeClaudeMarketplaceAdapter(manifest, loaded.dir, skills, target);
|
|
644
|
+
files.push(path.join(target, "adapters", "claude-marketplace", ".claude-plugin", "marketplace.json"), path.join(target, "adapters", "claude-marketplace", "plugins", pluginName(manifest), ".claude-plugin", "plugin.json"), path.join(target, "adapters", "claude-marketplace", "plugins", pluginName(manifest), ".mcp.json"), path.join(target, "adapters", "claude-marketplace", "plugins", pluginName(manifest), "skills"), path.join(target, "adapters", "claude-marketplace", "README.md"));
|
|
645
|
+
}
|
|
646
|
+
if (manifest.hostAdapters.includes("vercel-skills")) {
|
|
647
|
+
writeVercelSkillsAdapter(manifest, loaded.dir, skills, target);
|
|
648
|
+
files.push(path.join(target, "adapters", "vercel-skills", "skills"), path.join(target, "adapters", "vercel-skills", "README.md"), path.join(target, "adapters", "vercel-skills", "agent-native.app-skill.json"));
|
|
649
|
+
}
|
|
650
|
+
if (manifest.hostAdapters.includes("plain-skill")) {
|
|
651
|
+
copySkillAdapter(loaded.dir, skills, target, "plain-skill");
|
|
652
|
+
files.push(path.join(target, "adapters", "plain-skill", "skills"));
|
|
653
|
+
}
|
|
654
|
+
if (manifest.hostAdapters.includes("claude-skill")) {
|
|
655
|
+
copySkillAdapter(loaded.dir, skills, target, "claude-skill");
|
|
656
|
+
files.push(path.join(target, "adapters", "claude-skill", "skills"));
|
|
657
|
+
}
|
|
658
|
+
if (manifest.hostAdapters.includes("generic-mcp")) {
|
|
659
|
+
writeMcpAdapter(manifest, target, "generic-mcp");
|
|
660
|
+
files.push(path.join(target, "adapters", "generic-mcp", "mcp.json"));
|
|
661
|
+
}
|
|
662
|
+
if (manifest.hostAdapters.includes("chatgpt-mcp")) {
|
|
663
|
+
writeMcpAdapter(manifest, target, "chatgpt-mcp");
|
|
664
|
+
files.push(path.join(target, "adapters", "chatgpt-mcp", "connector.json"));
|
|
665
|
+
}
|
|
666
|
+
return { outDir: target, exportedSkillNames, files };
|
|
667
|
+
}
|
|
668
|
+
function appSkillCacheDir(appId) {
|
|
669
|
+
return path.join(os.homedir(), ".agent-native", "app-skills", appId, "app");
|
|
670
|
+
}
|
|
671
|
+
function resolveLocalSourceDir(loaded) {
|
|
672
|
+
const local = loaded.manifest.local;
|
|
673
|
+
if (local?.sourcePath) {
|
|
674
|
+
if (path.isAbsolute(local.sourcePath)) {
|
|
675
|
+
throw new Error("local.sourcePath must be relative to the manifest.");
|
|
676
|
+
}
|
|
677
|
+
const source = assertPathInside(loaded.dir, path.resolve(loaded.dir, local.sourcePath), "local.sourcePath");
|
|
678
|
+
if (fs.existsSync(source))
|
|
679
|
+
return source;
|
|
680
|
+
throw new Error(`local.sourcePath "${local.sourcePath}" does not exist. ` +
|
|
681
|
+
"Run pack from the directory containing the app source.");
|
|
682
|
+
}
|
|
683
|
+
if (local?.template) {
|
|
684
|
+
if (!/^[a-z0-9][a-z0-9-]*$/i.test(local.template)) {
|
|
685
|
+
throw new Error("local.template must be a direct template name.");
|
|
686
|
+
}
|
|
687
|
+
const repoTemplate = path.resolve(process.cwd(), "templates", local.template);
|
|
688
|
+
if (fs.existsSync(repoTemplate))
|
|
689
|
+
return repoTemplate;
|
|
690
|
+
}
|
|
691
|
+
return undefined;
|
|
692
|
+
}
|
|
693
|
+
export function resolveLaunchPlan(loaded, options = {}) {
|
|
694
|
+
const manifest = loaded.manifest;
|
|
695
|
+
const mode = options.mode ?? "hosted";
|
|
696
|
+
const serverName = options.serverName ?? manifest.mcp.serverName;
|
|
697
|
+
if (mode === "hosted") {
|
|
698
|
+
return {
|
|
699
|
+
mode,
|
|
700
|
+
appId: manifest.id,
|
|
701
|
+
url: manifest.hosted.url,
|
|
702
|
+
mcpUrl: manifest.hosted.mcpUrl,
|
|
703
|
+
serverName,
|
|
704
|
+
commands: {},
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
const defaultUrl = manifest.local?.defaultUrl ?? "http://127.0.0.1:8100";
|
|
708
|
+
const appDir = path.resolve(options.into ?? appSkillCacheDir(manifest.id));
|
|
709
|
+
return {
|
|
710
|
+
mode,
|
|
711
|
+
appId: manifest.id,
|
|
712
|
+
appDir,
|
|
713
|
+
sourceDir: resolveLocalSourceDir(loaded),
|
|
714
|
+
url: defaultUrl,
|
|
715
|
+
mcpUrl: withMcpPath(defaultUrl),
|
|
716
|
+
serverName,
|
|
717
|
+
commands: manifest.local?.commands ?? {
|
|
718
|
+
install: "pnpm install",
|
|
719
|
+
dev: "pnpm dev",
|
|
720
|
+
start: "pnpm start",
|
|
721
|
+
},
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
function printPlan(plan, log) {
|
|
725
|
+
log(` App: ${plan.appId}`);
|
|
726
|
+
log(` Mode: ${plan.mode}`);
|
|
727
|
+
log(` URL: ${plan.url}`);
|
|
728
|
+
log(` MCP URL: ${plan.mcpUrl}`);
|
|
729
|
+
log(` Server: ${plan.serverName}`);
|
|
730
|
+
if (plan.appDir)
|
|
731
|
+
log(` App dir: ${plan.appDir}`);
|
|
732
|
+
if (plan.sourceDir)
|
|
733
|
+
log(` Source: ${plan.sourceDir}`);
|
|
734
|
+
if (plan.commands.install)
|
|
735
|
+
log(` Install: ${plan.commands.install}`);
|
|
736
|
+
if (plan.commands.dev)
|
|
737
|
+
log(` Dev: ${plan.commands.dev}`);
|
|
738
|
+
}
|
|
739
|
+
function safeChildEnv() {
|
|
740
|
+
const env = {};
|
|
741
|
+
for (const key of SAFE_ENV_KEYS) {
|
|
742
|
+
const value = process.env[key];
|
|
743
|
+
if (value !== undefined)
|
|
744
|
+
env[key] = value;
|
|
745
|
+
}
|
|
746
|
+
return env;
|
|
747
|
+
}
|
|
748
|
+
function parsePackageCommand(command) {
|
|
749
|
+
const trimmed = command.trim();
|
|
750
|
+
if (!trimmed)
|
|
751
|
+
throw new Error("Command cannot be empty.");
|
|
752
|
+
if (/[\n\r;&|<>`$]/.test(trimmed)) {
|
|
753
|
+
throw new Error(`Unsafe shell syntax is not allowed in command: ${command}`);
|
|
754
|
+
}
|
|
755
|
+
const parts = trimmed.split(/\s+/);
|
|
756
|
+
const executable = parts[0];
|
|
757
|
+
if (!SAFE_PACKAGE_EXECUTABLES.has(executable)) {
|
|
758
|
+
throw new Error(`Unsupported app-skill command executable: ${executable}. Use pnpm, npm, bun, or yarn.`);
|
|
759
|
+
}
|
|
760
|
+
return { executable, args: parts.slice(1) };
|
|
761
|
+
}
|
|
762
|
+
function runShell(command, cwd) {
|
|
763
|
+
const { executable, args } = parsePackageCommand(command);
|
|
764
|
+
return new Promise((resolve, reject) => {
|
|
765
|
+
const child = spawn(executable, args, {
|
|
766
|
+
cwd,
|
|
767
|
+
env: safeChildEnv(),
|
|
768
|
+
stdio: "inherit",
|
|
769
|
+
});
|
|
770
|
+
child.on("error", reject);
|
|
771
|
+
child.on("exit", (code, signal) => {
|
|
772
|
+
if (signal) {
|
|
773
|
+
reject(new Error(`Command "${command}" was terminated by ${signal}.`));
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
resolve(code ?? 1);
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
function openUrl(url) {
|
|
781
|
+
const command = process.platform === "darwin"
|
|
782
|
+
? "open"
|
|
783
|
+
: process.platform === "win32"
|
|
784
|
+
? "cmd"
|
|
785
|
+
: "xdg-open";
|
|
786
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
787
|
+
const child = spawn(command, args, {
|
|
788
|
+
detached: true,
|
|
789
|
+
stdio: "ignore",
|
|
790
|
+
shell: process.platform === "win32",
|
|
791
|
+
});
|
|
792
|
+
child.unref();
|
|
793
|
+
}
|
|
794
|
+
export async function ensureAppSkill(loaded, options = {}) {
|
|
795
|
+
const manifest = loaded.manifest;
|
|
796
|
+
const mode = options.mode ?? "hosted";
|
|
797
|
+
const clients = options.clients ?? resolveClients("all");
|
|
798
|
+
const serverName = options.serverName ?? manifest.mcp.serverName;
|
|
799
|
+
const scope = options.scope ?? "user";
|
|
800
|
+
const plan = resolveLaunchPlan(loaded, { mode, serverName });
|
|
801
|
+
const result = {
|
|
802
|
+
mode,
|
|
803
|
+
serverName,
|
|
804
|
+
mcpUrl: plan.mcpUrl,
|
|
805
|
+
clients,
|
|
806
|
+
written: [],
|
|
807
|
+
};
|
|
808
|
+
if (options.dryRun)
|
|
809
|
+
return result;
|
|
810
|
+
if (options.confirm && !options.yes) {
|
|
811
|
+
await confirmMcpRegistration(result, loaded.manifest.displayName, scope);
|
|
812
|
+
}
|
|
813
|
+
result.written = writeConfigs(clients, serverName, plan.mcpUrl, undefined, scope, options.baseDir ?? process.cwd());
|
|
814
|
+
options.log?.(`Registered ${serverName} for ${clients.join(", ")} at ${plan.mcpUrl}`);
|
|
815
|
+
return result;
|
|
816
|
+
}
|
|
817
|
+
async function confirmMcpRegistration(result, displayName, scope) {
|
|
818
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
819
|
+
throw new Error("Refusing to register MCP servers from a manifest without confirmation. Re-run with --yes to approve.");
|
|
820
|
+
}
|
|
821
|
+
const readline = await import("node:readline/promises");
|
|
822
|
+
const rl = readline.createInterface({
|
|
823
|
+
input: process.stdin,
|
|
824
|
+
output: process.stdout,
|
|
825
|
+
});
|
|
826
|
+
try {
|
|
827
|
+
const answer = await rl.question([
|
|
828
|
+
`Register ${displayName} MCP server "${result.serverName}"?`,
|
|
829
|
+
` URL: ${result.mcpUrl}`,
|
|
830
|
+
` Clients: ${result.clients.join(", ")}`,
|
|
831
|
+
` Scope: ${scope}`,
|
|
832
|
+
"Proceed? [y/N] ",
|
|
833
|
+
].join("\n"));
|
|
834
|
+
if (!/^(y|yes)$/i.test(answer.trim())) {
|
|
835
|
+
throw new Error("Cancelled app-skill MCP registration.");
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
finally {
|
|
839
|
+
rl.close();
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
export async function launchAppSkill(loaded, options = {}) {
|
|
843
|
+
const log = options.log ?? ((message) => process.stdout.write(`${message}\n`));
|
|
844
|
+
const plan = resolveLaunchPlan(loaded, options);
|
|
845
|
+
if (options.dryRun) {
|
|
846
|
+
printPlan(plan, log);
|
|
847
|
+
return plan;
|
|
848
|
+
}
|
|
849
|
+
if (plan.mode === "hosted") {
|
|
850
|
+
log(`Opening ${loaded.manifest.displayName}: ${plan.url}`);
|
|
851
|
+
openUrl(plan.url);
|
|
852
|
+
return plan;
|
|
853
|
+
}
|
|
854
|
+
if (!plan.appDir)
|
|
855
|
+
throw new Error("Local launch plan is missing appDir.");
|
|
856
|
+
if (!fs.existsSync(path.join(plan.appDir, "package.json"))) {
|
|
857
|
+
if (!plan.sourceDir) {
|
|
858
|
+
throw new Error("Local launch needs bundled app source. Pass --into <path> from a manifest with local.sourcePath.");
|
|
859
|
+
}
|
|
860
|
+
fs.mkdirSync(path.dirname(plan.appDir), { recursive: true });
|
|
861
|
+
copyDirFiltered(plan.sourceDir, plan.appDir);
|
|
862
|
+
log(`Materialized ${loaded.manifest.displayName} at ${plan.appDir}`);
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
log(`Using existing local app at ${plan.appDir}`);
|
|
866
|
+
}
|
|
867
|
+
if (!options.noRegister) {
|
|
868
|
+
await ensureAppSkill(loaded, {
|
|
869
|
+
mode: "local",
|
|
870
|
+
clients: options.clients,
|
|
871
|
+
scope: options.scope,
|
|
872
|
+
serverName: options.serverName,
|
|
873
|
+
baseDir: options.baseDir,
|
|
874
|
+
confirm: options.confirm,
|
|
875
|
+
yes: options.yes,
|
|
876
|
+
log,
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
if (!options.skipInstall && plan.commands.install) {
|
|
880
|
+
const installCode = await runShell(plan.commands.install, plan.appDir);
|
|
881
|
+
if (installCode !== 0) {
|
|
882
|
+
throw new Error(`Install command failed with exit code ${installCode}.`);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
if (plan.commands.dev) {
|
|
886
|
+
const code = await runShell(plan.commands.dev, plan.appDir);
|
|
887
|
+
if (code !== 0)
|
|
888
|
+
throw new Error(`Dev command exited with code ${code}.`);
|
|
889
|
+
}
|
|
890
|
+
return plan;
|
|
891
|
+
}
|
|
892
|
+
function resolveArgsClients(parsed) {
|
|
893
|
+
return resolveClients(parsed.client);
|
|
894
|
+
}
|
|
895
|
+
export async function runAppSkill(argv) {
|
|
896
|
+
try {
|
|
897
|
+
const parsed = parseAppSkillArgs(argv);
|
|
898
|
+
if (parsed.command === "help") {
|
|
899
|
+
process.stdout.write(`${HELP}\n`);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
const loaded = loadAppSkillManifest(parsed.manifest);
|
|
903
|
+
const log = (message) => (parsed.printJson ? process.stderr : process.stdout).write(`${message}\n`);
|
|
904
|
+
if (parsed.command === "ensure") {
|
|
905
|
+
const result = await ensureAppSkill(loaded, {
|
|
906
|
+
mode: parsed.mode,
|
|
907
|
+
clients: resolveArgsClients(parsed),
|
|
908
|
+
scope: parsed.scope,
|
|
909
|
+
serverName: parsed.serverName,
|
|
910
|
+
dryRun: parsed.dryRun,
|
|
911
|
+
confirm: true,
|
|
912
|
+
yes: parsed.yes,
|
|
913
|
+
log,
|
|
914
|
+
});
|
|
915
|
+
if (parsed.printJson) {
|
|
916
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
917
|
+
}
|
|
918
|
+
else if (parsed.dryRun) {
|
|
919
|
+
process.stdout.write(`Would register ${result.serverName} at ${result.mcpUrl} for ${result.clients.join(", ")}\n`);
|
|
920
|
+
}
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (parsed.command === "launch") {
|
|
924
|
+
const plan = await launchAppSkill(loaded, {
|
|
925
|
+
mode: parsed.mode,
|
|
926
|
+
into: parsed.into,
|
|
927
|
+
dryRun: parsed.dryRun,
|
|
928
|
+
skipInstall: parsed.skipInstall,
|
|
929
|
+
noRegister: parsed.noRegister,
|
|
930
|
+
clients: resolveArgsClients(parsed),
|
|
931
|
+
scope: parsed.scope,
|
|
932
|
+
serverName: parsed.serverName,
|
|
933
|
+
confirm: true,
|
|
934
|
+
yes: parsed.yes,
|
|
935
|
+
log,
|
|
936
|
+
});
|
|
937
|
+
if (parsed.printJson) {
|
|
938
|
+
process.stdout.write(`${JSON.stringify(plan, null, 2)}\n`);
|
|
939
|
+
}
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
if (parsed.command === "pack") {
|
|
943
|
+
if (!parsed.out) {
|
|
944
|
+
throw new Error("Missing --out <dir> for app-skill pack.");
|
|
945
|
+
}
|
|
946
|
+
const result = buildAppSkillPack(loaded, parsed.out);
|
|
947
|
+
if (parsed.printJson) {
|
|
948
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
949
|
+
}
|
|
950
|
+
else {
|
|
951
|
+
process.stdout.write(`Packed ${loaded.manifest.displayName} app skill at ${result.outDir}\n`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
catch (err) {
|
|
956
|
+
process.stderr.write(`${err?.message ?? err}\n`);
|
|
957
|
+
process.exitCode = 1;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
//# sourceMappingURL=app-skill.js.map
|