@assemblyline-agents/cli 5.0.10 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/authoring/skills/assembly-line-authoring/SKILL.md +24 -21
  2. package/dist/add.d.ts +2 -13
  3. package/dist/add.d.ts.map +1 -1
  4. package/dist/add.js +169 -478
  5. package/dist/add.js.map +1 -1
  6. package/dist/args.d.ts.map +1 -1
  7. package/dist/args.js +1 -2
  8. package/dist/args.js.map +1 -1
  9. package/dist/auth.d.ts +1 -2
  10. package/dist/auth.d.ts.map +1 -1
  11. package/dist/auth.js +38 -45
  12. package/dist/auth.js.map +1 -1
  13. package/dist/channels.d.ts +7 -30
  14. package/dist/channels.d.ts.map +1 -1
  15. package/dist/channels.js +21 -161
  16. package/dist/channels.js.map +1 -1
  17. package/dist/deploy-definition.d.ts +1 -8
  18. package/dist/deploy-definition.d.ts.map +1 -1
  19. package/dist/deploy-definition.js +18 -66
  20. package/dist/deploy-definition.js.map +1 -1
  21. package/dist/deploy-migrations.d.ts +15 -0
  22. package/dist/deploy-migrations.d.ts.map +1 -0
  23. package/dist/deploy-migrations.js +72 -0
  24. package/dist/deploy-migrations.js.map +1 -0
  25. package/dist/deploy-preflight.d.ts +6 -0
  26. package/dist/deploy-preflight.d.ts.map +1 -0
  27. package/dist/deploy-preflight.js +40 -0
  28. package/dist/deploy-preflight.js.map +1 -0
  29. package/dist/deploy-receipts.d.ts +9 -0
  30. package/dist/deploy-receipts.d.ts.map +1 -0
  31. package/dist/deploy-receipts.js +45 -0
  32. package/dist/deploy-receipts.js.map +1 -0
  33. package/dist/deploy-sandbox-environments.d.ts +6 -0
  34. package/dist/deploy-sandbox-environments.d.ts.map +1 -0
  35. package/dist/deploy-sandbox-environments.js +109 -0
  36. package/dist/deploy-sandbox-environments.js.map +1 -0
  37. package/dist/deploy-secrets.d.ts +21 -0
  38. package/dist/deploy-secrets.d.ts.map +1 -0
  39. package/dist/deploy-secrets.js +83 -0
  40. package/dist/deploy-secrets.js.map +1 -0
  41. package/dist/deploy.d.ts +5 -31
  42. package/dist/deploy.d.ts.map +1 -1
  43. package/dist/deploy.js +50 -636
  44. package/dist/deploy.js.map +1 -1
  45. package/dist/eval-case-runner.d.ts +1 -1
  46. package/dist/eval-case-runner.d.ts.map +1 -1
  47. package/dist/eval-case-runner.js.map +1 -1
  48. package/dist/help.d.ts.map +1 -1
  49. package/dist/help.js +41 -89
  50. package/dist/help.js.map +1 -1
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +89 -32
  53. package/dist/index.js.map +1 -1
  54. package/dist/init.d.ts +1 -3
  55. package/dist/init.d.ts.map +1 -1
  56. package/dist/init.js +36 -39
  57. package/dist/init.js.map +1 -1
  58. package/dist/inspection.d.ts +44 -0
  59. package/dist/inspection.d.ts.map +1 -0
  60. package/dist/inspection.js +144 -0
  61. package/dist/inspection.js.map +1 -0
  62. package/dist/models.d.ts +2 -1
  63. package/dist/models.d.ts.map +1 -1
  64. package/dist/models.js +7 -13
  65. package/dist/models.js.map +1 -1
  66. package/dist/plugin-add.d.ts +36 -0
  67. package/dist/plugin-add.d.ts.map +1 -0
  68. package/dist/plugin-add.js +283 -0
  69. package/dist/plugin-add.js.map +1 -0
  70. package/dist/plugin-init.d.ts +2 -0
  71. package/dist/plugin-init.d.ts.map +1 -0
  72. package/dist/plugin-init.js +34 -0
  73. package/dist/plugin-init.js.map +1 -0
  74. package/dist/provider-deploy.d.ts +7 -0
  75. package/dist/provider-deploy.d.ts.map +1 -0
  76. package/dist/provider-deploy.js +163 -0
  77. package/dist/provider-deploy.js.map +1 -0
  78. package/dist/secrets.d.ts.map +1 -1
  79. package/dist/secrets.js +8 -28
  80. package/dist/secrets.js.map +1 -1
  81. package/package.json +9 -11
  82. package/dist/hosts.d.ts +0 -3
  83. package/dist/hosts.d.ts.map +0 -1
  84. package/dist/hosts.js +0 -60
  85. package/dist/hosts.js.map +0 -1
  86. package/dist/model-providers.d.ts +0 -5
  87. package/dist/model-providers.d.ts.map +0 -1
  88. package/dist/model-providers.js +0 -17
  89. package/dist/model-providers.js.map +0 -1
  90. package/dist/state.d.ts +0 -3
  91. package/dist/state.d.ts.map +0 -1
  92. package/dist/state.js +0 -79
  93. package/dist/state.js.map +0 -1
package/dist/add.js CHANGED
@@ -1,501 +1,192 @@
1
- import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { createRequire } from "node:module";
3
- import { dirname, join, resolve } from "node:path";
4
- import { fileURLToPath, pathToFileURL } from "node:url";
5
- import { allAdapterProviderMetadata, allToolPackPluginMetadata, connectionPluginMetadata, PLUGIN_MANIFEST_RELATIVE_PATH, pluginModuleFromImport, providerModuleFromImport } from "@assemblyline-agents/core";
6
- import { detectPackageManager } from "./pm.js";
7
- const GATEWAY_ROLES = new Set(["deploy", "runtime", "state", "blob", "sandbox", "scheduler", "media", "secrets"]);
8
- /** Channel scaffold helpers, verified against each channel package's exports. */
9
- const CHANNEL_SCAFFOLDS = {
10
- slack: { helper: "defineSlackChannel", packageName: "@assemblyline-agents/slack" },
11
- discord: { helper: "defineDiscordChannel", packageName: "@assemblyline-agents/discord" },
12
- telegram: { helper: "defineTelegramChannel", packageName: "@assemblyline-agents/telegram" },
13
- teams: { helper: "defineTeamsChannel", packageName: "@assemblyline-agents/teams" }
14
- };
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { isMap, isScalar, isSeq, parseDocument } from "yaml";
4
+ import { appendPluginSelection, runMdPluginAdd } from "./plugin-add.js";
5
+ /** Install, inspect, lock, and select a plugin using only its own declaration. */
15
6
  export async function runAddCommand(options) {
16
- const detection = detectPackageManager(options.agentRoot);
17
- const pm = normalizePm(options.pmOverride) ?? detection.pm;
18
- const installDir = detection.projectDir ?? process.cwd();
19
- let metadata;
20
- if (options.thing.includes("/")) {
21
- // Scoped/path plugin package: install first, then read kind/role from its assemblyLineProvider export.
22
- await installPackage(options, pm, installDir, options.thing);
23
- metadata = await metadataFromPackage(options.thing, options.agentRoot, installDir, options.role);
24
- }
25
- else {
26
- metadata = registryMetadata(options.thing, options.role);
27
- if (metadata.packageName)
28
- await installPackage(options, pm, installDir, metadata.packageName);
29
- }
30
- const outcome = wireDeclaration(metadata, options.agentRoot, installDir);
31
- for (const line of outcome.lines)
32
- console.log(line);
33
- for (const line of copyPluginSkills(metadata, options.agentRoot, installDir))
34
- console.log(line);
35
- printNextSteps(metadata, options.agentRoot, pm);
36
- }
37
- function normalizePm(pmOverride) {
38
- if (pmOverride === undefined)
39
- return undefined;
40
- if (pmOverride === "pnpm" || pmOverride === "yarn" || pmOverride === "npm")
41
- return pmOverride;
42
- throw new Error(`Unknown package manager "${pmOverride}". Use --pm pnpm, yarn, or npm.`);
43
- }
44
- async function installPackage(options, pm, installDir, packageName) {
45
- const installCommand = `${pm} add ${packageName}`;
46
- if (!options.install) {
47
- console.log(`Install (skipped by --no-install): ${installCommand}`);
7
+ if (!existsSync(join(options.agentRoot, "agent.md"))) {
8
+ throw new Error(`MD-first Assembly Line requires agent.md at ${options.agentRoot}.`);
9
+ }
10
+ const added = await runMdPluginAdd({
11
+ thing: options.thing,
12
+ agentRoot: options.agentRoot,
13
+ pmOverride: options.pmOverride,
14
+ install: options.install,
15
+ confirmUpgrade: options.confirmUpgrade === true,
16
+ selectCapability: false,
17
+ runCommand: options.runCommand
18
+ });
19
+ if (!added)
48
20
  return;
21
+ const candidates = declaredSelections(added);
22
+ const requestedRole = normalizeRole(options.role);
23
+ const eligible = requestedRole ? candidates.filter((candidate) => candidate.role === requestedRole) : candidates;
24
+ const selected = chooseSelection(added.name, eligible, requestedRole);
25
+ if (selected.role === "capability") {
26
+ if (added.packageName)
27
+ appendPluginSelection(options.agentRoot, added.name);
28
+ else
29
+ console.log(`Local Agent Plugin ${added.name} is active by presence.`);
30
+ }
31
+ else if (selected.role === "media") {
32
+ console.log(`Locked ${added.name}. Select ${selected.name} under channels.<channel>.audio when overriding a channel default.`);
49
33
  }
50
- console.log(`Installing plugin ${packageName} with ${pm} in ${installDir}.`);
51
- const result = await options.runCommand(pm, ["add", packageName], { cwd: installDir });
52
- if (result.exitCode !== 0) {
53
- throw new Error(`"${installCommand}" failed with exit code ${result.exitCode}.`);
54
- }
55
- }
56
- function registryMetadata(kind, role) {
57
- const all = [...allAdapterProviderMetadata(), ...allToolPackPluginMetadata()];
58
- const entries = all.filter((entry) => entry.kind === kind);
59
- if (entries.length === 0) {
60
- throw new Error([`Unknown plugin or adapter kind "${kind}". Available plugin contributions by role:`, ...kindsByRoleLines(all)].join("\n"));
61
- }
62
- if (role) {
63
- const match = entries.find((entry) => entry.role === role);
64
- if (!match) {
65
- throw new Error(`"${kind}" has no ${role} provider. Roles for "${kind}": ${roleList(entries)}.`);
66
- }
67
- return match;
68
- }
69
- // Prefer installable entries so e.g. `add postgres` means the state package,
70
- // not the package-less state-backed scheduler variant.
71
- const installable = entries.filter((entry) => entry.packageName);
72
- const pool = installable.length > 0 ? installable : entries;
73
- const roles = [...new Set(pool.map((entry) => entry.role))];
74
- if (roles.length > 1) {
75
- // Without --role, `add` defaults to the agent-folder contribution — a
76
- // channel scaffold first, then a connection or tool pack. Gateway slots
77
- // (state, secrets, ...) offered alongside one stay reachable via --role.
78
- for (const preferred of ["channel", "connection", "tools"]) {
79
- const match = pool.find((entry) => entry.role === preferred);
80
- if (match)
81
- return match;
82
- }
83
- }
84
- const first = pool[0];
85
- if (roles.length > 1 || !first) {
86
- throw new Error(`"${kind}" provides multiple roles: ${roles.join(", ")}. Pass --role <role>, e.g. assembly-line add ${kind} --role ${roles[0]}.`);
87
- }
88
- return first;
89
- }
90
- function roleList(entries) {
91
- return [...new Set(entries.map((entry) => entry.role))].sort().join(", ");
92
- }
93
- function kindsByRoleLines(all) {
94
- const byRole = new Map();
95
- for (const entry of all) {
96
- const kinds = byRole.get(entry.role) ?? new Set();
97
- kinds.add(entry.kind);
98
- byRole.set(entry.role, kinds);
99
- }
100
- return [...byRole.entries()]
101
- .sort(([a], [b]) => a.localeCompare(b))
102
- .map(([role, kinds]) => ` ${role}: ${[...kinds].sort().join(", ")}`);
103
- }
104
- async function metadataFromPackage(packageName, agentRoot, installDir, role) {
105
- const imported = await importPackage(packageName, [agentRoot, installDir]);
106
- const pluginModule = pluginModuleFromImport(imported);
107
- const pluginEntries = [
108
- ...(pluginModule?.connections ?? []).map((connection) => ({
109
- ...connection,
110
- packageName: connection.packageName ?? packageName
111
- })),
112
- ...(pluginModule?.toolPacks ?? []).map((toolPack) => ({
113
- ...toolPack,
114
- packageName: toolPack.packageName || packageName
115
- }))
116
- ];
117
- const providerModule = providerModuleFromImport(imported);
118
- if (!providerModule && pluginEntries.length === 0) {
119
- throw new Error(`Plugin package ${packageName} does not export assemblyLinePlugin or assemblyLineProvider, so its contributions cannot be determined.`);
120
- }
121
- const entries = [...pluginEntries, ...(providerModule?.providers ?? []).map((registration) => ({
122
- ...registration.metadata,
123
- packageName: registration.metadata.packageName ?? packageName
124
- }))];
125
- if (role) {
126
- const match = entries.find((entry) => entry.role === role);
127
- if (!match) {
128
- throw new Error(`Package ${packageName} has no ${role} provider. Available: ${entries.map((entry) => `${entry.role}:${entry.kind}`).join(", ")}.`);
129
- }
130
- return match;
131
- }
132
- // Without --role, plugin contributions (connections, tool packs) are what
133
- // `add` scaffolds; provider registrations such as gateway secrets stores
134
- // stay reachable through an explicit --role.
135
- const candidates = pluginEntries.length > 0 ? pluginEntries : entries;
136
- const roles = [...new Set(candidates.map((entry) => entry.role))];
137
- const first = candidates[0];
138
- if (roles.length > 1 || !first) {
139
- throw new Error(`Package ${packageName} provides multiple roles: ${[...new Set(entries.map((entry) => entry.role))].join(", ")}. Pass --role <role>.`);
140
- }
141
- return first;
142
- }
143
- async function importPackage(packageName, startDirs) {
144
- const errors = [];
145
- const specifiers = [packageName];
146
- for (const dir of startDirs) {
147
- try {
148
- const requireFrom = createRequire(pathToFileURL(join(dir, "package.json")).href);
149
- specifiers.push(pathToFileURL(requireFrom.resolve(packageName)).href);
150
- }
151
- catch (error) {
152
- errors.push(error instanceof Error ? error.message : String(error));
153
- }
154
- }
155
- for (const specifier of [...new Set(specifiers)]) {
156
- try {
157
- return await import(specifier);
158
- }
159
- catch (error) {
160
- errors.push(error instanceof Error ? error.message : String(error));
161
- }
162
- }
163
- throw new Error(`Failed to load plugin package ${packageName}: ${errors.join("; ")}. Install it first (or rerun without --no-install).`);
164
- }
165
- function wireDeclaration(metadata, agentRoot, installDir) {
166
- if (metadata.role === "tools")
167
- return wireToolPack(agentRoot, metadata);
168
- if (GATEWAY_ROLES.has(metadata.role))
169
- return wireGatewaySlot(agentRoot, metadata.role, metadata.kind);
170
- if (metadata.role === "channel")
171
- return wireChannel(agentRoot, metadata.kind, metadata.packageName, installDir);
172
- if (metadata.role === "connection") {
173
- const plugin = asConnectionPluginMetadata(metadata) ?? connectionPluginMetadata(metadata.kind);
174
- if (plugin)
175
- return wireConnection(agentRoot, plugin);
176
- }
177
- return {
178
- lines: [
179
- `${metadata.role} providers are declared in ${metadata.role}s/*.ts, so nothing was wired automatically.`,
180
- `Create ${metadata.role}s/${metadata.kind}.ts using the helper exported by ${metadata.packageName ?? "the plugin package"}.`
181
- ]
182
- };
183
- }
184
- function wireToolPack(agentRoot, plugin) {
185
- const lines = [];
186
- const configPath = plugin.config ? safePluginAgentPath(plugin.config.path) : undefined;
187
- if (plugin.config && configPath) {
188
- const target = join(agentRoot, configPath);
189
- if (existsSync(target)) {
190
- lines.push(`${configPath} already exists. Leaving it unchanged.`);
191
- }
192
- else {
193
- mkdirSync(dirname(target), { recursive: true });
194
- writeFileSync(target, [
195
- `import { ${plugin.config.helper} } from ${JSON.stringify(plugin.packageName)};`,
196
- "",
197
- `export default ${plugin.config.helper}(${JSON.stringify(plugin.config.options ?? {}, null, 2)});`,
198
- ""
199
- ].join("\n"));
200
- lines.push(`Created ${configPath} for developer-owned ${plugin.kind} configuration.`);
201
- }
202
- }
203
- mkdirSync(join(agentRoot, "tools"), { recursive: true });
204
- for (const tool of plugin.tools) {
205
- if (!/^[A-Za-z0-9_-]+$/u.test(tool.name)) {
206
- throw new Error(`Tool pack ${plugin.kind} declares invalid tool name "${tool.name}".`);
207
- }
208
- const relativePath = `tools/${tool.name}.ts`;
209
- const target = join(agentRoot, relativePath);
210
- if (existsSync(target)) {
211
- lines.push(`${relativePath} already exists. Leaving it unchanged.`);
34
+ else {
35
+ const changed = updateMarkdownSelection(options.agentRoot, selected);
36
+ console.log(changed
37
+ ? `Updated agent.md ${selected.role} selection to ${selected.value}.`
38
+ : `agent.md already selects ${selected.value} for ${selected.role}.`);
39
+ }
40
+ printRequirements(selected);
41
+ }
42
+ function declaredSelections(plugin) {
43
+ const entry = plugin.entry;
44
+ if (!entry)
45
+ return [{ role: "capability", name: plugin.name, value: plugin.name }];
46
+ const selections = [];
47
+ for (const [name, raw] of Object.entries(entry.sections.providers)) {
48
+ if (!isRecord(raw) || typeof raw.role !== "string")
212
49
  continue;
213
- }
214
- const configImport = configPath
215
- ? [
216
- `import config from ${JSON.stringify(`../${configPath.replace(/\.[^./]+$/u, "")}`)};`,
217
- ""
218
- ]
219
- : [];
220
- writeFileSync(target, [
221
- `import { ${tool.helper} } from ${JSON.stringify(plugin.packageName)};`,
222
- ...configImport,
223
- `const tool = ${tool.helper}(${configPath ? "config" : ""});`,
224
- "",
225
- "export default {",
226
- " ...tool,",
227
- ` description: ${JSON.stringify(tool.description)},`,
228
- " inputSchema: tool.inputSchema,",
229
- " outputSchema: tool.outputSchema,",
230
- ` needsApproval: ${tool.needsApproval === true ? "true" : "false"}`,
231
- "};",
232
- ""
233
- ].join("\n"));
234
- lines.push(`Created ${relativePath} using ${tool.helper}().`);
235
- }
236
- return { lines, already: lines.every((line) => line.includes("already exists")) };
237
- }
238
- function safePluginAgentPath(path) {
239
- const normalized = path.replace(/\\/gu, "/");
240
- if (!normalized.startsWith("tool-config/") || normalized.split("/").includes("..")) {
241
- throw new Error(`Plugin configuration path must stay below tool-config/: ${path}`);
242
- }
243
- return normalized;
244
- }
245
- function wireConnection(agentRoot, plugin) {
246
- const relativePath = `connections/${plugin.kind}.ts`;
247
- const path = join(agentRoot, relativePath);
248
- if (existsSync(path))
249
- return { lines: [`${relativePath} already exists. Leaving it unchanged.`], already: true };
250
- mkdirSync(join(agentRoot, "connections"), { recursive: true });
251
- const connectionOptions = plugin.protocol === "a2a"
252
- ? [
253
- ' agentCardUrl: "https://peer.example.com/.well-known/agent-card.json",',
254
- ' tokenEnv: "PEER_A2A_TOKEN",'
255
- ]
256
- : [];
257
- const accessOptions = plugin.protocol === "credential"
258
- ? []
259
- : [
260
- " // Reviewed tools are enabled and run without an approval surface by default.",
261
- " // Set access to \"approval-required\", \"read-only\", or a custom policy when needed."
262
- ];
263
- writeFileSync(path, [
264
- `import { ${plugin.helper} } from "${plugin.packageName}";`,
265
- "",
266
- `export default ${plugin.helper}({`,
267
- ...connectionOptions,
268
- ...accessOptions,
269
- "});",
270
- ""
271
- ].join("\n"));
272
- const accessSummary = plugin.protocol === "credential" ? "" : " with reviewed tools enabled";
273
- return { lines: [`Created ${relativePath} using ${plugin.helper}()${accessSummary}.`] };
274
- }
275
- export function copyPluginSkills(metadata, agentRoot, installDir) {
276
- if (metadata.role !== "tools")
277
- return [];
278
- const packageName = metadata.packageName;
279
- const skills = metadata.skills ?? [];
280
- if (!packageName || skills.length === 0)
281
- return [];
282
- const packageRoot = resolvePluginPackageRoot(packageName, metadata.kind, [agentRoot, installDir]);
283
- if (!packageRoot)
284
- return skills.map((skill) => `Skill ${skill} will be available after ${packageName} is installed.`);
285
- return skills.map((skill) => {
286
- const target = join(agentRoot, "skills", skill);
287
- if (existsSync(target)) {
288
- return `skills/${skill}/ already exists. Leaving it unchanged.`;
289
- }
290
- const source = join(packageRoot, "skills", skill);
291
- const bundleSkillNames = pluginBundleSkillNames(source);
292
- if (!existsSync(join(source, "SKILL.md")) && bundleSkillNames.length === 0) {
293
- return `Plugin ${packageName} does not include skills/${skill}/SKILL.md or a ${PLUGIN_MANIFEST_RELATIVE_PATH} skill plugin.`;
294
- }
295
- mkdirSync(dirname(target), { recursive: true });
296
- cpSync(source, target, { recursive: true });
297
- if (bundleSkillNames.length === 0)
298
- return `Copied skills/${skill}/ from ${packageName}.`;
299
- const collisions = bundleSkillNames.filter((name) => existsSync(join(agentRoot, "skills", name, "SKILL.md")));
300
- const lines = [`Copied skill plugin skills/${skill}/ (${bundleSkillNames.length} skills) from ${packageName}.`];
301
- for (const name of collisions) {
302
- lines.push(`Warning: skill name "${name}" is contributed twice (skill plugin ${skill} and skills/${name}/); compilation will fail until one is removed.`);
303
- }
304
- return lines.join("\n");
305
- });
306
- }
307
- /** Contributed skill names of a skill-plugin-shaped directory (empty for standalone skills). */
308
- function pluginBundleSkillNames(source) {
309
- const isBundle = existsSync(join(source, PLUGIN_MANIFEST_RELATIVE_PATH)) || existsSync(join(source, "skills"));
310
- if (!isBundle || existsSync(join(source, "SKILL.md")))
311
- return [];
312
- const nested = join(source, "skills");
313
- if (!existsSync(nested))
314
- return [];
315
- return readdirSync(nested)
316
- .filter((entry) => existsSync(join(nested, entry, "SKILL.md")))
317
- .sort();
318
- }
319
- function resolvePluginPackageRoot(packageName, kind, startDirs) {
320
- for (const startDir of startDirs) {
321
- try {
322
- // Walk up from the resolved entry to the package.json whose name matches:
323
- // nested manifests (dist/cjs/package.json shims) do not mark the root.
324
- let current = dirname(createRequire(pathToFileURL(join(startDir, "package.json")).href).resolve(packageName));
325
- while (current !== dirname(current)) {
326
- const packageJson = join(current, "package.json");
327
- if (existsSync(packageJson) && packageNameAtPath(packageJson) === packageName)
328
- return current;
329
- current = dirname(current);
330
- }
331
- }
332
- catch {
333
- // Try the next project root, then the monorepo fallback used by local development.
334
- }
335
- }
336
- const packagesDir = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
337
- const workspaceCandidate = join(packagesDir, kind);
338
- return existsSync(join(workspaceCandidate, "package.json")) ? workspaceCandidate : undefined;
339
- }
340
- function packageNameAtPath(packageJsonPath) {
50
+ const selectionName = typeof raw.name === "string" ? raw.name : name;
51
+ const metadata = contractMetadata(raw.contract, plugin.root);
52
+ selections.push({ role: raw.role, name: selectionName, value: selectionName, ...(metadata ? { metadata } : {}) });
53
+ }
54
+ for (const [name, raw] of Object.entries(entry.sections.channels)) {
55
+ selections.push({ role: "channel", name, value: name, ...(isRecord(raw) ? { metadata: raw } : {}) });
56
+ }
57
+ for (const [name, raw] of Object.entries(entry.sections.instrumentation)) {
58
+ selections.push({ role: "observability", name, value: name, ...(isRecord(raw) ? { metadata: raw } : {}) });
59
+ }
60
+ for (const [name, raw] of Object.entries(entry.sections.models)) {
61
+ if (!isRecord(raw) || typeof raw.defaultModel !== "string") {
62
+ throw new Error(`Model contribution ${plugin.name}.models.${name} must declare defaultModel for CLI selection.`);
63
+ }
64
+ const metadata = contractMetadata(raw.contract, plugin.root);
65
+ selections.push({ role: "model", name, value: raw.defaultModel, ...(metadata ? { metadata } : {}) });
66
+ }
67
+ if (hasCapabilitySurface(entry))
68
+ selections.push({ role: "capability", name: plugin.name, value: plugin.name });
69
+ return selections.length > 0 ? selections : [{ role: "capability", name: plugin.name, value: plugin.name }];
70
+ }
71
+ function chooseSelection(pluginName, candidates, requestedRole) {
72
+ if (candidates.length === 0) {
73
+ throw new Error(`Plugin ${pluginName} does not contribute role ${requestedRole}. Declared roles are in its ai.assemblyline entry.`);
74
+ }
75
+ if (candidates.length === 1)
76
+ return candidates[0];
77
+ const named = candidates.filter((candidate) => candidate.name === pluginName);
78
+ if (named.length === 1)
79
+ return named[0];
80
+ const roles = [...new Set(candidates.map((candidate) => candidate.role))].sort();
81
+ throw new Error(`Plugin ${pluginName} has multiple selectable contributions (${candidates.map((candidate) => `${candidate.role}:${candidate.name}`).join(", ")}). Pass --role ${roles.join("|")}.`);
82
+ }
83
+ function normalizeRole(role) {
84
+ if (role === "channel")
85
+ return "channel";
86
+ if (role === "connection" || role === "tools" || role === "plugin")
87
+ return "capability";
88
+ return role;
89
+ }
90
+ function hasCapabilitySurface(entry) {
91
+ return ["tools", "connections", "hooks", "lifecycle", "composition"]
92
+ .some((section) => Object.keys(entry.sections[section]).length > 0);
93
+ }
94
+ function contractMetadata(value, pluginRoot) {
95
+ if (isRecord(value))
96
+ return value;
97
+ if (typeof value !== "string" || !value.startsWith("./") || value.includes(".."))
98
+ return undefined;
341
99
  try {
342
- return JSON.parse(readFileSync(packageJsonPath, "utf8")).name;
100
+ const parsed = JSON.parse(readFileSync(resolve(pluginRoot, value), "utf8"));
101
+ return isRecord(parsed) ? parsed : undefined;
343
102
  }
344
103
  catch {
345
104
  return undefined;
346
105
  }
347
106
  }
348
- function asConnectionPluginMetadata(metadata) {
349
- const candidate = metadata;
350
- return candidate.role === "connection" && typeof candidate.helper === "string"
351
- && (candidate.protocol === "credential" || candidate.protocol === "mcp" || candidate.protocol === "a2a" || candidate.protocol === "openapi" || candidate.protocol === "http" || candidate.protocol === "sdk" || candidate.protocol === "cli")
352
- ? candidate
353
- : undefined;
354
- }
355
- function wireGatewaySlot(agentRoot, role, kind) {
356
- const path = join(agentRoot, "gateway.ts");
357
- const snippet = ` ${role}: adapter("${kind}"),`;
358
- if (!existsSync(path)) {
359
- writeFileSync(path, gatewayScaffoldSource({ [role]: kind }));
360
- return { lines: [`Created gateway.ts with ${role}: adapter("${kind}").`] };
361
- }
107
+ function updateMarkdownSelection(agentRoot, selection) {
108
+ const path = join(agentRoot, "agent.md");
362
109
  const source = readFileSync(path, "utf8");
363
- const slotPattern = new RegExp(`(\\b${role}\\s*:\\s*adapter\\(\\s*)(["'])([^"']*)\\2`, "u");
364
- const match = slotPattern.exec(source);
365
- if (match) {
366
- if (match[3] === kind) {
367
- return { lines: [`gateway.ts already sets ${role}: adapter("${kind}"). Nothing to change.`], already: true };
368
- }
369
- writeFileSync(path, source.replace(slotPattern, `$1$2${kind}$2`));
370
- return { lines: [`gateway.ts: ${role} now uses adapter("${kind}") (was "${match[3]}").`] };
371
- }
372
- const inserted = insertProperty(source, /defineGateway\(\s*\{/u, snippet);
373
- if (inserted) {
374
- writeFileSync(path, inserted);
375
- return { lines: [`gateway.ts: added ${role}: adapter("${kind}").`] };
376
- }
377
- return manualEditOutcome("gateway.ts", "defineGateway({ ... })", snippet);
378
- }
379
- function wireChannel(agentRoot, kind, packageName, installDir) {
380
- const relativePath = `channels/${kind}.ts`;
381
- const path = join(agentRoot, "channels", `${kind}.ts`);
382
- const scaffold = CHANNEL_SCAFFOLDS[kind];
383
- if (!scaffold) {
384
- return {
385
- lines: [
386
- `No channel scaffold is known for "${kind}", so nothing was wired automatically.`,
387
- `Create ${relativePath} exporting a ChannelDefinition from ${packageName ?? "the plugin package"}.`
388
- ]
389
- };
390
- }
391
- const lines = [];
392
- const channelExists = existsSync(path);
393
- if (channelExists) {
394
- lines.push(`${relativePath} already exists. Leaving it unchanged.`);
395
- }
396
- else {
397
- mkdirSync(join(agentRoot, "channels"), { recursive: true });
398
- writeFileSync(path, [
399
- `import { ${scaffold.helper} } from "${scaffold.packageName}";`,
400
- "",
401
- `export default ${scaffold.helper}();`,
402
- ""
403
- ].join("\n"));
404
- lines.push(`Created ${relativePath} using ${scaffold.helper}().`);
405
- }
406
- if (kind !== "slack")
407
- return { lines, already: channelExists };
408
- const manifestRelativePath = "slack-app-manifest.json";
409
- const manifestPath = join(agentRoot, manifestRelativePath);
410
- const manifestExists = existsSync(manifestPath);
411
- if (manifestExists) {
412
- lines.push(`${manifestRelativePath} already exists. Leaving it unchanged.`);
413
- }
414
- else {
415
- const packageRoot = resolvePluginPackageRoot(packageName ?? scaffold.packageName, kind, [agentRoot, installDir]);
416
- const templatePath = packageRoot ? join(packageRoot, "templates", "slack-app-manifest.json") : undefined;
417
- if (templatePath && existsSync(templatePath)) {
418
- cpSync(templatePath, manifestPath);
419
- lines.push(`Created ${manifestRelativePath} from the Slack plugin's default app manifest.`);
420
- }
421
- else {
422
- lines.push(`Slack app manifest template will be available after ${scaffold.packageName} is installed.`);
423
- }
424
- }
425
- return { lines, already: channelExists && manifestExists };
426
- }
427
- /** Never guess when the file shape is unexpected: print the exact snippet instead. */
428
- function manualEditOutcome(file, callShape, snippet) {
429
- return {
430
- lines: [
431
- `Could not confidently edit ${file}; paste this into the ${callShape} call yourself:`,
432
- snippet
433
- ]
434
- };
435
- }
436
- /**
437
- * Inserts a property right after the config call's opening brace. Requires the
438
- * `adapter` helper to be importable from the existing @assemblyline-agents/core import;
439
- * returns undefined (caller prints the snippet) when either edit is unsafe.
440
- */
441
- function insertProperty(source, openPattern, propertyLine) {
442
- const withImport = ensureCoreImport(source, "adapter");
443
- if (!withImport)
444
- return undefined;
445
- const match = openPattern.exec(withImport);
446
- if (!match)
110
+ const frontmatter = splitMarkdownFrontmatter(source);
111
+ const document = parseDocument(frontmatter.yaml, { uniqueKeys: true, version: "1.2" });
112
+ if (document.errors.length > 0 || !isMap(document.contents)) {
113
+ throw new Error(`agent.md frontmatter is malformed: ${document.errors.map((error) => error.message).join("; ")}`);
114
+ }
115
+ const key = selection.role === "channel" ? "channels" : selection.role;
116
+ const pair = mappingPair(document, key);
117
+ const changed = selection.role === "channel"
118
+ ? updateChannelNode(document, pair, selection.value)
119
+ : updateScalarNode(document, pair, key, selection.value);
120
+ if (changed)
121
+ writeFileSync(path, `---\n${String(document).trimEnd()}\n---${frontmatter.body}`);
122
+ return changed;
123
+ }
124
+ function updateScalarNode(document, pair, key, value) {
125
+ if (!pair) {
126
+ if (!isMap(document.contents))
127
+ throw new Error("agent.md frontmatter must be a mapping.");
128
+ document.contents.add({ key, value });
129
+ return true;
130
+ }
131
+ if (isScalar(pair.value) && pair.value.value === value)
132
+ return false;
133
+ pair.value = document.createNode(value);
134
+ return true;
135
+ }
136
+ function updateChannelNode(document, pair, name) {
137
+ if (!pair) {
138
+ if (!isMap(document.contents))
139
+ throw new Error("agent.md frontmatter must be a mapping.");
140
+ document.contents.add({ key: "channels", value: [name] });
141
+ return true;
142
+ }
143
+ if (isSeq(pair.value)) {
144
+ if (pair.value.items.some((item) => isScalar(item) && item.value === name))
145
+ return false;
146
+ pair.value.add(name);
147
+ return true;
148
+ }
149
+ if (isMap(pair.value)) {
150
+ if (pair.value.has(name))
151
+ return false;
152
+ pair.value.add({ key: name, value: document.createNode({}) });
153
+ return true;
154
+ }
155
+ throw new Error("agent.md channels must be a list or name-to-options mapping.");
156
+ }
157
+ function splitMarkdownFrontmatter(source) {
158
+ const opening = /^---[ \t]*\r?\n/u.exec(source);
159
+ if (!opening)
160
+ throw new Error("agent.md frontmatter is malformed.");
161
+ const remainder = source.slice(opening[0].length);
162
+ const closing = /^---[ \t]*(?:\r?\n|$)/mu.exec(remainder);
163
+ if (!closing || closing.index === undefined)
164
+ throw new Error("agent.md frontmatter is malformed.");
165
+ return { yaml: remainder.slice(0, closing.index), body: remainder.slice(closing.index + closing[0].length - (closing[0].endsWith("\n") ? 1 : 0)) };
166
+ }
167
+ function mappingPair(document, key) {
168
+ if (!isMap(document.contents))
447
169
  return undefined;
448
- const index = match.index + match[0].length;
449
- return `${withImport.slice(0, index)}\n${propertyLine}${withImport.slice(index)}`;
170
+ return document.contents.items.find((pair) => isScalar(pair.key) && pair.key.value === key);
450
171
  }
451
- function ensureCoreImport(source, importName) {
452
- const importPattern = /import\s*\{([^}]*)\}\s*from\s*(["'])(@assemblyline-agents\/core)\2/u;
453
- const match = importPattern.exec(source);
454
- if (!match || match[1] === undefined)
455
- return undefined;
456
- const names = match[1].split(",").map((name) => name.trim()).filter(Boolean);
457
- if (names.includes(importName))
458
- return source;
459
- return source.replace(importPattern, `import { ${[...names, importName].sort().join(", ")} } from ${match[2]}${match[3]}${match[2]}`);
460
- }
461
- function printNextSteps(metadata, agentRoot, pm) {
462
- const requiredConfig = "requiredConfig" in metadata ? metadata.requiredConfig ?? [] : [];
172
+ function printRequirements(selection) {
173
+ const requiredConfig = stringList(selection.metadata?.requiredConfig);
174
+ const optionalConfig = stringList(selection.metadata?.optionalConfig);
175
+ const requiredCredentials = stringList(selection.metadata?.requiredCredentials);
176
+ const optionalCredentials = stringList(selection.metadata?.optionalCredentials);
463
177
  if (requiredConfig.length > 0)
464
178
  console.log(`Set configuration: ${requiredConfig.join(", ")}`);
465
- const optionalConfig = "optionalConfig" in metadata ? metadata.optionalConfig ?? [] : [];
466
179
  if (optionalConfig.length > 0)
467
180
  console.log(`Optional configuration: ${optionalConfig.join(", ")}`);
468
- const requiredCredentials = "requiredCredentials" in metadata ? metadata.requiredCredentials ?? [] : [];
469
181
  if (requiredCredentials.length > 0)
470
- console.log(`Provision credentials: ${requiredCredentials.join(", ")}`);
471
- const optionalCredentials = "optionalCredentials" in metadata ? metadata.optionalCredentials ?? [] : [];
182
+ console.log(`Set credentials: ${requiredCredentials.join(", ")}`);
472
183
  if (optionalCredentials.length > 0)
473
184
  console.log(`Optional credentials: ${optionalCredentials.join(", ")}`);
474
- for (const setup of metadata.setup ?? []) {
475
- if (setup.required)
476
- console.log(`Setup: ${setup.message}`);
477
- }
478
- const runner = pm === "pnpm" ? "pnpm assembly-line" : pm === "yarn" ? "yarn assembly-line" : "npx assembly-line";
479
- console.log(`Run: ${runner} validate ${agentRoot}`);
480
185
  }
481
- /** The default gateway scaffold shared by `assembly-line init` and `assembly-line add`. */
482
- export function gatewayScaffoldSource(overrides = {}) {
483
- const slots = {
484
- deploy: "local",
485
- runtime: "node",
486
- state: "local",
487
- blob: "local",
488
- sandbox: "local",
489
- scheduler: "local",
490
- ...overrides
491
- };
492
- return [
493
- 'import { adapter, defineGateway } from "@assemblyline-agents/core";',
494
- "",
495
- "export default defineGateway({",
496
- Object.entries(slots).map(([role, kind]) => ` ${role}: adapter("${kind}")`).join(",\n"),
497
- "});",
498
- ""
499
- ].join("\n");
186
+ function stringList(value) {
187
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
188
+ }
189
+ function isRecord(value) {
190
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
500
191
  }
501
192
  //# sourceMappingURL=add.js.map