@amodalai/runtime 0.3.49 → 0.3.50

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 (161) hide show
  1. package/dist/src/agent/completion-tools.integration.test.d.ts +6 -0
  2. package/dist/src/agent/completion-tools.integration.test.js +263 -0
  3. package/dist/src/agent/completion-tools.integration.test.js.map +1 -0
  4. package/dist/src/agent/load-template-plan.integration.test.d.ts +6 -0
  5. package/dist/src/agent/load-template-plan.integration.test.js +152 -0
  6. package/dist/src/agent/load-template-plan.integration.test.js.map +1 -0
  7. package/dist/src/agent/local-server.js +64 -9
  8. package/dist/src/agent/local-server.js.map +1 -1
  9. package/dist/src/agent/loop-types.d.ts +12 -2
  10. package/dist/src/agent/loop.test.js +5 -2
  11. package/dist/src/agent/loop.test.js.map +1 -1
  12. package/dist/src/agent/propose-plan.integration.test.d.ts +6 -0
  13. package/dist/src/agent/propose-plan.integration.test.js +186 -0
  14. package/dist/src/agent/propose-plan.integration.test.js.map +1 -0
  15. package/dist/src/agent/routes/package-updates.d.ts +42 -0
  16. package/dist/src/agent/routes/package-updates.js +207 -0
  17. package/dist/src/agent/routes/package-updates.js.map +1 -0
  18. package/dist/src/agent/routes/package-updates.test.d.ts +6 -0
  19. package/dist/src/agent/routes/package-updates.test.js +25 -0
  20. package/dist/src/agent/routes/package-updates.test.js.map +1 -0
  21. package/dist/src/agent/setup-state.integration.test.d.ts +6 -0
  22. package/dist/src/agent/setup-state.integration.test.js +182 -0
  23. package/dist/src/agent/setup-state.integration.test.js.map +1 -0
  24. package/dist/src/agent/snapshot-server.js +1 -0
  25. package/dist/src/agent/snapshot-server.js.map +1 -1
  26. package/dist/src/agent/states/executing.d.ts +6 -0
  27. package/dist/src/agent/states/executing.js +63 -27
  28. package/dist/src/agent/states/executing.js.map +1 -1
  29. package/dist/src/agent/states/streaming.js +18 -2
  30. package/dist/src/agent/states/streaming.js.map +1 -1
  31. package/dist/src/agent/tool-executor-local.js +11 -2
  32. package/dist/src/agent/tool-executor-local.js.map +1 -1
  33. package/dist/src/agent/validate-connection.integration.test.d.ts +6 -0
  34. package/dist/src/agent/validate-connection.integration.test.js +160 -0
  35. package/dist/src/agent/validate-connection.integration.test.js.map +1 -0
  36. package/dist/src/api/create-agent.js +1 -0
  37. package/dist/src/api/create-agent.js.map +1 -1
  38. package/dist/src/index.d.ts +2 -0
  39. package/dist/src/index.js +9 -0
  40. package/dist/src/index.js.map +1 -1
  41. package/dist/src/intent/executor.d.ts +48 -0
  42. package/dist/src/intent/executor.js +420 -0
  43. package/dist/src/intent/executor.js.map +1 -0
  44. package/dist/src/intent/executor.test.d.ts +6 -0
  45. package/dist/src/intent/executor.test.js +543 -0
  46. package/dist/src/intent/executor.test.js.map +1 -0
  47. package/dist/src/intent/index.d.ts +10 -0
  48. package/dist/src/intent/index.js +9 -0
  49. package/dist/src/intent/index.js.map +1 -0
  50. package/dist/src/intent/loader.d.ts +16 -0
  51. package/dist/src/intent/loader.js +112 -0
  52. package/dist/src/intent/loader.js.map +1 -0
  53. package/dist/src/intent/loader.test.d.ts +6 -0
  54. package/dist/src/intent/loader.test.js +86 -0
  55. package/dist/src/intent/loader.test.js.map +1 -0
  56. package/dist/src/intent/matcher.d.ts +26 -0
  57. package/dist/src/intent/matcher.js +29 -0
  58. package/dist/src/intent/matcher.js.map +1 -0
  59. package/dist/src/intent/matcher.test.d.ts +6 -0
  60. package/dist/src/intent/matcher.test.js +53 -0
  61. package/dist/src/intent/matcher.test.js.map +1 -0
  62. package/dist/src/intent/onboarding.e2e.test.d.ts +6 -0
  63. package/dist/src/intent/onboarding.e2e.test.js +394 -0
  64. package/dist/src/intent/onboarding.e2e.test.js.map +1 -0
  65. package/dist/src/routes/ai-stream.js +96 -3
  66. package/dist/src/routes/ai-stream.js.map +1 -1
  67. package/dist/src/routes/session-resolver.js +16 -0
  68. package/dist/src/routes/session-resolver.js.map +1 -1
  69. package/dist/src/session/credential-scrubber.d.ts +35 -0
  70. package/dist/src/session/credential-scrubber.js +150 -0
  71. package/dist/src/session/credential-scrubber.js.map +1 -0
  72. package/dist/src/session/credential-scrubber.test.d.ts +6 -0
  73. package/dist/src/session/credential-scrubber.test.js +192 -0
  74. package/dist/src/session/credential-scrubber.test.js.map +1 -0
  75. package/dist/src/session/manager.intent.test.d.ts +6 -0
  76. package/dist/src/session/manager.intent.test.js +197 -0
  77. package/dist/src/session/manager.intent.test.js.map +1 -0
  78. package/dist/src/session/manager.js +114 -0
  79. package/dist/src/session/manager.js.map +1 -1
  80. package/dist/src/session/session-builder.d.ts +16 -1
  81. package/dist/src/session/session-builder.js +209 -41
  82. package/dist/src/session/session-builder.js.map +1 -1
  83. package/dist/src/session/store.js +48 -2
  84. package/dist/src/session/store.js.map +1 -1
  85. package/dist/src/session/tool-context-factory.js +12 -0
  86. package/dist/src/session/tool-context-factory.js.map +1 -1
  87. package/dist/src/session/types.d.ts +12 -1
  88. package/dist/src/setup/commit-setup.d.ts +94 -0
  89. package/dist/src/setup/commit-setup.js +154 -0
  90. package/dist/src/setup/commit-setup.js.map +1 -0
  91. package/dist/src/setup/commit-setup.test.d.ts +6 -0
  92. package/dist/src/setup/commit-setup.test.js +310 -0
  93. package/dist/src/setup/commit-setup.test.js.map +1 -0
  94. package/dist/src/tools/README.md +270 -0
  95. package/dist/src/tools/admin-tools.d.ts +27 -0
  96. package/dist/src/tools/admin-tools.js +734 -0
  97. package/dist/src/tools/admin-tools.js.map +1 -0
  98. package/dist/src/tools/agent-package-discovery.test.d.ts +6 -0
  99. package/dist/src/tools/agent-package-discovery.test.js +90 -0
  100. package/dist/src/tools/agent-package-discovery.test.js.map +1 -0
  101. package/dist/src/tools/builtin/ask-choice.d.ts +8 -0
  102. package/dist/src/tools/builtin/ask-choice.js +54 -0
  103. package/dist/src/tools/builtin/ask-choice.js.map +1 -0
  104. package/dist/src/tools/context.d.ts +154 -0
  105. package/dist/src/tools/context.js +30 -0
  106. package/dist/src/tools/context.js.map +1 -0
  107. package/dist/src/tools/custom-tool-adapter.d.ts +33 -2
  108. package/dist/src/tools/custom-tool-adapter.js +38 -1
  109. package/dist/src/tools/custom-tool-adapter.js.map +1 -1
  110. package/dist/src/tools/custom-tool-adapter.test.js +48 -0
  111. package/dist/src/tools/custom-tool-adapter.test.js.map +1 -1
  112. package/dist/src/tools/fetch-url-tool.js +2 -0
  113. package/dist/src/tools/fetch-url-tool.js.map +1 -1
  114. package/dist/src/tools/file-tools.js +16 -0
  115. package/dist/src/tools/file-tools.js.map +1 -1
  116. package/dist/src/tools/fs/local.test.d.ts +6 -0
  117. package/dist/src/tools/fs/local.test.js +126 -0
  118. package/dist/src/tools/fs/local.test.js.map +1 -0
  119. package/dist/src/tools/index.d.ts +35 -0
  120. package/dist/src/tools/index.js +11 -0
  121. package/dist/src/tools/index.js.map +1 -0
  122. package/dist/src/tools/mcp-tool-adapter.js +2 -0
  123. package/dist/src/tools/mcp-tool-adapter.js.map +1 -1
  124. package/dist/src/tools/memory-tool.js +23 -1
  125. package/dist/src/tools/memory-tool.js.map +1 -1
  126. package/dist/src/tools/permissions.d.ts +36 -0
  127. package/dist/src/tools/permissions.js +97 -0
  128. package/dist/src/tools/permissions.js.map +1 -0
  129. package/dist/src/tools/permissions.test.d.ts +6 -0
  130. package/dist/src/tools/permissions.test.js +62 -0
  131. package/dist/src/tools/permissions.test.js.map +1 -0
  132. package/dist/src/tools/request-tool.js +2 -0
  133. package/dist/src/tools/request-tool.js.map +1 -1
  134. package/dist/src/tools/sdk-context.d.ts +43 -0
  135. package/dist/src/tools/sdk-context.js +94 -0
  136. package/dist/src/tools/sdk-context.js.map +1 -0
  137. package/dist/src/tools/sdk-context.test.d.ts +6 -0
  138. package/dist/src/tools/sdk-context.test.js +134 -0
  139. package/dist/src/tools/sdk-context.test.js.map +1 -0
  140. package/dist/src/tools/store-tools.js +6 -0
  141. package/dist/src/tools/store-tools.js.map +1 -1
  142. package/dist/src/tools/types.d.ts +53 -14
  143. package/dist/src/tools/web-search-tool.js +2 -0
  144. package/dist/src/tools/web-search-tool.js.map +1 -1
  145. package/dist/src/types.d.ts +164 -28
  146. package/dist/src/types.js +9 -3
  147. package/dist/src/types.js.map +1 -1
  148. package/dist/tsconfig.tsbuildinfo +1 -1
  149. package/package.json +16 -4
  150. package/dist/src/tools/agent-config-tool.d.ts +0 -7
  151. package/dist/src/tools/agent-config-tool.js +0 -78
  152. package/dist/src/tools/agent-config-tool.js.map +0 -1
  153. package/dist/src/tools/collect-secret-tool.d.ts +0 -7
  154. package/dist/src/tools/collect-secret-tool.js +0 -47
  155. package/dist/src/tools/collect-secret-tool.js.map +0 -1
  156. package/dist/src/tools/fs/http.d.ts +0 -37
  157. package/dist/src/tools/fs/http.js +0 -88
  158. package/dist/src/tools/fs/http.js.map +0 -1
  159. package/dist/src/tools/http-file-tools.d.ts +0 -13
  160. package/dist/src/tools/http-file-tools.js +0 -146
  161. package/dist/src/tools/http-file-tools.js.map +0 -1
@@ -0,0 +1,734 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Amodal Labs, Inc.
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ /**
7
+ * Admin agent tools for the Onboarding v4 conversational setup flow.
8
+ *
9
+ * - `search_packages` — searches the public npm registry for keyword matches
10
+ * (e.g. `keywords:amodal-connection`) and returns top-N hits.
11
+ * - `install_package` — adds an npm package to `amodal.json#packages` and
12
+ * runs `npm install` in the agent repo.
13
+ * - `write_skill` — scaffolds a `skills/<name>/SKILL.md` with frontmatter.
14
+ *
15
+ * `start_oauth_connection` was removed in Phase H.1; replaced by the
16
+ * `present_connection` custom tool in agent-admin, which emits a
17
+ * `connection_panel` block via `ctx.emit` (auth-agnostic).
18
+ *
19
+ * These are gated by `sessionType === 'admin'` in the session builder.
20
+ *
21
+ * `show_preview` migrated to `@amodalai/agent-admin/tools/show_preview/`
22
+ * in Phase 0.6 — it is auto-discovered via the package's own tools
23
+ * directory, not hardcoded here.
24
+ *
25
+ * `ask_choice` was promoted to a runtime built-in in Phase 0.6 — see
26
+ * `tools/builtin/ask-choice.ts`. Every agent gets it, not just admin.
27
+ */
28
+ import { execFile } from 'node:child_process';
29
+ import { existsSync } from 'node:fs';
30
+ import { readFile, writeFile, mkdir, mkdtemp, cp, rm } from 'node:fs/promises';
31
+ // eslint-disable-next-line no-restricted-imports -- install_template needs a system temp dir to clone into and ~/.npm/_logs to surface install failures; no @amodalai/core helper exists yet
32
+ import * as os from 'node:os';
33
+ import * as path from 'node:path';
34
+ import { promisify } from 'node:util';
35
+ import { z } from 'zod';
36
+ import { composePlan } from '@amodalai/core';
37
+ import { SSEEventType } from '../types.js';
38
+ const execFileAsync = promisify(execFile);
39
+ const NPM_SEARCH_TIMEOUT_MS = 10_000;
40
+ const NPM_INSTALL_TIMEOUT_MS = 120_000;
41
+ const DEFAULT_SEARCH_LIMIT = 10;
42
+ const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org';
43
+ export function registerAdminTools(registry, opts) {
44
+ const { repoRoot, logger } = opts;
45
+ const registryUrl = opts.registryUrl ?? DEFAULT_NPM_REGISTRY;
46
+ // -------------------------------------------------------------------------
47
+ // search_packages — keyword-based npm registry search
48
+ // -------------------------------------------------------------------------
49
+ registry.register('search_packages', {
50
+ description: 'Search the npm registry for amodal connection / skill / template packages. Use keyword:amodal-connection, keyword:amodal-skill, etc.',
51
+ parameters: z.object({
52
+ query: z.string().describe('Search query (keywords + free text). Example: "keywords:amodal-connection slack".'),
53
+ limit: z.number().int().min(1).max(50).default(DEFAULT_SEARCH_LIMIT),
54
+ }),
55
+ readOnly: true,
56
+ metadata: { category: 'admin' },
57
+ runningLabel: 'Searching for "{{query}}"',
58
+ completedLabel: 'Searched for "{{query}}"',
59
+ async execute(params, _ctx) {
60
+ const url = `${registryUrl}/-/v1/search?text=${encodeURIComponent(params.query)}&size=${String(params.limit)}`;
61
+ logger.debug('admin_tool_search_packages', { query: params.query, limit: params.limit });
62
+ try {
63
+ const res = await fetch(url, { signal: AbortSignal.timeout(NPM_SEARCH_TIMEOUT_MS) });
64
+ if (!res.ok) {
65
+ return { error: `npm search returned ${String(res.status)}` };
66
+ }
67
+ const raw = await res.json();
68
+ return { results: extractSearchHits(raw) };
69
+ }
70
+ catch (err) {
71
+ return { error: err instanceof Error ? err.message : 'npm search failed' };
72
+ }
73
+ },
74
+ });
75
+ // -------------------------------------------------------------------------
76
+ // install_package — add to amodal.json#packages and npm install
77
+ // -------------------------------------------------------------------------
78
+ registry.register('install_package', {
79
+ description: 'Install a CONNECTION or SKILL npm package into the agent repo (e.g. `@amodalai/connection-slack`). **For templates, use `install_template({slug})` instead — install_package will reject template-shaped names.** Accepts npm names (scoped or unscoped) and npm GitHub shorthand (`<owner>/<repo>`). Adds the resolved package to amodal.json#packages and runs `npm install`. Returns `{ok: true, package: <resolved-npm-name>}` on success.',
80
+ parameters: z.object({
81
+ name: z.string().describe('npm package name OR GitHub `<owner>/<repo>` shorthand'),
82
+ version: z.string().optional().describe('Optional version range; ignored for GitHub installs'),
83
+ }),
84
+ readOnly: false,
85
+ metadata: { category: 'admin' },
86
+ runningLabel: 'Adding a package',
87
+ completedLabel: 'Added the package',
88
+ async execute(params, ctx) {
89
+ // Hard guardrail: templates are installed via `install_template`,
90
+ // not via `install_package`. The model has strong training
91
+ // priors for `@amodalai/template-*` npm names that don't actually
92
+ // exist on the registry — block them here and redirect with an
93
+ // educational error rather than letting npm 404 over and over.
94
+ if (/^@?[\w./-]*template-/i.test(params.name)) {
95
+ return {
96
+ error: `'${params.name}' looks like a template package. install_package can't install templates — use install_template({slug}) instead. The slug is in state.providedContext._templateSlug. install_template handles the platform-api lookup, GitHub install, and canonical-name resolution in one call.`,
97
+ };
98
+ }
99
+ const githubShorthand = isGithubShorthand(params.name);
100
+ const installSpec = githubShorthand
101
+ ? params.name
102
+ : params.version && params.version !== ''
103
+ ? `${params.name}@${params.version}`
104
+ : `${params.name}@latest`;
105
+ logger.debug('admin_tool_install_package', {
106
+ name: params.name,
107
+ version: params.version,
108
+ github: githubShorthand,
109
+ });
110
+ try {
111
+ await execFileAsync('npm', ['install', installSpec], {
112
+ cwd: repoRoot,
113
+ timeout: NPM_INSTALL_TIMEOUT_MS,
114
+ });
115
+ // After install, look up the canonical package name. For npm
116
+ // names this is the same as `params.name`; for GitHub shorthand
117
+ // we need to read the freshly-installed package.json#name.
118
+ const resolvedName = githubShorthand
119
+ ? await resolveGithubInstalledName(repoRoot, params.name) ?? params.name
120
+ : params.name;
121
+ await addToAmodalPackages(repoRoot, resolvedName);
122
+ const friendly = await readPackageDisplayName(repoRoot, resolvedName);
123
+ ctx.setLabel?.({ completed: friendly ? `Added ${friendly}` : 'Added the package' });
124
+ ctx.log(`Installed ${installSpec}${resolvedName !== params.name ? ` → ${resolvedName}` : ''}`);
125
+ return { ok: true, package: resolvedName };
126
+ }
127
+ catch (err) {
128
+ const message = err instanceof Error ? err.message : String(err);
129
+ logger.warn('admin_tool_install_package_failed', { name: params.name, error: message });
130
+ return { error: `Failed to install ${params.name}: ${message}` };
131
+ }
132
+ },
133
+ });
134
+ // -------------------------------------------------------------------------
135
+ // install_template — clone template + install connection deps + compose Plan
136
+ // -------------------------------------------------------------------------
137
+ //
138
+ // Templates are starting points, not runtime dependencies — so we
139
+ // CLONE the GitHub repo into a temp dir, copy the template's
140
+ // skills/knowledge/automations/etc. into the user's repo (vendored),
141
+ // install the connection packages the template references (those ARE
142
+ // real npm deps), compose the SetupPlan from the vendored template.json,
143
+ // and emit a `plan_summary` SSE event. One tool call, one full Path A
144
+ // installation step. The user's repo ends up with template files at
145
+ // root + connection packages in node_modules — same shape as a hand-
146
+ // crafted agent repo.
147
+ registry.register('install_template', {
148
+ description: '**THE ONLY TOOL FOR TEMPLATE INSTALL.** Clones the template repo into the user\'s working directory (vendored — not buried in node_modules), installs the connection packages the template references via npm, composes the SetupPlan from the template.json, and emits a plan_summary card. Pass the platform-api slug from `state.providedContext._templateSlug`. Returns `{ok: true, plan, displayName, slug}` on success. **You don\'t need to call `load_template_plan` after this** — the Plan is already in the result and the summary card is already emitted. Just `update_setup_state({phase: \'connecting_required\', plan})` with the returned plan and walk the slots.',
149
+ parameters: z.object({
150
+ slug: z.string().describe('Platform-api slug from state.providedContext._templateSlug'),
151
+ }),
152
+ readOnly: false,
153
+ metadata: { category: 'admin' },
154
+ runningLabel: "Installing template '{{slug}}'",
155
+ completedLabel: "Installed template '{{slug}}'",
156
+ async execute(params, ctx) {
157
+ logger.debug('admin_tool_install_template', { slug: params.slug });
158
+ ctx.setLabel?.({ running: `Looking up template '${params.slug}'` });
159
+ // 1. Resolve slug → metadata via Studio
160
+ const metadata = await fetchTemplateMetadata(params.slug);
161
+ if ('error' in metadata)
162
+ return { error: metadata.error };
163
+ const { githubRepo, defaultBranch, displayName, slug } = metadata;
164
+ ctx.setLabel?.({ running: `Cloning ${displayName}` });
165
+ // 2. Clone the template into a temp dir
166
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), 'amodal-template-'));
167
+ try {
168
+ const cloneUrl = `https://github.com/${githubRepo}.git`;
169
+ try {
170
+ // Pass an explicit cwd to git so it doesn't depend on the
171
+ // admin-agent process's cwd, which can become stale when
172
+ // the admin-agent cache (`~/.amodal/admin-agent/latest`) is
173
+ // wiped + resynced while the agent is running. The temp dir
174
+ // already lives inside os.tmpdir(), so using its parent is
175
+ // a safe and stable working directory for the clone.
176
+ await execFileAsync('git', ['clone', '--depth', '1', '--branch', defaultBranch, cloneUrl, tempDir], { timeout: NPM_INSTALL_TIMEOUT_MS, cwd: os.tmpdir() });
177
+ }
178
+ catch (err) {
179
+ const message = err instanceof Error ? err.message : String(err);
180
+ logger.warn('admin_tool_install_template_clone_failed', { githubRepo, error: message });
181
+ return { error: `Failed to clone ${githubRepo}: ${message}` };
182
+ }
183
+ // 3. Copy template directories into user's repo (vendored).
184
+ // `force: false` means we don't overwrite anything the user
185
+ // has there already.
186
+ const VENDORED_DIRS = [
187
+ 'skills', 'knowledge', 'automations', 'connections',
188
+ 'agents', 'tools', 'pages', 'evals', 'stores',
189
+ ];
190
+ for (const dir of VENDORED_DIRS) {
191
+ const src = path.join(tempDir, dir);
192
+ if (existsSync(src)) {
193
+ await cp(src, path.join(repoRoot, dir), { recursive: true, force: false });
194
+ }
195
+ }
196
+ // template.json itself goes to repoRoot too — composePlan reads it from there.
197
+ const tplJsonSrc = path.join(tempDir, 'template.json');
198
+ if (!existsSync(tplJsonSrc)) {
199
+ return { error: `Template ${githubRepo} has no template.json at the repo root` };
200
+ }
201
+ await cp(tplJsonSrc, path.join(repoRoot, 'template.json'), { force: false });
202
+ // Also copy the template's package.json — it lists the
203
+ // connection packages as real npm dependencies, which is what
204
+ // we want as the user's repo root. Without this, `npm install`
205
+ // walks up the directory tree looking for the nearest
206
+ // package.json (potentially landing on the user's home dir
207
+ // package.json) and "satisfies" the deps from there, leaving
208
+ // the local node_modules empty.
209
+ const tplPkgJsonSrc = path.join(tempDir, 'package.json');
210
+ const localPkgJson = path.join(repoRoot, 'package.json');
211
+ if (existsSync(tplPkgJsonSrc)) {
212
+ await cp(tplPkgJsonSrc, localPkgJson, { force: false });
213
+ }
214
+ else if (!existsSync(localPkgJson)) {
215
+ // Defensive fallback: template didn't ship one. Seed a
216
+ // minimal package.json so `npm install` operates locally.
217
+ const seedName = path.basename(repoRoot).replace(/[^a-z0-9-]/gi, '-').toLowerCase() || 'agent';
218
+ await writeFile(localPkgJson, `${JSON.stringify({ name: seedName, version: '1.0.0', private: true }, null, 2)}\n`, 'utf-8');
219
+ }
220
+ // 4. Read template.json from the clone and identify connection packages
221
+ const templateJsonRaw = JSON.parse(await readFile(tplJsonSrc, 'utf-8'));
222
+ const connectionPackages = collectConnectionPackages(templateJsonRaw);
223
+ ctx.setLabel?.({
224
+ running: connectionPackages.length === 1
225
+ ? `Installing 1 connection package`
226
+ : `Installing ${String(connectionPackages.length)} connection packages`,
227
+ });
228
+ // 5. Install dependencies declared in the template's
229
+ // package.json. The template author lists connection
230
+ // packages there with version constraints; `npm install`
231
+ // (no args) honors those exactly. We also pass the
232
+ // connection packages from `template.json#connections` as
233
+ // a defensive fallback so that even if the template's
234
+ // package.json is missing one (authoring bug), it still
235
+ // gets installed. Capture stdout/stderr so we can surface
236
+ // npm's output if anything goes sideways.
237
+ let npmStdout = '';
238
+ let npmStderr = '';
239
+ const installArgs = connectionPackages.length > 0
240
+ ? ['install', ...connectionPackages]
241
+ : ['install'];
242
+ try {
243
+ const result = await execFileAsync('npm', installArgs, {
244
+ cwd: repoRoot,
245
+ timeout: NPM_INSTALL_TIMEOUT_MS,
246
+ });
247
+ npmStdout = result.stdout;
248
+ npmStderr = result.stderr;
249
+ logger.info('admin_tool_install_template_deps_done', {
250
+ args: installArgs,
251
+ stdoutPreview: npmStdout.slice(-500),
252
+ stderrPreview: npmStderr.slice(-500),
253
+ });
254
+ }
255
+ catch (err) {
256
+ const message = err instanceof Error ? err.message : String(err);
257
+ const stdout = isObject(err) && typeof err['stdout'] === 'string' ? err['stdout'] : '';
258
+ const stderr = isObject(err) && typeof err['stderr'] === 'string' ? err['stderr'] : '';
259
+ logger.warn('admin_tool_install_template_deps_failed', {
260
+ args: installArgs,
261
+ error: message,
262
+ stdout,
263
+ stderr,
264
+ });
265
+ return {
266
+ error: `Cloned ${githubRepo} but couldn't install connection packages: ${message}\n\nnpm stderr:\n${stderr || '(empty)'}`,
267
+ };
268
+ }
269
+ // 6. Verify every option package is actually on disk. npm
270
+ // install can succeed for some packages and silently leave
271
+ // others uninstalled (e.g. peer-dep skips, scope auth
272
+ // misses, a single name being unpublished). If we let this
273
+ // slip, composePlan fills in `displayName='unknown'`
274
+ // placeholders and the agent emits Configure panels for
275
+ // packages that aren't there → 404 on click. Fail loudly.
276
+ const missing = [];
277
+ for (const pkg of connectionPackages) {
278
+ const pkgDir = path.join(repoRoot, 'node_modules', ...pkg.split('/'));
279
+ if (!existsSync(path.join(pkgDir, 'package.json'))) {
280
+ missing.push(pkg);
281
+ }
282
+ }
283
+ if (missing.length > 0) {
284
+ // Pull the last few lines of npm's debug log so we get a
285
+ // hint at WHY the packages didn't install — silent skips
286
+ // (auth, registry mismatch) are otherwise invisible.
287
+ let npmHint = '';
288
+ try {
289
+ const { readdir: rd, readFile: rf, stat } = await import('node:fs/promises');
290
+ const logsDir = path.join(os.homedir(), '.npm', '_logs');
291
+ if (existsSync(logsDir)) {
292
+ const logs = await rd(logsDir);
293
+ if (logs.length > 0) {
294
+ const stats = await Promise.all(logs.map(async (n) => ({ n, s: await stat(path.join(logsDir, n)) })));
295
+ stats.sort((a, b) => b.s.mtimeMs - a.s.mtimeMs);
296
+ if (stats[0]) {
297
+ const recent = await rf(path.join(logsDir, stats[0].n), 'utf-8');
298
+ npmHint = recent.split('\n').slice(-30).join('\n');
299
+ }
300
+ }
301
+ }
302
+ }
303
+ catch {
304
+ // best-effort; missing log shouldn't block the error
305
+ }
306
+ logger.warn('admin_tool_install_template_packages_missing', { missing, npmHintPreview: npmHint.slice(-400) });
307
+ return {
308
+ error: `Template ${githubRepo} cloned and npm install ran (exit 0), but ${missing.length} ` +
309
+ `connection package(s) didn't land on disk: ${missing.join(', ')}.\n\n` +
310
+ `Common causes: scope auth misconfigured (check ~/.npmrc for ${missing[0]?.split('/')[0]} ` +
311
+ `registry/_authToken), package not published under that name, or registry URL mismatch.\n\n` +
312
+ (npmHint
313
+ ? `Recent npm log tail:\n${npmHint}`
314
+ : `(no npm logs found at ~/.npm/_logs)`),
315
+ };
316
+ }
317
+ ctx.setLabel?.({ running: `Composing plan from ${displayName}` });
318
+ // 7. Compose the Plan. composePlan reads the now-vendored
319
+ // template.json from repoRoot and walks each connection
320
+ // option's package.json#amodal in node_modules. With the
321
+ // verification above, every option's package.json is on
322
+ // disk, so composePlan can't fall back to placeholders.
323
+ let plan;
324
+ try {
325
+ plan = await composePlan({ repoPath: repoRoot, templatePackage: slug });
326
+ }
327
+ catch (err) {
328
+ const message = err instanceof Error ? err.message : String(err);
329
+ logger.warn('admin_tool_install_template_compose_failed', { slug, error: message });
330
+ return { error: `Cloned ${githubRepo} but couldn't compose the Plan: ${message}` };
331
+ }
332
+ // 7. Emit plan_summary SSE event so the user sees the loaded
333
+ // plan rendered as a card inline.
334
+ ctx.emit?.(buildPlanSummaryEvent(plan));
335
+ ctx.setLabel?.({ completed: `Set up ${displayName}` });
336
+ ctx.log(`Cloned ${githubRepo} into agent repo (${connectionPackages.length} connection packages installed)`);
337
+ return { ok: true, slug, displayName, plan };
338
+ }
339
+ finally {
340
+ // Always clean up the temp clone — we vendored what we needed.
341
+ await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
342
+ }
343
+ },
344
+ });
345
+ // -------------------------------------------------------------------------
346
+ // write_skill — scaffold a skills/<name>/SKILL.md
347
+ // -------------------------------------------------------------------------
348
+ registry.register('write_skill', {
349
+ description: 'Scaffold a new SKILL.md at skills/<name>/SKILL.md. Use when no published skill package fits and you need to write a custom skill for the user.',
350
+ parameters: z.object({
351
+ name: z.string().regex(/^[a-z][a-z0-9-]*$/, 'kebab-case').describe('Skill name (kebab-case)'),
352
+ description: z.string().describe('One-line skill description'),
353
+ trigger: z.string().describe('When the agent should activate this skill'),
354
+ methodology: z.string().describe('Markdown body — the steps the agent should take'),
355
+ }),
356
+ readOnly: false,
357
+ metadata: { category: 'admin' },
358
+ runningLabel: 'Writing skill {{name}}',
359
+ completedLabel: 'Wrote skill {{name}}',
360
+ async execute(params, _ctx) {
361
+ const skillDir = path.join(repoRoot, 'skills', params.name);
362
+ const skillPath = path.join(skillDir, 'SKILL.md');
363
+ const body = renderSkillBody(params);
364
+ try {
365
+ await mkdir(skillDir, { recursive: true });
366
+ await writeFile(skillPath, body, 'utf-8');
367
+ return { ok: true, path: path.relative(repoRoot, skillPath) };
368
+ }
369
+ catch (err) {
370
+ return { error: err instanceof Error ? err.message : 'write_skill failed' };
371
+ }
372
+ },
373
+ });
374
+ }
375
+ /**
376
+ * Pull the relevant fields from npm's `/-/v1/search` response. The response
377
+ * shape is `{ objects: [{ package: { name, version, description, keywords } }] }`.
378
+ */
379
+ export function extractSearchHits(raw) {
380
+ if (typeof raw !== 'object' || raw === null)
381
+ return [];
382
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- system boundary: parsing npm registry JSON
383
+ const obj = raw;
384
+ const objects = obj['objects'];
385
+ if (!Array.isArray(objects))
386
+ return [];
387
+ const hits = [];
388
+ for (const entry of objects) {
389
+ if (typeof entry !== 'object' || entry === null)
390
+ continue;
391
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- system boundary: parsing npm registry JSON
392
+ const pkg = entry['package'];
393
+ if (typeof pkg !== 'object' || pkg === null)
394
+ continue;
395
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- system boundary: parsing npm registry JSON
396
+ const p = pkg;
397
+ const name = p['name'];
398
+ if (typeof name !== 'string')
399
+ continue;
400
+ const version = p['version'];
401
+ const description = p['description'];
402
+ const keywords = p['keywords'];
403
+ hits.push({
404
+ name,
405
+ version: typeof version === 'string' ? version : '',
406
+ description: typeof description === 'string' ? description : '',
407
+ keywords: Array.isArray(keywords)
408
+ ? keywords.filter((k) => typeof k === 'string')
409
+ : [],
410
+ });
411
+ }
412
+ return hits;
413
+ }
414
+ async function fetchTemplateMetadata(slug) {
415
+ const studioUrl = process.env['STUDIO_URL'];
416
+ if (!studioUrl) {
417
+ return { error: 'STUDIO_URL is not set — admin agent cannot reach the template resolver' };
418
+ }
419
+ const resolverUrl = `${studioUrl.replace(/\/$/, '')}/api/studio/template/${encodeURIComponent(slug)}`;
420
+ let res;
421
+ try {
422
+ res = await fetch(resolverUrl, { signal: AbortSignal.timeout(8_000) });
423
+ }
424
+ catch (err) {
425
+ return { error: `Template resolver unreachable: ${err instanceof Error ? err.message : String(err)}` };
426
+ }
427
+ if (res.status === 404)
428
+ return { error: `No template registered for slug "${slug}"` };
429
+ if (!res.ok)
430
+ return { error: `Template resolver returned ${String(res.status)}` };
431
+ let body;
432
+ try {
433
+ body = await res.json();
434
+ }
435
+ catch (err) {
436
+ return { error: `Resolver returned invalid JSON: ${err instanceof Error ? err.message : String(err)}` };
437
+ }
438
+ if (typeof body !== 'object' || body === null) {
439
+ return { error: 'Resolver returned a non-object response' };
440
+ }
441
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- system boundary: parsing JSON
442
+ const meta = body;
443
+ const githubRepo = meta['githubRepo'];
444
+ if (typeof githubRepo !== 'string' || githubRepo.length === 0) {
445
+ return { error: `Resolver response missing githubRepo for slug "${slug}"` };
446
+ }
447
+ return {
448
+ slug: typeof meta['slug'] === 'string' ? meta['slug'] : slug,
449
+ displayName: typeof meta['displayName'] === 'string' ? meta['displayName'] : slug,
450
+ githubRepo,
451
+ defaultBranch: typeof meta['defaultBranch'] === 'string' ? meta['defaultBranch'] : 'main',
452
+ };
453
+ }
454
+ /**
455
+ * Walk template.json#connections[] and collect every option's package
456
+ * name. Options can be either bare strings ("@amodalai/connection-slack")
457
+ * or `{packageName: "...", ...}` objects. Returns deduped list.
458
+ */
459
+ function collectConnectionPackages(templateJson) {
460
+ if (typeof templateJson !== 'object' || templateJson === null)
461
+ return [];
462
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- system boundary
463
+ const tj = templateJson;
464
+ const connections = tj['connections'];
465
+ if (!Array.isArray(connections))
466
+ return [];
467
+ const pkgs = new Set();
468
+ for (const slot of connections) {
469
+ if (typeof slot !== 'object' || slot === null)
470
+ continue;
471
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- system boundary
472
+ const options = slot['options'];
473
+ if (!Array.isArray(options))
474
+ continue;
475
+ for (const opt of options) {
476
+ if (typeof opt === 'string' && opt.length > 0) {
477
+ pkgs.add(opt);
478
+ }
479
+ else if (typeof opt === 'object' && opt !== null) {
480
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- system boundary
481
+ const pkgName = opt['packageName'];
482
+ if (typeof pkgName === 'string' && pkgName.length > 0)
483
+ pkgs.add(pkgName);
484
+ }
485
+ }
486
+ }
487
+ return [...pkgs];
488
+ }
489
+ /**
490
+ * Build a `plan_summary` SSE event from a composed `SetupPlan`.
491
+ * Shape mirrors `SSEPlanSummaryEvent` in `@amodalai/types`.
492
+ */
493
+ function buildPlanSummaryEvent(plan) {
494
+ const slotPayload = (s) => ({
495
+ label: s.label,
496
+ description: s.description,
497
+ options: s.options.map((o) => ({ display_name: o.displayName, package_name: o.packageName })),
498
+ });
499
+ return {
500
+ type: SSEEventType.PlanSummary,
501
+ template_title: plan.completion.title || plan.templatePackage,
502
+ required_slots: plan.slots.filter((s) => s.required).map(slotPayload),
503
+ optional_slots: plan.slots.filter((s) => !s.required).map(slotPayload),
504
+ config_questions: plan.config.map((q) => ({ key: q.key, question: q.question })),
505
+ completion_suggestions: plan.completion.suggestions,
506
+ timestamp: new Date().toISOString(),
507
+ };
508
+ }
509
+ /**
510
+ * Detect npm GitHub shorthand: `<owner>/<repo>`. npm accepts these as
511
+ * package specs to clone the repo and install it. Distinguishes from
512
+ * scoped npm packages (start with `@`) and unscoped names (no `/`).
513
+ */
514
+ function isGithubShorthand(name) {
515
+ if (name.startsWith('@'))
516
+ return false;
517
+ return name.includes('/');
518
+ }
519
+ function isObject(value) {
520
+ return typeof value === 'object' && value !== null;
521
+ }
522
+ /**
523
+ * Best-effort read of an installed package's friendly display name.
524
+ * Looks at `amodal.displayName` then top-level `displayName` in the
525
+ * package's package.json. Returns null when neither is set or the
526
+ * package isn't on disk yet.
527
+ */
528
+ async function readPackageDisplayName(repoPath, packageName) {
529
+ const pkgJsonPath = path.join(repoPath, 'node_modules', ...packageName.split('/'), 'package.json');
530
+ if (!existsSync(pkgJsonPath))
531
+ return null;
532
+ try {
533
+ const raw = JSON.parse(await readFile(pkgJsonPath, 'utf-8'));
534
+ if (!isObject(raw))
535
+ return null;
536
+ const amodal = raw['amodal'];
537
+ if (isObject(amodal) && typeof amodal['displayName'] === 'string' && amodal['displayName'].length > 0) {
538
+ return amodal['displayName'];
539
+ }
540
+ const top = raw['displayName'];
541
+ if (typeof top === 'string' && top.length > 0)
542
+ return top;
543
+ return null;
544
+ }
545
+ catch {
546
+ return null;
547
+ }
548
+ }
549
+ /**
550
+ * After `npm install <owner>/<repo>` succeeds, look up the canonical
551
+ * npm name (`package.json#name`) of what landed.
552
+ *
553
+ * Strategy 1 (cheap): read the root `package.json#dependencies`,
554
+ * find the entry whose value contains the GitHub spec.
555
+ *
556
+ * Strategy 2 (fallback): walk top-level `node_modules/<dir>` and
557
+ * `node_modules/@scope/<dir>`, read each `package.json`, return the
558
+ * `name` field of the first one whose `_resolved`, `_from`, or
559
+ * `repository.url` references the GitHub spec. Necessary when npm's
560
+ * dep entry doesn't literally contain the spec (e.g. it rewrote to
561
+ * `github:<owner>/<repo>#<sha>` and our prefix match misses).
562
+ *
563
+ * Returns null when both strategies miss; caller falls back to the
564
+ * literal spec.
565
+ */
566
+ async function resolveGithubInstalledName(repoRoot, githubSpec) {
567
+ // Strategy 1 — root package.json#dependencies
568
+ const fromRootDeps = await tryRootPackageJson(repoRoot, githubSpec);
569
+ if (fromRootDeps)
570
+ return fromRootDeps;
571
+ // Strategy 2 — walk node_modules
572
+ return tryWalkNodeModules(repoRoot, githubSpec);
573
+ }
574
+ async function tryRootPackageJson(repoRoot, githubSpec) {
575
+ const pkgJsonPath = path.join(repoRoot, 'package.json');
576
+ let raw;
577
+ try {
578
+ raw = await readFile(pkgJsonPath, 'utf-8');
579
+ }
580
+ catch {
581
+ return null;
582
+ }
583
+ let parsed;
584
+ try {
585
+ parsed = JSON.parse(raw);
586
+ }
587
+ catch {
588
+ return null;
589
+ }
590
+ if (typeof parsed !== 'object' || parsed === null)
591
+ return null;
592
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- system boundary: parsed root package.json
593
+ const config = parsed;
594
+ const deps = config['dependencies'];
595
+ if (typeof deps !== 'object' || deps === null)
596
+ return null;
597
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- system boundary
598
+ const entries = Object.entries(deps);
599
+ const match = entries.find(([, value]) => typeof value === 'string' && value.includes(githubSpec));
600
+ return match ? match[0] : null;
601
+ }
602
+ async function tryWalkNodeModules(repoRoot, githubSpec) {
603
+ const { readdir } = await import('node:fs/promises');
604
+ const nmDir = path.join(repoRoot, 'node_modules');
605
+ let topLevel;
606
+ try {
607
+ topLevel = await readdir(nmDir);
608
+ }
609
+ catch {
610
+ return null;
611
+ }
612
+ for (const entry of topLevel) {
613
+ if (entry.startsWith('.'))
614
+ continue;
615
+ if (entry.startsWith('@')) {
616
+ // Scoped package — recurse one level
617
+ const scopeDir = path.join(nmDir, entry);
618
+ let scopedEntries;
619
+ try {
620
+ scopedEntries = await readdir(scopeDir);
621
+ }
622
+ catch {
623
+ continue;
624
+ }
625
+ for (const sub of scopedEntries) {
626
+ const match = await readPackageNameIfMatching(path.join(scopeDir, sub), githubSpec);
627
+ if (match)
628
+ return match;
629
+ }
630
+ }
631
+ else {
632
+ const match = await readPackageNameIfMatching(path.join(nmDir, entry), githubSpec);
633
+ if (match)
634
+ return match;
635
+ }
636
+ }
637
+ return null;
638
+ }
639
+ async function readPackageNameIfMatching(pkgDir, githubSpec) {
640
+ const pkgJsonPath = path.join(pkgDir, 'package.json');
641
+ let raw;
642
+ try {
643
+ raw = await readFile(pkgJsonPath, 'utf-8');
644
+ }
645
+ catch {
646
+ return null;
647
+ }
648
+ let parsed;
649
+ try {
650
+ parsed = JSON.parse(raw);
651
+ }
652
+ catch {
653
+ return null;
654
+ }
655
+ if (typeof parsed !== 'object' || parsed === null)
656
+ return null;
657
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- system boundary
658
+ const pkg = parsed;
659
+ const name = pkg['name'];
660
+ if (typeof name !== 'string' || name.length === 0)
661
+ return null;
662
+ // Check the install metadata fields npm writes for git/github installs.
663
+ const candidates = [
664
+ pkg['_resolved'],
665
+ pkg['_from'],
666
+ typeof pkg['repository'] === 'object' && pkg['repository'] !== null
667
+ ? // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- system boundary
668
+ pkg['repository']['url']
669
+ : undefined,
670
+ ];
671
+ for (const c of candidates) {
672
+ if (typeof c === 'string' && c.includes(githubSpec))
673
+ return name;
674
+ }
675
+ return null;
676
+ }
677
+ /**
678
+ * Add `name` to `amodal.json#packages` if absent. Idempotent.
679
+ *
680
+ * No-ops silently when `amodal.json` is missing — that's the expected
681
+ * setup-mode state (empty repo, admin agent running its first
682
+ * install). `commit_setup` composes the final `amodal.json` from
683
+ * `state.plan.templatePackage` + `state.completed[]` at the end of
684
+ * setup, and Studio's repo-state polling watches for `amodal.json`
685
+ * as the "setup complete, switch to admin view" signal — eagerly
686
+ * scaffolding it here would flip the page mid-flow. The package is
687
+ * still installed by npm and visible in `node_modules/`; we just
688
+ * don't update the agent config until commit time.
689
+ */
690
+ async function addToAmodalPackages(repoRoot, name) {
691
+ const configPath = path.join(repoRoot, 'amodal.json');
692
+ let raw;
693
+ try {
694
+ raw = await readFile(configPath, 'utf-8');
695
+ }
696
+ catch (err) {
697
+ if (isNotFoundError(err))
698
+ return;
699
+ throw err;
700
+ }
701
+ const parsed = JSON.parse(raw);
702
+ if (typeof parsed !== 'object' || parsed === null) {
703
+ throw new Error('amodal.json is not an object');
704
+ }
705
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- guarded above
706
+ const config = parsed;
707
+ const existingRaw = config['packages'];
708
+ const existing = Array.isArray(existingRaw)
709
+ ? existingRaw.filter((p) => typeof p === 'string')
710
+ : [];
711
+ if (existing.includes(name))
712
+ return;
713
+ config['packages'] = [...existing, name];
714
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
715
+ }
716
+ function isNotFoundError(err) {
717
+ return (typeof err === 'object' &&
718
+ err !== null &&
719
+ 'code' in err &&
720
+ err.code === 'ENOENT');
721
+ }
722
+ function renderSkillBody(params) {
723
+ return [
724
+ `# ${params.description}`,
725
+ '',
726
+ `**Trigger:** ${params.trigger}`,
727
+ '',
728
+ '## Methodology',
729
+ '',
730
+ params.methodology.trim(),
731
+ '',
732
+ ].join('\n');
733
+ }
734
+ //# sourceMappingURL=admin-tools.js.map