@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.
- package/README.md +26 -0
- package/dist/gem/acpRecommender.js +259 -0
- package/dist/gem/acpRun.js +156 -0
- package/dist/gem/acpSession.js +79 -0
- package/dist/gem/analysisCache.js +55 -0
- package/dist/gem/archive.js +17 -0
- package/dist/gem/binPath.js +9 -0
- package/dist/gem/buildGem.js +4 -1
- package/dist/gem/channels.js +29 -0
- package/dist/gem/credentials.js +3 -2
- package/dist/gem/distill.js +162 -0
- package/dist/gem/draftStage.js +77 -0
- package/dist/gem/gemVerify.js +35 -0
- package/dist/gem/inputError.js +21 -0
- package/dist/gem/registry.js +23 -4
- package/dist/gem/runGem.js +161 -0
- package/dist/gem/safeFetch.js +112 -0
- package/dist/gem/sandbox.js +37 -0
- package/dist/gem/sandboxLaunch.js +55 -0
- package/dist/gem/scrub.js +108 -0
- package/dist/gem/search.js +34 -0
- package/dist/gem/share.js +21 -0
- package/dist/gem/targets.js +280 -16
- package/dist/gem/testbedFlavors.js +1 -0
- package/dist/gem/workflowScan.js +0 -0
- package/dist/gem/workspaces.js +4 -3
- package/dist/gem.controller.js +151 -16
- package/dist/gem.tools.js +53 -5
- package/dist/gemRunStream.js +67 -0
- package/dist/index.js +15 -0
- package/dist/originGuard.js +36 -0
- package/dist/public/index.html +444 -10
- package/dist/schemas.js +180 -7
- package/dist/workflowStream.js +78 -0
- package/package.json +7 -2
package/dist/gem.controller.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
+
}
|