@harness-mix/cli 0.1.4 → 0.1.6

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 (146) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +26 -96
  3. package/docs/chatgpt-web-sidebar.md +7 -1
  4. package/docs/cross-platform.md +15 -0
  5. package/docs/harness-handoff-design.md +377 -0
  6. package/docs/harness-management.md +28 -1
  7. package/docs/multi-agent-collaboration.md +9 -9
  8. package/docs/native-codex.md +21 -10
  9. package/output/native-build/harness-mix-shim.exe +0 -0
  10. package/output/native-build/renderer-extension.js +2596 -135
  11. package/package.json +20 -10
  12. package/scripts/chatgpt-context-ui-smoke.cjs +16 -0
  13. package/scripts/codex-accounts-test.cjs +81 -0
  14. package/scripts/codex-accounts-ui-smoke.cjs +70 -0
  15. package/scripts/collaboration-test.cjs +8 -3
  16. package/scripts/collaboration-ui-smoke.cjs +40 -26
  17. package/scripts/e2e-integrations.cjs +52 -0
  18. package/scripts/e2e-native-host.cjs +39 -0
  19. package/scripts/handoff-checkpoint-test.cjs +83 -0
  20. package/scripts/install-native.cjs +4 -1
  21. package/scripts/integrations-test.cjs +123 -0
  22. package/scripts/integrations-ui-smoke.cjs +124 -0
  23. package/scripts/native-protocol-test.cjs +180 -5
  24. package/scripts/native-secret-test.cjs +2 -2
  25. package/scripts/native-shim-test.cjs +20 -4
  26. package/scripts/native-sidebar-test.cjs +7 -0
  27. package/scripts/native-skill-roots-test.cjs +90 -0
  28. package/scripts/native-ui-smoke.cjs +58 -2
  29. package/scripts/publish-native-package.cjs +8 -5
  30. package/scripts/support/integrations-mcp-fixture.cjs +15 -0
  31. package/scripts/switch-harness-test.cjs +27 -1
  32. package/scripts/test-live-mentions.cjs +152 -0
  33. package/src/main/adapters/acp.js +3 -3
  34. package/src/main/adapters/antigravity.js +17 -17
  35. package/src/main/adapters/claude.js +50 -10
  36. package/src/main/adapters/codebuddy.js +1 -0
  37. package/src/main/adapters/codex-app-server.js +20 -13
  38. package/src/main/adapters/codex.js +25 -5
  39. package/src/main/adapters/cursor.js +5 -1
  40. package/src/main/adapters/dsh.js +9 -1
  41. package/src/main/adapters/grok.js +7 -3
  42. package/src/main/adapters/hermes.js +8 -0
  43. package/src/main/adapters/kiro.js +2 -1
  44. package/src/main/adapters/managed-mcp.js +25 -0
  45. package/src/main/adapters/native-acp.js +4 -4
  46. package/src/main/adapters/omp.js +11 -0
  47. package/src/main/adapters/openclaw.js +6 -0
  48. package/src/main/adapters/opencode.js +13 -3
  49. package/src/main/adapters/pi.js +5 -0
  50. package/src/main/adapters/qoder.js +1 -0
  51. package/src/main/adapters/trae.js +4 -0
  52. package/src/main/adapters/zcode.js +3 -0
  53. package/src/main/harness-adapter/manifest.js +17 -2
  54. package/src/main/host/collaboration.js +57 -11
  55. package/src/main/host/handoff-access.js +53 -0
  56. package/src/main/host/handoff-checkpoints.js +206 -0
  57. package/src/main/host/handoff-mcp.cjs +38 -0
  58. package/src/main/host/handoff-tools.js +13 -0
  59. package/src/main/host/handoff.js +28 -6
  60. package/src/main/host/integrations.js +339 -0
  61. package/src/main/host/runtime.js +95 -15
  62. package/src/main/host/store.js +4 -4
  63. package/src/main/native/codex-accounts.js +260 -0
  64. package/src/main/native/host.js +1 -1
  65. package/src/main/native/launcher.js +2 -2
  66. package/src/main/native/protocol.js +176 -14
  67. package/src/main/native/redact.js +1 -1
  68. package/src/main/native/rs/crates/shim/src/main.rs +155 -12
  69. package/src/main/native/thread-list.js +3 -0
  70. package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -1
  71. package/src/native-ui/renderer-extension/dist/types/harness-mix-settings.d.ts.map +1 -1
  72. package/src/native-ui/renderer-extension/dist/types/index.d.ts +4 -0
  73. package/src/native-ui/renderer-extension/dist/types/index.d.ts.map +1 -1
  74. package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts +2 -0
  75. package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts.map +1 -1
  76. package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
  77. package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts +36 -0
  78. package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts.map +1 -0
  79. package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts +1 -0
  80. package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts.map +1 -1
  81. package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts +3 -1
  82. package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts.map +1 -1
  83. package/src/native-ui/renderer-extension/dist/types/renderer-harness-handoff.d.ts +53 -0
  84. package/src/native-ui/renderer-extension/dist/types/renderer-harness-handoff.d.ts.map +1 -0
  85. package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts +11 -0
  86. package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts.map +1 -1
  87. package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts +105 -0
  88. package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts.map +1 -0
  89. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +7 -1
  90. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
  91. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts +1 -0
  92. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts.map +1 -1
  93. package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts +2 -0
  94. package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts.map +1 -1
  95. package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts +1 -0
  96. package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts.map +1 -1
  97. package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts +6 -0
  98. package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts.map +1 -0
  99. package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts +5 -0
  100. package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
  101. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +8 -4
  102. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
  103. package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
  104. package/src/native-ui/renderer-extension/src/harness-mix-settings.ts +4 -1
  105. package/src/native-ui/renderer-extension/src/index.ts +28 -0
  106. package/src/native-ui/renderer-extension/src/renderer-agent-icon.ts +4 -4
  107. package/src/native-ui/renderer-extension/src/renderer-agent-picker.ts +46 -6
  108. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +115 -18
  109. package/src/native-ui/renderer-extension/src/renderer-chatgpt-context.ts +87 -0
  110. package/src/native-ui/renderer-extension/src/renderer-codex-account-state.ts +10 -0
  111. package/src/native-ui/renderer-extension/src/renderer-composer-dom.ts +10 -0
  112. package/src/native-ui/renderer-extension/src/renderer-harness-handoff.ts +330 -0
  113. package/src/native-ui/renderer-extension/src/renderer-harness-mentions.ts +556 -123
  114. package/src/native-ui/renderer-extension/src/renderer-integrations-client.ts +59 -0
  115. package/src/native-ui/renderer-extension/src/renderer-model-client.ts +17 -1
  116. package/src/native-ui/renderer-extension/src/renderer-settings-lifecycle.ts +2 -0
  117. package/src/native-ui/renderer-extension/src/settings/accounts-list.ts +16 -1
  118. package/src/native-ui/renderer-extension/src/settings/accounts-page.ts +32 -3
  119. package/src/native-ui/renderer-extension/src/settings/integrations-page.ts +1474 -0
  120. package/src/native-ui/renderer-extension/src/settings/localization.ts +32 -13
  121. package/src/native-ui/renderer-extension/src/settings/pages.ts +49 -18
  122. package/src/native-ui/renderer-extension/src/settings/shell.css +103 -0
  123. package/src/native-ui/renderer-extension/test/renderer-agent-picker.test.ts +44 -13
  124. package/src/native-ui/renderer-extension/test/renderer-chatgpt-context.test.ts +21 -0
  125. package/src/native-ui/renderer-extension/test/renderer-codex-account-state.test.ts +13 -1
  126. package/src/native-ui/renderer-extension/test/renderer-harness-handoff.test.ts +20 -0
  127. package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +9 -2
  128. package/src/native-ui/renderer-extension/test/settings/localization.test.ts +1 -1
  129. package/src/native-ui/renderer-extension/test/settings/pages.test.ts +77 -8
  130. package/src/native-ui/renderer-extension/test/settings/shell.test.ts +6 -0
  131. package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts +19 -4
  132. package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts.map +1 -1
  133. package/src/native-ui/shared-contracts/dist/codex-accounts.js +2 -0
  134. package/src/native-ui/shared-contracts/dist/codex-accounts.js.map +1 -1
  135. package/src/native-ui/shared-contracts/dist/index.d.ts +2 -2
  136. package/src/native-ui/shared-contracts/dist/index.d.ts.map +1 -1
  137. package/src/native-ui/shared-contracts/dist/index.js +1 -1
  138. package/src/native-ui/shared-contracts/dist/index.js.map +1 -1
  139. package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts +29 -0
  140. package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts.map +1 -1
  141. package/src/native-ui/shared-contracts/dist/thread-harness-switch.js +13 -0
  142. package/src/native-ui/shared-contracts/dist/thread-harness-switch.js.map +1 -1
  143. package/src/native-ui/shared-contracts/dist/tsconfig.tsbuildinfo +1 -1
  144. package/src/native-ui/shared-contracts/src/codex-accounts.ts +2 -0
  145. package/src/native-ui/shared-contracts/src/index.ts +8 -1
  146. package/src/native-ui/shared-contracts/src/thread-harness-switch.ts +17 -0
@@ -20,7 +20,7 @@ const CLAUDE_PERMISSION_MODES = [
20
20
  { id: "acceptEdits", label: "接受编辑", description: "允许文件编辑;其他受保护操作前询问" },
21
21
  { id: "auto", label: "自动模式", description: "由 Claude 判断权限请求" },
22
22
  { id: "dontAsk", label: "免询问", description: "跳过权限询问(非绕过检查)" },
23
- { id: "bypassPermissions", label: "绕过权限", description: "跳过全部权限检查,谨慎使用", dangerous: true },
23
+ { id: "bypassPermissions", label: "绕过权限 (YOLO)", description: "跳过全部权限检查(YOLO 模式),自动放行工具执行", dangerous: true },
24
24
  ];
25
25
 
26
26
  function summarizeInput(input) {
@@ -244,14 +244,25 @@ async function loadSdk() {
244
244
  return import("@anthropic-ai/claude-agent-sdk");
245
245
  }
246
246
 
247
+ function normalizeClaudePermissionMode(mode) {
248
+ if (mode === 'yolo' || mode === 'skip' || mode === 'dangerously-skip-permissions') {
249
+ return 'bypassPermissions';
250
+ }
251
+ return mode;
252
+ }
253
+
247
254
  /** 建立一个常驻 SDK 会话(open 与 fork 共用):构造 query、启动事件泵 */
248
- function spawnSession(sdk, { cwd, resumeId, newSessionId, permissionMode, modelId, effort, emit, collaboration, onPlanLimit }) {
255
+ function spawnSession(sdk, { cwd, resumeId, newSessionId, permissionMode, modelId, effort, emit, collaboration, managedMcp = [], onPlanLimit, isWorker = false }) {
249
256
  const input = new MessageQueue();
257
+ const normalizedPermissionMode = normalizeClaudePermissionMode(permissionMode) || (isWorker ? 'bypassPermissions' : undefined);
258
+ const isBypass = normalizedPermissionMode === 'bypassPermissions' || isWorker;
259
+
250
260
  const session = {
251
261
  nativeSessionId: resumeId || newSessionId,
252
262
  cwd,
253
263
  collaborationEnabled: !!collaboration,
254
- permissionMode,
264
+ permissionMode: normalizedPermissionMode,
265
+ isWorker,
255
266
  model: undefined,
256
267
  input,
257
268
  query: undefined,
@@ -265,14 +276,22 @@ function spawnSession(sdk, { cwd, resumeId, newSessionId, permissionMode, modelI
265
276
  prompt: input,
266
277
  options: {
267
278
  cwd,
268
- ...(collaboration ? { mcpServers: { 'harness-mix': collaboration } } : {}),
279
+ mcpServers: require('./managed-mcp').namedServers(managedMcp, collaboration),
280
+ settingSources: ['user', 'project', 'local'],
269
281
  ...(resumeId ? { resume: resumeId } : {}),
270
282
  ...(!resumeId && newSessionId ? { sessionId: newSessionId } : {}),
271
283
  ...(effort ? { effort } : {}),
272
- ...(permissionMode ? { permissionMode } : {}),
284
+ ...(normalizedPermissionMode ? { permissionMode: normalizedPermissionMode } : {}),
285
+ ...(isBypass ? { allowDangerouslySkipPermissions: true } : {}),
273
286
  ...(modelId ? { model: modelId } : {}),
274
287
  ...(process.env.HARNESS_MIX_CLAUDE_EXECUTABLE ? { pathToClaudeCodeExecutable: process.env.HARNESS_MIX_CLAUDE_EXECUTABLE } : {}),
275
288
  canUseTool: (toolName, toolInput, { signal, suggestions }) => {
289
+ if (toolName !== 'AskUserQuestion') {
290
+ const currentMode = normalizeClaudePermissionMode(session.permissionMode);
291
+ if (currentMode === 'bypassPermissions' || session.isWorker) {
292
+ return { behavior: 'allow', updatedInput: toolInput };
293
+ }
294
+ }
276
295
  const requestId = randomUUID();
277
296
  emit(projectApproval(requestId, toolName, toolInput, suggestions));
278
297
  return new Promise((resolve) => {
@@ -373,17 +392,26 @@ function create() {
373
392
  : { available: true, detail: "Agent SDK 就绪(内置原生 CLI;未检测到独立 claude 命令)" };
374
393
  },
375
394
 
376
- async open({ thread, emit, collaboration }) {
395
+ async inspectIntegrations(session) {
396
+ const rows = await session.query.mcpServerStatus();
397
+ return rows.map(row => ({ name: row.name, status: row.status, tools: (row.tools || []).map(t => t.name) }));
398
+ },
399
+ async open({ thread, emit, collaboration, managedMcp }) {
377
400
  const sdk = await loadSdk();
401
+ const isWorker = Boolean(thread.parentThreadId);
402
+ const rawMode = thread.options?.permissionMode;
403
+ const permissionMode = normalizeClaudePermissionMode(rawMode) || (isWorker ? 'bypassPermissions' : undefined);
378
404
  return spawnSession(sdk, {
379
405
  cwd: thread.cwd,
380
406
  resumeId: thread.restore ? thread.nativeSessionId : undefined,
381
407
  newSessionId: thread.restore ? undefined : thread.nativeSessionId,
382
408
  effort: thread.options?.thinking,
383
- permissionMode: thread.options?.permissionMode,
409
+ permissionMode,
410
+ isWorker,
384
411
  modelId: thread.options?.model?.id,
385
412
  emit,
386
413
  collaboration,
414
+ managedMcp,
387
415
  onPlanLimit,
388
416
  });
389
417
  },
@@ -406,11 +434,19 @@ function create() {
406
434
  },
407
435
 
408
436
  async cancel(session) {
437
+ for (const pending of session.pendingApprovals?.values() ?? []) {
438
+ pending.resolve({ behavior: "deny", message: "用户取消了请求", interrupt: true });
439
+ }
440
+ session.pendingApprovals?.clear();
409
441
  // 原生优雅中断;超时后由 Host 走 close 兜底
410
442
  await Promise.race([
411
443
  session.query?.interrupt().catch(() => {}),
412
- new Promise((r) => setTimeout(r, 5_000)),
444
+ new Promise((r) => setTimeout(r, 2_000)),
413
445
  ]);
446
+ if (session.state.turn) {
447
+ session.state.turn.resolve();
448
+ session.state.turn = null;
449
+ }
414
450
  },
415
451
 
416
452
  async listCommands(session) {
@@ -446,10 +482,12 @@ function create() {
446
482
  const copied = await getSessionMessages(result.sessionId, { dir: source.cwd });
447
483
  const checkpointMap = Object.fromEntries(copied.map((entry, index) => [history[index].uuid, entry.uuid]));
448
484
  // Fork 出的新原生会话立即拉起常驻进程(resume 到新 sessionId),与 open() 同路径
485
+ const isWorker = Boolean(source.parentThreadId);
449
486
  const session = spawnSession(sdk, {
450
487
  cwd: source.cwd,
451
488
  resumeId: result.sessionId,
452
489
  permissionMode: source.options?.permissionMode,
490
+ isWorker,
453
491
  emit,
454
492
  onPlanLimit,
455
493
  });
@@ -470,8 +508,9 @@ function create() {
470
508
  return session.model;
471
509
  },
472
510
  async setPermissionMode(session, mode) {
473
- session.permissionMode = mode;
474
- await session.query?.setPermissionMode(mode);
511
+ const normalized = normalizeClaudePermissionMode(mode);
512
+ session.permissionMode = normalized;
513
+ await session.query?.setPermissionMode(normalized);
475
514
  },
476
515
  async setThinkingLevel(session, level) {
477
516
  const models = await this.listModelsFor(session);
@@ -509,6 +548,7 @@ function create() {
509
548
  };
510
549
  }
511
550
 
551
+ manifest.integrations = { mcp: true, skills: { global: ['.claude/skills'], project: ['.claude/skills'], overrides: { '.claude/skills': { env: 'CLAUDE_CONFIG_DIR', suffix: 'skills' } } } };
512
552
  module.exports = {
513
553
  manifest,
514
554
  create,
@@ -1,2 +1,3 @@
1
1
  const { nativeAcp } = require('./native-acp');
2
2
  module.exports = nativeAcp({ id: 'codebuddy', name: 'CodeBuddy', aliases: ['workbuddy', 'WorkBuddy'], args: ['--acp'] });
3
+ module.exports.manifest.integrations.skills = { global: ['.codebuddy/skills'], project: ['.codebuddy/skills'] };
@@ -1,6 +1,6 @@
1
1
  const { JsonlProcess, cliSpawn } = require('../host/jsonl');
2
2
 
3
- let shared;
3
+ const shared = new Map();
4
4
 
5
5
  /**
6
6
  * A single native Codex app-server connection shared by every Codex thread.
@@ -8,16 +8,18 @@ let shared;
8
8
  * traffic to the Adapter session that registered the native thread id.
9
9
  */
10
10
  class CodexAppServer {
11
- constructor(diagnostic) {
11
+ constructor(diagnostic, codexHome) {
12
12
  this.refs = 0;
13
13
  this.routes = new Map();
14
14
  this.notifications = new Set();
15
15
  this.diagnostics = new Set(diagnostic ? [diagnostic] : []);
16
16
  this.closed = false;
17
+ this.codexHome = codexHome ? require('node:path').resolve(codexHome) : null;
17
18
  // The Desktop-bundled CLI and the PATH CLI can be different versions.
18
19
  const executable = process.env.HARNESS_MIX_CODEX_EXECUTABLE || process.env.CODEXHOST_STOCK_CODEX_PATH;
19
20
  const { command, args } = executable ? { command: executable, args: ['app-server', '--listen', 'stdio://'] } : cliSpawn('codex', ['app-server', '--stdio']);
20
- this.process = new JsonlProcess(command, args, {}, {
21
+ const env = { ...process.env, ...(this.codexHome ? { CODEX_HOME: this.codexHome } : {}) };
22
+ this.process = new JsonlProcess(command, args, { env }, {
21
23
  onEvent: (message) => this.#notification(message),
22
24
  onRequest: (message) => this.#request(message),
23
25
  onDiagnostic: (line) => this.#diagnostic(line),
@@ -36,16 +38,21 @@ class CodexAppServer {
36
38
  });
37
39
  }
38
40
 
39
- static async acquire(diagnostic) {
40
- if (!shared || shared.closed) shared = new CodexAppServer(diagnostic);
41
- else if (diagnostic) shared.diagnostics.add(diagnostic);
42
- shared.refs++;
41
+ static async acquire(diagnostic, codexHome) {
42
+ const key = codexHome ? require('node:path').resolve(codexHome).toLowerCase() : '<default>';
43
+ let server = shared.get(key);
44
+ if (!server || server.closed) {
45
+ server = new CodexAppServer(diagnostic, codexHome);
46
+ server.sharedKey = key;
47
+ shared.set(key, server);
48
+ } else if (diagnostic) server.diagnostics.add(diagnostic);
49
+ server.refs++;
43
50
  try {
44
- await shared.ready;
45
- return shared;
51
+ await server.ready;
52
+ return server;
46
53
  } catch (error) {
47
- shared.refs--;
48
- if (shared.refs === 0) shared.stop();
54
+ server.refs--;
55
+ if (server.refs === 0) server.stop();
49
56
  throw error;
50
57
  }
51
58
  }
@@ -78,7 +85,7 @@ class CodexAppServer {
78
85
  this.routes.clear();
79
86
  this.notifications.clear();
80
87
  this.process.stop();
81
- if (shared === this) shared = undefined;
88
+ if (this.sharedKey && shared.get(this.sharedKey) === this) shared.delete(this.sharedKey);
82
89
  }
83
90
 
84
91
  #route(message) {
@@ -112,7 +119,7 @@ class CodexAppServer {
112
119
  this.closed = true;
113
120
  for (const route of this.routes.values()) route.onExit?.(error);
114
121
  this.routes.clear();
115
- if (shared === this) shared = undefined;
122
+ if (this.sharedKey && shared.get(this.sharedKey) === this) shared.delete(this.sharedKey);
116
123
  }
117
124
  }
118
125
 
@@ -356,10 +356,11 @@ function create() {
356
356
  : { available: true, detail: String(result.stdout).trim() };
357
357
  },
358
358
 
359
- async open({ thread, emit, diagnostic, collaboration }) {
360
- const host = await CodexAppServer.acquire(diagnostic);
359
+ async open({ thread, emit, diagnostic, collaboration, managedMcp = [] }) {
360
+ const host = await CodexAppServer.acquire(diagnostic, thread.options?.codexHome);
361
361
  try {
362
- const options = { ...threadOptions(thread), ...(collaboration ? { config: { 'mcp_servers.harness-mix': collaboration } } : {}) };
362
+ const servers = require('./managed-mcp').namedServers(managedMcp, collaboration);
363
+ const options = { ...threadOptions(thread), ...(Object.keys(servers).length ? { config: Object.fromEntries(Object.entries(servers).map(([name, value]) => [`mcp_servers.${name}`, value])) } : {}) };
363
364
  const result = thread.restore
364
365
  ? await host.request('thread/resume', { threadId: thread.nativeSessionId, ...options })
365
366
  : await host.request('thread/start', options);
@@ -400,8 +401,19 @@ function create() {
400
401
  },
401
402
 
402
403
  async cancel(session) {
404
+ for (const pending of session.pendingApprovals?.values() ?? []) {
405
+ pending.resolve({ action: 'decline' });
406
+ }
407
+ session.pendingApprovals?.clear();
403
408
  if (!session.state.nativeTurnId) return;
404
- await session.host.request('turn/interrupt', { threadId: session.nativeSessionId, turnId: session.state.nativeTurnId });
409
+ await Promise.race([
410
+ session.host.request('turn/interrupt', { threadId: session.nativeSessionId, turnId: session.state.nativeTurnId }).catch(() => {}),
411
+ new Promise((r) => setTimeout(r, 2_000)),
412
+ ]);
413
+ if (session.state.turn) {
414
+ session.state.turn.resolve();
415
+ session.state.turn = null;
416
+ }
405
417
  },
406
418
 
407
419
  async respond(session, requestId, response) {
@@ -521,7 +533,7 @@ function create() {
521
533
  async fork(source, { emit, diagnostic, message }) {
522
534
  const lastTurnId = message?.coreTurn?.nativeTurnRef?.turnId ?? message?.coreTurn?.nativeTurnRef?.checkpointId;
523
535
  if (message && !lastTurnId) throw new Error('这条回复缺少 Codex 原生 Turn ID,无法精确分支');
524
- const host = await CodexAppServer.acquire(diagnostic);
536
+ const host = await CodexAppServer.acquire(diagnostic, source.options?.codexHome);
525
537
  try {
526
538
  const result = await host.request('thread/fork', {
527
539
  threadId: source.nativeSessionId,
@@ -556,4 +568,12 @@ function create() {
556
568
  };
557
569
  }
558
570
 
571
+ // Global discovery: ~/.agents/skills is the recommended location; $CODEX_HOME/skills
572
+ // (~/.codex/skills) is deprecated upstream but still loaded for backwards compatibility.
573
+ // https://developers.openai.com/codex/skills
574
+ manifest.integrations = { mcp: true, skills: {
575
+ global: ['.agents/skills', '.codex/skills'],
576
+ project: ['.agents/skills'],
577
+ overrides: { '.codex/skills': { env: 'CODEX_HOME', suffix: 'skills' } },
578
+ } };
559
579
  module.exports = { manifest, create, projectNotification, queueRequest, usageView, modelView };
@@ -1,2 +1,6 @@
1
1
  const { nativeAcp } = require('./native-acp');
2
- module.exports = nativeAcp({ id: 'cursor-cli', name: 'Cursor', aliases: ['cursor', 'cursor-cli', 'cursorcli'], args: ['acp'] });
2
+ module.exports = nativeAcp({ id: 'cursor-cli', name: 'Cursor', aliases: ['cursor', 'cursor-cli', 'cursorcli'], args: ['acp'] });
3
+ module.exports.manifest.integrations.skills = {
4
+ global: ['.cursor/skills', '.agents/skills', '.claude/skills', '.codex/skills'],
5
+ project: ['.cursor/skills', '.agents/skills', '.claude/skills', '.codex/skills'],
6
+ };
@@ -431,7 +431,7 @@ function create() {
431
431
  }).create();
432
432
  const openWeb = web.open.bind(web);
433
433
  web.open = async context => {
434
- if (!context.collaboration) return openWeb(context);
434
+ if (!context.collaboration && !context.managedMcp?.length) return openWeb(context);
435
435
  const session = await acp.open(context);
436
436
  session.dshAcpLead = true;
437
437
  return session;
@@ -443,4 +443,12 @@ function create() {
443
443
  return web;
444
444
  }
445
445
 
446
+ manifest.integrations = { mcp: true, skills: {
447
+ global: ['.dsh/skills', '.agents/skills'],
448
+ project: ['.dsh/skills', '.agents/skills'],
449
+ overrides: {
450
+ '.dsh/skills': { env: 'DSH_HOME', suffix: 'skills' },
451
+ '.agents/skills': { env: 'DSH_AGENTS_HOME', suffix: 'skills' },
452
+ },
453
+ } };
446
454
  module.exports = { manifest, create, projectWireEvent, flattenCatalog };
@@ -31,7 +31,7 @@ function grokAdapter() {
31
31
  return new Promise(resolve => execFile(cli.command, cli.args, { windowsHide: true, timeout: 10000 }, (error, out) => resolve({ available: !error, detail: error ? `${name} CLI 不可用` : out.trim() })));
32
32
  },
33
33
  async describe() { return { models: null, thinkingLevels: [], permissionModes: [] }; },
34
- async open({ thread, emit, diagnostic = () => {}, collaboration }) {
34
+ async open({ thread, emit, diagnostic = () => {}, collaboration, managedMcp = [] }) {
35
35
  const cli = command(args);
36
36
  const session = { cwd: thread.cwd, nativeSessionId: null, collaborationEnabled: !!collaboration, state: { configOptions: [], models: null, modes: null, commands: [], usage: undefined, loading: true }, pendingApprovals: new Map(), tools: new Map(), emit };
37
37
  session.process = new JsonlProcess(cli.command, cli.args, { cwd: thread.cwd }, {
@@ -105,7 +105,7 @@ function grokAdapter() {
105
105
  session.state.agentCapabilities = init.agentCapabilities;
106
106
  session.state.commands = init._meta?.availableCommands || [];
107
107
  // L1 会话级注入:Grok 原生协议的 session/new 自带 mcpServers 槽(ACP 形态 stdio 定义)
108
- const mcpServers = collaboration ? [{ name: 'harness-mix', command: collaboration.command, args: collaboration.args, env: Object.entries(collaboration.env).map(([envName, value]) => ({ name: envName, value })) }] : [];
108
+ const mcpServers = require('./managed-mcp').acpServers(managedMcp, collaboration);
109
109
  const result = await session.process.request(thread.restore ? 'session/load' : 'session/new', { cwd: thread.cwd, mcpServers, ...(thread.restore ? { sessionId: thread.nativeSessionId } : {}) });
110
110
  session.nativeSessionId = result.sessionId || thread.nativeSessionId;
111
111
  session.state.configOptions = result.configOptions || result._meta?.['x.ai/sessionConfig']?.options || [];
@@ -274,4 +274,8 @@ async function doGrokCompact(session, userContext, hooks) {
274
274
  });
275
275
  hooks?.emit?.({ kind: 'completed', finalAnswer: outcome !== 'cancelled' });
276
276
  }
277
- module.exports = { ...grokAdapter(), projectUsage, parseGrokCompactionUpdate, doGrokCompact };
277
+ module.exports = { ...grokAdapter(), projectUsage, parseGrokCompactionUpdate, doGrokCompact };
278
+ module.exports.manifest.integrations = { mcp: true, skills: {
279
+ global: ['.grok/skills', '.agents/skills', '.claude/skills'],
280
+ project: ['.grok/skills', '.claude/skills'],
281
+ } };
@@ -22,3 +22,11 @@ module.exports = acpAdapter({
22
22
  usage: false,
23
23
  contextUsage: false,
24
24
  });
25
+ // ~/.hermes/skills is Hermes' single source of truth (categories become subdirectories);
26
+ // it has no documented project-scope skills directory. HERMES_HOME relocates the home.
27
+ // https://hermes-agent.nousresearch.com/docs/user-guide/features/skills
28
+ module.exports.manifest.integrations.skills = {
29
+ global: ['.hermes/skills'],
30
+ project: [],
31
+ overrides: { '.hermes/skills': { env: 'HERMES_HOME', suffix: 'skills' } },
32
+ };
@@ -1,2 +1,3 @@
1
1
  const { nativeAcp } = require('./native-acp');
2
- module.exports = nativeAcp({ id: 'kiro-cli', name: 'Kiro', aliases: ['kiro', 'kiro-cli', 'kirocli'], args: ['acp', '--agent-engine', 'v3', '--auth-method', 'cli'] });
2
+ module.exports = nativeAcp({ id: 'kiro-cli', name: 'Kiro', aliases: ['kiro', 'kiro-cli', 'kirocli'], args: ['acp', '--agent-engine', 'v3', '--auth-method', 'cli'] });
3
+ module.exports.manifest.integrations.skills = { global: ['.kiro/skills'], project: ['.kiro/skills'] };
@@ -0,0 +1,25 @@
1
+ // Native sessions own connections, tool discovery, authentication and approvals.
2
+ function nativeServers(managed = [], collaboration) {
3
+ return [...managed, ...(collaboration ? [{ name: 'harness-mix', ...collaboration }] : [])];
4
+ }
5
+ function acpServers(managed, collaboration) {
6
+ return nativeServers(managed, collaboration).map(s => {
7
+ if (s.url) {
8
+ return {
9
+ name: s.name,
10
+ url: s.url,
11
+ headers: Object.entries(s.http_headers || {}).map(([name, value]) => ({ name, value })),
12
+ };
13
+ }
14
+ return {
15
+ name: s.name,
16
+ command: s.command,
17
+ args: s.args || [],
18
+ env: Object.entries(s.env || {}).map(([name, value]) => ({ name, value })),
19
+ };
20
+ });
21
+ }
22
+ function namedServers(managed, collaboration) {
23
+ return Object.fromEntries(nativeServers(managed, collaboration).map(({ name, ...server }) => [name, server]));
24
+ }
25
+ module.exports = { nativeServers, acpServers, namedServers };
@@ -237,13 +237,12 @@ function nativeAcp({ id, name, args, aliases = [], command = argv => nativeComma
237
237
  async describe() { return { models: null, thinkingLevels: [], permissionModes: [] }; },
238
238
  async describeFor(s) { return catalog(s); },
239
239
  async listModelsFor(s) { return catalog(s).models; },
240
- async open({ thread, emit, diagnostic = () => {}, collaboration }) {
240
+ async open({ thread, emit, diagnostic = () => {}, collaboration, managedMcp = [] }) {
241
241
  const s = { vendor: id, cwd: thread.cwd, nativeSessionId: thread.restore ? thread.nativeSessionId : null,
242
242
  generation: 0, loading: true, active: false, closed: false, fault: null, emit, diagnostic,
243
243
  environment: thread.environment || {}, tools: new Map(), charges: new Map(), confirmed: {},
244
244
  state: { configOptions: [], models: null, modes: null, commands: [], usage: {} },
245
- mcpServers: collaboration ? [{ name: 'harness-mix', command: collaboration.command, args: collaboration.args,
246
- env: Object.entries(collaboration.env).map(([name, value]) => ({ name, value })) }] : [],
245
+ mcpServers: require('./managed-mcp').acpServers(managedMcp, collaboration),
247
246
  collaborationEnabled: !!collaboration,
248
247
  };
249
248
  s.interactions = new AcpInteractions(s, id);
@@ -430,6 +429,7 @@ function nativeAcp({ id, name, args, aliases = [], command = argv => nativeComma
430
429
  };
431
430
  return adapter;
432
431
  }
433
- return { manifest, create };
432
+ manifest.integrations = { mcp: true };
433
+ return { manifest, create };
434
434
  }
435
435
  module.exports = { nativeAcp, catalog, project };
@@ -10,3 +10,14 @@ module.exports = piFamily({
10
10
  packageHint: 'npm i -g @oh-my-pi/pi-coding-agent 或 https://omp.sh/install',
11
11
  aliases: ['omp', 'oh-my-pi'],
12
12
  });
13
+ module.exports.manifest.integrations = {
14
+ mcp: false,
15
+ skills: {
16
+ global: ['.omp/agent/skills', '.agents/skills', '.pi/agent/skills'],
17
+ project: ['.omp/skills', '.agents/skills', '.pi/skills'],
18
+ overrides: {
19
+ '.omp/agent/skills': { env: 'OMP_CODING_AGENT_DIR', suffix: 'skills' },
20
+ '.pi/agent/skills': { env: 'PI_CODING_AGENT_DIR', suffix: 'skills' },
21
+ },
22
+ },
23
+ };
@@ -18,6 +18,12 @@ const manifest = {
18
18
  capabilities: { streaming: true, thinking: false, tools: true, approvals: true, questions: false, models: true, thinkingLevels: true, permissionModes: false, resume: true, fork: false, forkFromMessage: false, compaction: false, usage: true, contextUsage: true, attachments: true },
19
19
  };
20
20
 
21
+ manifest.integrations = { mcp: false, skills: {
22
+ global: ['.openclaw/skills', '.agents/skills'],
23
+ project: ['skills', '.agents/skills'],
24
+ overrides: { '.openclaw/skills': { env: 'OPENCLAW_STATE_DIR', suffix: 'skills' } },
25
+ } };
26
+
21
27
  const textOfContent = (result) => (result?.content ?? []).map((c) => (c?.type === "text" ? c.text : "")).filter(Boolean).join("\n");
22
28
  const stringifyArgs = (args) => {
23
29
  if (args == null) return undefined;
@@ -54,9 +54,14 @@ function create() {
54
54
  return new Promise(resolve => execFile(cli.command, cli.args, { windowsHide: true, timeout: 10000 }, (error, stdout) => resolve({ available: !error, detail: error ? 'OpenCode Server CLI unavailable' : `OpenCode ${stdout.trim()} · native HTTP/SSE` })));
55
55
  },
56
56
  async describe() { return { models: null, thinkingLevels: [], permissionModes: [] }; },
57
- async open({ thread, emit, collaboration }) {
57
+ async inspectIntegrations(session) {
58
+ const rows = await session.host.request('GET', '/mcp', undefined, 5000);
59
+ return Object.entries(rows).map(([name, value]) => ({ name, status: value.status, tools: [] }));
60
+ },
61
+ async open({ thread, emit, collaboration, managedMcp = [] }) {
58
62
  // L2 注入:OPENCODE_CONFIG_CONTENT 是最高优先级的运行时内联配置,合并而非覆盖用户配置,会话结束即失效,不污染任何配置文件
59
- const overlay = collaboration ? { env: { OPENCODE_CONFIG_CONTENT: JSON.stringify({ mcp: { 'harness-mix': { type: 'local', command: [collaboration.command, ...collaboration.args], environment: collaboration.env } } }) } } : undefined;
63
+ const servers = require('./managed-mcp').nativeServers(managedMcp, collaboration);
64
+ const overlay = servers.length ? { env: { OPENCODE_CONFIG_CONTENT: JSON.stringify({ mcp: Object.fromEntries(servers.map(s => [s.name, { type: 'local', command: [s.command, ...s.args], environment: s.env }])) }) } } : undefined;
60
65
  const host = await new OpenCodeServer(thread.cwd, overlay).start();
61
66
  try {
62
67
  // Read the model catalog, never provider credentials or auth files.
@@ -175,4 +180,9 @@ function create() {
175
180
  };
176
181
  return adapter;
177
182
  }
178
- module.exports = { manifest, create, projectEvent, projectPart, describeSession };
183
+ manifest.integrations = { mcp: true, skills: {
184
+ global: ['.config/opencode/skills', '.claude/skills', '.agents/skills'],
185
+ project: ['.opencode/skills', '.claude/skills', '.agents/skills'],
186
+ overrides: { '.config/opencode/skills': { env: 'XDG_CONFIG_HOME', suffix: 'opencode/skills' } },
187
+ } };
188
+ module.exports = { manifest, create, projectEvent, projectPart, describeSession };
@@ -8,3 +8,8 @@ module.exports = piFamily({
8
8
  bin: 'pi',
9
9
  packageHint: 'npm i -g @earendil-works/pi-coding-agent',
10
10
  });
11
+ module.exports.manifest.integrations = { mcp: false, skills: {
12
+ global: ['.pi/agent/skills', '.agents/skills'],
13
+ project: ['.pi/skills', '.agents/skills'],
14
+ overrides: { '.pi/agent/skills': { env: 'PI_CODING_AGENT_DIR', suffix: 'skills' } },
15
+ } };
@@ -5,3 +5,4 @@ module.exports = nativeAcp({
5
5
  id: 'qoder', name: 'Qoder', args: ['--acp'],
6
6
  capabilities: { questions: false, thinkingLevels: false, usage: true, contextUsage: true, attachments: true, fork: false, compaction: false },
7
7
  });
8
+ module.exports.manifest.integrations.skills = { global: ['.qoder/skills'], project: ['.qoder/skills'] };
@@ -5,3 +5,7 @@ module.exports = nativeAcp({
5
5
  id: 'trae', name: 'Trae', args: [],
6
6
  capabilities: { questions: false, thinkingLevels: false, usage: true, contextUsage: true, attachments: false, fork: false, compaction: false },
7
7
  });
8
+ // Trae IDE uses .trae/skills; TraeCode CLI uses .traecli/skills. Both are registered so the
9
+ // CLI-backed (ACP) session and the IDE share one managed root set.
10
+ // https://docs.trae.cn/cli_skills
11
+ module.exports.manifest.integrations.skills = { global: ['.trae/skills', '.traecli/skills'], project: ['.trae/skills', '.traecli/skills', '.agents/skills'] };
@@ -5,3 +5,6 @@ module.exports = nativeAcp({
5
5
  id: 'zcode', name: 'ZCode', args: [],
6
6
  capabilities: { questions: false, thinkingLevels: false, usage: true, contextUsage: true, attachments: false, fork: false, compaction: false },
7
7
  });
8
+ // ZCode scans, per scope: .zcode/skills then .agents/skills (deeper workspace levels win).
9
+ // https://zcode.z.ai/en/docs/skill
10
+ module.exports.manifest.integrations.skills = { global: ['.zcode/skills', '.agents/skills'], project: ['.zcode/skills', '.agents/skills'] };
@@ -1,4 +1,5 @@
1
- const { CAPABILITY_GROUPS, createCapabilities } = require('../shared-contracts');
1
+ const path = require('node:path');
2
+ const { CAPABILITY_GROUPS, createCapabilities } = require('../shared-contracts');
2
3
 
3
4
  // Adapter Manifest 校验与能力归一化(§22)。
4
5
  // 现有 Adapter 使用扁平 capability flags;Core/UI 消费分组结构(§22)。
@@ -16,7 +17,21 @@ function validateManifest(manifest) {
16
17
  if (!manifest || typeof manifest !== 'object') return ['manifest must be an object'];
17
18
  if (typeof manifest.id !== 'string' || !manifest.id) errors.push('manifest.id is required');
18
19
  if (typeof manifest.name !== 'string' || !manifest.name) errors.push('manifest.name is required');
19
- if (manifest.icon != null && typeof manifest.icon !== 'string') errors.push('manifest.icon must be a string');
20
+ if (manifest.icon != null && typeof manifest.icon !== 'string') errors.push('manifest.icon must be a string');
21
+ if (manifest.integrations != null) {
22
+ const integrations = manifest.integrations;
23
+ if (!integrations || typeof integrations !== 'object' || Array.isArray(integrations)) errors.push('manifest.integrations must be an object');
24
+ else {
25
+ if (typeof integrations.mcp !== 'boolean') errors.push('manifest.integrations.mcp must be a boolean');
26
+ if (integrations.skills != null) {
27
+ const skills = integrations.skills;
28
+ if (!skills || typeof skills !== 'object' || Array.isArray(skills)) errors.push('manifest.integrations.skills must be an object');
29
+ else for (const scope of ['global', 'project']) {
30
+ if (!Array.isArray(skills[scope]) || skills[scope].some(value => typeof value !== 'string' || path.isAbsolute(value) || value.includes('..'))) errors.push(`manifest.integrations.skills.${scope} must contain safe relative paths`);
31
+ }
32
+ }
33
+ }
34
+ }
20
35
  const caps = manifest.capabilities;
21
36
  if (!caps || typeof caps !== 'object' || Array.isArray(caps)) {
22
37
  errors.push('manifest.capabilities must be an object');