aws-runtime-bridge 1.7.40 → 1.7.42

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 (30) hide show
  1. package/dist/routes/terminal.d.ts +5 -0
  2. package/dist/routes/terminal.d.ts.map +1 -1
  3. package/dist/routes/terminal.js +38 -12
  4. package/dist/routes/terminal.test.js +91 -1
  5. package/dist/services/instance-state.d.ts.map +1 -1
  6. package/dist/services/instance-state.js +33 -18
  7. package/dist/services/instance-state.test.js +41 -9
  8. package/dist/services/startup-config-wizard.d.ts.map +1 -1
  9. package/dist/services/startup-config-wizard.js +12 -13
  10. package/dist/utils/file-utils.d.ts +2 -1
  11. package/dist/utils/file-utils.d.ts.map +1 -1
  12. package/dist/utils/file-utils.js +35 -5
  13. package/dist/utils/file-utils.test.js +43 -2
  14. package/package/aws-client-agent-mcp/dist/agent-client.js +4 -1
  15. package/package/aws-client-agent-mcp/dist/agent-client.js.map +1 -1
  16. package/package/aws-client-agent-mcp/dist/agent-client.test.js +44 -0
  17. package/package/aws-client-agent-mcp/dist/agent-client.test.js.map +1 -1
  18. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +10 -0
  19. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
  20. package/package/aws-client-agent-mcp/dist/mcp-server.js +32 -3
  21. package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
  22. package/package/aws-client-agent-mcp/dist/mcp-server.test.js +165 -15
  23. package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +1 -1
  24. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts.map +1 -1
  25. package/package/aws-client-agent-mcp/dist/mcp-tools.js +3 -3
  26. package/package/aws-client-agent-mcp/dist/mcp-tools.js.map +1 -1
  27. package/package/aws-client-agent-mcp/dist/types.d.ts +1 -0
  28. package/package/aws-client-agent-mcp/dist/types.d.ts.map +1 -1
  29. package/package/aws-client-agent-mcp/dist/types.js.map +1 -1
  30. package/package.json +1 -1
@@ -60,6 +60,11 @@ export declare function evaluatePersistedSessionReuse(session: Pick<PersistedSes
60
60
  */
61
61
  export declare function formatTerminalPrompt(currentDirectory: string): string;
62
62
  export declare function buildRuntimeEnv(agentId: string, workspacePath: string | undefined, baseEnv: NodeJS.ProcessEnv, envOverrides?: Record<string, unknown>): Record<string, string>;
63
+ /**
64
+ * 将 SDK 工具结束事件转换为时间线返回数据状态。
65
+ * 主流程:格式化工具结果 -> 保留 MCP 专用展示 -> 其它工具按动作类型追加到对应时间线事件。
66
+ */
67
+ export declare function buildToolResultTimelineActionInfo(event: ProviderEvent): RuntimeStatusActionInfo | undefined;
63
68
  export declare function formatSdkOutputEvent(event: ProviderEvent): string | undefined;
64
69
  export declare function resolveSdkProviderId(command?: string): string;
65
70
  //# sourceMappingURL=terminal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"terminal.d.ts","sourceRoot":"","sources":["../../src/routes/terminal.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAKxC,OAAO,EAEL,KAAK,oBAAoB,EAEzB,KAAK,aAAa,EAClB,KAAK,uBAAuB,EAI7B,MAAM,qBAAqB,CAAC;AAc7B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,eAAO,MAAM,cAAc,4CAAW,CAAC;AAIvC,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,oBAAoB,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAGD,eAAO,MAAM,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAa,CAAC;AAKnE,wBAAgB,wBAAwB,CAAC,UAAU,CAAC,EAAE,uBAAuB;;;cAS5E;AAaD;;GAEG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEpE;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAC3C,QAAQ,GAAE,MAAM,CAAC,QAA2B,EAC5C,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CAMR;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CAAC,QAAQ,SAAkC,GAAG,WAAW,CAMnG;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,UAAQ,GAAG,MAAM,CAQnG;AAED;;GAEG;AACH,wBAAgB,kCAAkC,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAiB7F;AA2CD,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,YAAY,GAAG,aAAa,GAAG,UAAU,CAAC;CACnD;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,EACtC,YAAY,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAA0B,GACxD,6BAA6B,CAU/B;AA4BD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,CAErE;AAyGD,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,OAAO,EAAE,MAAM,CAAC,UAAU,EAC1B,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACrC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA+BxB;AA8BD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,GAAG,SAAS,CAU7E;AAYD,wBAAgB,oBAAoB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7D"}
1
+ {"version":3,"file":"terminal.d.ts","sourceRoot":"","sources":["../../src/routes/terminal.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAKxC,OAAO,EAEL,KAAK,oBAAoB,EAGzB,KAAK,aAAa,EAClB,KAAK,uBAAuB,EAI7B,MAAM,qBAAqB,CAAC;AAc7B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,eAAO,MAAM,cAAc,4CAAW,CAAC;AAIvC,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,oBAAoB,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAGD,eAAO,MAAM,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAa,CAAC;AAKnE,wBAAgB,wBAAwB,CAAC,UAAU,CAAC,EAAE,uBAAuB;;;cAS5E;AAaD;;GAEG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEpE;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAC3C,QAAQ,GAAE,MAAM,CAAC,QAA2B,EAC5C,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CAMR;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CAAC,QAAQ,SAAkC,GAAG,WAAW,CAMnG;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,UAAQ,GAAG,MAAM,CAQnG;AAED;;GAEG;AACH,wBAAgB,kCAAkC,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAiB7F;AA2CD,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,YAAY,GAAG,aAAa,GAAG,UAAU,CAAC;CACnD;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,EACtC,YAAY,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAA0B,GACxD,6BAA6B,CAU/B;AA4BD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,CAErE;AAyGD,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,OAAO,EAAE,MAAM,CAAC,UAAU,EAC1B,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACrC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA+BxB;AA8BD;;;GAGG;AACH,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,aAAa,GAAG,uBAAuB,GAAG,SAAS,CAiC3G;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,GAAG,SAAS,CAU7E;AAYD,wBAAgB,oBAAoB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7D"}
@@ -11,7 +11,7 @@ import path from 'node:path';
11
11
  import { TextDecoder } from 'node:util';
12
12
  import { Router } from 'express';
13
13
  import { v4 as uuidv4 } from 'uuid';
14
- import { adapterRegistry, registerSdkProviders, resolveSdkProviderIdByCommand, } from '../adapter/index.js';
14
+ import { adapterRegistry, getToolActionInfo, registerSdkProviders, resolveSdkProviderIdByCommand, } from '../adapter/index.js';
15
15
  import { validateToken } from '../middleware/auth.js';
16
16
  import { getAgentProcessManager } from '../services/agent-process-manager.js';
17
17
  import { enqueueMcpLaunchBinding } from '../services/mcp-launch-binding-queue.js';
@@ -317,6 +317,40 @@ function isMcpProviderEvent(event) {
317
317
  const toolName = String(event.data.toolName || '').toLowerCase();
318
318
  return toolName.startsWith('mcp__') || toolName.includes('-mcp__') || toolName.includes('mcp_');
319
319
  }
320
+ /**
321
+ * 将 SDK 工具结束事件转换为时间线返回数据状态。
322
+ * 主流程:格式化工具结果 -> 保留 MCP 专用展示 -> 其它工具按动作类型追加到对应时间线事件。
323
+ */
324
+ export function buildToolResultTimelineActionInfo(event) {
325
+ if (event.type !== 'tool_use_end') {
326
+ return undefined;
327
+ }
328
+ const actionResult = formatToolResultForTimeline(event.data.toolResult ?? event.data.actionResult);
329
+ if (!actionResult) {
330
+ return undefined;
331
+ }
332
+ const inferredAction = getToolActionInfo(event.data.toolName, event.data.toolInput);
333
+ const actionType = event.data.actionType || inferredAction.actionType;
334
+ if (actionType === 'idle') {
335
+ return undefined;
336
+ }
337
+ if (actionType === 'mcp' && isMcpProviderEvent(event)) {
338
+ return {
339
+ actionType: 'mcp',
340
+ actionLabel: 'MCP返回',
341
+ actionDetail: event.data.actionDetail || event.data.toolName,
342
+ actionId: event.data.actionId || event.data.toolUseId,
343
+ actionResult,
344
+ };
345
+ }
346
+ return {
347
+ actionType,
348
+ actionLabel: event.data.actionLabel || inferredAction.actionLabel,
349
+ actionDetail: event.data.actionDetail || inferredAction.actionDetail || event.data.toolName,
350
+ actionId: event.data.actionId || event.data.toolUseId,
351
+ actionResult,
352
+ };
353
+ }
320
354
  export function formatSdkOutputEvent(event) {
321
355
  if (event.type === 'text_delta' || event.type === 'thinking') {
322
356
  return event.data.text || undefined;
@@ -351,17 +385,9 @@ function wireSdkAdapterEvents(adapter, definition) {
351
385
  if (entry) {
352
386
  console.log(`[${definition.displayName} Adapter] Event: ${event.type} for session ${event.sessionId}`);
353
387
  forwardSdkOutputEvent(event, entry);
354
- if (event.type === 'tool_use_end' && isMcpProviderEvent(event)) {
355
- const actionResult = formatToolResultForTimeline(event.data.toolResult);
356
- if (actionResult) {
357
- void sendStatus(entry.agentId, event.sessionId, 'tool_result', {
358
- actionType: 'mcp',
359
- actionLabel: 'MCP返回',
360
- actionDetail: event.data.toolName,
361
- actionId: event.data.toolUseId,
362
- actionResult,
363
- });
364
- }
388
+ const toolResultActionInfo = buildToolResultTimelineActionInfo(event);
389
+ if (toolResultActionInfo) {
390
+ void sendStatus(entry.agentId, event.sessionId, 'tool_result', toolResultActionInfo);
365
391
  }
366
392
  if (event.type === 'turn_complete' && event.data.usage) {
367
393
  sendStatus(entry.agentId, event.sessionId, 'waiting_input', undefined, resolveStatusChangeUsage({ usage: event.data.usage }));
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { SDK_PROVIDER_DEFINITIONS } from '../adapter/SdkProviderSpi.js';
3
- import { buildRuntimeEnv, createTerminalOutputDecoder, decodeTerminalOutputChunk, evaluatePersistedSessionReuse, formatTerminalPrompt, formatSdkOutputEvent, normalizeTerminalCommandInput, parseTerminalDirectoryChangeTarget, resolveTerminalOutputEncoding, resolveSdkProviderId, resolveStatusChangeUsage, } from './terminal.js';
3
+ import { buildRuntimeEnv, buildToolResultTimelineActionInfo, createTerminalOutputDecoder, decodeTerminalOutputChunk, evaluatePersistedSessionReuse, formatTerminalPrompt, formatSdkOutputEvent, normalizeTerminalCommandInput, parseTerminalDirectoryChangeTarget, resolveTerminalOutputEncoding, resolveSdkProviderId, resolveStatusChangeUsage, } from './terminal.js';
4
4
  describe('terminal route validation', () => {
5
5
  it('requires agentId and workspacePath for start', () => {
6
6
  const validateStartRequest = (body) => {
@@ -188,3 +188,93 @@ describe('SDK output forwarding', () => {
188
188
  expect(formatSdkOutputEvent(toolEvent)).toBeUndefined();
189
189
  });
190
190
  });
191
+ describe('SDK tool result timeline action info', () => {
192
+ it('keeps MCP tool results on MCP timeline events', () => {
193
+ const event = {
194
+ type: 'tool_use_end',
195
+ sessionId: 'session-1',
196
+ timestamp: new Date(0).toISOString(),
197
+ data: {
198
+ toolName: 'mcp__aws-mcp__get_profile',
199
+ toolUseId: 'tool-1',
200
+ toolResult: '{"displayName":"Agent"}',
201
+ },
202
+ };
203
+ expect(buildToolResultTimelineActionInfo(event)).toEqual({
204
+ actionType: 'mcp',
205
+ actionLabel: 'MCP返回',
206
+ actionDetail: 'mcp__aws-mcp__get_profile',
207
+ actionId: 'tool-1',
208
+ actionResult: '{"displayName":"Agent"}',
209
+ });
210
+ });
211
+ it('forwards bash tool results to command timeline events', () => {
212
+ const event = {
213
+ type: 'tool_use_end',
214
+ sessionId: 'session-1',
215
+ timestamp: new Date(0).toISOString(),
216
+ data: {
217
+ toolName: 'bash',
218
+ toolInput: { command: 'npm run build' },
219
+ toolUseId: 'tool-2',
220
+ toolResult: 'build passed',
221
+ },
222
+ };
223
+ expect(buildToolResultTimelineActionInfo(event)).toEqual({
224
+ actionType: 'bash',
225
+ actionLabel: '执行命令',
226
+ actionDetail: 'npm run build',
227
+ actionId: 'tool-2',
228
+ actionResult: 'build passed',
229
+ });
230
+ });
231
+ it('forwards read tool results to file-read timeline events', () => {
232
+ const event = {
233
+ type: 'tool_use_end',
234
+ sessionId: 'session-1',
235
+ timestamp: new Date(0).toISOString(),
236
+ data: {
237
+ toolName: 'read_file',
238
+ toolInput: { path: 'src/index.ts' },
239
+ actionId: 'tool-3',
240
+ actionResult: 'export const ok = true',
241
+ },
242
+ };
243
+ expect(buildToolResultTimelineActionInfo(event)).toEqual({
244
+ actionType: 'read_file',
245
+ actionLabel: '读取文件',
246
+ actionDetail: 'src/index.ts',
247
+ actionId: 'tool-3',
248
+ actionResult: 'export const ok = true',
249
+ });
250
+ });
251
+ it('keeps MCP-named filesystem results on semantic file-read timeline events', () => {
252
+ const event = {
253
+ type: 'tool_use_end',
254
+ sessionId: 'session-1',
255
+ timestamp: new Date(0).toISOString(),
256
+ data: {
257
+ toolName: 'mcp__filesystem__read_file',
258
+ toolInput: { path: 'src/index.ts' },
259
+ toolUseId: 'tool-4',
260
+ toolResult: 'export const ok = true',
261
+ },
262
+ };
263
+ expect(buildToolResultTimelineActionInfo(event)).toEqual({
264
+ actionType: 'read_file',
265
+ actionLabel: '读取文件',
266
+ actionDetail: 'src/index.ts',
267
+ actionId: 'tool-4',
268
+ actionResult: 'export const ok = true',
269
+ });
270
+ });
271
+ it('ignores tool result events without displayable result content', () => {
272
+ const event = {
273
+ type: 'tool_use_end',
274
+ sessionId: 'session-1',
275
+ timestamp: new Date(0).toISOString(),
276
+ data: { toolName: 'read_file', toolResult: ' ' },
277
+ };
278
+ expect(buildToolResultTimelineActionInfo(event)).toBeUndefined();
279
+ });
280
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"instance-state.d.ts","sourceRoot":"","sources":["../../src/services/instance-state.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAKjD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAiB/E;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,OAAO,CAAC,aAAa,CAAC,GAC5B,OAAO,CAAC,aAAa,CAAC,CAiBxB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD"}
1
+ {"version":3,"file":"instance-state.d.ts","sourceRoot":"","sources":["../../src/services/instance-state.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAqBjD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAgB/E;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,OAAO,CAAC,aAAa,CAAC,GAC5B,OAAO,CAAC,aAAa,CAAC,CAiBxB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD"}
@@ -3,12 +3,27 @@
3
3
  *
4
4
  * 管理 Agent 实例的初始化状态持久化
5
5
  */
6
- import path from 'node:path';
7
- import os from 'node:os';
8
6
  import { promises as fs } from 'node:fs';
9
- import { ensureEnabledTools } from '../utils/validation.js';
10
- import { parseBoolean } from '../utils/validation.js';
11
- import { ensureDir } from '../utils/file-utils.js';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import { getRuntimeHomeDir } from '../config.js';
10
+ import { atomicWriteJsonFile, ensureDir } from '../utils/file-utils.js';
11
+ import { ensureEnabledTools, parseBoolean } from '../utils/validation.js';
12
+ function getLegacyInstanceStateFile(agentId) {
13
+ return path.join(os.tmpdir(), 'agentswork-runtime-init', String(agentId), 'state.json');
14
+ }
15
+ function createDefaultInstanceState() {
16
+ return {
17
+ initialized: false,
18
+ skills: [],
19
+ mcpServers: [],
20
+ enabledTools: ['claude', 'opencode', 'codex'],
21
+ toolStatus: {},
22
+ skillEnabled: true,
23
+ mcpEnabled: true,
24
+ updatedAt: null,
25
+ };
26
+ }
12
27
  /**
13
28
  * 获取实例状态文件路径
14
29
  *
@@ -16,7 +31,7 @@ import { ensureDir } from '../utils/file-utils.js';
16
31
  * @returns 状态文件路径
17
32
  */
18
33
  export function getInstanceStateFile(agentId) {
19
- return path.join(os.tmpdir(), 'agentswork-runtime-init', String(agentId), 'state.json');
34
+ return path.join(getRuntimeHomeDir(), '.aws-bridge', 'instances', String(agentId), 'state.json');
20
35
  }
21
36
  /**
22
37
  * 加载实例状态
@@ -26,21 +41,21 @@ export function getInstanceStateFile(agentId) {
26
41
  */
27
42
  export async function loadInstanceState(agentId) {
28
43
  const filePath = getInstanceStateFile(agentId);
44
+ const legacyFilePath = getLegacyInstanceStateFile(agentId);
29
45
  try {
30
46
  const raw = await fs.readFile(filePath, 'utf-8');
31
47
  return JSON.parse(raw);
32
48
  }
33
49
  catch {
34
- return {
35
- initialized: false,
36
- skills: [],
37
- mcpServers: [],
38
- enabledTools: ['claude', 'opencode', 'codex'],
39
- toolStatus: {},
40
- skillEnabled: true,
41
- mcpEnabled: true,
42
- updatedAt: null,
43
- };
50
+ try {
51
+ const raw = await fs.readFile(legacyFilePath, 'utf-8');
52
+ const legacyState = JSON.parse(raw);
53
+ await atomicWriteJsonFile(filePath, legacyState);
54
+ return legacyState;
55
+ }
56
+ catch {
57
+ return createDefaultInstanceState();
58
+ }
44
59
  }
45
60
  }
46
61
  /**
@@ -65,7 +80,7 @@ export async function saveInstanceState(agentId, state) {
65
80
  mcpEnabled: parseBoolean(state?.mcpEnabled, true),
66
81
  updatedAt: new Date().toISOString(),
67
82
  };
68
- await fs.writeFile(filePath, JSON.stringify(next, null, 2), 'utf-8');
83
+ await atomicWriteJsonFile(filePath, next);
69
84
  return next;
70
85
  }
71
86
  /**
@@ -75,5 +90,5 @@ export async function saveInstanceState(agentId, state) {
75
90
  * @returns 初始化根目录路径
76
91
  */
77
92
  export function getAgentInitRoot(agentId) {
78
- return path.join(os.tmpdir(), 'agentswork-runtime-init', String(agentId));
93
+ return path.join(getRuntimeHomeDir(), '.aws-bridge', 'instances', String(agentId));
79
94
  }
@@ -1,33 +1,46 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { promises as fs } from 'node:fs';
2
3
  import os from 'node:os';
3
4
  import path from 'node:path';
4
- import { promises as fs } from 'node:fs';
5
5
  import { getInstanceStateFile, loadInstanceState, saveInstanceState, getAgentInitRoot, } from './instance-state.js';
6
6
  describe('instance-state service', () => {
7
7
  const testAgentId = 'test-agent-123';
8
- const stateFilePath = getInstanceStateFile(testAgentId);
8
+ const originalRuntimeHomeDir = process.env.AWS_RUNTIME_HOME_DIR;
9
+ const testRuntimeHomeDir = path.join(os.tmpdir(), `aws-instance-state-home-${Date.now()}`);
10
+ function getCurrentStateFilePath() {
11
+ return getInstanceStateFile(testAgentId);
12
+ }
9
13
  beforeEach(async () => {
14
+ process.env.AWS_RUNTIME_HOME_DIR = testRuntimeHomeDir;
10
15
  // 清理测试文件
11
16
  try {
12
- await fs.rm(path.dirname(stateFilePath), { recursive: true, force: true });
17
+ await fs.rm(testRuntimeHomeDir, { recursive: true, force: true });
18
+ await fs.rm(path.join(os.tmpdir(), 'agentswork-runtime-init', testAgentId), { recursive: true, force: true });
13
19
  }
14
20
  catch {
15
21
  // 目录不存在,忽略
16
22
  }
17
23
  });
18
24
  afterEach(async () => {
25
+ if (originalRuntimeHomeDir === undefined) {
26
+ delete process.env.AWS_RUNTIME_HOME_DIR;
27
+ }
28
+ else {
29
+ process.env.AWS_RUNTIME_HOME_DIR = originalRuntimeHomeDir;
30
+ }
19
31
  // 清理测试文件
20
32
  try {
21
- await fs.rm(path.dirname(stateFilePath), { recursive: true, force: true });
33
+ await fs.rm(testRuntimeHomeDir, { recursive: true, force: true });
34
+ await fs.rm(path.join(os.tmpdir(), 'agentswork-runtime-init', testAgentId), { recursive: true, force: true });
22
35
  }
23
36
  catch {
24
37
  // 目录不存在,忽略
25
38
  }
26
39
  });
27
40
  describe('getInstanceStateFile', () => {
28
- it('should return correct path in temp directory', () => {
41
+ it('should return correct path in runtime home directory', () => {
29
42
  const result = getInstanceStateFile(testAgentId);
30
- const expected = path.join(os.tmpdir(), 'agentswork-runtime-init', testAgentId, 'state.json');
43
+ const expected = path.join(testRuntimeHomeDir, '.aws-bridge', 'instances', testAgentId, 'state.json');
31
44
  expect(result).toBe(expected);
32
45
  });
33
46
  it('should handle agentId with special characters', () => {
@@ -45,7 +58,7 @@ describe('instance-state service', () => {
45
58
  describe('getAgentInitRoot', () => {
46
59
  it('should return correct init root directory', () => {
47
60
  const result = getAgentInitRoot(testAgentId);
48
- const expected = path.join(os.tmpdir(), 'agentswork-runtime-init', testAgentId);
61
+ const expected = path.join(testRuntimeHomeDir, '.aws-bridge', 'instances', testAgentId);
49
62
  expect(result).toBe(expected);
50
63
  });
51
64
  });
@@ -92,6 +105,7 @@ describe('instance-state service', () => {
92
105
  });
93
106
  it('should handle corrupted JSON file gracefully', async () => {
94
107
  // 创建损坏的 JSON 文件
108
+ const stateFilePath = getCurrentStateFilePath();
95
109
  await fs.mkdir(path.dirname(stateFilePath), { recursive: true });
96
110
  await fs.writeFile(stateFilePath, '{ invalid json }', 'utf-8');
97
111
  const state = await loadInstanceState(testAgentId);
@@ -99,6 +113,24 @@ describe('instance-state service', () => {
99
113
  expect(state.initialized).toBe(false);
100
114
  expect(state.skills).toEqual([]);
101
115
  });
116
+ it('should migrate readable legacy temp state into runtime home', async () => {
117
+ const legacyFilePath = path.join(os.tmpdir(), 'agentswork-runtime-init', testAgentId, 'state.json');
118
+ const legacyState = {
119
+ initialized: true,
120
+ skills: [],
121
+ mcpServers: [],
122
+ enabledTools: ['claude'],
123
+ toolStatus: {},
124
+ skillEnabled: true,
125
+ mcpEnabled: false,
126
+ updatedAt: '2026-01-01T00:00:00.000Z',
127
+ };
128
+ await fs.mkdir(path.dirname(legacyFilePath), { recursive: true });
129
+ await fs.writeFile(legacyFilePath, JSON.stringify(legacyState), 'utf-8');
130
+ const state = await loadInstanceState(testAgentId);
131
+ expect(state).toEqual(legacyState);
132
+ expect(JSON.parse(await fs.readFile(getInstanceStateFile(testAgentId), 'utf-8'))).toEqual(legacyState);
133
+ });
102
134
  });
103
135
  describe('saveInstanceState', () => {
104
136
  it('should save complete state with all fields', async () => {
@@ -128,7 +160,7 @@ describe('instance-state service', () => {
128
160
  expect(result.mcpEnabled).toBe(true);
129
161
  expect(result.updatedAt).not.toBeNull();
130
162
  // 验证文件已创建
131
- const fileContent = await fs.readFile(stateFilePath, 'utf-8');
163
+ const fileContent = await fs.readFile(getCurrentStateFilePath(), 'utf-8');
132
164
  const parsed = JSON.parse(fileContent);
133
165
  expect(parsed.initialized).toBe(true);
134
166
  });
@@ -198,7 +230,7 @@ describe('instance-state service', () => {
198
230
  const result = await saveInstanceState(testAgentId, { initialized: false });
199
231
  expect(result.initialized).toBe(false);
200
232
  // 验证文件内容
201
- const fileContent = await fs.readFile(stateFilePath, 'utf-8');
233
+ const fileContent = await fs.readFile(getCurrentStateFilePath(), 'utf-8');
202
234
  const parsed = JSON.parse(fileContent);
203
235
  expect(parsed.initialized).toBe(false);
204
236
  });
@@ -1 +1 @@
1
- {"version":3,"file":"startup-config-wizard.d.ts","sourceRoot":"","sources":["../../src/services/startup-config-wizard.ts"],"names":[],"mappings":"AAuBA,UAAU,QAAQ;IAChB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,KAAK,IAAI,IAAI,CAAC;CACf;AAED,UAAU,0BAA0B;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,qBAAqB,CAAC,EAAE,MAAM,MAAM,CAAC;CACtC;AAED,MAAM,MAAM,yBAAyB,GACjC,QAAQ,GACR,SAAS,GACT,YAAY,GACZ,SAAS,GACT,iBAAiB,GACjB,QAAQ,CAAC;AAqKb,wBAAsB,mBAAmB,CACvC,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,yBAAyB,CAAC,CAiDpC;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,yBAAyB,CAAC,CA0BpC"}
1
+ {"version":3,"file":"startup-config-wizard.d.ts","sourceRoot":"","sources":["../../src/services/startup-config-wizard.ts"],"names":[],"mappings":"AAuBA,UAAU,QAAQ;IAChB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,KAAK,IAAI,IAAI,CAAC;CACf;AAED,UAAU,0BAA0B;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,qBAAqB,CAAC,EAAE,MAAM,MAAM,CAAC;CACtC;AAED,MAAM,MAAM,yBAAyB,GACjC,QAAQ,GACR,SAAS,GACT,YAAY,GACZ,SAAS,GACT,iBAAiB,GACjB,QAAQ,CAAC;AAoKb,wBAAsB,mBAAmB,CACvC,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,yBAAyB,CAAC,CAiDpC;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,yBAAyB,CAAC,CA0BpC"}
@@ -1,9 +1,9 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { existsSync, readFileSync } from "node:fs";
3
3
  import os from "node:os";
4
- import path from "node:path";
5
4
  import { stdin as input, stdout as output } from "node:process";
6
5
  import { createInterface } from "node:readline/promises";
6
+ import { atomicWriteJsonFile } from "../utils/file-utils.js";
7
7
  import { logger } from "../utils/logger.js";
8
8
  import { getAutoRegisterConfigFilePath } from "./auto-register.js";
9
9
  function normalizeAnswer(answer) {
@@ -15,10 +15,6 @@ function isYes(answer) {
15
15
  function isNo(answer) {
16
16
  return ["n", "no", "否", "不", "跳过", "skip", "s"].includes(normalizeAnswer(answer));
17
17
  }
18
- function writeConfigFile(configPath, config) {
19
- mkdirSync(path.dirname(configPath), { recursive: true });
20
- writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
21
- }
22
18
  function readExistingConfig(configPath) {
23
19
  if (!existsSync(configPath)) {
24
20
  return {};
@@ -38,9 +34,12 @@ function readExistingConfig(configPath) {
38
34
  function generateDefaultConnectionKey() {
39
35
  return `awsb_${randomBytes(24).toString("base64url")}`;
40
36
  }
41
- function writeSkippedConfig(configPath, generateConnectionKey) {
37
+ async function writeConfigFile(configPath, config) {
38
+ await atomicWriteJsonFile(configPath, config);
39
+ }
40
+ async function writeSkippedConfig(configPath, generateConnectionKey) {
42
41
  const connectionKey = generateConnectionKey();
43
- writeConfigFile(configPath, { connectionKey });
42
+ await writeConfigFile(configPath, { connectionKey });
44
43
  const message = `[runtime-bridge] 已创建配置文件: ${configPath}\n` +
45
44
  `[runtime-bridge] 自动生成 connectionKey: ${connectionKey}\n` +
46
45
  "[runtime-bridge] 请保存该 connectionKey;server/面板连接此 Bridge 时需要使用它。";
@@ -115,13 +114,13 @@ export async function ensureStartupConfig(options = {}) {
115
114
  }
116
115
  const env = options.env || process.env;
117
116
  if (env.AWS_BRIDGE_SKIP_SETUP === "true") {
118
- writeSkippedConfig(configPath, generateConnectionKey);
117
+ await writeSkippedConfig(configPath, generateConnectionKey);
119
118
  logger.info("[runtime-bridge] AWS_BRIDGE_SKIP_SETUP=true,跳过首次配置引导");
120
119
  return "skipped";
121
120
  }
122
121
  const interactive = options.interactive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
123
122
  if (!interactive) {
124
- writeSkippedConfig(configPath, generateConnectionKey);
123
+ await writeSkippedConfig(configPath, generateConnectionKey);
125
124
  logger.info(`[runtime-bridge] 未检测到配置文件: ${configPath};当前为非交互环境,跳过首次配置引导`);
126
125
  return "non-interactive";
127
126
  }
@@ -129,12 +128,12 @@ export async function ensureStartupConfig(options = {}) {
129
128
  try {
130
129
  const shouldConfigure = await askShouldConfigure(prompt);
131
130
  if (!shouldConfigure) {
132
- writeSkippedConfig(configPath, generateConnectionKey);
131
+ await writeSkippedConfig(configPath, generateConnectionKey);
133
132
  logger.info("[runtime-bridge] 已跳过首次配置引导");
134
133
  return "skipped";
135
134
  }
136
135
  const config = await collectStartupConfig(prompt);
137
- writeConfigFile(configPath, config);
136
+ await writeConfigFile(configPath, config);
138
137
  logger.info(`[runtime-bridge] 已写入配置文件: ${configPath}`);
139
138
  return "created";
140
139
  }
@@ -160,7 +159,7 @@ export async function configureStartupConfig(options = {}) {
160
159
  try {
161
160
  const existingConfig = readExistingConfig(configPath);
162
161
  const config = await collectStartupConfig(prompt, existingConfig);
163
- writeConfigFile(configPath, config);
162
+ await writeConfigFile(configPath, config);
164
163
  logger.info(`[runtime-bridge] 已更新配置文件: ${configPath}`);
165
164
  return "configured";
166
165
  }
@@ -1,7 +1,8 @@
1
- import type { SkillPackage, DownloadResult, Skill } from '../types.js';
2
1
  import type { AppFlags, InstalledSkill, CcSwitchSdk } from '@cc-switch/sdk';
2
+ import type { SkillPackage, DownloadResult, Skill } from '../types.js';
3
3
  export declare function ensureDir(targetDir: string): Promise<string>;
4
4
  export declare function withFileLock<T>(targetPath: string, operation: () => Promise<T>): Promise<T>;
5
+ export declare function stringifyStableJson(value: unknown): string;
5
6
  export declare function atomicWriteFile(targetPath: string, content: string): Promise<void>;
6
7
  export declare function atomicWriteJsonFile(targetPath: string, value: unknown): Promise<void>;
7
8
  export declare function downloadSkillZip(skillPackage: SkillPackage, targetDir: string): Promise<DownloadResult | null>;
@@ -1 +1 @@
1
- {"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/utils/file-utils.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAI5E,wBAAsB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGlE;AAED,wBAAsB,YAAY,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAqBjG;AAED,wBAAsB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWxF;AAED,wBAAsB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAE3F;AAED,wBAAsB,gBAAgB,CACpC,YAAY,EAAE,YAAY,EAC1B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CA+ChC;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB/E;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,wBAAsB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBhG;AAED,wBAAsB,oCAAoC,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CASlG;AAED,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,QAAQ,CAW1E;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAM1D;AAED,wBAAsB,qBAAqB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKhF;AAED,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,WAAW,EAChB,YAAY,EAAE,YAAY,EAC1B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAyBhC"}
1
+ {"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/utils/file-utils.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAG5E,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAIvE,wBAAsB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGlE;AAED,wBAAsB,YAAY,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAqBjG;AA2BD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAE1D;AAED,wBAAsB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBxF;AAED,wBAAsB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAE3F;AAED,wBAAsB,gBAAgB,CACpC,YAAY,EAAE,YAAY,EAC1B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CA+ChC;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB/E;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,wBAAsB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBhG;AAED,wBAAsB,oCAAoC,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CASlG;AAED,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,QAAQ,CAW1E;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAM1D;AAED,wBAAsB,qBAAqB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKhF;AAED,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,WAAW,EAChB,YAAY,EAAE,YAAY,EAC1B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAyBhC"}
@@ -1,7 +1,7 @@
1
- import path from 'node:path';
2
1
  import { spawn } from 'node:child_process';
3
- import { promises as fs } from 'node:fs';
4
2
  import { createHash, randomUUID } from 'node:crypto';
3
+ import { promises as fs } from 'node:fs';
4
+ import path from 'node:path';
5
5
  import axios from 'axios';
6
6
  import { schedulerBaseUrl } from '../config.js';
7
7
  import { getRuntimeAccessToken } from '../services/runtime-binding.js';
@@ -30,16 +30,46 @@ export async function withFileLock(targetPath, operation) {
30
30
  }
31
31
  }
32
32
  }
33
+ function isPlainObject(value) {
34
+ if (!value || typeof value !== 'object') {
35
+ return false;
36
+ }
37
+ const prototype = Object.getPrototypeOf(value);
38
+ return prototype === Object.prototype || prototype === null;
39
+ }
40
+ function sortJsonValue(value) {
41
+ if (Array.isArray(value)) {
42
+ return value.map((item) => sortJsonValue(item));
43
+ }
44
+ if (isPlainObject(value)) {
45
+ return Object.keys(value)
46
+ .sort()
47
+ .reduce((acc, key) => {
48
+ acc[key] = sortJsonValue(value[key]);
49
+ return acc;
50
+ }, {});
51
+ }
52
+ return value;
53
+ }
54
+ export function stringifyStableJson(value) {
55
+ return `${JSON.stringify(sortJsonValue(value), null, 2)}\n`;
56
+ }
33
57
  export async function atomicWriteFile(targetPath, content) {
34
58
  await ensureDir(path.dirname(targetPath));
35
59
  const targetDir = path.dirname(targetPath);
36
60
  const targetBase = path.basename(targetPath);
37
61
  const tmpPath = path.join(targetDir, `.${targetBase}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
38
- await fs.writeFile(tmpPath, content, 'utf-8');
39
- await fs.rename(tmpPath, targetPath);
62
+ try {
63
+ await fs.writeFile(tmpPath, content, 'utf-8');
64
+ await fs.rename(tmpPath, targetPath);
65
+ }
66
+ catch (error) {
67
+ await fs.rm(tmpPath, { force: true }).catch(() => undefined);
68
+ throw error;
69
+ }
40
70
  }
41
71
  export async function atomicWriteJsonFile(targetPath, value) {
42
- await atomicWriteFile(targetPath, `${JSON.stringify(value, null, 2)}\n`);
72
+ await atomicWriteFile(targetPath, stringifyStableJson(value));
43
73
  }
44
74
  export async function downloadSkillZip(skillPackage, targetDir) {
45
75
  if (!skillPackage || !skillPackage.downloadUrl) {
@@ -1,8 +1,8 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import path from 'node:path';
3
2
  import { promises as fs } from 'node:fs';
4
3
  import os from 'node:os';
5
- import { ensureDir, escapePowerShellLiteral, resolveSkillSourceDirFromExtractRoot, toSdkAppsFromEnabledTools, resolveSkillSourceDir, } from './file-utils.js';
4
+ import path from 'node:path';
5
+ import { atomicWriteJsonFile, ensureDir, escapePowerShellLiteral, resolveSkillSourceDirFromExtractRoot, stringifyStableJson, toSdkAppsFromEnabledTools, resolveSkillSourceDir, } from './file-utils.js';
6
6
  // Mock axios for download tests
7
7
  vi.mock('axios', () => ({
8
8
  default: {
@@ -47,6 +47,47 @@ describe('file-utils', () => {
47
47
  expect(result).toBe(tempDir);
48
48
  });
49
49
  });
50
+ describe('atomicWriteJsonFile', () => {
51
+ const tempDir = path.join(os.tmpdir(), `aws-file-utils-json-test-${Date.now()}`);
52
+ afterEach(async () => {
53
+ await fs.rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
54
+ });
55
+ it('should write stable sorted json for deterministic config files', async () => {
56
+ const filePath = path.join(tempDir, 'config.json');
57
+ await atomicWriteJsonFile(filePath, {
58
+ z: 1,
59
+ nested: { b: true, a: false },
60
+ array: [{ d: 4, c: 3 }],
61
+ });
62
+ expect(await fs.readFile(filePath, 'utf-8')).toBe('{\n' +
63
+ ' "array": [\n' +
64
+ ' {\n' +
65
+ ' "c": 3,\n' +
66
+ ' "d": 4\n' +
67
+ ' }\n' +
68
+ ' ],\n' +
69
+ ' "nested": {\n' +
70
+ ' "a": false,\n' +
71
+ ' "b": true\n' +
72
+ ' },\n' +
73
+ ' "z": 1\n' +
74
+ '}\n');
75
+ });
76
+ });
77
+ describe('stringifyStableJson', () => {
78
+ it('should preserve array order while sorting object keys', () => {
79
+ expect(stringifyStableJson([{ b: 2, a: 1 }, { d: 4, c: 3 }])).toBe('[\n' +
80
+ ' {\n' +
81
+ ' "a": 1,\n' +
82
+ ' "b": 2\n' +
83
+ ' },\n' +
84
+ ' {\n' +
85
+ ' "c": 3,\n' +
86
+ ' "d": 4\n' +
87
+ ' }\n' +
88
+ ']\n');
89
+ });
90
+ });
50
91
  describe('escapePowerShellLiteral', () => {
51
92
  it('should escape single quotes by doubling them', () => {
52
93
  expect(escapePowerShellLiteral("test'value")).toBe("test''value");
@@ -347,7 +347,10 @@ export class AgentClient {
347
347
  if (options.blockTimeoutMs && options.blockTimeoutMs > 0) {
348
348
  dmArgs.blockTimeoutMs = options.blockTimeoutMs;
349
349
  }
350
- const directMessagesResponse = await this.httpClient.callTool("get_dm_messages", dmArgs);
350
+ const dmToolName = options.includePipelineTasks
351
+ ? "poll_message"
352
+ : "get_dm_messages";
353
+ const directMessagesResponse = await this.httpClient.callTool(dmToolName, dmArgs);
351
354
  const groups = await this.discoverGroupRooms();
352
355
  const roomIds = this.collectRoomIds(groups, targetProject);
353
356
  const groupMessageResponses = await Promise.all(roomIds.map(async (currentRoomId) => {