@ninemind/agentgem 0.2.0 → 0.3.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.
@@ -1,7 +1,7 @@
1
1
  import { __decorate, __metadata } from "tslib";
2
2
  // src/gem.controller.ts
3
- import { existsSync } from "node:fs";
4
- import { basename } from "node:path";
3
+ import { existsSync, writeFileSync, readFileSync } from "node:fs";
4
+ import { basename, resolve, sep } from "node:path";
5
5
  import { api, get, post } from "@agentback/openapi";
6
6
  import { introspectConfig, introspectProject } from "./gem/introspect.js";
7
7
  import { buildGem } from "./gem/buildGem.js";
@@ -12,58 +12,87 @@ import { createWorkspace, listWorkspaces, readWorkspace, renderTarget, deleteWor
12
12
  import { writeGemArchive, readGemArchive } from "./gem/archive.js";
13
13
  import { writeArchiveDir, readArchiveDir } from "./gem/archiveFs.js";
14
14
  import { packTar } from "./gem/archiveTar.js";
15
+ import { importGem } from "./gem/share.js";
16
+ import { fetchGemBytes } from "./gem/safeFetch.js";
15
17
  import { readDeployRecord, writeDeployRecord, clearDeployRecord } from "./gem/deployRecord.js";
16
18
  import { undeployManagedAgent, anthropicPublishClient } from "./publish.js";
17
19
  import { undeployAgentcoreHarness, realAgentcoreControlClient } from "./gem/agentcorePublish.js";
18
- import { InventorySchema, GemSchema, GemRequestSchema, DirQuerySchema, PickQuerySchema, PickFolderSchema, ScaffoldChecksRequestSchema, ScaffoldChecksResponseSchema, MaterializeRequestSchema, MaterializeResponseSchema, PublishPreviewRequestSchema, PublishRequestSchema, PublishPreviewResponseSchema, PublishReadyResponseSchema, PublishResultSchema, DeployTargetsResponseSchema, DeployReadyQuerySchema, ArchiveRequestSchema, ArchiveResponseSchema, CreateWorkspaceRequestSchema, WorkspaceQuerySchema, RenderRequestSchema, WorkspaceNameRequestSchema, WorkspaceSummarySchema, WorkspaceDetailSchema, RenderResultSchema, ListWorkspacesResponseSchema, DeleteWorkspaceResponseSchema, RunReadyQuerySchema, RunReadyResponseSchema, RunRequestSchema, RunStatusQuerySchema, RunStateSchema, RunStopRequestSchema, RunStopResponseSchema, CredentialRequestSchema, CredentialResponseSchema, TestbedDetectQuerySchema, TestbedDetectResponseSchema, TestbedSuggestionQuerySchema, TestbedSuggestionResponseSchema, TestbedRecentsResponseSchema, TestbedProjectsQuerySchema, TestbedProjectsResponseSchema, TestbedScaffoldRequestSchema, TestbedScaffoldResponseSchema, TestbedImportRequestSchema, TestbedImportResponseSchema, AgentcoreReadyResponseSchema, AgentcoreDeployRequestSchema, AgentcoreStatusQuerySchema, AgentcoreDeployStateSchema, RegistryReadyResponseSchema, RegistryIndexResponseSchema, RegistryResolveRequestSchema, RegistryResolveResponseSchema, RegistryInstallRequestSchema, RegistryInstallResponseSchema, RegistryPublishRequestSchema, RegistryPublishResponseSchema, UndeployRequestSchema, UndeployResponseSchema, DeployRecordQuerySchema, DeployRecordResponseSchema, WorkflowAnalyzeRequestSchema, WorkflowAnalyzeResponseSchema, } from "./schemas.js";
20
+ import { InventorySchema, GemSchema, GemRequestSchema, DirQuerySchema, PickQuerySchema, PickFolderSchema, ScaffoldChecksRequestSchema, ScaffoldChecksResponseSchema, MaterializeRequestSchema, MaterializeResponseSchema, PublishPreviewRequestSchema, PublishRequestSchema, PublishPreviewResponseSchema, PublishReadyResponseSchema, PublishResultSchema, DeployTargetsResponseSchema, DeployReadyQuerySchema, ArchiveRequestSchema, ArchiveResponseSchema, CreateWorkspaceRequestSchema, WorkspaceQuerySchema, RenderRequestSchema, WorkspaceNameRequestSchema, WorkspaceSummarySchema, WorkspaceDetailSchema, RenderResultSchema, ListWorkspacesResponseSchema, DeleteWorkspaceResponseSchema, RunReadyQuerySchema, RunReadyResponseSchema, RunRequestSchema, RunStatusQuerySchema, RunStateSchema, RunStopRequestSchema, RunStopResponseSchema, CredentialRequestSchema, CredentialResponseSchema, TestbedDetectQuerySchema, TestbedDetectResponseSchema, TestbedSuggestionQuerySchema, TestbedSuggestionResponseSchema, TestbedRecentsResponseSchema, TestbedProjectsQuerySchema, TestbedProjectsResponseSchema, TestbedScaffoldRequestSchema, TestbedScaffoldResponseSchema, TestbedImportRequestSchema, TestbedImportResponseSchema, AgentcoreReadyResponseSchema, AgentcoreDeployRequestSchema, AgentcoreStatusQuerySchema, AgentcoreDeployStateSchema, RegistryReadyResponseSchema, RegistryIndexResponseSchema, RegistrySearchQuerySchema, RegistrySearchResponseSchema, RegistryResolveRequestSchema, RegistryResolveResponseSchema, RegistryInstallRequestSchema, RegistryInstallResponseSchema, RegistryPublishRequestSchema, RegistryPublishResponseSchema, UndeployRequestSchema, UndeployResponseSchema, DeployRecordQuerySchema, DeployRecordResponseSchema, WorkflowAnalyzeRequestSchema, WorkflowAnalyzeResponseSchema, DistilledSkillSchema, WorkflowDraftWriteResponseSchema, GemRunRequestSchema, GemRunResponseSchema, GemRunPrepareRequestSchema, GemRunPrepareResponseSchema, } from "./schemas.js";
19
21
  import { claudeTranscriptsForCwd, scanWorkflow } from "./gem/workflowScan.js";
20
22
  import { recommendWorkflow, recommendationToSelection } from "./gem/acpRecommender.js";
23
+ import { distillWorkflow } from "./gem/distill.js";
24
+ import { writeDistilledDraft, stageDraftsByEvidence } from "./gem/draftStage.js";
21
25
  import { runReadiness, startLocal, stopLocal, getRunStatus, deployVercel, deployCloudflare, undeployVercel, undeployCloudflare } from "./gem/run.js";
22
26
  import { setCredential } from "./gem/credentials.js";
23
27
  import { agentcoreReadiness, deployAgentcore, getAgentcoreStatus } from "./gem/agentcoreRun.js";
24
28
  import { scaffoldTestbed, importArtifacts } from "./gem/testbed.js";
29
+ import { materializeAndRunGem, materializeGemToTestbed, registerRun, AGENT_ADAPTERS } from "./gem/runGem.js";
25
30
  import { detectFlavor, suggestTestbed, discoverProjects } from "./gem/testbedFlavors.js";
26
31
  import { readRecents, upsertRecent } from "./gem/recents.js";
27
32
  import { resolveInstall, publishGem } from "./gem/registry.js";
33
+ import { searchIndex } from "./gem/search.js";
28
34
  import { githubRegistrySource, githubRegistryPublisher, registryConfigFromEnv, registryReady } from "./gem/registryGithub.js";
29
35
  import { resolveDirs, resolveProject, agentgemHome } from "./resolveDir.js";
30
36
  import { pickFolder } from "./pickFolder.js";
37
+ // Server-derived run directory for a Gem. NEVER taken from client input: a caller-controlled path is
38
+ // a path-injection sink, and the ACP agent then runs there with tool permissions. The gem name is
39
+ // sanitized to one path segment; the sanitizer keeps '.', so a name of ".." must not escape — we
40
+ // assert the resolved path stays inside the runs root.
41
+ function deriveRunDir(gemName) {
42
+ let safeName = gemName.replace(/[^A-Za-z0-9._-]/g, "-");
43
+ if (safeName === "" || safeName === "." || safeName === "..")
44
+ safeName = "gem";
45
+ const runsRoot = resolve(agentgemHome(), ".agentgem", "runs");
46
+ const runDir = resolve(runsRoot, safeName);
47
+ if (!runDir.startsWith(runsRoot + sep))
48
+ throw new Error("derived run dir escaped the runs root");
49
+ return runDir;
50
+ }
31
51
  let GemController = class GemController {
32
52
  async inventory(input) {
33
53
  return introspectAll(input.query.dir, parseProjectsQuery(input.query.projects));
34
54
  }
35
55
  async gem(input) {
36
56
  const dirs = resolveDirs(input.body.dir);
37
- const inventory = introspectAll(input.body.dir, input.body.projects);
57
+ // Fold any accepted distilled drafts into the inventory (by evidence.root)
58
+ // before resolution, so a selection can reference one by name (proposal §7b).
59
+ const inventory = stageDraftsByEvidence(introspectAll(input.body.dir, input.body.projects), input.body.distilledDrafts ?? []);
38
60
  return buildGem(inventory, input.body.selection, {
39
61
  name: input.body.name ?? "gem",
40
62
  createdFrom: dirs.claudeDir,
41
63
  checks: input.body.checks,
64
+ channels: input.body.channels,
42
65
  });
43
66
  }
44
67
  async scaffoldChecks(input) {
45
68
  const dirs = resolveDirs(input.body.dir);
46
- const inventory = introspectAll(input.body.dir, input.body.projects);
69
+ const inventory = stageDraftsByEvidence(introspectAll(input.body.dir, input.body.projects), input.body.distilledDrafts ?? []);
47
70
  const gem = buildGem(inventory, input.body.selection, { name: input.body.name ?? "gem", createdFrom: dirs.claudeDir });
48
71
  return { checks: scaffoldChecks(gem) };
49
72
  }
50
73
  async materialize(input) {
51
74
  const target = input.body.target;
52
75
  let gem;
53
- if (input.body.archivePath) {
76
+ if (input.body.gemPath || input.body.gemUrl) {
77
+ const bytes = input.body.gemUrl
78
+ ? await fetchGemBytes(input.body.gemUrl) // SSRF-guarded: rejects non-public hosts
79
+ : readFileSync(input.body.gemPath);
80
+ gem = importGem(bytes).gem; // unpack + verify gem.lock; throws on tampering
81
+ }
82
+ else if (input.body.archivePath) {
54
83
  gem = readGemArchive(readArchiveDir(input.body.archivePath));
55
84
  }
56
85
  else {
57
86
  const dirs = resolveDirs(input.body.dir);
58
87
  const inventory = introspectAll(input.body.dir, input.body.projects);
59
- gem = buildGem(inventory, input.body.selection, { name: input.body.name ?? "gem", createdFrom: dirs.claudeDir });
88
+ gem = buildGem(inventory, input.body.selection, { name: input.body.name ?? "gem", createdFrom: dirs.claudeDir, channels: input.body.channels });
60
89
  }
61
90
  return { target, ...materialize(gem, target, { a2aServer: input.body.a2aServer }), compatibility: compatibility(gem) };
62
91
  }
63
92
  async archive(input) {
64
93
  const dirs = resolveDirs(input.body.dir);
65
94
  const inventory = introspectAll(input.body.dir, input.body.projects);
66
- const gem = buildGem(inventory, input.body.selection, { name: input.body.name ?? "gem", createdFrom: dirs.claudeDir });
95
+ const gem = buildGem(inventory, input.body.selection, { name: input.body.name ?? "gem", createdFrom: dirs.claudeDir, channels: input.body.channels });
67
96
  const { files, skipped } = writeGemArchive(gem, { version: input.body.version });
68
97
  const lock = JSON.parse(files["gem.lock"]);
69
98
  let path = null;
@@ -71,13 +100,18 @@ let GemController = class GemController {
71
100
  writeArchiveDir(input.body.outDir, files);
72
101
  path = input.body.outDir;
73
102
  }
103
+ let gemFile = null;
104
+ if (input.body.outFile) {
105
+ writeFileSync(input.body.outFile, packTar(files));
106
+ gemFile = input.body.outFile;
107
+ }
74
108
  const tarGz = input.body.tar ? packTar(files).toString("base64") : null;
75
- return { files, lock, skipped, path, tarGz };
109
+ return { files, lock, skipped, path, gemFile, tarGz };
76
110
  }
77
111
  async createWorkspace(input) {
78
112
  const dirs = resolveDirs(input.body.dir);
79
113
  const inventory = introspectAll(input.body.dir, input.body.projects);
80
- const gem = buildGem(inventory, input.body.selection, { name: input.body.name, createdFrom: dirs.claudeDir });
114
+ const gem = buildGem(inventory, input.body.selection, { name: input.body.name, createdFrom: dirs.claudeDir, channels: input.body.channels });
81
115
  return createWorkspace(input.body.name, gem, { version: input.body.version });
82
116
  }
83
117
  async listWorkspaces(_input) {
@@ -87,7 +121,7 @@ let GemController = class GemController {
87
121
  return readWorkspace(input.query.name);
88
122
  }
89
123
  async renderWorkspace(input) {
90
- return renderTarget(input.body.name, input.body.target);
124
+ return renderTarget(input.body.name, input.body.target, { a2aServer: input.body.a2aServer });
91
125
  }
92
126
  async deleteWorkspace(input) {
93
127
  deleteWorkspace(input.body.name);
@@ -132,7 +166,7 @@ let GemController = class GemController {
132
166
  async publishPreview(input) {
133
167
  const dirs = resolveDirs(input.body.dir);
134
168
  const inventory = introspectAll(input.body.dir, input.body.projects);
135
- const gem = buildGem(inventory, input.body.selection, { name: input.body.name ?? "gem", createdFrom: dirs.claudeDir });
169
+ const gem = buildGem(inventory, input.body.selection, { name: input.body.name ?? "gem", createdFrom: dirs.claudeDir, channels: input.body.channels });
136
170
  const target = (input.body.target ?? "claude-managed");
137
171
  return DEPLOY_REGISTRY[target].preview(gem);
138
172
  }
@@ -146,7 +180,7 @@ let GemController = class GemController {
146
180
  async publish(input) {
147
181
  const dirs = resolveDirs(input.body.dir);
148
182
  const inventory = introspectAll(input.body.dir, input.body.projects);
149
- const gem = buildGem(inventory, input.body.selection, { name: input.body.name ?? "gem", createdFrom: dirs.claudeDir });
183
+ const gem = buildGem(inventory, input.body.selection, { name: input.body.name ?? "gem", createdFrom: dirs.claudeDir, channels: input.body.channels });
150
184
  const target = (input.body.target ?? "claude-managed");
151
185
  const result = await DEPLOY_REGISTRY[target].deploy(gem, input.body.requestId);
152
186
  if (input.body.wsName) {
@@ -229,6 +263,32 @@ let GemController = class GemController {
229
263
  const rawInv = introspectConfig({ ...resolveDirs(input.body.dir), redact: false });
230
264
  return importArtifacts(resolveProject(input.body.root), input.body.selection, rawInv, (input.body.flavor ?? "claude"));
231
265
  }
266
+ // Test-run a Gem from a .gem archive with a locally-installed ACP coding agent:
267
+ // materialize into a runnable testbed dir, drive the agent against the task,
268
+ // and (when expectations are given) attach a verification report.
269
+ async runGem(input) {
270
+ const b = input.body;
271
+ const gem = b.archivePath
272
+ ? readGemArchive(readArchiveDir(b.archivePath))
273
+ : buildGem(introspectAll(b.dir, b.projects), b.selection, { name: b.name ?? "gem", createdFrom: resolveDirs(b.dir).claudeDir });
274
+ const agent = (b.agent ?? "claude");
275
+ const runDir = deriveRunDir(gem.name);
276
+ const out = await materializeAndRunGem({ gem, dir: runDir, task: b.task, agent, expectations: b.expectations });
277
+ return { dir: runDir, agent: out.agent, materialized: out.materialized, run: out.run, verification: out.verification };
278
+ }
279
+ // Step 1 of the streaming flow: materialize the Gem (carries the full selection
280
+ // over POST) and hand back an opaque runId. GET /api/gem/run/stream then runs it.
281
+ async prepareGemRun(input) {
282
+ const b = input.body;
283
+ const gem = b.archivePath
284
+ ? readGemArchive(readArchiveDir(b.archivePath))
285
+ : buildGem(introspectAll(b.dir, b.projects), b.selection, { name: b.name ?? "gem", createdFrom: resolveDirs(b.dir).claudeDir });
286
+ const agent = (b.agent ?? "claude");
287
+ const runDir = deriveRunDir(gem.name);
288
+ const materialized = materializeGemToTestbed(gem, runDir, AGENT_ADAPTERS[agent].flavor);
289
+ const runId = registerRun(runDir, agent);
290
+ return { runId, runDir, agent, materialized };
291
+ }
232
292
  // Resolve the configured registry source, or throw a clear error the UI can surface.
233
293
  registrySource() {
234
294
  const cfg = registryConfigFromEnv();
@@ -242,15 +302,19 @@ let GemController = class GemController {
242
302
  async registryIndex(_input) {
243
303
  return this.registrySource().source.getIndex();
244
304
  }
305
+ async registrySearch(input) {
306
+ const index = await this.registrySource().source.getIndex();
307
+ return { results: searchIndex(index, input.query.q ?? "", { kind: input.query.kind, tag: input.query.tag, limit: input.query.limit }) };
308
+ }
245
309
  async registryResolve(input) {
246
310
  const { source } = this.registrySource();
247
- const { plan } = await resolveInstall({ refs: input.body.refs, mode: input.body.mode, target: input.body.target, source });
311
+ const { plan } = await resolveInstall({ refs: input.body.refs, mode: input.body.mode, target: input.body.target, source, a2aServer: input.body.a2aServer });
248
312
  return { plan };
249
313
  }
250
314
  // Apply: materialize into `dest`, or land the merged Gem in the workspace store.
251
315
  async registryInstall(input) {
252
316
  const { source } = this.registrySource();
253
- const { plan, gem } = await resolveInstall({ refs: input.body.refs, mode: input.body.mode, target: input.body.target, source });
317
+ const { plan, gem } = await resolveInstall({ refs: input.body.refs, mode: input.body.mode, target: input.body.target, source, a2aServer: input.body.a2aServer });
254
318
  if (input.body.mode === "materialize") {
255
319
  if (!input.body.dest)
256
320
  throw new Error("materialize mode requires `dest`");
@@ -270,6 +334,7 @@ let GemController = class GemController {
270
334
  return publishGem({
271
335
  gem, scope: input.body.scope, name: input.body.name, version: input.body.version,
272
336
  dependencies: input.body.dependencies, index, publisher: githubRegistryPublisher(cfg),
337
+ description: input.body.description, tags: input.body.tags,
273
338
  });
274
339
  }
275
340
  // Pop the OS-native folder picker and return the chosen absolute path (null if cancelled).
@@ -289,16 +354,31 @@ let GemController = class GemController {
289
354
  // The top-level inventory IS the global/plugin inventory; the project section
290
355
  // is namespaced separately. Scan + recommend over both.
291
356
  const scanInv = { project, global: { skills: inventory.skills, mcpServers: inventory.mcpServers, hooks: inventory.hooks } };
292
- const signal = scanWorkflow(paths, scanInv);
293
- const { analysis, degraded } = await recommendWorkflow(signal, scanInv);
357
+ const signal = scanWorkflow(paths, scanInv, { retainSequences: true });
358
+ // Selective recommendation + skill distillation run concurrently — both
359
+ // never throw, so wall-clock stays max(...) not sum (proposal §5).
360
+ const [{ analysis, degraded }, distill] = await Promise.all([
361
+ recommendWorkflow(signal, scanInv),
362
+ distillWorkflow(signal, scanInv),
363
+ ]);
294
364
  const candidates = analysis.candidates.map((c) => ({ ...c, selection: recommendationToSelection(c) }));
295
365
  return {
296
366
  candidates,
297
367
  gaps: analysis.gaps,
368
+ distilled: distill.distilled,
298
369
  signalSummary: { sessionsScanned: signal.sessions.scanned, spanDays: signal.sessions.spanDays, notes: signal.notes },
299
370
  degraded,
300
371
  };
301
372
  }
373
+ // Accept a distilled draft: persist it to .agentgem/distilled/<name>/SKILL.md for
374
+ // the user to review/promote (proposal §7) — NOT into .claude/skills/. The name is
375
+ // re-validated as a kebab slug here (defense in depth) since it composes a path.
376
+ async writeWorkflowDraft(input) {
377
+ const skill = input.body;
378
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(skill.name))
379
+ throw new Error(`invalid draft name '${skill.name}'`);
380
+ return { path: writeDistilledDraft(skill) };
381
+ }
302
382
  };
303
383
  __decorate([
304
384
  get("/inventory", { query: DirQuerySchema, response: InventorySchema }),
@@ -480,6 +560,18 @@ __decorate([
480
560
  __metadata("design:paramtypes", [Object]),
481
561
  __metadata("design:returntype", Promise)
482
562
  ], GemController.prototype, "importTestbed", null);
563
+ __decorate([
564
+ post("/gem/run", { body: GemRunRequestSchema, response: GemRunResponseSchema }),
565
+ __metadata("design:type", Function),
566
+ __metadata("design:paramtypes", [Object]),
567
+ __metadata("design:returntype", Promise)
568
+ ], GemController.prototype, "runGem", null);
569
+ __decorate([
570
+ post("/gem/run/prepare", { body: GemRunPrepareRequestSchema, response: GemRunPrepareResponseSchema }),
571
+ __metadata("design:type", Function),
572
+ __metadata("design:paramtypes", [Object]),
573
+ __metadata("design:returntype", Promise)
574
+ ], GemController.prototype, "prepareGemRun", null);
483
575
  __decorate([
484
576
  get("/registry/ready", { query: PickQuerySchema, response: RegistryReadyResponseSchema }),
485
577
  __metadata("design:type", Function),
@@ -492,6 +584,12 @@ __decorate([
492
584
  __metadata("design:paramtypes", [Object]),
493
585
  __metadata("design:returntype", Promise)
494
586
  ], GemController.prototype, "registryIndex", null);
587
+ __decorate([
588
+ get("/registry/search", { query: RegistrySearchQuerySchema, response: RegistrySearchResponseSchema }),
589
+ __metadata("design:type", Function),
590
+ __metadata("design:paramtypes", [Object]),
591
+ __metadata("design:returntype", Promise)
592
+ ], GemController.prototype, "registrySearch", null);
495
593
  __decorate([
496
594
  post("/registry/resolve", { body: RegistryResolveRequestSchema, response: RegistryResolveResponseSchema }),
497
595
  __metadata("design:type", Function),
@@ -522,6 +620,12 @@ __decorate([
522
620
  __metadata("design:paramtypes", [Object]),
523
621
  __metadata("design:returntype", Promise)
524
622
  ], GemController.prototype, "workflowAnalyze", null);
623
+ __decorate([
624
+ post("/workflow/draft", { body: DistilledSkillSchema, response: WorkflowDraftWriteResponseSchema }),
625
+ __metadata("design:type", Function),
626
+ __metadata("design:paramtypes", [Object]),
627
+ __metadata("design:returntype", Promise)
628
+ ], GemController.prototype, "writeWorkflowDraft", null);
525
629
  GemController = __decorate([
526
630
  api({ basePath: "/api" })
527
631
  ], GemController);
package/dist/gem.tools.js CHANGED
@@ -7,13 +7,20 @@ import { buildGem } from "./gem/buildGem.js";
7
7
  import { GemSelectionSchema } from "./schemas.js";
8
8
  import { resolveDirs, resolveProject } from "./resolveDir.js";
9
9
  import { resolveInstall, publishGem } from "./gem/registry.js";
10
+ import { searchIndex } from "./gem/search.js";
10
11
  import { githubRegistrySource, githubRegistryPublisher, registryConfigFromEnv } from "./gem/registryGithub.js";
11
12
  import { readWorkspace } from "./gem/workspaces.js";
12
13
  import { readGemArchive } from "./gem/archive.js";
14
+ import { exportGem, importGem } from "./gem/share.js";
15
+ import { fetchGemBytes } from "./gem/safeFetch.js";
16
+ import { readFileSync } from "node:fs";
13
17
  const InventoryInput = z.object({ dir: z.string().optional(), projects: z.array(z.string()).optional() });
14
18
  const GemInput = z.object({ selection: GemSelectionSchema, name: z.string().optional(), dir: z.string().optional(), projects: z.array(z.string()).optional() });
15
- const RegistryRefsInput = z.object({ refs: z.array(z.string()).min(1), mode: z.enum(["materialize", "workspace"]), target: z.string().optional() });
16
- const RegistryPublishInput = z.object({ workspace: z.string(), scope: z.string(), name: z.string().optional(), version: z.string(), dependencies: z.array(z.string()).optional() });
19
+ const RegistryRefsInput = z.object({ refs: z.array(z.string()).min(1), mode: z.enum(["materialize", "workspace"]), target: z.string().optional(), a2aServer: z.boolean().optional() });
20
+ const RegistryPublishInput = z.object({ workspace: z.string(), scope: z.string(), name: z.string().optional(), version: z.string(), dependencies: z.array(z.string()).optional(), description: z.string().optional(), tags: z.array(z.string()).optional() });
21
+ const RegistrySearchInput = z.object({ q: z.string().optional(), kind: z.string().optional(), tag: z.string().optional(), limit: z.number().int().positive().max(100).optional() });
22
+ const GemExportInput = z.object({ selection: GemSelectionSchema, name: z.string().optional(), version: z.string().optional(), dir: z.string().optional(), projects: z.array(z.string()).optional() });
23
+ const GemInstallInput = z.object({ gemUrl: z.string().optional(), gemPath: z.string().optional(), bytesBase64: z.string().optional() });
17
24
  function registrySourceOrThrow() {
18
25
  const cfg = registryConfigFromEnv();
19
26
  if (!cfg)
@@ -35,24 +42,41 @@ let GemTools = class GemTools {
35
42
  const dirs = resolveDirs(input.dir);
36
43
  return buildGem(introspectAll(input.dir, input.projects), input.selection, { name: input.name ?? "gem", createdFrom: dirs.claudeDir });
37
44
  }
45
+ async gemExport(input) {
46
+ const dirs = resolveDirs(input.dir);
47
+ const gem = buildGem(introspectAll(input.dir, input.projects), input.selection, { name: input.name ?? "gem", createdFrom: dirs.claudeDir });
48
+ const { filename, bytes, skipped } = exportGem(gem, { version: input.version });
49
+ return { filename, bytesBase64: bytes.toString("base64"), skipped };
50
+ }
51
+ async gemInstall(input) {
52
+ const bytes = input.gemUrl ? await fetchGemBytes(input.gemUrl)
53
+ : input.gemPath ? readFileSync(input.gemPath)
54
+ : input.bytesBase64 ? Buffer.from(input.bytesBase64, "base64")
55
+ : (() => { throw new Error("provide one of gemUrl, gemPath, or bytesBase64"); })();
56
+ return importGem(bytes);
57
+ }
38
58
  async registryIndex(_input) {
39
59
  return registrySourceOrThrow().source.getIndex();
40
60
  }
61
+ async registrySearch(input) {
62
+ const index = await registrySourceOrThrow().source.getIndex();
63
+ return { results: searchIndex(index, input.q ?? "", { kind: input.kind, tag: input.tag, limit: input.limit }) };
64
+ }
41
65
  async registryResolve(input) {
42
66
  const { source } = registrySourceOrThrow();
43
- const { plan } = await resolveInstall({ refs: input.refs, mode: input.mode, target: input.target, source });
67
+ const { plan } = await resolveInstall({ refs: input.refs, mode: input.mode, target: input.target, source, a2aServer: input.a2aServer });
44
68
  return plan;
45
69
  }
46
70
  async registryInstall(input) {
47
71
  const { source } = registrySourceOrThrow();
48
- const { plan, gem } = await resolveInstall({ refs: input.refs, mode: input.mode, target: input.target, source });
72
+ const { plan, gem } = await resolveInstall({ refs: input.refs, mode: input.mode, target: input.target, source, a2aServer: input.a2aServer });
49
73
  return { plan, gem };
50
74
  }
51
75
  async registryPublish(input) {
52
76
  const { cfg, source } = registrySourceOrThrow();
53
77
  const gem = readGemArchive(readWorkspace(input.workspace).files);
54
78
  const index = await source.getIndex();
55
- return publishGem({ gem, scope: input.scope, name: input.name, version: input.version, dependencies: input.dependencies, index, publisher: githubRegistryPublisher(cfg) });
79
+ return publishGem({ gem, scope: input.scope, name: input.name, version: input.version, dependencies: input.dependencies, index, publisher: githubRegistryPublisher(cfg), description: input.description, tags: input.tags });
56
80
  }
57
81
  };
58
82
  __decorate([
@@ -73,12 +97,36 @@ __decorate([
73
97
  __metadata("design:paramtypes", [Object]),
74
98
  __metadata("design:returntype", Promise)
75
99
  ], GemTools.prototype, "gem", null);
100
+ __decorate([
101
+ tool("gem_export", {
102
+ description: "Export a Gem (built from a selection of the local config) as a single portable .gem archive, returned base64-encoded. Share those bytes as a file/upload/gist; install elsewhere with gem_install. Secrets are redacted; no registry required.",
103
+ input: GemExportInput,
104
+ }),
105
+ __metadata("design:type", Function),
106
+ __metadata("design:paramtypes", [Object]),
107
+ __metadata("design:returntype", Promise)
108
+ ], GemTools.prototype, "gemExport", null);
109
+ __decorate([
110
+ tool("gem_install", {
111
+ description: "Read and verify a shared .gem from a URL, local file path, or base64 bytes — returning the lock-verified Gem and its manifest meta. URL fetches are SSRF-guarded; tampered archives are rejected. Disk placement is performed via the REST /materialize endpoint.",
112
+ input: GemInstallInput,
113
+ }),
114
+ __metadata("design:type", Function),
115
+ __metadata("design:paramtypes", [Object]),
116
+ __metadata("design:returntype", Promise)
117
+ ], GemTools.prototype, "gemInstall", null);
76
118
  __decorate([
77
119
  tool("registry_index", { description: "List the gems available in the configured registry (names, versions, dependencies).", input: z.object({}) }),
78
120
  __metadata("design:type", Function),
79
121
  __metadata("design:paramtypes", [Object]),
80
122
  __metadata("design:returntype", Promise)
81
123
  ], GemTools.prototype, "registryIndex", null);
124
+ __decorate([
125
+ tool("registry_search", { description: "Search the configured registry for gems by name/tags/description. Pass a `kind` (skill|mcp_server|instructions|hook) or `tag` to filter; an empty query browses the catalog. Returns ranked hits with latest version and description.", input: RegistrySearchInput }),
126
+ __metadata("design:type", Function),
127
+ __metadata("design:paramtypes", [Object]),
128
+ __metadata("design:returntype", Promise)
129
+ ], GemTools.prototype, "registrySearch", null);
82
130
  __decorate([
83
131
  tool("registry_resolve", { description: "Resolve registry refs into an install plan (items, artifacts, required secrets, and a materialize preview for a target). No writes.", input: RegistryRefsInput }),
84
132
  __metadata("design:type", Function),
@@ -0,0 +1,67 @@
1
+ // src/gemRunStream.ts
2
+ //
3
+ // SSE endpoint for running an already-prepared Gem with a local ACP coding agent.
4
+ // This is step 2 of the streaming flow: POST /api/gem/run/prepare materializes the
5
+ // Gem and returns an opaque runId; this GET streams the agent run for that runId
6
+ // (materializing → running → per-tool + token deltas → done). Splitting prepare
7
+ // (POST, carries the selection) from stream (GET, simple query params) lets the UI
8
+ // use native EventSource while keeping the run dir off the wire — the client only
9
+ // ever holds the opaque id, never a path it could redirect the agent to.
10
+ import { runGemWithAgent, hasTestConnectFn } from "./gem/acpRun.js";
11
+ import { resolveRun, resolveOrFetchAdapter, AGENT_ADAPTERS } from "./gem/runGem.js";
12
+ import { verifyGemRun } from "./gem/gemVerify.js";
13
+ const str = (v) => (typeof v === "string" ? v : "");
14
+ export async function streamGemRun(req, res) {
15
+ const runId = str(req.query.runId);
16
+ const task = str(req.query.task);
17
+ const expectTools = str(req.query.expectTools)
18
+ ? str(req.query.expectTools).split(",").map((s) => s.trim()).filter(Boolean)
19
+ : undefined;
20
+ const expectText = str(req.query.expectText) || undefined;
21
+ res.writeHead(200, {
22
+ "Content-Type": "text/event-stream",
23
+ "Cache-Control": "no-cache, no-transform",
24
+ Connection: "keep-alive",
25
+ "X-Accel-Buffering": "no",
26
+ });
27
+ const send = (event, data) => {
28
+ res.write(`event: ${event}\n`);
29
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
30
+ };
31
+ try {
32
+ const reg = resolveRun(runId);
33
+ if (!reg) {
34
+ send("failed", { message: "unknown or expired runId — prepare the run again" });
35
+ return;
36
+ }
37
+ if (!task) {
38
+ send("failed", { message: "missing task" });
39
+ return;
40
+ }
41
+ // Resolve the adapter (fetching on demand if needed), streaming a phase so a
42
+ // one-time download shows progress instead of a hang.
43
+ const adapter = AGENT_ADAPTERS[reg.agent];
44
+ const command = hasTestConnectFn()
45
+ ? [adapter.bin]
46
+ : await resolveOrFetchAdapter(adapter, {
47
+ onFetch: () => send("phase", { phase: "preparing-adapter", agent: reg.agent, pkg: adapter.pkg }),
48
+ });
49
+ send("phase", { phase: "running", agent: reg.agent });
50
+ const run = await runGemWithAgent({
51
+ dir: reg.dir,
52
+ task,
53
+ descriptor: { id: adapter.id, name: adapter.name, command },
54
+ onToolCall: (t) => send("tool", t),
55
+ onDelta: (c) => send("delta", { text: c }),
56
+ });
57
+ const expectations = expectTools || expectText ? { expectTools, expectText } : undefined;
58
+ const verification = expectations ? verifyGemRun(run, expectations) : undefined;
59
+ send("done", { runId, agent: reg.agent, run, verification });
60
+ }
61
+ catch (err) {
62
+ send("failed", { message: err?.message ?? String(err) });
63
+ }
64
+ finally {
65
+ res.end();
66
+ }
67
+ }
package/dist/index.js CHANGED
@@ -18,6 +18,8 @@ import { installMcpHttp } from "@agentback/mcp-http";
18
18
  import { GemController } from "./gem.controller.js";
19
19
  import { GemTools } from "./gem.tools.js";
20
20
  import { streamWorkflowAnalyze } from "./workflowStream.js";
21
+ import { streamGemRun } from "./gemRunStream.js";
22
+ import { originGuard } from "./originGuard.js";
21
23
  const here = dirname(fileURLToPath(import.meta.url));
22
24
  function pageHtml() {
23
25
  for (const p of [join(here, "public", "index.html"), join(here, "..", "src", "public", "index.html")]) {
@@ -35,6 +37,10 @@ export async function createApp(port) {
35
37
  app.configure("servers.MCPServer").to({ name: "agentgem", version: "0.1.0", transports: { stdio: false } });
36
38
  app.restController(GemController);
37
39
  app.service(GemTools);
40
+ // CSRF / drive-by guard: reject browser-initiated cross-site requests to the loopback API
41
+ // (controller routes). Same-origin UI and non-browser clients (CLI/MCP/tests) pass. Mounted in
42
+ // the framework middleware chain so it runs before controller dispatch.
43
+ app.expressMiddleware("middleware.originGuard", originGuard);
38
44
  await installExplorer(app, { title: "agentgem API" });
39
45
  await installMcpHttp(app);
40
46
  const server = await app.restServer;
@@ -42,8 +48,12 @@ export async function createApp(port) {
42
48
  server.expressApp.get("/", (_req, res) => res.type("html").send(html));
43
49
  // SSE progress stream for workflow analysis (raw Express — the decorator
44
50
  // framework only returns single JSON bodies). The POST /api/workflow/analyze
45
- // route stays for programmatic/test callers.
46
- server.expressApp.get("/api/workflow/analyze/stream", streamWorkflowAnalyze);
51
+ // route stays for programmatic/test callers. originGuard is applied per-route because these raw
52
+ // routes are registered directly on expressApp, outside the controller dispatch chain.
53
+ server.expressApp.get("/api/workflow/analyze/stream", originGuard, streamWorkflowAnalyze);
54
+ // SSE progress stream for running a Gem with a local ACP agent (materialize →
55
+ // run → tool/token deltas → done). POST /api/gem/run stays for programmatic callers.
56
+ server.expressApp.get("/api/gem/run/stream", originGuard, streamGemRun);
47
57
  return app;
48
58
  }
49
59
  // Start the server and print where its surfaces live. Shared by the default
@@ -0,0 +1,36 @@
1
+ // Methods with no side effect — a direct navigation (Sec-Fetch-Site: none) to one of these is benign.
2
+ const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
3
+ function block(res) {
4
+ res.status(403).type("application/json").send(JSON.stringify({ error: "cross-site request blocked" }));
5
+ }
6
+ export function originGuard(req, res, next) {
7
+ const site = req.get("sec-fetch-site");
8
+ if (site !== undefined) {
9
+ if (site === "same-origin") {
10
+ next();
11
+ return;
12
+ }
13
+ // "none" = user-initiated (typed URL, bookmark) — allow only for safe methods. A state-changing
14
+ // POST is never legitimately "none" from our same-origin UI (its fetch() sends same-origin), so
15
+ // requiring same-origin there closes the top-level-navigation/form-POST drive-by.
16
+ if (site === "none" && SAFE_METHODS.has(req.method.toUpperCase())) {
17
+ next();
18
+ return;
19
+ }
20
+ block(res);
21
+ return;
22
+ }
23
+ const origin = req.get("origin");
24
+ if (origin === undefined) {
25
+ next();
26
+ return;
27
+ } // non-browser client: no ambient browser context
28
+ try {
29
+ if (new URL(origin).host === req.get("host")) {
30
+ next();
31
+ return;
32
+ } // same-origin
33
+ }
34
+ catch { /* malformed Origin -> fall through to block */ }
35
+ block(res);
36
+ }