@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/dist/schemas.js CHANGED
@@ -34,11 +34,24 @@ export const HookArtifactSchema = z.object({
34
34
  source: z.string().optional(),
35
35
  secretRefs: z.array(z.object({ name: z.string(), location: z.string() })).optional(),
36
36
  });
37
+ export const ChannelPlatformSchema = z.enum(["slack", "telegram", "discord", "teams", "twilio", "github"]);
38
+ export const ChannelArtifactSchema = z.object({
39
+ type: z.literal("channel"),
40
+ name: z.string(),
41
+ platform: ChannelPlatformSchema,
42
+ secretRefs: z.array(z.object({ name: z.string(), location: z.string() })),
43
+ description: z.string().optional(),
44
+ });
45
+ // Declared channels on a request body. Shared by every endpoint that builds a Gem from a selection
46
+ // (gem preview, materialize, archive, create-workspace, publish) so channels are never dropped on one
47
+ // path while present on another. Each platform entry becomes a channel artifact via buildGem.
48
+ export const ChannelDeclSchema = z.array(z.object({ platform: ChannelPlatformSchema, name: z.string().optional() })).optional();
37
49
  export const GemArtifactSchema = z.discriminatedUnion("type", [
38
50
  SkillArtifactSchema,
39
51
  McpServerArtifactSchema,
40
52
  InstructionsArtifactSchema,
41
53
  HookArtifactSchema,
54
+ ChannelArtifactSchema,
42
55
  ]);
43
56
  export const SecretRequirementSchema = z.object({
44
57
  name: z.string(),
@@ -105,25 +118,48 @@ export const GemSelectionSchema = z.union([
105
118
  projects: ProjectSelectionSchema.optional(),
106
119
  }),
107
120
  ]);
121
+ // A draft skill distilled from the builtin procedure (proposal §2). status is
122
+ // always "draft" — never installed by this pipeline. Defined before the build
123
+ // request schemas that reference it (module load order).
124
+ export const DistilledSkillSchema = z.object({
125
+ name: z.string(),
126
+ description: z.string(),
127
+ triggers: z.array(z.string()),
128
+ tools: z.array(z.string()),
129
+ mutating: z.boolean(),
130
+ body: z.string(),
131
+ evidence: z.object({
132
+ sessions: z.number(),
133
+ exampleSequence: z.array(z.string()),
134
+ root: z.string(),
135
+ }),
136
+ status: z.literal("draft"),
137
+ confidence: z.enum(["high", "medium", "low"]),
138
+ });
108
139
  export const GemRequestSchema = z.object({
109
140
  selection: GemSelectionSchema,
110
141
  name: z.string().optional(),
111
142
  dir: z.string().optional(),
112
143
  projects: z.array(z.string()).optional(),
113
144
  checks: z.array(GemCheckSchema).optional(),
145
+ channels: ChannelDeclSchema,
146
+ // Accepted distilled drafts to fold into the build — staged into the inventory
147
+ // (by evidence.root) before resolution, so a selection can reference one by name.
148
+ distilledDrafts: z.array(DistilledSkillSchema).optional(),
114
149
  });
115
150
  export const ScaffoldChecksRequestSchema = z.object({
116
151
  selection: GemSelectionSchema,
117
152
  name: z.string().optional(),
118
153
  dir: z.string().optional(),
119
154
  projects: z.array(z.string()).optional(),
155
+ distilledDrafts: z.array(DistilledSkillSchema).optional(),
120
156
  });
121
157
  export const ScaffoldChecksResponseSchema = z.object({ checks: z.array(GemCheckSchema) });
122
158
  const TARGET_IDS = Object.keys(TARGET_REGISTRY);
123
159
  export const TargetIdSchema = z.enum(TARGET_IDS);
124
160
  export const SkippedArtifactSchema = z.object({
125
161
  artifact: z.string(),
126
- type: z.enum(["skill", "mcp_server", "instructions", "hook"]),
162
+ type: z.enum(["skill", "mcp_server", "instructions", "hook", "channel"]),
127
163
  reason: z.string(),
128
164
  });
129
165
  export const MaterializeResponseSchema = z.object({
@@ -140,7 +176,7 @@ export const GemLockSchema = z.object({
140
176
  signature: z.string().nullable(),
141
177
  });
142
178
  export const GemManifestArtifactSchema = z.object({
143
- type: z.enum(["skill", "mcp_server", "instructions", "hook"]),
179
+ type: z.enum(["skill", "mcp_server", "instructions", "hook", "channel"]),
144
180
  name: z.string(),
145
181
  path: z.string(),
146
182
  description: z.string().optional(),
@@ -162,24 +198,31 @@ export const ArchiveRequestSchema = z.object({
162
198
  dir: z.string().optional(),
163
199
  projects: z.array(z.string()).optional(),
164
200
  outDir: z.string().optional(), // when set, write the tree here and return its path
201
+ outFile: z.string().optional(), // when set, write one portable .gem (tar.gz) here
165
202
  tar: z.boolean().optional(), // when true, also return the tree as a base64 .tar.gz
203
+ channels: ChannelDeclSchema,
166
204
  });
167
205
  export const ArchiveResponseSchema = z.object({
168
206
  files: z.record(z.string(), z.string()),
169
207
  lock: GemLockSchema,
170
208
  skipped: z.array(SkippedArtifactSchema),
171
209
  path: z.string().nullable(),
210
+ gemFile: z.string().nullable(), // path to the written .gem when `outFile` was set, else null
172
211
  tarGz: z.string().nullable(), // base64 .tar.gz when `tar` was requested, else null
173
212
  });
174
213
  export const MaterializeRequestSchema = z.object({
175
214
  selection: GemSelectionSchema.optional(),
176
215
  archivePath: z.string().optional(),
216
+ gemPath: z.string().optional(), // install from a single .gem (tar.gz) file on disk
217
+ gemUrl: z.string().optional(), // install from a .gem fetched over http(s)
177
218
  target: TargetIdSchema,
178
219
  name: z.string().optional(),
179
220
  dir: z.string().optional(),
180
221
  projects: z.array(z.string()).optional(),
181
- }).refine((d) => d.selection !== undefined || d.archivePath !== undefined, {
182
- message: "provide either selection or archivePath",
222
+ a2aServer: z.boolean().optional(), // a2a target: also emit the runnable server, not just the Agent Card
223
+ channels: ChannelDeclSchema, // applied only when building from `selection` (ignored for archive/gem sources)
224
+ }).refine((d) => d.selection !== undefined || d.archivePath !== undefined || d.gemPath !== undefined || d.gemUrl !== undefined, {
225
+ message: "provide one of selection, archivePath, gemPath, or gemUrl",
183
226
  });
184
227
  export const DeployTargetIdSchema = z.enum(deployTargetIds);
185
228
  export const DeployReadyQuerySchema = z.object({ target: DeployTargetIdSchema.optional() });
@@ -193,6 +236,7 @@ export const PublishPreviewRequestSchema = z.object({
193
236
  dir: z.string().optional(),
194
237
  projects: z.array(z.string()).optional(),
195
238
  target: DeployTargetIdSchema.optional(),
239
+ channels: ChannelDeclSchema,
196
240
  });
197
241
  export const PublishRequestSchema = PublishPreviewRequestSchema.extend({ requestId: z.string().min(8).max(128), wsName: z.string().optional() });
198
242
  const ManagedAgentPayloadSchema = z.object({
@@ -236,6 +280,41 @@ export const PublishResultSchema = z.discriminatedUnion("kind", [ManagedAgentRes
236
280
  export const DirQuerySchema = z.object({ dir: z.string().optional(), projects: z.string().optional() });
237
281
  export const PickQuerySchema = z.object({});
238
282
  export const PickFolderSchema = z.object({ path: z.string().nullable() });
283
+ // ── Workflow-aware Gem recommendation ──
284
+ export const WorkflowAnalyzeRequestSchema = z.object({
285
+ dir: z.string().optional(), // .claude dir (resolveDirs handles the default)
286
+ root: z.string(), // the project root to analyze (one of the discovered cwds)
287
+ });
288
+ const RecommendedItemSchema = z.object({
289
+ // mirrors ArtifactType (incl. "channel") — RecommendedItem.type is ArtifactType, so this must stay in sync
290
+ type: z.enum(["skill", "mcp_server", "instructions", "hook", "channel"]),
291
+ name: z.string(),
292
+ reason: z.string(),
293
+ root: z.string().nullable(), // project root, or null for a global/plugin artifact
294
+ });
295
+ // One candidate Gem, carrying its own ready-to-POST GemSelection.
296
+ const GemCandidateSchema = z.object({
297
+ name: z.string(),
298
+ description: z.string(),
299
+ root: z.string(),
300
+ includeInstructions: z.boolean(),
301
+ include: z.array(RecommendedItemSchema),
302
+ confidence: z.enum(["high", "medium", "low"]),
303
+ selection: z.record(z.string(), z.unknown()), // a GemSelection; buildGem validates structurally at /api/gem
304
+ });
305
+ export const WorkflowAnalyzeResponseSchema = z.object({
306
+ candidates: z.array(GemCandidateSchema),
307
+ gaps: z.array(z.string()), // project-level: used but absent from inventory
308
+ distilled: z.array(DistilledSkillSchema), // draft skills distilled from the builtin procedure
309
+ signalSummary: z.object({
310
+ sessionsScanned: z.number(),
311
+ spanDays: z.number(),
312
+ notes: z.array(z.string()),
313
+ }),
314
+ degraded: z.boolean(),
315
+ });
316
+ // Accept a distilled draft → write it to .agentgem/distilled/<name>/SKILL.md.
317
+ export const WorkflowDraftWriteResponseSchema = z.object({ path: z.string() });
239
318
  export const GemSchema = z.object({
240
319
  name: z.string(),
241
320
  createdFrom: z.string(),
@@ -268,9 +347,10 @@ export const CreateWorkspaceRequestSchema = z.object({
268
347
  dir: z.string().optional(),
269
348
  projects: z.array(z.string()).optional(),
270
349
  version: z.string().optional(),
350
+ channels: ChannelDeclSchema,
271
351
  });
272
352
  export const WorkspaceQuerySchema = z.object({ name: z.string() });
273
- export const RenderRequestSchema = z.object({ name: z.string(), target: TargetIdSchema });
353
+ export const RenderRequestSchema = z.object({ name: z.string(), target: TargetIdSchema, a2aServer: z.boolean().optional() });
274
354
  export const WorkspaceNameRequestSchema = z.object({ name: z.string() });
275
355
  export const ListWorkspacesResponseSchema = z.object({ workspaces: z.array(WorkspaceSummarySchema) });
276
356
  export const DeleteWorkspaceResponseSchema = z.object({ deleted: z.string() });
@@ -337,7 +417,8 @@ export const TestbedImportRequestSchema = z.object({
337
417
  flavor: TestbedFlavorIdSchema.optional(),
338
418
  });
339
419
  export const ImportedRefSchema = z.object({
340
- type: z.enum(["skill", "mcp_server", "instructions", "hook"]),
420
+ // mirrors ArtifactType (incl. "channel") — ImportedRef.type is ArtifactType, so this must stay in sync
421
+ type: z.enum(["skill", "mcp_server", "instructions", "hook", "channel"]),
341
422
  name: z.string(),
342
423
  overwritten: z.boolean(),
343
424
  });
@@ -345,6 +426,74 @@ export const TestbedImportResponseSchema = z.object({
345
426
  written: z.array(ImportedRefSchema),
346
427
  skipped: z.array(z.object({ artifact: z.string(), reason: z.string() })),
347
428
  });
429
+ // ── Run a Gem with a local ACP coding agent ──
430
+ export const ToolInvocationSchema = z.object({
431
+ toolCallId: z.string(),
432
+ title: z.string(),
433
+ kind: z.string().optional(),
434
+ status: z.string().optional(),
435
+ });
436
+ export const RunResultSchema = z.object({
437
+ text: z.string(),
438
+ toolCalls: z.array(ToolInvocationSchema),
439
+ });
440
+ export const GemRunOutcomeSchema = z.object({
441
+ ok: z.boolean(),
442
+ error: z.string().optional(),
443
+ result: RunResultSchema,
444
+ sandbox: z.object({ backend: z.string(), isolated: z.boolean() }),
445
+ });
446
+ export const GemExpectationsSchema = z.object({
447
+ expectTools: z.array(z.string()).optional(),
448
+ expectText: z.string().optional(),
449
+ forbidToolFailures: z.boolean().optional(),
450
+ });
451
+ export const VerificationReportSchema = z.object({
452
+ passed: z.boolean(),
453
+ checks: z.array(z.object({ name: z.string(), passed: z.boolean(), detail: z.string() })),
454
+ });
455
+ export const GemRunRequestSchema = z.object({
456
+ // The Gem: either a built selection (like /materialize) or a .gem archive dir.
457
+ selection: GemSelectionSchema.optional(),
458
+ archivePath: z.string().optional(),
459
+ name: z.string().optional(),
460
+ dir: z.string().optional(), // introspect home (selection mode), like /materialize
461
+ projects: z.array(z.string()).optional(),
462
+ task: z.string(),
463
+ // runDir is intentionally NOT accepted from the client: a caller-controlled path is a path-injection
464
+ // sink (and the agent runs there with tool permissions). The server always derives it under
465
+ // AGENTGEM_HOME from the gem name. See gem.controller runGem/prepareGemRun.
466
+ agent: z.enum(["claude", "codex"]).optional(), // which local ACP adapter to drive
467
+ expectations: GemExpectationsSchema.optional(),
468
+ }).refine((d) => d.selection !== undefined || d.archivePath !== undefined, {
469
+ message: "provide either selection or archivePath",
470
+ });
471
+ export const GemRunResponseSchema = z.object({
472
+ dir: z.string(),
473
+ agent: z.string(),
474
+ materialized: TestbedImportResponseSchema,
475
+ run: GemRunOutcomeSchema,
476
+ verification: VerificationReportSchema.optional(),
477
+ });
478
+ // Streaming split: prepare (POST, carries the selection) materializes and hands
479
+ // back an opaque runId; the GET stream then runs it with simple query params.
480
+ export const GemRunPrepareRequestSchema = z.object({
481
+ selection: GemSelectionSchema.optional(),
482
+ archivePath: z.string().optional(),
483
+ name: z.string().optional(),
484
+ dir: z.string().optional(),
485
+ projects: z.array(z.string()).optional(),
486
+ // runDir is intentionally NOT accepted from the client (see GemRunRequestSchema). Server-derived only.
487
+ agent: z.enum(["claude", "codex"]).optional(),
488
+ }).refine((d) => d.selection !== undefined || d.archivePath !== undefined, {
489
+ message: "provide either selection or archivePath",
490
+ });
491
+ export const GemRunPrepareResponseSchema = z.object({
492
+ runId: z.string(),
493
+ runDir: z.string(),
494
+ agent: z.string(),
495
+ materialized: TestbedImportResponseSchema,
496
+ });
348
497
  // ── AgentCore deploy (Phase 2) ──
349
498
  export const AgentcoreReadyResponseSchema = z.object({ cli: z.boolean(), awsCreds: z.boolean() });
350
499
  export const AgentcoreDeployRequestSchema = z.object({ name: z.string() });
@@ -357,14 +506,35 @@ export const AgentcoreDeployStateSchema = z.object({
357
506
  // ── Gem Registry ──
358
507
  export const RegistryReadyResponseSchema = z.object({ ready: z.boolean() });
359
508
  const RegistryItemVersionSchema = z.object({ path: z.string(), gemDigest: z.string(), dependencies: z.array(z.string()) });
509
+ const RegistryItemDiscoverySchema = z.object({
510
+ description: z.string().optional(),
511
+ tags: z.array(z.string()).optional(),
512
+ author: z.string().optional(),
513
+ artifactKinds: z.array(z.string()).optional(),
514
+ updatedAt: z.string().optional(),
515
+ });
360
516
  export const RegistryIndexResponseSchema = z.object({
361
517
  formatVersion: z.number(),
362
- items: z.record(z.string(), z.object({ latest: z.string(), versions: z.record(z.string(), RegistryItemVersionSchema) })),
518
+ items: z.record(z.string(), z.object({ latest: z.string(), versions: z.record(z.string(), RegistryItemVersionSchema), discovery: RegistryItemDiscoverySchema.optional() })),
519
+ });
520
+ export const RegistrySearchQuerySchema = z.object({
521
+ q: z.string().optional(),
522
+ kind: z.string().optional(),
523
+ tag: z.string().optional(),
524
+ limit: z.coerce.number().int().positive().max(100).optional(),
525
+ });
526
+ export const RegistrySearchResponseSchema = z.object({
527
+ results: z.array(z.object({
528
+ key: z.string(), latest: z.string(), score: z.number(),
529
+ description: z.string().optional(), tags: z.array(z.string()).optional(),
530
+ author: z.string().optional(), artifactKinds: z.array(z.string()).optional(), updatedAt: z.string().optional(),
531
+ })),
363
532
  });
364
533
  export const RegistryResolveRequestSchema = z.object({
365
534
  refs: z.array(z.string()).min(1),
366
535
  mode: z.enum(["materialize", "workspace"]),
367
536
  target: TargetIdSchema.optional(),
537
+ a2aServer: z.boolean().optional(),
368
538
  });
369
539
  const InstallPlanSchema = z.object({
370
540
  items: z.array(z.object({ key: z.string(), version: z.string() })),
@@ -383,6 +553,7 @@ export const RegistryInstallRequestSchema = z.object({
383
553
  target: TargetIdSchema.optional(),
384
554
  dest: z.string().optional(),
385
555
  workspaceName: z.string().optional(),
556
+ a2aServer: z.boolean().optional(),
386
557
  });
387
558
  export const RegistryInstallResponseSchema = z.object({
388
559
  plan: InstallPlanSchema,
@@ -397,6 +568,8 @@ export const RegistryPublishRequestSchema = z.object({
397
568
  name: z.string().optional(),
398
569
  version: z.string(),
399
570
  dependencies: z.array(z.string()).optional(),
571
+ description: z.string().optional(), // discovery metadata for search
572
+ tags: z.array(z.string()).optional(),
400
573
  });
401
574
  export const RegistryPublishResponseSchema = z.object({
402
575
  ref: z.string(), version: z.string(), gemDigest: z.string(), commit: z.string(), path: z.string(),
@@ -0,0 +1,78 @@
1
+ // src/workflowStream.ts
2
+ //
3
+ // SSE endpoint for the workflow analysis. agentgem's decorator framework returns
4
+ // a single JSON body, so streaming progress (scan → agent token stream → done)
5
+ // is served by a raw Express handler registered on `server.expressApp`. The
6
+ // non-streaming POST /api/workflow/analyze stays for programmatic/test callers.
7
+ import { introspectConfig, introspectProject } from "./gem/introspect.js";
8
+ import { resolveDirs, resolveProject } from "./resolveDir.js";
9
+ import { claudeTranscriptsForCwd, scanWorkflow } from "./gem/workflowScan.js";
10
+ import { recommendWorkflow, recommendationToSelection } from "./gem/acpRecommender.js";
11
+ import { distillWorkflow } from "./gem/distill.js";
12
+ import { transcriptToken, readAnalysisCache, writeAnalysisCache } from "./gem/analysisCache.js";
13
+ export async function streamWorkflowAnalyze(req, res) {
14
+ const root = typeof req.query.root === "string" ? req.query.root : "";
15
+ const dir = typeof req.query.dir === "string" ? req.query.dir : undefined;
16
+ const fresh = req.query.fresh === "1"; // bypass the cache (Re-analyze)
17
+ res.writeHead(200, {
18
+ "Content-Type": "text/event-stream",
19
+ "Cache-Control": "no-cache, no-transform",
20
+ Connection: "keep-alive",
21
+ "X-Accel-Buffering": "no", // disable proxy buffering so events flush immediately
22
+ });
23
+ const send = (event, data) => {
24
+ res.write(`event: ${event}\n`);
25
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
26
+ };
27
+ try {
28
+ if (!root) {
29
+ send("failed", { message: "missing root" });
30
+ return;
31
+ }
32
+ const dirs = resolveDirs(dir);
33
+ const project = introspectProject(resolveProject(root));
34
+ const globalInv = introspectConfig(dirs); // global + plugin artifacts
35
+ const scanInv = { project, global: { skills: globalInv.skills, mcpServers: globalInv.mcpServers, hooks: globalInv.hooks } };
36
+ send("phase", { phase: "scanning" });
37
+ const paths = claudeTranscriptsForCwd(dirs.claudeDir, root);
38
+ // Cache hit (unless Re-analyze): return the prior result instantly so the
39
+ // user can revisit a project to pick another candidate without re-running
40
+ // the agent. Token invalidates when sessions are added/updated.
41
+ const token = transcriptToken(paths);
42
+ if (!fresh) {
43
+ const cached = readAnalysisCache(root, token);
44
+ if (cached) {
45
+ send("done", { ...cached, cached: true });
46
+ return;
47
+ }
48
+ }
49
+ const signal = scanWorkflow(paths, scanInv, { retainSequences: true });
50
+ send("phase", { phase: "scanned", transcripts: paths.length, sessions: signal.sessions.scanned });
51
+ send("phase", { phase: "thinking" });
52
+ // Selective recommendation streams its tokens; distillation runs concurrently
53
+ // but SILENT — two delta streams would interleave and garble the display
54
+ // (proposal §5). Both never throw, so wall-clock is max(...), not the sum.
55
+ const [{ analysis, degraded }, distill] = await Promise.all([
56
+ recommendWorkflow(signal, scanInv, { onDelta: (chunk) => send("delta", { text: chunk }) }),
57
+ distillWorkflow(signal, scanInv),
58
+ ]);
59
+ send("phase", { phase: "validating" });
60
+ const candidates = analysis.candidates.map((c) => ({ ...c, selection: recommendationToSelection(c) }));
61
+ const payload = {
62
+ candidates,
63
+ gaps: analysis.gaps,
64
+ distilled: distill.distilled,
65
+ signalSummary: { sessionsScanned: signal.sessions.scanned, spanDays: signal.sessions.spanDays, notes: signal.notes },
66
+ degraded,
67
+ };
68
+ if (!degraded)
69
+ writeAnalysisCache(root, token, payload, Date.now()); // don't cache fallbacks
70
+ send("done", { ...payload, cached: false });
71
+ }
72
+ catch (err) {
73
+ send("failed", { message: err?.message ?? String(err) });
74
+ }
75
+ finally {
76
+ res.end();
77
+ }
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ninemind/agentgem",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "A local web UI that introspects your coding-agent config, redacts secrets at capture, and builds a portable, composable Gem.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -39,7 +39,7 @@
39
39
  "type": "module",
40
40
  "packageManager": "pnpm@10.29.2",
41
41
  "scripts": {
42
- "build": "tsc -b && mkdir -p dist/public && cp src/public/index.html dist/public/index.html",
42
+ "build": "tsc -b && node scripts/copy-public.mjs",
43
43
  "dev": "pnpm build && node dist/index.js",
44
44
  "start": "node dist/index.js",
45
45
  "build:site": "node website/build.mjs",
@@ -54,17 +54,22 @@
54
54
  "@agentback/openapi": "^0.5.2",
55
55
  "@agentback/rest": "^0.5.2",
56
56
  "@agentback/rest-explorer": "^0.5.2",
57
+ "@agentclientprotocol/sdk": "^0.28.1",
57
58
  "@anthropic-ai/sdk": "^0.104.2",
58
59
  "@aws-sdk/client-bedrock-agentcore-control": "^3.1073.0",
59
60
  "dotenv": "^17.4.2",
60
61
  "tslib": "^2.8.1",
62
+ "undici": "^6.27.0",
61
63
  "vercel": "^54.14.2",
62
64
  "zod": "^4.4.3"
63
65
  },
64
66
  "devDependencies": {
65
67
  "@types/node": "^24",
66
68
  "@types/supertest": "^6",
69
+ "eve": "^0.15.0",
70
+ "highlight.js": "^11.11.1",
67
71
  "marked": "^18.0.5",
72
+ "marked-highlight": "^2.2.4",
68
73
  "supertest": "^7",
69
74
  "typescript": "^6",
70
75
  "vitest": "^3"