aws-runtime-bridge 1.7.48 → 1.7.50

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 (166) hide show
  1. package/dist/adapter/AcodeSdkAdapter.d.ts +77 -0
  2. package/dist/adapter/AcodeSdkAdapter.d.ts.map +1 -0
  3. package/dist/adapter/AcodeSdkAdapter.js +322 -0
  4. package/dist/adapter/AcodeSdkAdapter.test.d.ts +2 -0
  5. package/dist/adapter/AcodeSdkAdapter.test.d.ts.map +1 -0
  6. package/dist/adapter/AcodeSdkAdapter.test.js +273 -0
  7. package/dist/adapter/AdapterRegistry.d.ts +1 -1
  8. package/dist/adapter/AdapterRegistry.d.ts.map +1 -1
  9. package/dist/adapter/AdapterRegistry.js +2 -0
  10. package/dist/adapter/AdapterRegistry.test.js +10 -1
  11. package/dist/adapter/SdkProviderSpi.d.ts.map +1 -1
  12. package/dist/adapter/SdkProviderSpi.js +10 -1
  13. package/dist/adapter/index.d.ts +1 -0
  14. package/dist/adapter/index.d.ts.map +1 -1
  15. package/dist/adapter/index.js +1 -0
  16. package/dist/adapters/cc-switch/index.d.ts +2 -0
  17. package/dist/adapters/cc-switch/index.d.ts.map +1 -1
  18. package/dist/adapters/cc-switch/index.js +6 -0
  19. package/dist/adapters/cc-switch/mcp-acode.d.ts +17 -0
  20. package/dist/adapters/cc-switch/mcp-acode.d.ts.map +1 -0
  21. package/dist/adapters/cc-switch/mcp-acode.js +71 -0
  22. package/dist/adapters/cc-switch/mcp-acode.test.d.ts +2 -0
  23. package/dist/adapters/cc-switch/mcp-acode.test.d.ts.map +1 -0
  24. package/dist/adapters/cc-switch/mcp-acode.test.js +80 -0
  25. package/dist/adapters/cc-switch/mcp-claude.d.ts.map +1 -1
  26. package/dist/adapters/cc-switch/mcp-claude.js +1 -0
  27. package/dist/adapters/cc-switch/mcp-claudecode.d.ts.map +1 -1
  28. package/dist/adapters/cc-switch/mcp-claudecode.js +1 -0
  29. package/dist/adapters/cc-switch/mcp-claudecode.test.js +1 -0
  30. package/dist/adapters/cc-switch/mcp-codex.d.ts.map +1 -1
  31. package/dist/adapters/cc-switch/mcp-codex.js +1 -0
  32. package/dist/adapters/cc-switch/mcp-codex.test.js +1 -0
  33. package/dist/adapters/cc-switch/mcp-opencode.d.ts.map +1 -1
  34. package/dist/adapters/cc-switch/mcp-opencode.js +1 -0
  35. package/dist/adapters/cc-switch/mcp-opencode.test.js +1 -0
  36. package/dist/adapters/cc-switch/skill-acode.d.ts +11 -0
  37. package/dist/adapters/cc-switch/skill-acode.d.ts.map +1 -0
  38. package/dist/adapters/cc-switch/skill-acode.js +12 -0
  39. package/dist/adapters/cc-switch/skill-directory.test.js +1 -0
  40. package/dist/config.d.ts +4 -0
  41. package/dist/config.d.ts.map +1 -1
  42. package/dist/config.js +10 -0
  43. package/dist/index.js +2 -0
  44. package/dist/routes/ai-sources.js +3 -2
  45. package/dist/routes/instance.js +1 -1
  46. package/dist/routes/instance.test.js +2 -1
  47. package/dist/routes/mcp.js +2 -2
  48. package/dist/routes/mcp.test.js +2 -1
  49. package/dist/routes/runtime-binding.d.ts +3 -2
  50. package/dist/routes/runtime-binding.d.ts.map +1 -1
  51. package/dist/routes/runtime-binding.js +9 -56
  52. package/dist/routes/runtime-binding.test.js +4 -80
  53. package/dist/routes/sessions.d.ts +5 -0
  54. package/dist/routes/sessions.d.ts.map +1 -1
  55. package/dist/routes/sessions.js +36 -16
  56. package/dist/routes/sessions.test.js +13 -0
  57. package/dist/routes/skills.js +2 -2
  58. package/dist/routes/skills.test.js +2 -1
  59. package/dist/routes/terminal.d.ts +24 -4
  60. package/dist/routes/terminal.d.ts.map +1 -1
  61. package/dist/routes/terminal.js +99 -10
  62. package/dist/routes/terminal.test.js +74 -8
  63. package/dist/services/acode-package.d.ts +8 -0
  64. package/dist/services/acode-package.d.ts.map +1 -0
  65. package/dist/services/acode-package.js +82 -0
  66. package/dist/services/ai-source-apply.d.ts +4 -0
  67. package/dist/services/ai-source-apply.d.ts.map +1 -1
  68. package/dist/services/ai-source-apply.js +38 -9
  69. package/dist/services/ai-source-apply.test.js +58 -1
  70. package/dist/services/cc-switch-sdk.d.ts.map +1 -1
  71. package/dist/services/cc-switch-sdk.js +9 -1
  72. package/dist/services/instance-init-service.d.ts.map +1 -1
  73. package/dist/services/instance-init-service.js +3 -2
  74. package/dist/services/instance-init-service.test.js +6 -5
  75. package/dist/services/instance-service.d.ts.map +1 -1
  76. package/dist/services/instance-service.js +1 -0
  77. package/dist/services/instance-state.js +1 -1
  78. package/dist/services/instance-state.test.js +4 -4
  79. package/dist/services/tool-installer.d.ts.map +1 -1
  80. package/dist/services/tool-installer.js +10 -0
  81. package/dist/types.d.ts +5 -0
  82. package/dist/types.d.ts.map +1 -1
  83. package/dist/utils/file-utils.d.ts.map +1 -1
  84. package/dist/utils/file-utils.js +1 -0
  85. package/dist/utils/mcp-utils.d.ts.map +1 -1
  86. package/dist/utils/mcp-utils.js +4 -1
  87. package/dist/utils/validation.js +1 -1
  88. package/node_modules/@cc-switch/sdk/dist/constants.d.ts.map +1 -1
  89. package/node_modules/@cc-switch/sdk/dist/constants.js +7 -0
  90. package/node_modules/@cc-switch/sdk/dist/schemas.d.ts.map +1 -1
  91. package/node_modules/@cc-switch/sdk/dist/schemas.js +1 -0
  92. package/node_modules/@cc-switch/sdk/dist/sdk.d.ts.map +1 -1
  93. package/node_modules/@cc-switch/sdk/dist/sdk.js +7 -3
  94. package/node_modules/@cc-switch/sdk/dist/services/ai-config-service.d.ts +7 -0
  95. package/node_modules/@cc-switch/sdk/dist/services/ai-config-service.d.ts.map +1 -1
  96. package/node_modules/@cc-switch/sdk/dist/services/ai-config-service.js +29 -2
  97. package/node_modules/@cc-switch/sdk/dist/types.d.ts +4 -1
  98. package/node_modules/@cc-switch/sdk/dist/types.d.ts.map +1 -1
  99. package/package/acode/README.md +88 -0
  100. package/package/acode/dist/built-in-file-tools.d.ts +16 -0
  101. package/package/acode/dist/built-in-file-tools.d.ts.map +1 -0
  102. package/package/acode/dist/built-in-file-tools.js +231 -0
  103. package/package/acode/dist/cli.d.ts +3 -0
  104. package/package/acode/dist/cli.d.ts.map +1 -0
  105. package/package/acode/dist/cli.js +37 -0
  106. package/package/acode/dist/commands/commandLoader.d.ts +7 -0
  107. package/package/acode/dist/commands/commandLoader.d.ts.map +1 -0
  108. package/package/acode/dist/commands/commandLoader.js +33 -0
  109. package/package/acode/dist/commands/commandParser.d.ts +7 -0
  110. package/package/acode/dist/commands/commandParser.d.ts.map +1 -0
  111. package/package/acode/dist/commands/commandParser.js +77 -0
  112. package/package/acode/dist/commands/commandRegistry.d.ts +13 -0
  113. package/package/acode/dist/commands/commandRegistry.d.ts.map +1 -0
  114. package/package/acode/dist/commands/commandRegistry.js +30 -0
  115. package/package/acode/dist/config.d.ts +4 -0
  116. package/package/acode/dist/config.d.ts.map +1 -0
  117. package/package/acode/dist/config.js +80 -0
  118. package/package/acode/dist/hooks/hookBus.d.ts +19 -0
  119. package/package/acode/dist/hooks/hookBus.d.ts.map +1 -0
  120. package/package/acode/dist/hooks/hookBus.js +40 -0
  121. package/package/acode/dist/index.d.ts +8 -0
  122. package/package/acode/dist/index.d.ts.map +1 -0
  123. package/package/acode/dist/index.js +7 -0
  124. package/package/acode/dist/runtime.d.ts +34 -0
  125. package/package/acode/dist/runtime.d.ts.map +1 -0
  126. package/package/acode/dist/runtime.js +571 -0
  127. package/package/acode/dist/types.d.ts +99 -0
  128. package/package/acode/dist/types.d.ts.map +1 -0
  129. package/package/acode/dist/types.js +1 -0
  130. package/package/acode/package.json +24 -0
  131. package/package/aws-client-agent-mcp/dist/agent-client.d.ts +4 -4
  132. package/package/aws-client-agent-mcp/dist/agent-client.d.ts.map +1 -1
  133. package/package/aws-client-agent-mcp/dist/agent-client.js +9 -6
  134. package/package/aws-client-agent-mcp/dist/agent-client.js.map +1 -1
  135. package/package/aws-client-agent-mcp/dist/agent-client.test.js +21 -0
  136. package/package/aws-client-agent-mcp/dist/agent-client.test.js.map +1 -1
  137. package/package/aws-client-agent-mcp/dist/http-client.d.ts +1 -17
  138. package/package/aws-client-agent-mcp/dist/http-client.d.ts.map +1 -1
  139. package/package/aws-client-agent-mcp/dist/http-client.js +7 -113
  140. package/package/aws-client-agent-mcp/dist/http-client.js.map +1 -1
  141. package/package/aws-client-agent-mcp/dist/http-client.test.js +22 -2
  142. package/package/aws-client-agent-mcp/dist/http-client.test.js.map +1 -1
  143. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +0 -5
  144. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
  145. package/package/aws-client-agent-mcp/dist/mcp-server.js +1 -23
  146. package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
  147. package/package/aws-client-agent-mcp/dist/mcp-server.test.js +89 -6
  148. package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +1 -1
  149. package/package/aws-client-agent-mcp/dist/types.d.ts +3 -0
  150. package/package/aws-client-agent-mcp/dist/types.d.ts.map +1 -1
  151. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts +0 -13
  152. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts.map +1 -1
  153. package/package/aws-client-agent-mcp/dist/websocket-client.js +3 -100
  154. package/package/aws-client-agent-mcp/dist/websocket-client.js.map +1 -1
  155. package/package/cc-switch-sdk/dist/constants.d.ts.map +1 -1
  156. package/package/cc-switch-sdk/dist/constants.js +7 -0
  157. package/package/cc-switch-sdk/dist/schemas.d.ts.map +1 -1
  158. package/package/cc-switch-sdk/dist/schemas.js +1 -0
  159. package/package/cc-switch-sdk/dist/sdk.d.ts.map +1 -1
  160. package/package/cc-switch-sdk/dist/sdk.js +7 -3
  161. package/package/cc-switch-sdk/dist/services/ai-config-service.d.ts +7 -0
  162. package/package/cc-switch-sdk/dist/services/ai-config-service.d.ts.map +1 -1
  163. package/package/cc-switch-sdk/dist/services/ai-config-service.js +29 -2
  164. package/package/cc-switch-sdk/dist/types.d.ts +4 -1
  165. package/package/cc-switch-sdk/dist/types.d.ts.map +1 -1
  166. package/package.json +8 -4
@@ -0,0 +1,34 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import type { ACodeRuntimeConfig } from './types.js';
3
+ export declare class ACodeRuntime extends EventEmitter {
4
+ private readonly config;
5
+ private readonly hookBus;
6
+ private state;
7
+ private abortController;
8
+ constructor(config: ACodeRuntimeConfig);
9
+ private emitAcodeEvent;
10
+ /**
11
+ * 启动 ACode headless 会话。
12
+ * 主流程:加载 `.acode/config.json` 与 skills 目录 -> 发出 MCP/Skill 状态 ->
13
+ * 如有初始 prompt 则执行一轮响应。
14
+ */
15
+ start(): Promise<void>;
16
+ /**
17
+ * 处理一轮用户输入。
18
+ * 主流程:将用户输入追加到会话 -> 调用 OpenAI-compatible 模型 -> 如模型请求
19
+ * MCP tool call 则执行工具并继续同一轮,最后输出 assistant 文本与 usage。
20
+ */
21
+ submit(message: string): Promise<void>;
22
+ /** 中止当前轮次;当前骨架无长任务,仅发出完成状态。 */
23
+ abort(): Promise<void>;
24
+ /** 终止 ACode 会话并发出 session_complete。 */
25
+ stop(): Promise<void>;
26
+ private resolveCommandDirs;
27
+ private runModelTurn;
28
+ private requestChatCompletion;
29
+ private handleToolCalls;
30
+ private connectMcpServers;
31
+ private createMcpTransport;
32
+ private disconnectMcpServers;
33
+ }
34
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAa3C,OAAO,KAAK,EAOV,kBAAkB,EAEnB,MAAM,YAAY,CAAC;AA6PpB,qBAAa,YAAa,SAAQ,YAAY;IAmBhC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAlBnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IAEvC,OAAO,CAAC,KAAK,CAYX;IAEF,OAAO,CAAC,eAAe,CAAgC;gBAE1B,MAAM,EAAE,kBAAkB;IAevD,OAAO,CAAC,cAAc;IAUtB;;;;OAIG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAuE5B;;;;OAIG;IACG,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA+C5C,+BAA+B;IACzB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ5B,uCAAuC;IACjC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAU3B,OAAO,CAAC,kBAAkB;YASZ,YAAY;YA6BZ,qBAAqB;YA+BrB,eAAe;YAiFf,iBAAiB;IA8B/B,OAAO,CAAC,kBAAkB;YAuBZ,oBAAoB;CAYnC"}
@@ -0,0 +1,571 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
3
+ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
4
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
5
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
6
+ import { createBuiltInFileTools } from './built-in-file-tools.js';
7
+ import { loadAcodeCommands } from './commands/commandLoader.js';
8
+ import { ACodeCommandRegistry } from './commands/commandRegistry.js';
9
+ import { loadAcodeConfig, loadAcodeSkills } from './config.js';
10
+ import { ACodeHookBus, findDeniedHookResult } from './hooks/hookBus.js';
11
+ function nowIso() {
12
+ return new Date().toISOString();
13
+ }
14
+ function isRecord(value) {
15
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
16
+ }
17
+ function isJsonObject(value) {
18
+ return isRecord(value);
19
+ }
20
+ function normalizeText(value) {
21
+ const text = String(value ?? '').trim();
22
+ return text || undefined;
23
+ }
24
+ /**
25
+ * 解析 ACode 当前会话模型:优先显式启动参数,其次兼容 cc-switch 写入的
26
+ * OpenCode-style `provider.default.model`,最后回落到旧版 top-level model。
27
+ */
28
+ function resolveProviderConfig(config, acodeConfig) {
29
+ const defaultProvider = acodeConfig.provider?.default;
30
+ const providerOptions = defaultProvider?.options;
31
+ return {
32
+ model: config.model || defaultProvider?.model || acodeConfig.model || 'acode-default',
33
+ baseURL: normalizeText(providerOptions?.baseURL || providerOptions?.baseUrl),
34
+ apiKey: normalizeText(providerOptions?.apiKey),
35
+ systemPrompt: config.systemPrompt || acodeConfig.systemPrompt,
36
+ };
37
+ }
38
+ /**
39
+ * 解析 ACode 工具权限配置。
40
+ * 主流程:运行时配置优先 -> `.acode/config.json` 次之 -> 兼容旧式 allowedTools/deniedTools 字段。
41
+ */
42
+ function resolvePermissionConfig(config, acodeConfig) {
43
+ const configured = config.permissions ?? acodeConfig.permissions ?? {};
44
+ const legacyAllow = config.permissions ? [] : acodeConfig.allowedTools ?? [];
45
+ const legacyDeny = config.permissions ? [] : acodeConfig.deniedTools ?? [];
46
+ return {
47
+ mode: configured.mode ?? 'default',
48
+ allow: [...(configured.allow ?? []), ...legacyAllow].map((rule) => rule.trim()).filter(Boolean),
49
+ deny: [...(configured.deny ?? []), ...legacyDeny].map((rule) => rule.trim()).filter(Boolean),
50
+ };
51
+ }
52
+ function wildcardMatches(pattern, value) {
53
+ if (pattern === '*') {
54
+ return true;
55
+ }
56
+ if (pattern.endsWith('*')) {
57
+ return value.startsWith(pattern.slice(0, -1));
58
+ }
59
+ return pattern === value;
60
+ }
61
+ function toolPermissionNames(tool) {
62
+ if (tool.kind === 'builtin') {
63
+ return [tool.exposedName, tool.toolName, `builtin.${tool.toolName}`];
64
+ }
65
+ return [tool.exposedName, tool.toolName, `${tool.serverName}.${tool.toolName}`, `mcp.${tool.serverName}.${tool.toolName}`];
66
+ }
67
+ function matchesToolRule(rule, tool) {
68
+ return toolPermissionNames(tool).some((name) => wildcardMatches(rule, name));
69
+ }
70
+ function assertToolAllowedByRules(tool, rules, message) {
71
+ if (rules.length > 0 && !rules.some((rule) => matchesToolRule(rule, tool))) {
72
+ throw new Error(message);
73
+ }
74
+ }
75
+ function assertToolPermission(tool, permissions, commandAllowedTools) {
76
+ if (permissions.mode === 'bypassPermissions') {
77
+ return;
78
+ }
79
+ if (permissions.deny.some((rule) => matchesToolRule(rule, tool))) {
80
+ throw new Error(`ACode tool denied by permission rule: ${tool.exposedName}`);
81
+ }
82
+ assertToolAllowedByRules(tool, permissions.allow, `ACode tool is not in allowed permission rules: ${tool.exposedName}`);
83
+ assertToolAllowedByRules(tool, commandAllowedTools ?? [], `ACode slash command does not allow tool: ${tool.exposedName}`);
84
+ }
85
+ function isToolAllowed(tool, permissions, commandAllowedTools) {
86
+ try {
87
+ assertToolPermission(tool, permissions, commandAllowedTools);
88
+ return true;
89
+ }
90
+ catch {
91
+ return false;
92
+ }
93
+ }
94
+ function normalizeChatCompletionsUrl(baseURL) {
95
+ const trimmed = baseURL.replace(/\/+$/g, '');
96
+ if (/\/chat\/completions$/i.test(trimmed)) {
97
+ return trimmed;
98
+ }
99
+ return `${trimmed}/chat/completions`;
100
+ }
101
+ function coerceArguments(rawArguments) {
102
+ try {
103
+ const parsed = JSON.parse(rawArguments);
104
+ return isJsonObject(parsed) ? parsed : {};
105
+ }
106
+ catch {
107
+ return {};
108
+ }
109
+ }
110
+ function createFallbackResponse(message, state) {
111
+ const trimmed = message.trim();
112
+ return `ACode 已接收任务。当前已加载 ${state.configMcpCount} 个 MCP 配置、${state.exposedTools.length} 个 MCP Tool 与 ${state.skills.length} 个 Skill。${trimmed ? `输入摘要:${trimmed.slice(0, 200)}` : ''}`;
113
+ }
114
+ function toToolResultText(result) {
115
+ if (!isRecord(result)) {
116
+ return String(result ?? '');
117
+ }
118
+ const content = result.content;
119
+ if (Array.isArray(content)) {
120
+ const parts = content
121
+ .map((item) => {
122
+ if (!isRecord(item)) {
123
+ return '';
124
+ }
125
+ if (typeof item.text === 'string') {
126
+ return item.text;
127
+ }
128
+ return JSON.stringify(item);
129
+ })
130
+ .filter(Boolean);
131
+ if (parts.length > 0) {
132
+ return parts.join('\n');
133
+ }
134
+ }
135
+ return JSON.stringify(result);
136
+ }
137
+ function buildSkillPrompt(skills) {
138
+ if (skills.length === 0) {
139
+ return '';
140
+ }
141
+ return [
142
+ 'Available skills:',
143
+ ...skills.map((skill) => `- ${skill.name}${skill.description ? `: ${skill.description}` : ''}`),
144
+ 'Use these skills as guidance when they match the user task.',
145
+ ].join('\n');
146
+ }
147
+ function buildCommandPrompt(commands) {
148
+ if (commands.length === 0) {
149
+ return '';
150
+ }
151
+ return [
152
+ 'Available slash commands:',
153
+ ...commands.map((command) => `- /${command.name}${command.description ? `: ${command.description}` : ''}`),
154
+ 'When the user invokes a slash command, ACode expands it before model execution.',
155
+ ].join('\n');
156
+ }
157
+ function toOpenAiTools(tools) {
158
+ return tools.map((tool) => ({
159
+ type: 'function',
160
+ function: {
161
+ name: tool.exposedName,
162
+ description: tool.description || tool.toolName,
163
+ parameters: tool.inputSchema,
164
+ },
165
+ }));
166
+ }
167
+ function normalizeToolName(serverName, toolName) {
168
+ const normalized = `${serverName}__${toolName}`.replace(/[^a-zA-Z0-9_-]/g, '_');
169
+ return normalized.slice(0, 64) || 'mcp_tool';
170
+ }
171
+ function buildRequestHeaders(provider) {
172
+ const headers = { 'Content-Type': 'application/json' };
173
+ if (provider.apiKey) {
174
+ headers.Authorization = `Bearer ${provider.apiKey}`;
175
+ }
176
+ return headers;
177
+ }
178
+ export class ACodeRuntime extends EventEmitter {
179
+ constructor(config) {
180
+ super();
181
+ this.config = config;
182
+ this.state = {
183
+ started: false,
184
+ terminated: false,
185
+ configMcpCount: 0,
186
+ skills: [],
187
+ provider: { model: 'acode-default' },
188
+ permissions: { mode: 'default', allow: [], deny: [] },
189
+ mcpConnections: [],
190
+ exposedTools: [],
191
+ commands: [],
192
+ commandRegistry: new ACodeCommandRegistry([]),
193
+ conversation: [],
194
+ };
195
+ this.abortController = null;
196
+ this.hookBus = new ACodeHookBus({
197
+ sessionId: config.sessionId,
198
+ onError: (event, error) => {
199
+ this.emitAcodeEvent('error', {
200
+ message: `ACode hook failed: ${event.name}: ${error instanceof Error ? error.message : String(error)}`,
201
+ });
202
+ },
203
+ });
204
+ for (const handler of config.hooks ?? []) {
205
+ this.hookBus.register(handler);
206
+ }
207
+ }
208
+ emitAcodeEvent(type, data) {
209
+ const event = {
210
+ type,
211
+ sessionId: this.config.sessionId,
212
+ timestamp: nowIso(),
213
+ data,
214
+ };
215
+ this.emit('event', event);
216
+ }
217
+ /**
218
+ * 启动 ACode headless 会话。
219
+ * 主流程:加载 `.acode/config.json` 与 skills 目录 -> 发出 MCP/Skill 状态 ->
220
+ * 如有初始 prompt 则执行一轮响应。
221
+ */
222
+ async start() {
223
+ const acodeConfig = await loadAcodeConfig(this.config.configPath);
224
+ const skills = await loadAcodeSkills(this.config.skillsDir);
225
+ const provider = resolveProviderConfig(this.config, acodeConfig);
226
+ const permissions = resolvePermissionConfig(this.config, acodeConfig);
227
+ const mcpConnections = await this.connectMcpServers(acodeConfig.mcpServers ?? {});
228
+ const builtInTools = createBuiltInFileTools(this.config.workingDirectory);
229
+ const exposedTools = [...builtInTools, ...mcpConnections.flatMap((connection) => connection.tools)];
230
+ const commandDirs = this.resolveCommandDirs(acodeConfig);
231
+ const commands = await loadAcodeCommands(commandDirs);
232
+ const commandRegistry = new ACodeCommandRegistry(commands);
233
+ const skillPrompt = buildSkillPrompt(skills);
234
+ const commandPrompt = buildCommandPrompt(commands);
235
+ const systemPrompt = [provider.systemPrompt, skillPrompt, commandPrompt]
236
+ .map((item) => item?.trim())
237
+ .filter((item) => Boolean(item))
238
+ .join('\n\n');
239
+ this.state = {
240
+ started: true,
241
+ terminated: false,
242
+ configMcpCount: Object.keys(acodeConfig.mcpServers ?? {}).length,
243
+ skills,
244
+ provider,
245
+ permissions,
246
+ mcpConnections,
247
+ exposedTools,
248
+ commands,
249
+ commandRegistry,
250
+ conversation: systemPrompt ? [{ role: 'system', content: systemPrompt }] : [],
251
+ };
252
+ await this.hookBus.dispatch('SessionStart', {
253
+ workingDirectory: this.config.workingDirectory,
254
+ model: provider.model,
255
+ commandCount: commands.length,
256
+ });
257
+ this.emitAcodeEvent('session_started', {
258
+ workingDirectory: this.config.workingDirectory,
259
+ model: provider.model,
260
+ commandCount: commands.length,
261
+ });
262
+ this.emitAcodeEvent('mcp_status', {
263
+ configPath: this.config.configPath,
264
+ installedCount: this.state.configMcpCount,
265
+ servers: Object.keys(acodeConfig.mcpServers ?? {}),
266
+ connectedCount: mcpConnections.length,
267
+ builtInToolCount: builtInTools.length,
268
+ toolCount: exposedTools.length,
269
+ permissionMode: permissions.mode,
270
+ allowedToolRules: permissions.allow,
271
+ deniedToolRules: permissions.deny,
272
+ tools: exposedTools.map((tool) => ({
273
+ name: tool.exposedName,
274
+ source: tool.kind === 'mcp' ? `${tool.serverName}.${tool.toolName}` : `builtin.${tool.toolName}`,
275
+ })),
276
+ });
277
+ this.emitAcodeEvent('skill_status', {
278
+ skillsDir: this.config.skillsDir,
279
+ installedCount: skills.length,
280
+ skills: skills.map((skill) => ({ name: skill.name, description: skill.description })),
281
+ });
282
+ this.emitAcodeEvent('command_status', {
283
+ action: 'commands_loaded',
284
+ installedCommandCount: commands.length,
285
+ commandDirs,
286
+ commands: commands.map((command) => ({ name: command.name, description: command.description })),
287
+ });
288
+ if (this.config.initialPrompt) {
289
+ await this.submit(this.config.initialPrompt);
290
+ }
291
+ }
292
+ /**
293
+ * 处理一轮用户输入。
294
+ * 主流程:将用户输入追加到会话 -> 调用 OpenAI-compatible 模型 -> 如模型请求
295
+ * MCP tool call 则执行工具并继续同一轮,最后输出 assistant 文本与 usage。
296
+ */
297
+ async submit(message) {
298
+ if (!this.state.started || this.state.terminated) {
299
+ throw new Error('ACode runtime is not active');
300
+ }
301
+ const originalPrompt = message.trim();
302
+ const promptHookResults = await this.hookBus.dispatch('UserPromptSubmit', { prompt: originalPrompt });
303
+ const promptDeny = findDeniedHookResult(promptHookResults);
304
+ if (promptDeny) {
305
+ this.emitAcodeEvent('error', { message: promptDeny.message || 'ACode prompt was denied by hook' });
306
+ this.emitAcodeEvent('turn_complete', { failed: true });
307
+ return;
308
+ }
309
+ const expandedCommand = this.state.commandRegistry.expand(originalPrompt);
310
+ const trimmed = expandedCommand?.prompt.trim() || originalPrompt;
311
+ const commandAllowedTools = expandedCommand?.command.allowedTools;
312
+ if (expandedCommand) {
313
+ this.emitAcodeEvent('command_status', {
314
+ action: 'slash_command',
315
+ command: expandedCommand.command.name,
316
+ arguments: expandedCommand.argumentsText,
317
+ sourcePath: expandedCommand.command.sourcePath,
318
+ });
319
+ }
320
+ if (!this.state.provider.baseURL || !this.state.provider.apiKey) {
321
+ const text = createFallbackResponse(trimmed, this.state);
322
+ this.emitAcodeEvent('assistant_delta', { text });
323
+ this.emitAcodeEvent('turn_complete', { usage: { inputTokens: trimmed.length, outputTokens: text.length } });
324
+ return;
325
+ }
326
+ this.state.conversation.push({ role: 'user', content: trimmed });
327
+ try {
328
+ const result = await this.runModelTurn(commandAllowedTools);
329
+ if (result.text) {
330
+ this.emitAcodeEvent('assistant_delta', { text: result.text });
331
+ }
332
+ this.emitAcodeEvent('turn_complete', {
333
+ usage: result.usage ?? { inputTokens: trimmed.length, outputTokens: result.text.length },
334
+ });
335
+ }
336
+ catch (error) {
337
+ this.emitAcodeEvent('error', {
338
+ message: error instanceof Error ? error.message : String(error),
339
+ });
340
+ this.emitAcodeEvent('turn_complete', { failed: true });
341
+ }
342
+ }
343
+ /** 中止当前轮次;当前骨架无长任务,仅发出完成状态。 */
344
+ async abort() {
345
+ this.abortController?.abort();
346
+ this.abortController = null;
347
+ if (!this.state.terminated) {
348
+ this.emitAcodeEvent('turn_complete', { aborted: true });
349
+ }
350
+ }
351
+ /** 终止 ACode 会话并发出 session_complete。 */
352
+ async stop() {
353
+ if (this.state.terminated) {
354
+ return;
355
+ }
356
+ await this.disconnectMcpServers();
357
+ await this.hookBus.dispatch('Stop', { reason: 'session_stop' });
358
+ this.state.terminated = true;
359
+ this.emitAcodeEvent('session_complete', { exitCode: 0 });
360
+ }
361
+ resolveCommandDirs(acodeConfig) {
362
+ const configured = [
363
+ ...(this.config.commandDirs ?? []),
364
+ ...(acodeConfig.commandDirs ?? []),
365
+ ...(acodeConfig.commands?.directories ?? []),
366
+ ];
367
+ return [...new Set(configured.map((directory) => directory.trim()).filter(Boolean))];
368
+ }
369
+ async runModelTurn(commandAllowedTools) {
370
+ let finalText = '';
371
+ let finalUsage;
372
+ for (let step = 0; step < 5; step += 1) {
373
+ const response = await this.requestChatCompletion(commandAllowedTools);
374
+ const choice = response.choices?.[0];
375
+ const message = choice?.message;
376
+ const content = typeof message?.content === 'string' ? message.content : '';
377
+ const toolCalls = Array.isArray(message?.tool_calls) ? message.tool_calls : [];
378
+ finalUsage = {
379
+ inputTokens: response.usage?.prompt_tokens ?? response.usage?.input_tokens ?? 0,
380
+ outputTokens: response.usage?.completion_tokens ?? response.usage?.output_tokens ?? 0,
381
+ };
382
+ this.state.conversation.push({
383
+ role: 'assistant',
384
+ content: content || null,
385
+ tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
386
+ });
387
+ if (content) {
388
+ finalText = content;
389
+ }
390
+ if (toolCalls.length === 0) {
391
+ return { text: finalText, usage: finalUsage };
392
+ }
393
+ await this.handleToolCalls(toolCalls, commandAllowedTools);
394
+ }
395
+ return { text: finalText || 'ACode 已达到本轮 MCP 工具调用上限。', usage: finalUsage };
396
+ }
397
+ async requestChatCompletion(commandAllowedTools) {
398
+ const provider = this.state.provider;
399
+ if (!provider.baseURL) {
400
+ throw new Error('ACode provider baseURL is not configured');
401
+ }
402
+ this.abortController = new AbortController();
403
+ const body = {
404
+ model: provider.model,
405
+ messages: this.state.conversation,
406
+ stream: false,
407
+ };
408
+ const allowedTools = this.state.exposedTools.filter((tool) => isToolAllowed(tool, this.state.permissions, commandAllowedTools));
409
+ if (allowedTools.length > 0) {
410
+ body.tools = toOpenAiTools(allowedTools);
411
+ body.tool_choice = 'auto';
412
+ }
413
+ const response = await fetch(normalizeChatCompletionsUrl(provider.baseURL), {
414
+ method: 'POST',
415
+ headers: buildRequestHeaders(provider),
416
+ body: JSON.stringify(body),
417
+ signal: this.abortController.signal,
418
+ });
419
+ this.abortController = null;
420
+ if (!response.ok) {
421
+ throw new Error(`ACode provider request failed: ${response.status} ${await response.text()}`);
422
+ }
423
+ const payload = await response.json();
424
+ return isRecord(payload) ? payload : {};
425
+ }
426
+ async handleToolCalls(toolCalls, commandAllowedTools) {
427
+ for (const toolCall of toolCalls) {
428
+ const tool = this.state.exposedTools.find((item) => item.exposedName === toolCall.function.name);
429
+ if (!tool) {
430
+ this.state.conversation.push({
431
+ role: 'tool',
432
+ tool_call_id: toolCall.id,
433
+ content: `ACode MCP tool not found: ${toolCall.function.name}`,
434
+ });
435
+ continue;
436
+ }
437
+ if (tool.kind === 'builtin') {
438
+ const toolArguments = coerceArguments(toolCall.function.arguments);
439
+ assertToolPermission(tool, this.state.permissions, commandAllowedTools);
440
+ const preResults = await this.hookBus.dispatch('PreToolUse', {
441
+ source: 'builtin',
442
+ toolName: tool.toolName,
443
+ exposedName: tool.exposedName,
444
+ arguments: toolArguments,
445
+ });
446
+ const denied = findDeniedHookResult(preResults);
447
+ if (denied) {
448
+ throw new Error(denied.message || `ACode tool denied by hook: ${tool.exposedName}`);
449
+ }
450
+ this.emitAcodeEvent('mcp_status', { action: 'call_tool', server: 'builtin', tool: tool.toolName });
451
+ const result = await tool.execute(toolArguments);
452
+ await this.hookBus.dispatch('PostToolUse', {
453
+ source: 'builtin',
454
+ toolName: tool.toolName,
455
+ exposedName: tool.exposedName,
456
+ result: toToolResultText(result),
457
+ });
458
+ this.state.conversation.push({
459
+ role: 'tool',
460
+ tool_call_id: toolCall.id,
461
+ content: toToolResultText(result),
462
+ });
463
+ continue;
464
+ }
465
+ const connection = this.state.mcpConnections.find((item) => item.serverName === tool.serverName);
466
+ if (!connection) {
467
+ this.state.conversation.push({
468
+ role: 'tool',
469
+ tool_call_id: toolCall.id,
470
+ content: `ACode MCP server not connected: ${tool.serverName}`,
471
+ });
472
+ continue;
473
+ }
474
+ const toolArguments = coerceArguments(toolCall.function.arguments);
475
+ assertToolPermission(tool, this.state.permissions, commandAllowedTools);
476
+ const preResults = await this.hookBus.dispatch('PreToolUse', {
477
+ source: 'mcp',
478
+ serverName: tool.serverName,
479
+ toolName: tool.toolName,
480
+ exposedName: tool.exposedName,
481
+ arguments: toolArguments,
482
+ });
483
+ const denied = findDeniedHookResult(preResults);
484
+ if (denied) {
485
+ throw new Error(denied.message || `ACode tool denied by hook: ${tool.exposedName}`);
486
+ }
487
+ this.emitAcodeEvent('mcp_status', { action: 'call_tool', server: tool.serverName, tool: tool.toolName });
488
+ const result = await connection.client.callTool({
489
+ name: tool.toolName,
490
+ arguments: toolArguments,
491
+ });
492
+ await this.hookBus.dispatch('PostToolUse', {
493
+ source: 'mcp',
494
+ serverName: tool.serverName,
495
+ toolName: tool.toolName,
496
+ exposedName: tool.exposedName,
497
+ result: toToolResultText(result),
498
+ });
499
+ this.state.conversation.push({
500
+ role: 'tool',
501
+ tool_call_id: toolCall.id,
502
+ content: toToolResultText(result),
503
+ });
504
+ }
505
+ }
506
+ async connectMcpServers(servers) {
507
+ const connections = [];
508
+ for (const [serverName, serverSpec] of Object.entries(servers)) {
509
+ try {
510
+ const client = new Client({ name: 'acode', version: '0.1.0' });
511
+ const transport = this.createMcpTransport(serverSpec);
512
+ await client.connect(transport);
513
+ const { tools } = await client.listTools();
514
+ connections.push({
515
+ serverName,
516
+ client,
517
+ transport,
518
+ tools: tools.map((tool) => ({
519
+ serverName,
520
+ toolName: tool.name,
521
+ kind: 'mcp',
522
+ exposedName: normalizeToolName(serverName, tool.name),
523
+ description: tool.description,
524
+ inputSchema: isJsonObject(tool.inputSchema) ? tool.inputSchema : { type: 'object', properties: {} },
525
+ })),
526
+ });
527
+ }
528
+ catch (error) {
529
+ this.emitAcodeEvent('error', {
530
+ message: `ACode MCP server failed to connect: ${serverName}: ${error instanceof Error ? error.message : String(error)}`,
531
+ });
532
+ }
533
+ }
534
+ return connections;
535
+ }
536
+ createMcpTransport(serverSpec) {
537
+ if (serverSpec.type === 'stdio' || serverSpec.command) {
538
+ if (!serverSpec.command) {
539
+ throw new Error('stdio MCP server requires command');
540
+ }
541
+ return new StdioClientTransport({
542
+ command: serverSpec.command,
543
+ args: serverSpec.args,
544
+ cwd: serverSpec.cwd || this.config.workingDirectory,
545
+ env: { ...(this.config.env ?? {}), ...(serverSpec.env ?? {}) },
546
+ stderr: 'pipe',
547
+ });
548
+ }
549
+ if (!serverSpec.url) {
550
+ throw new Error('remote MCP server requires url');
551
+ }
552
+ const requestInit = serverSpec.headers ? { headers: serverSpec.headers } : undefined;
553
+ if (serverSpec.type === 'sse') {
554
+ return new SSEClientTransport(new URL(serverSpec.url), { requestInit });
555
+ }
556
+ return new StreamableHTTPClientTransport(new URL(serverSpec.url), { requestInit });
557
+ }
558
+ async disconnectMcpServers() {
559
+ const connections = this.state.mcpConnections;
560
+ this.state.mcpConnections = [];
561
+ this.state.exposedTools = [];
562
+ await Promise.all(connections.map(async (connection) => {
563
+ try {
564
+ await connection.transport.close();
565
+ }
566
+ catch {
567
+ // Best-effort shutdown; stale MCP transports must not block runtime termination.
568
+ }
569
+ }));
570
+ }
571
+ }