@geminilight/mindos 0.6.28 → 0.6.30

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.
Files changed (113) hide show
  1. package/README.md +10 -4
  2. package/app/app/api/a2a/agents/route.ts +9 -0
  3. package/app/app/api/a2a/delegations/route.ts +9 -0
  4. package/app/app/api/a2a/discover/route.ts +2 -0
  5. package/app/app/api/a2a/route.ts +6 -6
  6. package/app/app/api/acp/config/route.ts +82 -0
  7. package/app/app/api/acp/detect/route.ts +114 -0
  8. package/app/app/api/acp/install/route.ts +51 -0
  9. package/app/app/api/acp/registry/route.ts +31 -0
  10. package/app/app/api/acp/session/route.ts +185 -0
  11. package/app/app/api/ask/route.ts +116 -13
  12. package/app/app/api/workflows/route.ts +156 -0
  13. package/app/app/layout.tsx +2 -0
  14. package/app/app/page.tsx +7 -2
  15. package/app/components/ActivityBar.tsx +12 -4
  16. package/app/components/AskModal.tsx +4 -1
  17. package/app/components/DirView.tsx +64 -2
  18. package/app/components/FileTree.tsx +40 -10
  19. package/app/components/GuideCard.tsx +7 -17
  20. package/app/components/HomeContent.tsx +1 -0
  21. package/app/components/MarkdownView.tsx +2 -0
  22. package/app/components/Panel.tsx +1 -0
  23. package/app/components/RightAskPanel.tsx +5 -1
  24. package/app/components/SearchModal.tsx +234 -80
  25. package/app/components/SidebarLayout.tsx +6 -0
  26. package/app/components/agents/AgentDetailContent.tsx +266 -52
  27. package/app/components/agents/AgentsContentPage.tsx +32 -6
  28. package/app/components/agents/AgentsPanelA2aTab.tsx +684 -0
  29. package/app/components/agents/AgentsPanelSessionsTab.tsx +166 -0
  30. package/app/components/agents/SkillDetailPopover.tsx +4 -9
  31. package/app/components/agents/agents-content-model.ts +2 -2
  32. package/app/components/ask/AgentSelectorCapsule.tsx +218 -0
  33. package/app/components/ask/AskContent.tsx +197 -239
  34. package/app/components/ask/FileChip.tsx +82 -17
  35. package/app/components/ask/MentionPopover.tsx +21 -3
  36. package/app/components/ask/MessageList.tsx +30 -9
  37. package/app/components/ask/SlashCommandPopover.tsx +21 -3
  38. package/app/components/help/HelpContent.tsx +9 -9
  39. package/app/components/panels/AgentsPanel.tsx +2 -0
  40. package/app/components/panels/AgentsPanelAgentDetail.tsx +5 -8
  41. package/app/components/panels/AgentsPanelHubNav.tsx +16 -2
  42. package/app/components/panels/EchoPanel.tsx +5 -1
  43. package/app/components/panels/EchoSidebarStats.tsx +136 -0
  44. package/app/components/panels/WorkflowsPanel.tsx +206 -0
  45. package/app/components/renderers/workflow-yaml/StepEditor.tsx +157 -0
  46. package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +201 -0
  47. package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +226 -0
  48. package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +126 -0
  49. package/app/components/renderers/workflow-yaml/execution.ts +177 -0
  50. package/app/components/renderers/workflow-yaml/index.ts +6 -0
  51. package/app/components/renderers/workflow-yaml/manifest.ts +21 -0
  52. package/app/components/renderers/workflow-yaml/parser.ts +172 -0
  53. package/app/components/renderers/workflow-yaml/selectors.tsx +522 -0
  54. package/app/components/renderers/workflow-yaml/serializer.ts +56 -0
  55. package/app/components/renderers/workflow-yaml/types.ts +46 -0
  56. package/app/components/settings/KnowledgeTab.tsx +3 -6
  57. package/app/components/settings/McpSkillsSection.tsx +4 -5
  58. package/app/components/settings/McpTab.tsx +6 -8
  59. package/app/components/setup/StepSecurity.tsx +4 -5
  60. package/app/components/setup/index.tsx +5 -11
  61. package/app/components/ui/Toaster.tsx +39 -0
  62. package/app/hooks/useA2aRegistry.ts +6 -1
  63. package/app/hooks/useAcpConfig.ts +96 -0
  64. package/app/hooks/useAcpDetection.ts +120 -0
  65. package/app/hooks/useAcpRegistry.ts +86 -0
  66. package/app/hooks/useAskModal.ts +12 -5
  67. package/app/hooks/useAskPanel.ts +8 -5
  68. package/app/hooks/useAskSession.ts +19 -2
  69. package/app/hooks/useDelegationHistory.ts +49 -0
  70. package/app/hooks/useImageUpload.ts +152 -0
  71. package/app/lib/a2a/client.ts +49 -5
  72. package/app/lib/a2a/orchestrator.ts +0 -1
  73. package/app/lib/a2a/task-handler.ts +4 -4
  74. package/app/lib/a2a/types.ts +15 -0
  75. package/app/lib/acp/acp-tools.ts +95 -0
  76. package/app/lib/acp/agent-descriptors.ts +274 -0
  77. package/app/lib/acp/bridge.ts +144 -0
  78. package/app/lib/acp/index.ts +40 -0
  79. package/app/lib/acp/registry.ts +202 -0
  80. package/app/lib/acp/session.ts +717 -0
  81. package/app/lib/acp/subprocess.ts +495 -0
  82. package/app/lib/acp/types.ts +274 -0
  83. package/app/lib/agent/model.ts +18 -3
  84. package/app/lib/agent/to-agent-messages.ts +25 -2
  85. package/app/lib/agent/tools.ts +2 -1
  86. package/app/lib/i18n/_core.ts +22 -0
  87. package/app/lib/i18n/index.ts +35 -0
  88. package/app/lib/i18n/modules/ai-chat.ts +215 -0
  89. package/app/lib/i18n/modules/common.ts +71 -0
  90. package/app/lib/i18n/modules/features.ts +153 -0
  91. package/app/lib/i18n/modules/knowledge.ts +429 -0
  92. package/app/lib/i18n/modules/navigation.ts +153 -0
  93. package/app/lib/i18n/modules/onboarding.ts +523 -0
  94. package/app/lib/i18n/modules/panels.ts +1196 -0
  95. package/app/lib/i18n/modules/settings.ts +585 -0
  96. package/app/lib/i18n-en.ts +2 -1518
  97. package/app/lib/i18n-zh.ts +2 -1542
  98. package/app/lib/i18n.ts +3 -6
  99. package/app/lib/pi-integration/skills.ts +21 -6
  100. package/app/lib/renderers/index.ts +2 -2
  101. package/app/lib/settings.ts +10 -0
  102. package/app/lib/toast.ts +79 -0
  103. package/app/lib/types.ts +12 -1
  104. package/app/next-env.d.ts +1 -1
  105. package/app/package.json +3 -1
  106. package/bin/cli.js +25 -25
  107. package/bin/commands/file.js +29 -2
  108. package/bin/commands/space.js +249 -91
  109. package/package.json +1 -1
  110. package/templates/en/.mindos/workflows/Sprint Release.flow.yaml +130 -0
  111. package/templates/zh/.mindos/workflows//345/221/250/350/277/255/344/273/243/346/243/200/346/237/245.flow.yaml +84 -0
  112. package/app/components/renderers/workflow/WorkflowRenderer.tsx +0 -409
  113. package/app/components/renderers/workflow/manifest.ts +0 -14
@@ -0,0 +1,274 @@
1
+ /**
2
+ * ACP Agent Descriptors — Single source of truth for agent detection, launch, and install.
3
+ * Replaces the previously separate AGENT_BINARY_MAP, AGENT_OVERRIDES, and INSTALL_COMMANDS maps.
4
+ */
5
+
6
+ import type { AcpRegistryEntry, AcpTransportType } from './types';
7
+
8
+ /* ── Types ─────────────────────────────────────────────────────────────── */
9
+
10
+ /** Complete agent launch/detection metadata. */
11
+ export interface AcpAgentDescriptor {
12
+ /** Binary name for `which` detection (e.g., "gemini", "codebuddy") */
13
+ binary: string;
14
+ /** Command to execute when spawning */
15
+ cmd: string;
16
+ /** CLI args for ACP mode */
17
+ args: string[];
18
+ /** Install command shown in UI / used by auto-install */
19
+ installCmd?: string;
20
+ /** Curated display name (overrides registry name) */
21
+ displayName?: string;
22
+ /** Curated description (overrides registry description) */
23
+ description?: string;
24
+ }
25
+
26
+ /** User override for a specific agent, persisted in settings. */
27
+ export interface AcpAgentOverride {
28
+ /** Override command path (e.g., "/usr/local/bin/gemini") */
29
+ command?: string;
30
+ /** Override CLI args (e.g., ["--acp", "--verbose"]) */
31
+ args?: string[];
32
+ /** Extra environment variables */
33
+ env?: Record<string, string>;
34
+ /** false = skip this agent entirely (default: true) */
35
+ enabled?: boolean;
36
+ }
37
+
38
+ /** Fully resolved command ready for spawn, with provenance. */
39
+ export interface ResolvedAgentCommand {
40
+ cmd: string;
41
+ args: string[];
42
+ env?: Record<string, string>;
43
+ /** Where the command came from */
44
+ source: 'user-override' | 'descriptor' | 'registry';
45
+ /** Binary name for detection */
46
+ binary: string;
47
+ /** Install command for UI */
48
+ installCmd?: string;
49
+ /** Whether agent is enabled */
50
+ enabled: boolean;
51
+ }
52
+
53
+ /* ── Canonical Descriptors ─────────────────────────────────────────────── */
54
+
55
+ /**
56
+ * All known ACP agents with their detection binary, launch command, and install hint.
57
+ * Both detection (`which binary?`) and launch (`spawn cmd args`) read from here.
58
+ */
59
+ export const AGENT_DESCRIPTORS: Record<string, AcpAgentDescriptor> = {
60
+ // Gemini CLI — Google's AI coding agent
61
+ 'gemini': { binary: 'gemini', cmd: 'gemini', args: ['--experimental-acp'], installCmd: 'npm install -g @google/gemini-cli',
62
+ displayName: 'Gemini CLI',
63
+ description: 'Google Gemini 驱动的编程智能体。支持多文件编辑、代码审查、调试和项目级重构,原生集成 Google 搜索实时查询技术文档。' },
64
+ 'gemini-cli': { binary: 'gemini', cmd: 'gemini', args: ['--experimental-acp'], installCmd: 'npm install -g @google/gemini-cli',
65
+ displayName: 'Gemini CLI',
66
+ description: 'Google Gemini 驱动的编程智能体。支持多文件编辑、代码审查、调试和项目级重构,原生集成 Google 搜索实时查询技术文档。' },
67
+ // Claude Code — Anthropic's AI coding agent
68
+ 'claude': { binary: 'claude', cmd: 'npx', args: ['--yes', '@agentclientprotocol/claude-agent-acp'], installCmd: 'npm install -g @anthropic-ai/claude-code',
69
+ displayName: 'Claude Code',
70
+ description: 'Anthropic Claude 驱动的编程智能体。擅长复杂推理、长上下文理解和安全代码生成,支持多文件编辑与 agentic 工作流。' },
71
+ 'claude-code': { binary: 'claude', cmd: 'npx', args: ['--yes', '@agentclientprotocol/claude-agent-acp'], installCmd: 'npm install -g @anthropic-ai/claude-code',
72
+ displayName: 'Claude Code',
73
+ description: 'Anthropic Claude 驱动的编程智能体。擅长复杂推理、长上下文理解和安全代码生成,支持多文件编辑与 agentic 工作流。' },
74
+ 'claude-acp': { binary: 'claude', cmd: 'npx', args: ['--yes', '@agentclientprotocol/claude-agent-acp'], installCmd: 'npm install -g @anthropic-ai/claude-code',
75
+ displayName: 'Claude Code',
76
+ description: 'Anthropic Claude 驱动的编程智能体。擅长复杂推理、长上下文理解和安全代码生成,支持多文件编辑与 agentic 工作流。' },
77
+ // CodeBuddy Code — Tencent Cloud's AI coding agent
78
+ 'codebuddy-code': { binary: 'codebuddy', cmd: 'codebuddy', args: ['--acp'], installCmd: 'npm install -g @tencent-ai/codebuddy-code',
79
+ displayName: 'CodeBuddy Code',
80
+ description: '腾讯云智能编程助手。基于混元大模型,支持代码补全、生成、审查和多文件重构,深度理解中文语境,适配国内开发生态。' },
81
+ 'codebuddy': { binary: 'codebuddy', cmd: 'codebuddy', args: ['--acp'], installCmd: 'npm install -g @tencent-ai/codebuddy-code',
82
+ displayName: 'CodeBuddy Code',
83
+ description: '腾讯云智能编程助手。基于混元大模型,支持代码补全、生成、审查和多文件重构,深度理解中文语境,适配国内开发生态。' },
84
+ // Codex — OpenAI's coding agent
85
+ 'codex-acp': { binary: 'codex', cmd: 'codex', args: [], installCmd: 'npm install -g @openai/codex',
86
+ displayName: 'Codex',
87
+ description: 'OpenAI Codex 编程智能体。基于 GPT 系列模型,擅长代码生成、自动化任务和多语言编程支持。' },
88
+ 'codex': { binary: 'codex', cmd: 'codex', args: [], installCmd: 'npm install -g @openai/codex',
89
+ displayName: 'Codex',
90
+ description: 'OpenAI Codex 编程智能体。基于 GPT 系列模型,擅长代码生成、自动化任务和多语言编程支持。' },
91
+ // Cursor — AI-first code editor agent
92
+ 'cursor': { binary: 'cursor', cmd: 'cursor', args: [],
93
+ displayName: 'Cursor',
94
+ description: 'Cursor AI 编程智能体。AI-first 代码编辑器的 CLI 模式,支持上下文感知的代码编辑、Tab 补全和多文件协同修改。' },
95
+ 'cline': { binary: 'cline', cmd: 'cline', args: [], installCmd: 'npm install -g cline',
96
+ displayName: 'Cline',
97
+ description: '开源自主编程智能体。支持多模型后端,内置文件编辑、终端执行和浏览器自动化能力。' },
98
+ 'github-copilot-cli': { binary: 'github-copilot', cmd: 'github-copilot', args: [], installCmd: 'npm install -g @github/copilot',
99
+ displayName: 'GitHub Copilot',
100
+ description: 'GitHub Copilot 编程智能体。基于海量开源代码训练,擅长代码补全、测试生成和跨语言编程支持。' },
101
+ 'goose': { binary: 'goose', cmd: 'goose', args: [], installCmd: 'pip install goose-ai',
102
+ displayName: 'Goose',
103
+ description: 'Block 开源自主编程智能体。支持多模型后端,可扩展插件架构,擅长复杂任务自动化。' },
104
+ 'opencode': { binary: 'opencode', cmd: 'opencode', args: [], installCmd: 'go install github.com/opencode-ai/opencode@latest',
105
+ displayName: 'OpenCode',
106
+ description: '开源终端编程智能体。Go 实现,轻量快速,支持多模型后端和丰富的代码编辑工具。' },
107
+ 'kilo': { binary: 'kilo', cmd: 'kilo', args: [], installCmd: 'npm install -g @kilocode/cli',
108
+ displayName: 'Kilo Code',
109
+ description: 'Kilo Code 编程智能体。开源 VS Code 扩展的 CLI 模式,支持多模型、自动审批和代码差异预览。' },
110
+ 'openclaw': { binary: 'openclaw', cmd: 'openclaw', args: [],
111
+ displayName: 'OpenClaw',
112
+ description: 'OpenClaw 编程智能体。开源 Claude Code 替代方案,支持多模型后端和完整的 agentic 工作流。' },
113
+ 'pi': { binary: 'pi', cmd: 'pi', args: [],
114
+ displayName: 'Pi Agent',
115
+ description: 'Pi Agent 编程智能体。轻量级终端编程助手。' },
116
+ 'pi-acp': { binary: 'pi', cmd: 'pi', args: [],
117
+ displayName: 'Pi Agent',
118
+ description: 'Pi Agent 编程智能体。轻量级终端编程助手。' },
119
+ 'auggie': { binary: 'auggie', cmd: 'auggie', args: [],
120
+ displayName: 'Auggie',
121
+ description: 'Augment Code 编程智能体。支持代码理解、生成和全仓库上下文感知。' },
122
+ 'iflow': { binary: 'iflow', cmd: 'iflow', args: [],
123
+ displayName: 'iFlow',
124
+ description: 'iFlow 编程智能体。AI 驱动的工作流自动化工具。' },
125
+ 'kimi': { binary: 'kimi', cmd: 'kimi', args: [],
126
+ displayName: 'Kimi',
127
+ description: 'Moonshot AI Kimi 编程智能体。擅长超长上下文理解,支持中文语境下的代码生成与分析。' },
128
+ 'qwen-code': { binary: 'qwen-code', cmd: 'qwen-code', args: [], installCmd: 'npm install -g @qwen-code/qwen-code',
129
+ displayName: 'Qwen Code',
130
+ description: '阿里通义千问 Qwen 编程智能体。基于 Qwen 大模型,支持代码生成、审查和多语言编程,深度适配中文开发场景。' },
131
+ };
132
+
133
+ /* ── Resolution ────────────────────────────────────────────────────────── */
134
+
135
+ /**
136
+ * Resolve the final command for an agent by layering:
137
+ * 1. User override (highest priority)
138
+ * 2. Built-in descriptor
139
+ * 3. Registry entry (fallback for unknown agents)
140
+ * 4. Transport-based default (last resort)
141
+ */
142
+ export function resolveAgentCommand(
143
+ agentId: string,
144
+ registryEntry?: AcpRegistryEntry,
145
+ userOverride?: AcpAgentOverride,
146
+ ): ResolvedAgentCommand {
147
+ const descriptor = AGENT_DESCRIPTORS[agentId];
148
+ const enabled = userOverride?.enabled !== false;
149
+
150
+ // Layer 1: User override
151
+ if (userOverride && (userOverride.command || userOverride.args)) {
152
+ return {
153
+ cmd: userOverride.command ?? descriptor?.cmd ?? registryEntry?.command ?? agentId,
154
+ args: userOverride.args ?? descriptor?.args ?? [],
155
+ env: userOverride.env,
156
+ source: 'user-override',
157
+ binary: descriptor?.binary ?? agentId,
158
+ installCmd: descriptor?.installCmd,
159
+ enabled,
160
+ };
161
+ }
162
+
163
+ // Layer 2: Built-in descriptor
164
+ if (descriptor) {
165
+ return {
166
+ cmd: descriptor.cmd,
167
+ args: descriptor.args,
168
+ env: userOverride?.env,
169
+ source: 'descriptor',
170
+ binary: descriptor.binary,
171
+ installCmd: descriptor.installCmd,
172
+ enabled,
173
+ };
174
+ }
175
+
176
+ // Layer 3: Registry entry
177
+ if (registryEntry) {
178
+ const { cmd, args } = registryToCommand(registryEntry);
179
+ return {
180
+ cmd,
181
+ args,
182
+ env: userOverride?.env,
183
+ source: 'registry',
184
+ binary: agentId,
185
+ installCmd: registryEntry.packageName ? `npm install -g ${registryEntry.packageName}` : undefined,
186
+ enabled,
187
+ };
188
+ }
189
+
190
+ // Layer 4: Last resort — try using agentId as command
191
+ return {
192
+ cmd: agentId,
193
+ args: [],
194
+ env: userOverride?.env,
195
+ source: 'registry',
196
+ binary: agentId,
197
+ enabled,
198
+ };
199
+ }
200
+
201
+ /** Convert a registry entry's transport info to a spawn command. */
202
+ function registryToCommand(entry: AcpRegistryEntry): { cmd: string; args: string[] } {
203
+ const transport: AcpTransportType = entry.transport;
204
+ switch (transport) {
205
+ case 'npx':
206
+ return { cmd: 'npx', args: ['--yes', entry.command, ...(entry.args ?? [])] };
207
+ case 'uvx':
208
+ return { cmd: 'uvx', args: [entry.command, ...(entry.args ?? [])] };
209
+ case 'binary':
210
+ case 'stdio':
211
+ default:
212
+ return { cmd: entry.command, args: entry.args ?? [] };
213
+ }
214
+ }
215
+
216
+ /* ── Helpers ───────────────────────────────────────────────────────────── */
217
+
218
+ /** Get the binary name for detection (used by detect endpoint). */
219
+ export function getDescriptorBinary(agentId: string): string | undefined {
220
+ return AGENT_DESCRIPTORS[agentId]?.binary;
221
+ }
222
+
223
+ /** Get the install command for UI display. */
224
+ export function getDescriptorInstallCmd(agentId: string): string | undefined {
225
+ return AGENT_DESCRIPTORS[agentId]?.installCmd;
226
+ }
227
+
228
+ /** Get curated display name (overrides registry name if available). */
229
+ export function getDescriptorDisplayName(agentId: string): string | undefined {
230
+ return AGENT_DESCRIPTORS[agentId]?.displayName;
231
+ }
232
+
233
+ /** Get curated description (overrides registry description if available). */
234
+ export function getDescriptorDescription(agentId: string): string | undefined {
235
+ return AGENT_DESCRIPTORS[agentId]?.description;
236
+ }
237
+
238
+ /** Parse and validate acpAgents config from raw settings JSON. */
239
+ export function parseAcpAgentOverrides(raw: unknown): Record<string, AcpAgentOverride> | undefined {
240
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined;
241
+ const obj = raw as Record<string, unknown>;
242
+ const result: Record<string, AcpAgentOverride> = {};
243
+ let hasEntries = false;
244
+
245
+ for (const [key, val] of Object.entries(obj)) {
246
+ if (!val || typeof val !== 'object' || Array.isArray(val)) continue;
247
+ const entry = val as Record<string, unknown>;
248
+ const override: AcpAgentOverride = {};
249
+
250
+ if (typeof entry.command === 'string' && entry.command.trim()) {
251
+ override.command = entry.command.trim();
252
+ }
253
+ if (Array.isArray(entry.args)) {
254
+ override.args = entry.args.filter((a): a is string => typeof a === 'string');
255
+ }
256
+ if (entry.env && typeof entry.env === 'object' && !Array.isArray(entry.env)) {
257
+ const env: Record<string, string> = {};
258
+ for (const [ek, ev] of Object.entries(entry.env as Record<string, unknown>)) {
259
+ if (typeof ev === 'string') env[ek] = ev;
260
+ }
261
+ if (Object.keys(env).length > 0) override.env = env;
262
+ }
263
+ if (typeof entry.enabled === 'boolean') {
264
+ override.enabled = entry.enabled;
265
+ }
266
+
267
+ if (Object.keys(override).length > 0) {
268
+ result[key] = override;
269
+ hasEntries = true;
270
+ }
271
+ }
272
+
273
+ return hasEntries ? result : undefined;
274
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * A2A-ACP Bridge — Translate between A2A protocol and ACP protocol.
3
+ * Allows A2A agents to delegate to ACP agents transparently,
4
+ * and ACP session results to be returned as A2A tasks.
5
+ */
6
+
7
+ import type { A2AMessage, A2ATask, TaskState } from '@/lib/a2a/types';
8
+ import type { AcpPromptResponse, AcpSessionUpdate } from './types';
9
+ import { createSession, prompt, closeSession } from './session';
10
+
11
+ /* ── A2A → ACP ─────────────────────────────────────────────────────────── */
12
+
13
+ /**
14
+ * Bridge an A2A SendMessage request to an ACP session/prompt.
15
+ * Creates a session, sends the message, returns the result, then closes.
16
+ */
17
+ export async function bridgeA2aToAcp(
18
+ a2aMessage: A2AMessage,
19
+ acpAgentId: string,
20
+ ): Promise<A2ATask> {
21
+ const taskId = `acp-bridge-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
22
+
23
+ // Extract text from A2A message parts
24
+ const text = a2aMessage.parts
25
+ .map(p => p.text ?? '')
26
+ .filter(Boolean)
27
+ .join('\n');
28
+
29
+ if (!text) {
30
+ return makeFailedTask(taskId, 'No text content in A2A message');
31
+ }
32
+
33
+ let session;
34
+ try {
35
+ session = await createSession(acpAgentId);
36
+ } catch (err) {
37
+ return makeFailedTask(taskId, `Failed to create ACP session: ${(err as Error).message}`);
38
+ }
39
+
40
+ try {
41
+ const response = await prompt(session.id, text);
42
+ return bridgeAcpResponseToA2a(taskId, response);
43
+ } catch (err) {
44
+ return makeFailedTask(taskId, `ACP prompt failed: ${(err as Error).message}`);
45
+ } finally {
46
+ await closeSession(session.id).catch(() => {});
47
+ }
48
+ }
49
+
50
+ /* ── ACP → A2A ─────────────────────────────────────────────────────────── */
51
+
52
+ /**
53
+ * Convert a completed ACP prompt response to an A2A task result.
54
+ */
55
+ export function bridgeAcpResponseToA2a(
56
+ taskId: string,
57
+ response: AcpPromptResponse,
58
+ ): A2ATask {
59
+ return {
60
+ id: taskId,
61
+ status: {
62
+ state: 'TASK_STATE_COMPLETED',
63
+ message: {
64
+ role: 'ROLE_AGENT',
65
+ parts: [{ text: response.text }],
66
+ },
67
+ timestamp: new Date().toISOString(),
68
+ },
69
+ artifacts: response.text ? [{
70
+ artifactId: `${taskId}-artifact`,
71
+ parts: [{ text: response.text }],
72
+ }] : undefined,
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Convert a stream of ACP session updates to A2A task updates.
78
+ * Aggregates text updates and maps final state.
79
+ */
80
+ export function bridgeAcpUpdatesToA2a(
81
+ taskId: string,
82
+ updates: AcpSessionUpdate[],
83
+ ): A2ATask {
84
+ let aggregatedText = '';
85
+ let finalState: TaskState = 'TASK_STATE_WORKING';
86
+ let errorMessage = '';
87
+
88
+ for (const update of updates) {
89
+ switch (update.type) {
90
+ case 'text':
91
+ case 'agent_message_chunk':
92
+ aggregatedText += update.text ?? '';
93
+ break;
94
+ case 'agent_thought_chunk':
95
+ // Include thought in output with label
96
+ if (update.text) aggregatedText += update.text;
97
+ break;
98
+ case 'done':
99
+ finalState = 'TASK_STATE_COMPLETED';
100
+ break;
101
+ case 'error':
102
+ finalState = 'TASK_STATE_FAILED';
103
+ errorMessage = update.error ?? 'Unknown error';
104
+ break;
105
+ // tool_call, tool_call_update, plan, etc. — pass through for now
106
+ }
107
+ }
108
+
109
+ if (finalState === 'TASK_STATE_FAILED') {
110
+ return makeFailedTask(taskId, errorMessage);
111
+ }
112
+
113
+ return {
114
+ id: taskId,
115
+ status: {
116
+ state: finalState,
117
+ message: aggregatedText ? {
118
+ role: 'ROLE_AGENT',
119
+ parts: [{ text: aggregatedText }],
120
+ } : undefined,
121
+ timestamp: new Date().toISOString(),
122
+ },
123
+ artifacts: aggregatedText ? [{
124
+ artifactId: `${taskId}-artifact`,
125
+ parts: [{ text: aggregatedText }],
126
+ }] : undefined,
127
+ };
128
+ }
129
+
130
+ /* ── Internal ──────────────────────────────────────────────────────────── */
131
+
132
+ function makeFailedTask(taskId: string, error: string): A2ATask {
133
+ return {
134
+ id: taskId,
135
+ status: {
136
+ state: 'TASK_STATE_FAILED',
137
+ message: {
138
+ role: 'ROLE_AGENT',
139
+ parts: [{ text: error }],
140
+ },
141
+ timestamp: new Date().toISOString(),
142
+ },
143
+ };
144
+ }
@@ -0,0 +1,40 @@
1
+ export { fetchAcpRegistry, getAcpAgents, findAcpAgent, clearRegistryCache } from './registry';
2
+ export { spawnAcpAgent, sendMessage, sendAndWait, onMessage, onRequest, sendResponse, installAutoApproval, killAgent, killAllAgents, getProcess, getActiveProcesses } from './subprocess';
3
+ export { createSession, createSessionFromEntry, loadSession, listSessions, prompt, promptStream, cancelPrompt, setMode, setConfigOption, closeSession, getSession, getActiveSessions, closeAllSessions } from './session';
4
+ export { bridgeA2aToAcp, bridgeAcpResponseToA2a, bridgeAcpUpdatesToA2a } from './bridge';
5
+ export { acpTools } from './acp-tools';
6
+ export { ACP_ERRORS } from './types';
7
+ export type {
8
+ AcpAgentCapabilities,
9
+ AcpClientCapabilities,
10
+ AcpContentBlock,
11
+ AcpStopReason,
12
+ AcpMode,
13
+ AcpConfigOption,
14
+ AcpConfigOptionEntry,
15
+ AcpAuthMethod,
16
+ AcpSessionState,
17
+ AcpSession,
18
+ AcpSessionInfo,
19
+ AcpJsonRpcRequest,
20
+ AcpJsonRpcResponse,
21
+ AcpJsonRpcError,
22
+ AcpPromptRequest,
23
+ AcpPromptResponse,
24
+ AcpUpdateType,
25
+ AcpSessionUpdate,
26
+ AcpToolCall,
27
+ AcpToolCallFull,
28
+ AcpToolCallKind,
29
+ AcpToolCallStatus,
30
+ AcpToolResult,
31
+ AcpPlan,
32
+ AcpPlanEntry,
33
+ AcpPlanEntryStatus,
34
+ AcpPlanEntryPriority,
35
+ AcpPermissionOutcome,
36
+ AcpRegistryEntry,
37
+ AcpRegistry,
38
+ AcpTransportType,
39
+ } from './types';
40
+ export type { AcpProcess, AcpIncomingRequest } from './subprocess';
@@ -0,0 +1,202 @@
1
+ /**
2
+ * ACP Registry Client — Fetch and cache the ACP agent registry.
3
+ * The registry lists available ACP agents (Gemini CLI, Claude, Copilot, etc.)
4
+ * with their transport type, command, and metadata.
5
+ */
6
+
7
+ import type { AcpRegistry, AcpRegistryEntry } from './types';
8
+ import { getDescriptorDisplayName, getDescriptorDescription } from './agent-descriptors';
9
+
10
+ /* ── Constants ─────────────────────────────────────────────────────────── */
11
+
12
+ const REGISTRY_URL = 'https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json';
13
+ const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
14
+ const FETCH_TIMEOUT_MS = 10_000;
15
+
16
+ /* ── Cache ─────────────────────────────────────────────────────────────── */
17
+
18
+ let cachedRegistry: AcpRegistry | null = null;
19
+
20
+ /* ── Public API ────────────────────────────────────────────────────────── */
21
+
22
+ /**
23
+ * Fetch the ACP registry from the CDN. Caches for 1 hour.
24
+ * Returns null if the fetch fails.
25
+ */
26
+ export async function fetchAcpRegistry(): Promise<AcpRegistry | null> {
27
+ // Return cached if still valid
28
+ if (cachedRegistry && Date.now() - new Date(cachedRegistry.fetchedAt).getTime() < CACHE_TTL_MS) {
29
+ return cachedRegistry;
30
+ }
31
+
32
+ try {
33
+ const res = await fetch(REGISTRY_URL, {
34
+ headers: { 'Accept': 'application/json' },
35
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
36
+ });
37
+
38
+ if (!res.ok) return cachedRegistry ?? null;
39
+
40
+ const data = await res.json();
41
+
42
+ // The registry JSON may have varying shapes; normalize it
43
+ const agents: AcpRegistryEntry[] = parseRegistryEntries(data);
44
+
45
+ cachedRegistry = {
46
+ version: data.version ?? '1',
47
+ agents,
48
+ fetchedAt: new Date().toISOString(),
49
+ };
50
+
51
+ return cachedRegistry;
52
+ } catch {
53
+ // Return stale cache if available, otherwise null
54
+ return cachedRegistry ?? null;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Get all available ACP agents from the registry.
60
+ */
61
+ export async function getAcpAgents(): Promise<AcpRegistryEntry[]> {
62
+ const registry = await fetchAcpRegistry();
63
+ return registry?.agents ?? [];
64
+ }
65
+
66
+ /**
67
+ * Find a specific ACP agent by ID.
68
+ */
69
+ export async function findAcpAgent(id: string): Promise<AcpRegistryEntry | null> {
70
+ const agents = await getAcpAgents();
71
+ return agents.find(a => a.id === id) ?? null;
72
+ }
73
+
74
+ /**
75
+ * Clear the registry cache (useful for testing).
76
+ */
77
+ export function clearRegistryCache(): void {
78
+ cachedRegistry = null;
79
+ }
80
+
81
+ /* ── Internal ──────────────────────────────────────────────────────────── */
82
+
83
+ /**
84
+ * Parse raw registry JSON into typed entries.
85
+ * Handles both array and object-keyed formats.
86
+ */
87
+ function parseRegistryEntries(data: unknown): AcpRegistryEntry[] {
88
+ if (!data || typeof data !== 'object') return [];
89
+
90
+ // If data has an `agents` array, use that
91
+ const obj = data as Record<string, unknown>;
92
+ let rawAgents: unknown[];
93
+
94
+ if (Array.isArray(obj.agents)) {
95
+ rawAgents = obj.agents;
96
+ } else if (Array.isArray(data)) {
97
+ rawAgents = data;
98
+ } else {
99
+ // Object-keyed format: { "agent-id": { ... }, ... }
100
+ rawAgents = Object.entries(obj)
101
+ .filter(([key]) => key !== 'version' && key !== '$schema')
102
+ .map(([key, val]) => ({ ...(val as object), id: key }));
103
+ }
104
+
105
+ return rawAgents
106
+ .map(normalizeEntry)
107
+ .filter((e): e is AcpRegistryEntry => e !== null);
108
+ }
109
+
110
+ function normalizeEntry(raw: unknown): AcpRegistryEntry | null {
111
+ if (!raw || typeof raw !== 'object') return null;
112
+ const entry = raw as Record<string, unknown>;
113
+
114
+ const id = String(entry.id ?? entry.name ?? '');
115
+ const name = String(entry.name ?? entry.id ?? '');
116
+ if (!id && !name) return null;
117
+
118
+ // Extract transport/command/args from the `distribution` field (ACP registry v1 format)
119
+ const { transport, command, packageName, args: distArgs } = extractDistribution(entry);
120
+
121
+ const agentId = id || name;
122
+
123
+ // Apply curated display name and description from local descriptors (if available)
124
+ const curatedName = getDescriptorDisplayName(agentId);
125
+ const curatedDesc = getDescriptorDescription(agentId);
126
+
127
+ return {
128
+ id: agentId,
129
+ name: curatedName ?? name ?? id,
130
+ description: curatedDesc ?? String(entry.description ?? ''),
131
+ version: entry.version ? String(entry.version) : undefined,
132
+ transport,
133
+ command,
134
+ packageName,
135
+ args: distArgs ?? (Array.isArray(entry.args) ? entry.args.map(String) : undefined),
136
+ env: entry.env && typeof entry.env === 'object' ? entry.env as Record<string, string> : undefined,
137
+ tags: Array.isArray(entry.tags) ? entry.tags.map(String) : undefined,
138
+ homepage: entry.homepage ?? entry.website ? String(entry.homepage ?? entry.website) : undefined,
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Extract transport type, command, packageName, and args from
144
+ * the registry's `distribution` field. Falls back to legacy
145
+ * `transport`/`command` fields if `distribution` is absent.
146
+ */
147
+ function extractDistribution(entry: Record<string, unknown>): {
148
+ transport: AcpRegistryEntry['transport'];
149
+ command: string;
150
+ packageName?: string;
151
+ args?: string[];
152
+ } {
153
+ const dist = entry.distribution as Record<string, unknown> | undefined;
154
+
155
+ if (dist && typeof dist === 'object') {
156
+ // npx transport: { npx: { package: "@scope/name@version", args?: [...] } }
157
+ if (dist.npx && typeof dist.npx === 'object') {
158
+ const npx = dist.npx as Record<string, unknown>;
159
+ const fullPkg = String(npx.package ?? '');
160
+ // Strip version suffix: "@scope/name@1.2.3" -> "@scope/name"
161
+ const packageName = stripVersion(fullPkg);
162
+ const args = Array.isArray(npx.args) ? npx.args.map(String) : undefined;
163
+ // Also extract env if present at npx level
164
+ return { transport: 'npx', command: packageName, packageName, args };
165
+ }
166
+
167
+ // uvx transport: { uvx: { package: "name@version", args?: [...] } }
168
+ if (dist.uvx && typeof dist.uvx === 'object') {
169
+ const uvx = dist.uvx as Record<string, unknown>;
170
+ const fullPkg = String(uvx.package ?? '');
171
+ const packageName = stripVersion(fullPkg);
172
+ const args = Array.isArray(uvx.args) ? uvx.args.map(String) : undefined;
173
+ return { transport: 'uvx', command: packageName, packageName, args };
174
+ }
175
+
176
+ // binary transport: { binary: { "linux-x86_64": { cmd, args } } }
177
+ if (dist.binary && typeof dist.binary === 'object') {
178
+ return { transport: 'binary', command: '' };
179
+ }
180
+ }
181
+
182
+ // Legacy fallback: read flat `transport`/`command` fields
183
+ return {
184
+ transport: normalizeTransport(entry.transport),
185
+ command: String(entry.command ?? entry.cmd ?? ''),
186
+ };
187
+ }
188
+
189
+ /** Strip trailing @version from a package name. "@scope/pkg@1.0" -> "@scope/pkg" */
190
+ function stripVersion(pkg: string): string {
191
+ if (!pkg) return '';
192
+ // Scoped: @scope/name@version — find the last @ that isn't position 0
193
+ const lastAt = pkg.lastIndexOf('@');
194
+ if (lastAt > 0) return pkg.slice(0, lastAt);
195
+ return pkg;
196
+ }
197
+
198
+ function normalizeTransport(raw: unknown): AcpRegistryEntry['transport'] {
199
+ const t = String(raw ?? 'stdio').toLowerCase();
200
+ if (t === 'npx' || t === 'uvx' || t === 'binary') return t;
201
+ return 'stdio';
202
+ }