@ninemind/agentgem 0.1.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.
@@ -0,0 +1,130 @@
1
+ // src/publish.ts
2
+ // Network publish: render a Gem, register each skill as a custom Agent Skill (Skills API), then
3
+ // create the agent referencing those skills. The PublishClient is injected so the orchestration is
4
+ // unit-tested without a key or network. Only confirmed SDK bindings are used.
5
+ import Anthropic, { toFile } from "@anthropic-ai/sdk";
6
+ import { renderManagedAgent } from "./gem/publish.js";
7
+ const PUBLISH_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
8
+ const publishRequests = new Map();
9
+ export function publishManagedAgentOnce(requestId, fingerprint, publish) {
10
+ const now = Date.now();
11
+ for (const [id, entry] of publishRequests)
12
+ if (entry.expiresAt <= now)
13
+ publishRequests.delete(id);
14
+ const existing = publishRequests.get(requestId);
15
+ if (existing) {
16
+ if (existing.fingerprint !== fingerprint)
17
+ return Promise.reject(new Error("publish requestId was reused with a different payload"));
18
+ return existing.promise;
19
+ }
20
+ const promise = publish().catch((error) => {
21
+ publishRequests.delete(requestId);
22
+ throw error;
23
+ });
24
+ publishRequests.set(requestId, { fingerprint, expiresAt: now + PUBLISH_CACHE_TTL_MS, promise });
25
+ const timer = setTimeout(() => {
26
+ if (publishRequests.get(requestId)?.promise === promise)
27
+ publishRequests.delete(requestId);
28
+ }, PUBLISH_CACHE_TTL_MS);
29
+ timer.unref();
30
+ return promise;
31
+ }
32
+ export async function publishManagedAgent(gem, client) {
33
+ const render = renderManagedAgent(gem);
34
+ const registeredSkills = [];
35
+ let environmentId;
36
+ try {
37
+ for (const s of render.skillsToRegister) {
38
+ const { skillId, version } = await client.createSkill(s.name, s.content);
39
+ registeredSkills.push({ name: s.name, skillId, version });
40
+ }
41
+ environmentId = (await client.createEnvironment(`${gem.name} sandbox`)).id;
42
+ const skills = registeredSkills.map((r) => ({ type: "custom", skill_id: r.skillId, version: r.version }));
43
+ const agent = await client.createAgent({ ...render.payload, skills });
44
+ return { agentId: agent.id, environmentId, version: agent.version, registeredSkills, skipped: render.skipped, vaultSecrets: render.vaultSecrets };
45
+ }
46
+ catch (error) {
47
+ const cleanupErrors = [];
48
+ if (environmentId) {
49
+ try {
50
+ await client.deleteEnvironment(environmentId);
51
+ }
52
+ catch (cleanupError) {
53
+ cleanupErrors.push(new Error(`failed to delete environment ${environmentId}`, { cause: cleanupError }));
54
+ }
55
+ }
56
+ const skillCleanup = await Promise.allSettled(registeredSkills.map((s) => client.deleteSkill(s.skillId)));
57
+ skillCleanup.forEach((result, index) => {
58
+ if (result.status === "rejected")
59
+ cleanupErrors.push(new Error(`failed to delete skill ${registeredSkills[index].skillId}`, { cause: result.reason }));
60
+ });
61
+ if (cleanupErrors.length)
62
+ throw new AggregateError([error, ...cleanupErrors], "publish failed and rollback was incomplete");
63
+ throw error;
64
+ }
65
+ }
66
+ // Tear down a managed-agent deploy: delete the agent, its environment, then each registered skill.
67
+ // Each delete is independent — a failure in one does NOT abort the others (so an already-gone
68
+ // resource doesn't strand the rest) — but real failures are collected and rethrown so the caller
69
+ // (and the user) learns the teardown was partial. Mirrors publishManagedAgent's rollback contract.
70
+ export async function undeployManagedAgent(rec, client) {
71
+ const errors = [];
72
+ const attempt = async (what, fn) => {
73
+ try {
74
+ await fn();
75
+ }
76
+ catch (cause) {
77
+ errors.push(new Error(`failed to delete ${what}`, { cause }));
78
+ }
79
+ };
80
+ if (rec.agentId)
81
+ await attempt(`agent ${rec.agentId}`, async () => {
82
+ if (!client.deleteAgent)
83
+ throw new Error("client does not support deleting agents");
84
+ await client.deleteAgent(rec.agentId);
85
+ });
86
+ if (rec.environmentId)
87
+ await attempt(`environment ${rec.environmentId}`, () => client.deleteEnvironment(rec.environmentId));
88
+ for (const sid of rec.skillIds ?? [])
89
+ await attempt(`skill ${sid}`, () => client.deleteSkill(sid));
90
+ if (errors.length)
91
+ throw new AggregateError(errors, `undeploy completed with ${errors.length} failure(s)`);
92
+ }
93
+ const safeUploadDirectory = (name) => {
94
+ const safe = name.normalize("NFKC").replace(/[^A-Za-z0-9._-]/g, "_");
95
+ return safe === "." || safe === ".." || safe.length === 0 ? "skill" : safe;
96
+ };
97
+ // Real client. skills.create uploads a single SKILL.md under a top-level dir named for the skill;
98
+ // the API extracts name/description from it and returns the skill id + latest_version. The SDK sets
99
+ // the skills-2025-10-02 / managed-agents-2026-04-01 beta headers automatically.
100
+ export function anthropicPublishClient(apiKey) {
101
+ const client = new Anthropic({ apiKey });
102
+ return {
103
+ async createSkill(name, skillMd) {
104
+ const file = await toFile(Buffer.from(skillMd), `${safeUploadDirectory(name)}/SKILL.md`);
105
+ const created = await client.beta.skills.create({ display_title: name, files: [file] });
106
+ return { skillId: created.id, version: created.latest_version ?? "latest" };
107
+ },
108
+ async deleteSkill(skillId) { await client.beta.skills.delete(skillId); },
109
+ async createEnvironment(name) {
110
+ const environment = await client.beta.environments.create({
111
+ name,
112
+ description: "Managed sandbox created by agentgem",
113
+ config: { type: "cloud", networking: { type: "limited", allow_mcp_servers: true, allow_package_managers: false, allowed_hosts: [] } },
114
+ });
115
+ return { id: environment.id };
116
+ },
117
+ async deleteEnvironment(environmentId) { await client.beta.environments.delete(environmentId); },
118
+ async createAgent(payload) {
119
+ const agent = await client.beta.agents.create(payload);
120
+ return { id: agent.id, version: String(agent.version ?? "") };
121
+ },
122
+ // NOTE: @anthropic-ai/sdk does not expose client.beta.agents.delete() as of this writing.
123
+ // The agents resource only has create/retrieve/update/list/archive. We fall back to the
124
+ // low-level client.delete() which sends a raw DELETE to /v1/agents/{id} with the required
125
+ // managed-agents-2026-04-01 beta header.
126
+ async deleteAgent(agentId) {
127
+ await client.delete(`/v1/agents/${agentId}`, { headers: { "anthropic-beta": "managed-agents-2026-04-01" } });
128
+ },
129
+ };
130
+ }
@@ -0,0 +1,26 @@
1
+ // src/resolveDir.ts
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, resolve } from "node:path";
4
+ export function resolveDir(dir) {
5
+ return dir && dir.length > 0 ? dir : join(homedir(), ".claude");
6
+ }
7
+ // Normalize a project root to an absolute path. The path comes from the OS-native folder
8
+ // picker (explicit user selection), so it is trusted; we only canonicalize it.
9
+ export function resolveProject(p) {
10
+ return resolve(p);
11
+ }
12
+ // Derive every discovery root from one base. In production `dir` is undefined, so the
13
+ // claude dir is ~/.claude and its parent is the home dir — giving ~/.agents/skills and
14
+ // ~/.codex. When `dir` is overridden (tests, non-default homes) the agent/codex roots
15
+ // resolve relative to that same parent, keeping introspection self-contained.
16
+ export function resolveDirs(dir) {
17
+ const claudeDir = resolveDir(dir);
18
+ const home = dirname(claudeDir);
19
+ return { claudeDir, agentDir: join(home, ".agents", "skills"), codexDir: join(home, ".codex"), hermesDir: join(home, ".hermes") };
20
+ }
21
+ // Base dir for agentgem's own state (recents, etc.): ~/.agentgem lives under this.
22
+ // AGENTGEM_HOME overrides the home root for tests / non-default setups.
23
+ export function agentgemHome() {
24
+ const override = process.env.AGENTGEM_HOME;
25
+ return override && override.length > 0 ? override : homedir();
26
+ }
@@ -0,0 +1,407 @@
1
+ // src/schemas.ts
2
+ import { z } from "zod";
3
+ import { RUNNER_REGISTRY } from "./gem/checks.js";
4
+ import { TARGET_REGISTRY } from "./gem/targets.js";
5
+ import { deployTargetIds } from "./gem/deploy.js";
6
+ import { flavorIds } from "./gem/testbedFlavors.js";
7
+ import { CREDENTIAL_KEYS } from "./gem/credentials.js";
8
+ export const SkillArtifactSchema = z.object({
9
+ type: z.literal("skill"),
10
+ name: z.string(),
11
+ description: z.string().optional(),
12
+ source: z.string(),
13
+ content: z.string(),
14
+ });
15
+ export const McpServerArtifactSchema = z.object({
16
+ type: z.literal("mcp_server"),
17
+ name: z.string(),
18
+ transport: z.enum(["stdio", "http", "sse"]),
19
+ config: z.record(z.string(), z.unknown()),
20
+ source: z.string().optional(),
21
+ secretRefs: z.array(z.object({ name: z.string(), location: z.string() })).optional(),
22
+ });
23
+ export const InstructionsArtifactSchema = z.object({
24
+ type: z.literal("instructions"),
25
+ name: z.string(),
26
+ content: z.string(),
27
+ });
28
+ export const HookArtifactSchema = z.object({
29
+ type: z.literal("hook"),
30
+ name: z.string(),
31
+ event: z.string(),
32
+ matcher: z.string().optional(),
33
+ config: z.record(z.string(), z.unknown()),
34
+ source: z.string().optional(),
35
+ secretRefs: z.array(z.object({ name: z.string(), location: z.string() })).optional(),
36
+ });
37
+ export const GemArtifactSchema = z.discriminatedUnion("type", [
38
+ SkillArtifactSchema,
39
+ McpServerArtifactSchema,
40
+ InstructionsArtifactSchema,
41
+ HookArtifactSchema,
42
+ ]);
43
+ export const SecretRequirementSchema = z.object({
44
+ name: z.string(),
45
+ artifact: z.string(),
46
+ location: z.string(),
47
+ });
48
+ export const EvalAssertionSchema = z.discriminatedUnion("type", [
49
+ z.object({ type: z.literal("file_exists"), path: z.string() }),
50
+ z.object({ type: z.literal("file_contains"), path: z.string(), substring: z.string() }),
51
+ z.object({ type: z.literal("command_succeeds"), command: z.string() }),
52
+ z.object({ type: z.literal("output_contains"), substring: z.string() }),
53
+ z.object({ type: z.literal("tool_called"), tool: z.string() }),
54
+ ]);
55
+ export const BehavioralCheckSchema = z.object({
56
+ kind: z.literal("behavioral"),
57
+ name: z.string(),
58
+ description: z.string().optional(),
59
+ task: z.string(),
60
+ setup: z.object({ files: z.array(z.object({ path: z.string(), content: z.string() })).optional() }).optional(),
61
+ assertions: z.array(EvalAssertionSchema),
62
+ judge: z.object({ rubric: z.string(), passThreshold: z.number().min(0).max(1).optional() }).optional(),
63
+ timeoutSec: z.number().optional(),
64
+ });
65
+ // runner validates against the registry keys, so a gem can't declare a check no runner can run.
66
+ const RUNNER_IDS = Object.keys(RUNNER_REGISTRY);
67
+ export const ExternalCheckSchema = z.object({
68
+ kind: z.literal("external"),
69
+ name: z.string(),
70
+ description: z.string().optional(),
71
+ runner: z.enum(RUNNER_IDS),
72
+ with: z.record(z.string(), z.unknown()).optional(),
73
+ });
74
+ export const GemCheckSchema = z.discriminatedUnion("kind", [BehavioralCheckSchema, ExternalCheckSchema]);
75
+ export const ProjectInventorySchema = z.object({
76
+ root: z.string(),
77
+ name: z.string(),
78
+ skills: z.array(SkillArtifactSchema),
79
+ mcpServers: z.array(McpServerArtifactSchema),
80
+ instructions: z.array(InstructionsArtifactSchema),
81
+ hooks: z.array(HookArtifactSchema),
82
+ });
83
+ export const InventorySchema = z.object({
84
+ skills: z.array(SkillArtifactSchema),
85
+ mcpServers: z.array(McpServerArtifactSchema),
86
+ instructions: z.array(InstructionsArtifactSchema),
87
+ hooks: z.array(HookArtifactSchema),
88
+ projects: z.array(ProjectInventorySchema).optional(),
89
+ });
90
+ // Per-project selection is keyed by the project's root path so a same-named artifact in
91
+ // two projects never collides.
92
+ const ProjectSelectionSchema = z.record(z.string(), z.object({
93
+ skills: z.array(z.string()).optional(),
94
+ mcpServers: z.array(z.string()).optional(),
95
+ includeInstructions: z.boolean().optional(),
96
+ hooks: z.array(z.string()).optional(),
97
+ }));
98
+ export const GemSelectionSchema = z.union([
99
+ z.object({ all: z.literal(true) }),
100
+ z.object({
101
+ skills: z.array(z.string()).optional(),
102
+ mcpServers: z.array(z.string()).optional(),
103
+ includeInstructions: z.boolean().optional(),
104
+ hooks: z.array(z.string()).optional(),
105
+ projects: ProjectSelectionSchema.optional(),
106
+ }),
107
+ ]);
108
+ export const GemRequestSchema = z.object({
109
+ selection: GemSelectionSchema,
110
+ name: z.string().optional(),
111
+ dir: z.string().optional(),
112
+ projects: z.array(z.string()).optional(),
113
+ checks: z.array(GemCheckSchema).optional(),
114
+ });
115
+ export const ScaffoldChecksRequestSchema = z.object({
116
+ selection: GemSelectionSchema,
117
+ name: z.string().optional(),
118
+ dir: z.string().optional(),
119
+ projects: z.array(z.string()).optional(),
120
+ });
121
+ export const ScaffoldChecksResponseSchema = z.object({ checks: z.array(GemCheckSchema) });
122
+ const TARGET_IDS = Object.keys(TARGET_REGISTRY);
123
+ export const TargetIdSchema = z.enum(TARGET_IDS);
124
+ export const SkippedArtifactSchema = z.object({
125
+ artifact: z.string(),
126
+ type: z.enum(["skill", "mcp_server", "instructions", "hook"]),
127
+ reason: z.string(),
128
+ });
129
+ export const MaterializeResponseSchema = z.object({
130
+ target: TargetIdSchema,
131
+ files: z.record(z.string(), z.string()),
132
+ skipped: z.array(SkippedArtifactSchema),
133
+ compatibility: z.record(TargetIdSchema, z.object({ supported: z.number(), skipped: z.number() })),
134
+ });
135
+ // ── Gem archive ──
136
+ export const GemLockSchema = z.object({
137
+ formatVersion: z.number(),
138
+ files: z.record(z.string(), z.string()),
139
+ gemDigest: z.string(),
140
+ signature: z.string().nullable(),
141
+ });
142
+ export const GemManifestArtifactSchema = z.object({
143
+ type: z.enum(["skill", "mcp_server", "instructions", "hook"]),
144
+ name: z.string(),
145
+ path: z.string(),
146
+ description: z.string().optional(),
147
+ source: z.string().optional(),
148
+ });
149
+ export const GemManifestSchema = z.object({
150
+ formatVersion: z.number(),
151
+ name: z.string(),
152
+ version: z.string(),
153
+ createdFrom: z.string(),
154
+ artifacts: z.array(GemManifestArtifactSchema),
155
+ requiredSecrets: z.array(SecretRequirementSchema),
156
+ checks: z.array(z.object({ name: z.string(), path: z.string() })),
157
+ });
158
+ export const ArchiveRequestSchema = z.object({
159
+ selection: GemSelectionSchema,
160
+ name: z.string().optional(),
161
+ version: z.string().optional(),
162
+ dir: z.string().optional(),
163
+ projects: z.array(z.string()).optional(),
164
+ outDir: z.string().optional(), // when set, write the tree here and return its path
165
+ tar: z.boolean().optional(), // when true, also return the tree as a base64 .tar.gz
166
+ });
167
+ export const ArchiveResponseSchema = z.object({
168
+ files: z.record(z.string(), z.string()),
169
+ lock: GemLockSchema,
170
+ skipped: z.array(SkippedArtifactSchema),
171
+ path: z.string().nullable(),
172
+ tarGz: z.string().nullable(), // base64 .tar.gz when `tar` was requested, else null
173
+ });
174
+ export const MaterializeRequestSchema = z.object({
175
+ selection: GemSelectionSchema.optional(),
176
+ archivePath: z.string().optional(),
177
+ target: TargetIdSchema,
178
+ name: z.string().optional(),
179
+ dir: z.string().optional(),
180
+ projects: z.array(z.string()).optional(),
181
+ }).refine((d) => d.selection !== undefined || d.archivePath !== undefined, {
182
+ message: "provide either selection or archivePath",
183
+ });
184
+ export const DeployTargetIdSchema = z.enum(deployTargetIds);
185
+ export const DeployReadyQuerySchema = z.object({ target: DeployTargetIdSchema.optional() });
186
+ export const DeployTargetsResponseSchema = z.object({
187
+ targets: z.array(z.object({ id: DeployTargetIdSchema, label: z.string(), ready: z.boolean() })),
188
+ });
189
+ // ── Managed Agents publish ──
190
+ export const PublishPreviewRequestSchema = z.object({
191
+ selection: GemSelectionSchema,
192
+ name: z.string().optional(),
193
+ dir: z.string().optional(),
194
+ projects: z.array(z.string()).optional(),
195
+ target: DeployTargetIdSchema.optional(),
196
+ });
197
+ export const PublishRequestSchema = PublishPreviewRequestSchema.extend({ requestId: z.string().min(8).max(128), wsName: z.string().optional() });
198
+ const ManagedAgentPayloadSchema = z.object({
199
+ name: z.string(),
200
+ model: z.string(),
201
+ system: z.string(),
202
+ mcp_servers: z.array(z.object({ type: z.literal("url"), name: z.string(), url: z.string() })),
203
+ tools: z.array(z.union([
204
+ z.object({ type: z.literal("agent_toolset_20260401") }),
205
+ z.object({ type: z.literal("mcp_toolset"), mcp_server_name: z.string() }),
206
+ ])),
207
+ });
208
+ const ManagedAgentPreviewSchema = z.object({
209
+ kind: z.literal("managed-agent"),
210
+ payload: ManagedAgentPayloadSchema,
211
+ skillsToRegister: z.array(z.string()),
212
+ skipped: z.array(SkippedArtifactSchema),
213
+ vaultSecrets: z.array(SecretRequirementSchema),
214
+ });
215
+ const AgentcorePreviewSchema = z.object({
216
+ kind: z.literal("agentcore-harness"),
217
+ request: z.record(z.string(), z.unknown()),
218
+ skipped: z.array(SkippedArtifactSchema),
219
+ vaultSecrets: z.array(SecretRequirementSchema),
220
+ });
221
+ export const PublishPreviewResponseSchema = z.discriminatedUnion("kind", [ManagedAgentPreviewSchema, AgentcorePreviewSchema]);
222
+ export const PublishReadyResponseSchema = z.object({ ready: z.boolean() });
223
+ const ManagedAgentResultSchema = z.object({
224
+ kind: z.literal("managed-agent"),
225
+ agentId: z.string(), environmentId: z.string(), version: z.string(),
226
+ registeredSkills: z.array(z.object({ name: z.string(), skillId: z.string(), version: z.string() })),
227
+ skipped: z.array(SkippedArtifactSchema), vaultSecrets: z.array(SecretRequirementSchema),
228
+ });
229
+ const AgentcoreResultSchema = z.object({
230
+ kind: z.literal("agentcore-harness"),
231
+ harnessArn: z.string(), harnessId: z.string(), harnessName: z.string(), harnessVersion: z.string(), status: z.string(),
232
+ skipped: z.array(SkippedArtifactSchema), vaultSecrets: z.array(SecretRequirementSchema),
233
+ });
234
+ export const PublishResultSchema = z.discriminatedUnion("kind", [ManagedAgentResultSchema, AgentcoreResultSchema]);
235
+ // `projects` is a JSON-encoded string array of root paths (query params can't carry arrays cleanly).
236
+ export const DirQuerySchema = z.object({ dir: z.string().optional(), projects: z.string().optional() });
237
+ export const PickQuerySchema = z.object({});
238
+ export const PickFolderSchema = z.object({ path: z.string().nullable() });
239
+ export const GemSchema = z.object({
240
+ name: z.string(),
241
+ createdFrom: z.string(),
242
+ artifacts: z.array(GemArtifactSchema),
243
+ checks: z.array(GemCheckSchema),
244
+ requiredSecrets: z.array(SecretRequirementSchema),
245
+ });
246
+ // ── Workspaces ──
247
+ export const WorkspaceSummarySchema = z.object({
248
+ name: z.string(),
249
+ gemName: z.string(),
250
+ version: z.string(),
251
+ artifactCounts: z.object({ skill: z.number(), mcp_server: z.number(), instructions: z.number(), hook: z.number() }),
252
+ checks: z.number(),
253
+ renderedTargets: z.array(TargetIdSchema),
254
+ });
255
+ export const WorkspaceDetailSchema = WorkspaceSummarySchema.extend({
256
+ files: z.record(z.string(), z.string()),
257
+ compatibility: z.record(TargetIdSchema, z.object({ supported: z.number(), skipped: z.number() })),
258
+ });
259
+ export const RenderResultSchema = z.object({
260
+ target: TargetIdSchema,
261
+ files: z.record(z.string(), z.string()),
262
+ skipped: z.array(SkippedArtifactSchema),
263
+ path: z.string(),
264
+ });
265
+ export const CreateWorkspaceRequestSchema = z.object({
266
+ name: z.string(),
267
+ selection: GemSelectionSchema,
268
+ dir: z.string().optional(),
269
+ projects: z.array(z.string()).optional(),
270
+ version: z.string().optional(),
271
+ });
272
+ export const WorkspaceQuerySchema = z.object({ name: z.string() });
273
+ export const RenderRequestSchema = z.object({ name: z.string(), target: TargetIdSchema });
274
+ export const WorkspaceNameRequestSchema = z.object({ name: z.string() });
275
+ export const ListWorkspacesResponseSchema = z.object({ workspaces: z.array(WorkspaceSummarySchema) });
276
+ export const DeleteWorkspaceResponseSchema = z.object({ deleted: z.string() });
277
+ export const RunReadyQuerySchema = z.object({ name: z.string(), target: TargetIdSchema });
278
+ export const RunReadyResponseSchema = z.object({ local: z.boolean(), vercel: z.boolean(), cloudflare: z.boolean() });
279
+ export const RunRequestSchema = z.object({ name: z.string(), target: TargetIdSchema, mode: z.enum(["local", "vercel", "cloudflare"]), eveAuth: z.enum(["placeholder", "public"]).optional() });
280
+ export const RunStatusQuerySchema = z.object({ name: z.string(), target: TargetIdSchema });
281
+ export const RunStateSchema = z.object({
282
+ mode: z.enum(["local", "vercel", "cloudflare"]),
283
+ state: z.enum(["idle", "installing", "building", "running", "deploying", "failed"]),
284
+ url: z.string().optional(),
285
+ logTail: z.array(z.string()),
286
+ });
287
+ export const RunStopRequestSchema = z.object({ name: z.string(), target: TargetIdSchema });
288
+ export const RunStopResponseSchema = z.object({ stopped: z.boolean() });
289
+ // Set a server-side credential (allowlisted keys only). Response is just ok — the UI re-fetches
290
+ // the relevant backend readiness (run-ready / publish-ready) on re-render.
291
+ export const CredentialRequestSchema = z.object({ key: z.enum(CREDENTIAL_KEYS), value: z.string().min(1) });
292
+ export const CredentialResponseSchema = z.object({ ok: z.boolean() });
293
+ // ── Testbed (testbed-first on-ramp) ──
294
+ const FLAVOR_IDS = flavorIds();
295
+ export const TestbedFlavorIdSchema = z.enum(FLAVOR_IDS);
296
+ export const TestbedDetectQuerySchema = z.object({ root: z.string() });
297
+ export const TestbedDetectResponseSchema = z.object({ flavor: TestbedFlavorIdSchema.nullable() });
298
+ // cwd probe for the front door. `cwd` overrides process.cwd() (tests); production omits it.
299
+ export const TestbedSuggestionQuerySchema = z.object({ cwd: z.string().optional() });
300
+ export const TestbedSuggestionResponseSchema = z.object({
301
+ cwd: z.string(),
302
+ looksLikeProject: z.boolean(),
303
+ flavor: TestbedFlavorIdSchema.nullable(),
304
+ name: z.string(),
305
+ });
306
+ // Persisted "testbeds opened in agentgem". `exists` is computed per-request (stale paths).
307
+ export const RecentEntrySchema = z.object({
308
+ path: z.string(),
309
+ flavor: TestbedFlavorIdSchema,
310
+ name: z.string(),
311
+ lastUsed: z.string(),
312
+ exists: z.boolean(),
313
+ });
314
+ export const TestbedRecentsResponseSchema = z.object({ recents: z.array(RecentEntrySchema) });
315
+ // Cross-repo discovery: projects harvested from Claude/Codex session history (ungated).
316
+ // `dir` overrides the ~/.claude base (tests / non-default homes); production omits it.
317
+ export const TestbedProjectsQuerySchema = z.object({ dir: z.string().optional() });
318
+ export const ProjectCandidateSchema = z.object({
319
+ path: z.string(),
320
+ flavor: TestbedFlavorIdSchema,
321
+ lastUsed: z.string().nullable(),
322
+ exists: z.boolean(),
323
+ });
324
+ export const TestbedProjectsResponseSchema = z.object({ projects: z.array(ProjectCandidateSchema) });
325
+ export const TestbedImportSelectionSchema = z.object({
326
+ skills: z.array(z.string()).optional(),
327
+ mcpServers: z.array(z.string()).optional(),
328
+ hooks: z.array(z.string()).optional(),
329
+ includeInstructions: z.boolean().optional(),
330
+ });
331
+ export const TestbedScaffoldRequestSchema = z.object({ root: z.string(), name: z.string(), flavor: TestbedFlavorIdSchema.optional() });
332
+ export const TestbedScaffoldResponseSchema = z.object({ root: z.string(), created: z.array(z.string()) });
333
+ export const TestbedImportRequestSchema = z.object({
334
+ root: z.string(),
335
+ selection: TestbedImportSelectionSchema,
336
+ dir: z.string().optional(),
337
+ flavor: TestbedFlavorIdSchema.optional(),
338
+ });
339
+ export const ImportedRefSchema = z.object({
340
+ type: z.enum(["skill", "mcp_server", "instructions", "hook"]),
341
+ name: z.string(),
342
+ overwritten: z.boolean(),
343
+ });
344
+ export const TestbedImportResponseSchema = z.object({
345
+ written: z.array(ImportedRefSchema),
346
+ skipped: z.array(z.object({ artifact: z.string(), reason: z.string() })),
347
+ });
348
+ // ── AgentCore deploy (Phase 2) ──
349
+ export const AgentcoreReadyResponseSchema = z.object({ cli: z.boolean(), awsCreds: z.boolean() });
350
+ export const AgentcoreDeployRequestSchema = z.object({ name: z.string() });
351
+ export const AgentcoreStatusQuerySchema = z.object({ name: z.string() });
352
+ export const AgentcoreDeployStateSchema = z.object({
353
+ state: z.enum(["idle", "installing", "building", "running", "deploying", "failed"]),
354
+ url: z.string().optional(),
355
+ logTail: z.array(z.string()),
356
+ });
357
+ // ── Gem Registry ──
358
+ export const RegistryReadyResponseSchema = z.object({ ready: z.boolean() });
359
+ const RegistryItemVersionSchema = z.object({ path: z.string(), gemDigest: z.string(), dependencies: z.array(z.string()) });
360
+ export const RegistryIndexResponseSchema = z.object({
361
+ formatVersion: z.number(),
362
+ items: z.record(z.string(), z.object({ latest: z.string(), versions: z.record(z.string(), RegistryItemVersionSchema) })),
363
+ });
364
+ export const RegistryResolveRequestSchema = z.object({
365
+ refs: z.array(z.string()).min(1),
366
+ mode: z.enum(["materialize", "workspace"]),
367
+ target: TargetIdSchema.optional(),
368
+ });
369
+ const InstallPlanSchema = z.object({
370
+ items: z.array(z.object({ key: z.string(), version: z.string() })),
371
+ totalArtifacts: z.number(),
372
+ requiredSecrets: z.array(z.object({ name: z.string(), artifact: z.string(), location: z.string() })),
373
+ overrides: z.array(z.object({ artifact: z.string(), winner: z.string(), loser: z.string() })),
374
+ materialize: z.object({
375
+ files: z.record(z.string(), z.string()),
376
+ skipped: z.array(z.object({ artifact: z.string(), type: z.string(), reason: z.string() })),
377
+ }).optional(),
378
+ });
379
+ export const RegistryResolveResponseSchema = z.object({ plan: InstallPlanSchema });
380
+ export const RegistryInstallRequestSchema = z.object({
381
+ refs: z.array(z.string()).min(1),
382
+ mode: z.enum(["materialize", "workspace"]),
383
+ target: TargetIdSchema.optional(),
384
+ dest: z.string().optional(),
385
+ workspaceName: z.string().optional(),
386
+ });
387
+ export const RegistryInstallResponseSchema = z.object({
388
+ plan: InstallPlanSchema,
389
+ applied: z.discriminatedUnion("mode", [
390
+ z.object({ mode: z.literal("materialize"), dest: z.string(), written: z.array(z.string()) }),
391
+ z.object({ mode: z.literal("workspace"), workspace: z.string() }),
392
+ ]),
393
+ });
394
+ export const RegistryPublishRequestSchema = z.object({
395
+ workspace: z.string(),
396
+ scope: z.string(),
397
+ name: z.string().optional(),
398
+ version: z.string(),
399
+ dependencies: z.array(z.string()).optional(),
400
+ });
401
+ export const RegistryPublishResponseSchema = z.object({
402
+ ref: z.string(), version: z.string(), gemDigest: z.string(), commit: z.string(), path: z.string(),
403
+ });
404
+ export const UndeployRequestSchema = z.object({ name: z.string(), target: z.enum(["eve", "flue", "claude-managed", "agentcore"]) });
405
+ export const UndeployResponseSchema = z.object({ removed: z.boolean(), logTail: z.array(z.string()).optional() });
406
+ export const DeployRecordQuerySchema = z.object({ name: z.string(), backend: z.enum(["eve", "flue", "claude-managed", "agentcore"]) });
407
+ export const DeployRecordResponseSchema = z.object({ record: z.record(z.string(), z.unknown()).nullable() });
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@ninemind/agentgem",
3
+ "version": "0.1.0",
4
+ "description": "A local web UI that introspects your coding-agent config, redacts secrets at capture, and builds a portable, composable Gem.",
5
+ "license": "MIT",
6
+ "bin": {
7
+ "agentgem": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "!dist/**/__tests__",
12
+ "!dist/**/*.test.js",
13
+ "!dist/**/*.test.d.ts"
14
+ ],
15
+ "engines": {
16
+ "node": ">=22"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "author": "ninemind.ai",
22
+ "homepage": "https://agentgem.ninemind.ai",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/ninemindai/agentgem.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/ninemindai/agentgem/issues"
29
+ },
30
+ "keywords": [
31
+ "claude",
32
+ "coding-agent",
33
+ "mcp",
34
+ "agentback",
35
+ "secret-redaction",
36
+ "agent-config",
37
+ "gem"
38
+ ],
39
+ "type": "module",
40
+ "packageManager": "pnpm@10.29.2",
41
+ "scripts": {
42
+ "build": "tsc -b && mkdir -p dist/public && cp src/public/index.html dist/public/index.html",
43
+ "dev": "pnpm build && node dist/index.js",
44
+ "start": "node dist/index.js",
45
+ "build:site": "node website/build.mjs",
46
+ "test": "tsc -b && vitest run",
47
+ "clean": "rm -rf dist *.tsbuildinfo",
48
+ "prepublishOnly": "pnpm clean && pnpm build"
49
+ },
50
+ "dependencies": {
51
+ "@agentback/core": "^0.5.2",
52
+ "@agentback/mcp": "^0.5.2",
53
+ "@agentback/mcp-http": "^0.5.2",
54
+ "@agentback/openapi": "^0.5.2",
55
+ "@agentback/rest": "^0.5.2",
56
+ "@agentback/rest-explorer": "^0.5.2",
57
+ "@anthropic-ai/sdk": "^0.104.2",
58
+ "@aws-sdk/client-bedrock-agentcore-control": "^3.1073.0",
59
+ "dotenv": "^17.4.2",
60
+ "tslib": "^2.8.1",
61
+ "vercel": "^54.14.2",
62
+ "zod": "^4.4.3"
63
+ },
64
+ "devDependencies": {
65
+ "@types/node": "^24",
66
+ "@types/supertest": "^6",
67
+ "marked": "^18.0.5",
68
+ "supertest": "^7",
69
+ "typescript": "^6",
70
+ "vitest": "^3"
71
+ }
72
+ }