@ninemind/agentgem 0.1.1 → 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,56 +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, } 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";
21
+ import { claudeTranscriptsForCwd, scanWorkflow } from "./gem/workflowScan.js";
22
+ import { recommendWorkflow, recommendationToSelection } from "./gem/acpRecommender.js";
23
+ import { distillWorkflow } from "./gem/distill.js";
24
+ import { writeDistilledDraft, stageDraftsByEvidence } from "./gem/draftStage.js";
19
25
  import { runReadiness, startLocal, stopLocal, getRunStatus, deployVercel, deployCloudflare, undeployVercel, undeployCloudflare } from "./gem/run.js";
20
26
  import { setCredential } from "./gem/credentials.js";
21
27
  import { agentcoreReadiness, deployAgentcore, getAgentcoreStatus } from "./gem/agentcoreRun.js";
22
28
  import { scaffoldTestbed, importArtifacts } from "./gem/testbed.js";
29
+ import { materializeAndRunGem, materializeGemToTestbed, registerRun, AGENT_ADAPTERS } from "./gem/runGem.js";
23
30
  import { detectFlavor, suggestTestbed, discoverProjects } from "./gem/testbedFlavors.js";
24
31
  import { readRecents, upsertRecent } from "./gem/recents.js";
25
32
  import { resolveInstall, publishGem } from "./gem/registry.js";
33
+ import { searchIndex } from "./gem/search.js";
26
34
  import { githubRegistrySource, githubRegistryPublisher, registryConfigFromEnv, registryReady } from "./gem/registryGithub.js";
27
35
  import { resolveDirs, resolveProject, agentgemHome } from "./resolveDir.js";
28
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
+ }
29
51
  let GemController = class GemController {
30
52
  async inventory(input) {
31
53
  return introspectAll(input.query.dir, parseProjectsQuery(input.query.projects));
32
54
  }
33
55
  async gem(input) {
34
56
  const dirs = resolveDirs(input.body.dir);
35
- 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 ?? []);
36
60
  return buildGem(inventory, input.body.selection, {
37
61
  name: input.body.name ?? "gem",
38
62
  createdFrom: dirs.claudeDir,
39
63
  checks: input.body.checks,
64
+ channels: input.body.channels,
40
65
  });
41
66
  }
42
67
  async scaffoldChecks(input) {
43
68
  const dirs = resolveDirs(input.body.dir);
44
- const inventory = introspectAll(input.body.dir, input.body.projects);
69
+ const inventory = stageDraftsByEvidence(introspectAll(input.body.dir, input.body.projects), input.body.distilledDrafts ?? []);
45
70
  const gem = buildGem(inventory, input.body.selection, { name: input.body.name ?? "gem", createdFrom: dirs.claudeDir });
46
71
  return { checks: scaffoldChecks(gem) };
47
72
  }
48
73
  async materialize(input) {
49
74
  const target = input.body.target;
50
75
  let gem;
51
- 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) {
52
83
  gem = readGemArchive(readArchiveDir(input.body.archivePath));
53
84
  }
54
85
  else {
55
86
  const dirs = resolveDirs(input.body.dir);
56
87
  const inventory = introspectAll(input.body.dir, input.body.projects);
57
- 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 });
58
89
  }
59
- return { target, ...materialize(gem, target), compatibility: compatibility(gem) };
90
+ return { target, ...materialize(gem, target, { a2aServer: input.body.a2aServer }), compatibility: compatibility(gem) };
60
91
  }
61
92
  async archive(input) {
62
93
  const dirs = resolveDirs(input.body.dir);
63
94
  const inventory = introspectAll(input.body.dir, input.body.projects);
64
- 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 });
65
96
  const { files, skipped } = writeGemArchive(gem, { version: input.body.version });
66
97
  const lock = JSON.parse(files["gem.lock"]);
67
98
  let path = null;
@@ -69,13 +100,18 @@ let GemController = class GemController {
69
100
  writeArchiveDir(input.body.outDir, files);
70
101
  path = input.body.outDir;
71
102
  }
103
+ let gemFile = null;
104
+ if (input.body.outFile) {
105
+ writeFileSync(input.body.outFile, packTar(files));
106
+ gemFile = input.body.outFile;
107
+ }
72
108
  const tarGz = input.body.tar ? packTar(files).toString("base64") : null;
73
- return { files, lock, skipped, path, tarGz };
109
+ return { files, lock, skipped, path, gemFile, tarGz };
74
110
  }
75
111
  async createWorkspace(input) {
76
112
  const dirs = resolveDirs(input.body.dir);
77
113
  const inventory = introspectAll(input.body.dir, input.body.projects);
78
- 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 });
79
115
  return createWorkspace(input.body.name, gem, { version: input.body.version });
80
116
  }
81
117
  async listWorkspaces(_input) {
@@ -85,7 +121,7 @@ let GemController = class GemController {
85
121
  return readWorkspace(input.query.name);
86
122
  }
87
123
  async renderWorkspace(input) {
88
- return renderTarget(input.body.name, input.body.target);
124
+ return renderTarget(input.body.name, input.body.target, { a2aServer: input.body.a2aServer });
89
125
  }
90
126
  async deleteWorkspace(input) {
91
127
  deleteWorkspace(input.body.name);
@@ -130,7 +166,7 @@ let GemController = class GemController {
130
166
  async publishPreview(input) {
131
167
  const dirs = resolveDirs(input.body.dir);
132
168
  const inventory = introspectAll(input.body.dir, input.body.projects);
133
- 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 });
134
170
  const target = (input.body.target ?? "claude-managed");
135
171
  return DEPLOY_REGISTRY[target].preview(gem);
136
172
  }
@@ -144,7 +180,7 @@ let GemController = class GemController {
144
180
  async publish(input) {
145
181
  const dirs = resolveDirs(input.body.dir);
146
182
  const inventory = introspectAll(input.body.dir, input.body.projects);
147
- 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 });
148
184
  const target = (input.body.target ?? "claude-managed");
149
185
  const result = await DEPLOY_REGISTRY[target].deploy(gem, input.body.requestId);
150
186
  if (input.body.wsName) {
@@ -227,6 +263,32 @@ let GemController = class GemController {
227
263
  const rawInv = introspectConfig({ ...resolveDirs(input.body.dir), redact: false });
228
264
  return importArtifacts(resolveProject(input.body.root), input.body.selection, rawInv, (input.body.flavor ?? "claude"));
229
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
+ }
230
292
  // Resolve the configured registry source, or throw a clear error the UI can surface.
231
293
  registrySource() {
232
294
  const cfg = registryConfigFromEnv();
@@ -240,15 +302,19 @@ let GemController = class GemController {
240
302
  async registryIndex(_input) {
241
303
  return this.registrySource().source.getIndex();
242
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
+ }
243
309
  async registryResolve(input) {
244
310
  const { source } = this.registrySource();
245
- 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 });
246
312
  return { plan };
247
313
  }
248
314
  // Apply: materialize into `dest`, or land the merged Gem in the workspace store.
249
315
  async registryInstall(input) {
250
316
  const { source } = this.registrySource();
251
- 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 });
252
318
  if (input.body.mode === "materialize") {
253
319
  if (!input.body.dest)
254
320
  throw new Error("materialize mode requires `dest`");
@@ -268,12 +334,51 @@ let GemController = class GemController {
268
334
  return publishGem({
269
335
  gem, scope: input.body.scope, name: input.body.name, version: input.body.version,
270
336
  dependencies: input.body.dependencies, index, publisher: githubRegistryPublisher(cfg),
337
+ description: input.body.description, tags: input.body.tags,
271
338
  });
272
339
  }
273
340
  // Pop the OS-native folder picker and return the chosen absolute path (null if cancelled).
274
341
  async pickFolder(_input) {
275
342
  return { path: await pickFolder() };
276
343
  }
344
+ async workflowAnalyze(input) {
345
+ const { dir, root } = input.body;
346
+ // Inventory for exactly this one project (project-namespaced selection target).
347
+ const inventory = introspectAll(dir, [root]);
348
+ // introspectAll canonicalizes roots via resolveProject (path.resolve); match the same way.
349
+ const project = (inventory.projects ?? []).find((p) => p.root === resolveProject(root));
350
+ if (!project)
351
+ throw new Error(`Project '${root}' not found in inventory`);
352
+ const dirs = resolveDirs(dir);
353
+ const paths = claudeTranscriptsForCwd(dirs.claudeDir, root);
354
+ // The top-level inventory IS the global/plugin inventory; the project section
355
+ // is namespaced separately. Scan + recommend over both.
356
+ const scanInv = { project, global: { skills: inventory.skills, mcpServers: inventory.mcpServers, hooks: inventory.hooks } };
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
+ ]);
364
+ const candidates = analysis.candidates.map((c) => ({ ...c, selection: recommendationToSelection(c) }));
365
+ return {
366
+ candidates,
367
+ gaps: analysis.gaps,
368
+ distilled: distill.distilled,
369
+ signalSummary: { sessionsScanned: signal.sessions.scanned, spanDays: signal.sessions.spanDays, notes: signal.notes },
370
+ degraded,
371
+ };
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
+ }
277
382
  };
278
383
  __decorate([
279
384
  get("/inventory", { query: DirQuerySchema, response: InventorySchema }),
@@ -455,6 +560,18 @@ __decorate([
455
560
  __metadata("design:paramtypes", [Object]),
456
561
  __metadata("design:returntype", Promise)
457
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);
458
575
  __decorate([
459
576
  get("/registry/ready", { query: PickQuerySchema, response: RegistryReadyResponseSchema }),
460
577
  __metadata("design:type", Function),
@@ -467,6 +584,12 @@ __decorate([
467
584
  __metadata("design:paramtypes", [Object]),
468
585
  __metadata("design:returntype", Promise)
469
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);
470
593
  __decorate([
471
594
  post("/registry/resolve", { body: RegistryResolveRequestSchema, response: RegistryResolveResponseSchema }),
472
595
  __metadata("design:type", Function),
@@ -491,6 +614,18 @@ __decorate([
491
614
  __metadata("design:paramtypes", [Object]),
492
615
  __metadata("design:returntype", Promise)
493
616
  ], GemController.prototype, "pickFolder", null);
617
+ __decorate([
618
+ post("/workflow/analyze", { body: WorkflowAnalyzeRequestSchema, response: WorkflowAnalyzeResponseSchema }),
619
+ __metadata("design:type", Function),
620
+ __metadata("design:paramtypes", [Object]),
621
+ __metadata("design:returntype", Promise)
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);
494
629
  GemController = __decorate([
495
630
  api({ basePath: "/api" })
496
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
@@ -17,6 +17,9 @@ import { MCPComponent } from "@agentback/mcp";
17
17
  import { installMcpHttp } from "@agentback/mcp-http";
18
18
  import { GemController } from "./gem.controller.js";
19
19
  import { GemTools } from "./gem.tools.js";
20
+ import { streamWorkflowAnalyze } from "./workflowStream.js";
21
+ import { streamGemRun } from "./gemRunStream.js";
22
+ import { originGuard } from "./originGuard.js";
20
23
  const here = dirname(fileURLToPath(import.meta.url));
21
24
  function pageHtml() {
22
25
  for (const p of [join(here, "public", "index.html"), join(here, "..", "src", "public", "index.html")]) {
@@ -34,11 +37,23 @@ export async function createApp(port) {
34
37
  app.configure("servers.MCPServer").to({ name: "agentgem", version: "0.1.0", transports: { stdio: false } });
35
38
  app.restController(GemController);
36
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);
37
44
  await installExplorer(app, { title: "agentgem API" });
38
45
  await installMcpHttp(app);
39
46
  const server = await app.restServer;
40
47
  const html = pageHtml();
41
48
  server.expressApp.get("/", (_req, res) => res.type("html").send(html));
49
+ // SSE progress stream for workflow analysis (raw Express — the decorator
50
+ // framework only returns single JSON bodies). The POST /api/workflow/analyze
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);
42
57
  return app;
43
58
  }
44
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
+ }