@geminilight/mindos 0.6.67 → 0.6.69

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 (179) hide show
  1. package/_standalone/.mindos-build-version +1 -1
  2. package/_standalone/.next/BUILD_ID +1 -1
  3. package/_standalone/.next/app-path-routes-manifest.json +17 -17
  4. package/_standalone/.next/build-manifest.json +3 -3
  5. package/_standalone/.next/cache/.previewinfo +1 -1
  6. package/_standalone/.next/cache/.rscinfo +1 -1
  7. package/_standalone/.next/cache/config.json +3 -3
  8. package/_standalone/.next/prerender-manifest.json +3 -3
  9. package/_standalone/.next/react-loadable-manifest.json +4 -4
  10. package/_standalone/.next/server/app/.well-known/agent-card.json/route_client-reference-manifest.js +1 -1
  11. package/_standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  12. package/_standalone/.next/server/app/_global-error.html +2 -2
  13. package/_standalone/.next/server/app/_global-error.rsc +1 -1
  14. package/_standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  15. package/_standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  16. package/_standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  17. package/_standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  18. package/_standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  19. package/_standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  20. package/_standalone/.next/server/app/_not-found/page.js +1 -1
  21. package/_standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  22. package/_standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  23. package/_standalone/.next/server/app/agents/[agentKey]/page.js +1 -1
  24. package/_standalone/.next/server/app/agents/[agentKey]/page.js.nft.json +1 -1
  25. package/_standalone/.next/server/app/agents/[agentKey]/page_client-reference-manifest.js +1 -1
  26. package/_standalone/.next/server/app/agents/page.js +1 -1
  27. package/_standalone/.next/server/app/agents/page.js.nft.json +1 -1
  28. package/_standalone/.next/server/app/agents/page_client-reference-manifest.js +1 -1
  29. package/_standalone/.next/server/app/api/a2a/agents/route_client-reference-manifest.js +1 -1
  30. package/_standalone/.next/server/app/api/a2a/delegations/route_client-reference-manifest.js +1 -1
  31. package/_standalone/.next/server/app/api/a2a/discover/route_client-reference-manifest.js +1 -1
  32. package/_standalone/.next/server/app/api/a2a/route_client-reference-manifest.js +1 -1
  33. package/_standalone/.next/server/app/api/acp/config/route_client-reference-manifest.js +1 -1
  34. package/_standalone/.next/server/app/api/acp/detect/route.js +1 -1
  35. package/_standalone/.next/server/app/api/acp/detect/route_client-reference-manifest.js +1 -1
  36. package/_standalone/.next/server/app/api/acp/install/route_client-reference-manifest.js +1 -1
  37. package/_standalone/.next/server/app/api/acp/registry/route.js +1 -1
  38. package/_standalone/.next/server/app/api/acp/registry/route_client-reference-manifest.js +1 -1
  39. package/_standalone/.next/server/app/api/acp/session/route_client-reference-manifest.js +1 -1
  40. package/_standalone/.next/server/app/api/agent-activity/route_client-reference-manifest.js +1 -1
  41. package/_standalone/.next/server/app/api/agents/copy-skill/route_client-reference-manifest.js +1 -1
  42. package/_standalone/.next/server/app/api/agents/custom/detect/route_client-reference-manifest.js +1 -1
  43. package/_standalone/.next/server/app/api/agents/custom/route_client-reference-manifest.js +1 -1
  44. package/_standalone/.next/server/app/api/ask/route.js +8 -8
  45. package/_standalone/.next/server/app/api/ask/route_client-reference-manifest.js +1 -1
  46. package/_standalone/.next/server/app/api/ask-sessions/route_client-reference-manifest.js +1 -1
  47. package/_standalone/.next/server/app/api/auth/route_client-reference-manifest.js +1 -1
  48. package/_standalone/.next/server/app/api/backlinks/route_client-reference-manifest.js +1 -1
  49. package/_standalone/.next/server/app/api/bootstrap/route_client-reference-manifest.js +1 -1
  50. package/_standalone/.next/server/app/api/changes/route_client-reference-manifest.js +1 -1
  51. package/_standalone/.next/server/app/api/connect/route_client-reference-manifest.js +1 -1
  52. package/_standalone/.next/server/app/api/export/route_client-reference-manifest.js +1 -1
  53. package/_standalone/.next/server/app/api/extract-pdf/route_client-reference-manifest.js +1 -1
  54. package/_standalone/.next/server/app/api/file/import/route_client-reference-manifest.js +1 -1
  55. package/_standalone/.next/server/app/api/file/raw/route_client-reference-manifest.js +1 -1
  56. package/_standalone/.next/server/app/api/file/route_client-reference-manifest.js +1 -1
  57. package/_standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  58. package/_standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  59. package/_standalone/.next/server/app/api/graph/route_client-reference-manifest.js +1 -1
  60. package/_standalone/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
  61. package/_standalone/.next/server/app/api/im/config/route_client-reference-manifest.js +1 -1
  62. package/_standalone/.next/server/app/api/im/status/route_client-reference-manifest.js +1 -1
  63. package/_standalone/.next/server/app/api/im/test/route_client-reference-manifest.js +1 -1
  64. package/_standalone/.next/server/app/api/inbox/clip/route_client-reference-manifest.js +1 -1
  65. package/_standalone/.next/server/app/api/inbox/route_client-reference-manifest.js +1 -1
  66. package/_standalone/.next/server/app/api/init/route_client-reference-manifest.js +1 -1
  67. package/_standalone/.next/server/app/api/lint/route_client-reference-manifest.js +1 -1
  68. package/_standalone/.next/server/app/api/mcp/agents/route_client-reference-manifest.js +1 -1
  69. package/_standalone/.next/server/app/api/mcp/direct-tools/route_client-reference-manifest.js +1 -1
  70. package/_standalone/.next/server/app/api/mcp/install/route_client-reference-manifest.js +1 -1
  71. package/_standalone/.next/server/app/api/mcp/install-skill/route_client-reference-manifest.js +1 -1
  72. package/_standalone/.next/server/app/api/mcp/restart/route_client-reference-manifest.js +1 -1
  73. package/_standalone/.next/server/app/api/mcp/status/route_client-reference-manifest.js +1 -1
  74. package/_standalone/.next/server/app/api/mcp/tools/route_client-reference-manifest.js +1 -1
  75. package/_standalone/.next/server/app/api/mcp/uninstall/route_client-reference-manifest.js +1 -1
  76. package/_standalone/.next/server/app/api/monitoring/route_client-reference-manifest.js +1 -1
  77. package/_standalone/.next/server/app/api/recent-files/route_client-reference-manifest.js +1 -1
  78. package/_standalone/.next/server/app/api/restart/route_client-reference-manifest.js +1 -1
  79. package/_standalone/.next/server/app/api/search/route_client-reference-manifest.js +1 -1
  80. package/_standalone/.next/server/app/api/settings/list-models/route_client-reference-manifest.js +1 -1
  81. package/_standalone/.next/server/app/api/settings/reset-token/route_client-reference-manifest.js +1 -1
  82. package/_standalone/.next/server/app/api/settings/route_client-reference-manifest.js +1 -1
  83. package/_standalone/.next/server/app/api/settings/test-key/route_client-reference-manifest.js +1 -1
  84. package/_standalone/.next/server/app/api/setup/check-path/route_client-reference-manifest.js +1 -1
  85. package/_standalone/.next/server/app/api/setup/check-port/route_client-reference-manifest.js +1 -1
  86. package/_standalone/.next/server/app/api/setup/generate-token/route_client-reference-manifest.js +1 -1
  87. package/_standalone/.next/server/app/api/setup/ls/route_client-reference-manifest.js +1 -1
  88. package/_standalone/.next/server/app/api/setup/route_client-reference-manifest.js +1 -1
  89. package/_standalone/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
  90. package/_standalone/.next/server/app/api/space-overview/route_client-reference-manifest.js +1 -1
  91. package/_standalone/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
  92. package/_standalone/.next/server/app/api/tree-version/route_client-reference-manifest.js +1 -1
  93. package/_standalone/.next/server/app/api/uninstall/route_client-reference-manifest.js +1 -1
  94. package/_standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  95. package/_standalone/.next/server/app/api/update-check/route_client-reference-manifest.js +1 -1
  96. package/_standalone/.next/server/app/api/update-status/route_client-reference-manifest.js +1 -1
  97. package/_standalone/.next/server/app/api/workflows/route_client-reference-manifest.js +1 -1
  98. package/_standalone/.next/server/app/changelog/page.js +1 -1
  99. package/_standalone/.next/server/app/changelog/page.js.nft.json +1 -1
  100. package/_standalone/.next/server/app/changelog/page_client-reference-manifest.js +1 -1
  101. package/_standalone/.next/server/app/changes/page.js +1 -1
  102. package/_standalone/.next/server/app/changes/page.js.nft.json +1 -1
  103. package/_standalone/.next/server/app/changes/page_client-reference-manifest.js +1 -1
  104. package/_standalone/.next/server/app/echo/[segment]/page.js +1 -1
  105. package/_standalone/.next/server/app/echo/[segment]/page.js.nft.json +1 -1
  106. package/_standalone/.next/server/app/echo/[segment]/page_client-reference-manifest.js +1 -1
  107. package/_standalone/.next/server/app/echo/page.js +1 -1
  108. package/_standalone/.next/server/app/echo/page.js.nft.json +1 -1
  109. package/_standalone/.next/server/app/echo/page_client-reference-manifest.js +1 -1
  110. package/_standalone/.next/server/app/explore/page.js +1 -1
  111. package/_standalone/.next/server/app/explore/page.js.nft.json +1 -1
  112. package/_standalone/.next/server/app/explore/page_client-reference-manifest.js +1 -1
  113. package/_standalone/.next/server/app/help/page.js +1 -1
  114. package/_standalone/.next/server/app/help/page.js.nft.json +1 -1
  115. package/_standalone/.next/server/app/help/page_client-reference-manifest.js +1 -1
  116. package/_standalone/.next/server/app/inbox/history/page.js +1 -1
  117. package/_standalone/.next/server/app/inbox/history/page.js.nft.json +1 -1
  118. package/_standalone/.next/server/app/inbox/history/page_client-reference-manifest.js +1 -1
  119. package/_standalone/.next/server/app/login/page.js +1 -1
  120. package/_standalone/.next/server/app/login/page.js.nft.json +1 -1
  121. package/_standalone/.next/server/app/login/page_client-reference-manifest.js +1 -1
  122. package/_standalone/.next/server/app/page.js +1 -1
  123. package/_standalone/.next/server/app/page.js.nft.json +1 -1
  124. package/_standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  125. package/_standalone/.next/server/app/setup/page.js +1 -1
  126. package/_standalone/.next/server/app/setup/page.js.nft.json +1 -1
  127. package/_standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  128. package/_standalone/.next/server/app/trash/page.js +2 -2
  129. package/_standalone/.next/server/app/trash/page_client-reference-manifest.js +1 -1
  130. package/_standalone/.next/server/app/view/[...path]/page.js +3 -3
  131. package/_standalone/.next/server/app/view/[...path]/page.js.nft.json +1 -1
  132. package/_standalone/.next/server/app/view/[...path]/page_client-reference-manifest.js +1 -1
  133. package/_standalone/.next/server/app/wiki/page.js +1 -1
  134. package/_standalone/.next/server/app/wiki/page.js.nft.json +1 -1
  135. package/_standalone/.next/server/app/wiki/page_client-reference-manifest.js +1 -1
  136. package/_standalone/.next/server/app-paths-manifest.json +17 -17
  137. package/_standalone/.next/server/chunks/1750.js +1 -1
  138. package/_standalone/.next/server/chunks/6022.js +31 -31
  139. package/_standalone/.next/server/chunks/6539.js +1 -1
  140. package/_standalone/.next/server/chunks/{8947.js → 7200.js} +3 -3
  141. package/_standalone/.next/server/chunks/953.js +1 -1
  142. package/_standalone/.next/server/middleware-build-manifest.js +1 -1
  143. package/_standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  144. package/_standalone/.next/server/pages/500.html +2 -2
  145. package/_standalone/.next/server/server-reference-manifest.js +1 -1
  146. package/_standalone/.next/server/server-reference-manifest.json +1 -1
  147. package/_standalone/.next/static/chunks/{5998.7bd28de9747440b5.js → 3466.bc6ab3172ad66091.js} +1 -1
  148. package/_standalone/.next/static/chunks/{476.463546c195b89cce.js → 8735.66a049abcf0971fb.js} +2 -2
  149. package/_standalone/.next/static/chunks/app/layout-bd0652768781e726.js +133 -0
  150. package/_standalone/.next/static/chunks/app/trash/page-8280ba2f5c20861e.js +1 -0
  151. package/_standalone/.next/static/chunks/app/view/[...path]/page-b942374e2f88c53e.js +12 -0
  152. package/_standalone/.next/static/chunks/webpack-9e42857d6c44fe70.js +1 -0
  153. package/_standalone/.next/trace +71 -71
  154. package/_standalone/__tests__/acp/agent-descriptors.test.ts +65 -3
  155. package/_standalone/__tests__/acp/registry.test.ts +34 -5
  156. package/_standalone/__tests__/acp/session.test.ts +1 -1
  157. package/_standalone/components/Breadcrumb.tsx +11 -11
  158. package/_standalone/components/panels/AgentsPanel.tsx +8 -1
  159. package/_standalone/lib/acp/index.ts +2 -0
  160. package/_standalone/package-lock.json +2 -2
  161. package/_standalone/package.json +1 -1
  162. package/_standalone/tsconfig.tsbuildinfo +1 -1
  163. package/app/app/api/acp/detect/route.ts +9 -15
  164. package/app/components/Breadcrumb.tsx +11 -11
  165. package/app/components/panels/AgentsPanel.tsx +8 -1
  166. package/app/lib/acp/agent-descriptors.ts +51 -29
  167. package/app/lib/acp/index.ts +2 -0
  168. package/app/lib/acp/registry.ts +42 -46
  169. package/app/lib/acp/session.ts +36 -7
  170. package/app/lib/acp/subprocess.ts +40 -2
  171. package/app/lib/agent/tools.ts +4 -3
  172. package/app/package.json +1 -1
  173. package/package.json +1 -1
  174. package/_standalone/.next/static/chunks/app/layout-9bb19a959ffb87ac.js +0 -133
  175. package/_standalone/.next/static/chunks/app/trash/page-bebb28bf472cf691.js +0 -1
  176. package/_standalone/.next/static/chunks/app/view/[...path]/page-dd5698f3df138835.js +0 -12
  177. package/_standalone/.next/static/chunks/webpack-043f40ef7816d8c4.js +0 -1
  178. /package/_standalone/.next/static/{0JtsgqDZLSJ6MrIZIV6gC → HL8b6d3NCfyoAXNuY2kcn}/_buildManifest.js +0 -0
  179. /package/_standalone/.next/static/{0JtsgqDZLSJ6MrIZIV6gC → HL8b6d3NCfyoAXNuY2kcn}/_ssgManifest.js +0 -0
@@ -2,8 +2,7 @@ export const dynamic = 'force-dynamic';
2
2
 
3
3
  import { NextResponse } from 'next/server';
4
4
  import { exec } from 'child_process';
5
- import { getAcpAgents } from '@/lib/acp/registry';
6
- import { getDescriptorBinary, getDescriptorInstallCmd, resolveAgentCommand } from '@/lib/acp/agent-descriptors';
5
+ import { getDetectableAgents, resolveAgentCommand } from '@/lib/acp/agent-descriptors';
7
6
  import { readSettings } from '@/lib/settings';
8
7
  import { handleRouteErrorSimple } from '@/lib/errors';
9
8
 
@@ -43,8 +42,6 @@ function whichBatch(binaries: string[]): Promise<Map<string, string | null>> {
43
42
  const unique = [...new Set(binaries)];
44
43
  if (unique.length === 0) return Promise.resolve(new Map());
45
44
 
46
- // Build a shell snippet: for each binary, print path or empty line
47
- // e.g. `which gemini 2>/dev/null || echo ""; which claude 2>/dev/null || echo ""`
48
45
  const script = unique
49
46
  .map(bin => `which ${bin} 2>/dev/null || echo ""`)
50
47
  .join('; ');
@@ -53,7 +50,6 @@ function whichBatch(binaries: string[]): Promise<Map<string, string | null>> {
53
50
  exec(script, { encoding: 'utf-8', timeout: 3000 }, (err, stdout) => {
54
51
  const map = new Map<string, string | null>();
55
52
  if (err) {
56
- // On total failure, mark all as not found
57
53
  for (const bin of unique) map.set(bin, null);
58
54
  resolve(map);
59
55
  return;
@@ -77,22 +73,22 @@ export async function GET(req: Request) {
77
73
  return NextResponse.json(detectCache.data);
78
74
  }
79
75
 
80
- const agents = await getAcpAgents();
76
+ // Pure local detection — no CDN fetch, instant response
77
+ const agents = getDetectableAgents();
81
78
  const settings = readSettings();
82
79
 
83
- const binaryNames = agents.map((a) => getDescriptorBinary(a.id)).filter(Boolean) as string[];
80
+ const binaryNames = [...new Set(agents.map(a => a.binary))];
84
81
  const whichMap = await whichBatch(binaryNames);
85
82
 
86
83
  const installed: InstalledAgent[] = [];
87
84
  const notInstalled: NotInstalledAgent[] = [];
88
85
 
89
86
  for (const agent of agents) {
90
- const binary = getDescriptorBinary(agent.id);
91
- const binaryPath = binary ? (whichMap.get(binary) ?? null) : null;
87
+ const binaryPath = whichMap.get(agent.binary) ?? null;
92
88
 
93
89
  if (binaryPath) {
94
90
  const userOverride = settings.acpAgents?.[agent.id];
95
- const resolved = resolveAgentCommand(agent.id, agent, userOverride);
91
+ const resolved = resolveAgentCommand(agent.id, undefined, userOverride);
96
92
  installed.push({
97
93
  id: agent.id,
98
94
  name: agent.name,
@@ -100,14 +96,12 @@ export async function GET(req: Request) {
100
96
  resolvedCommand: { cmd: resolved.cmd, args: resolved.args, source: resolved.source },
101
97
  });
102
98
  } else {
103
- const installCmd =
104
- getDescriptorInstallCmd(agent.id) ??
105
- (agent.packageName ? `npm install -g ${agent.packageName}` : '');
99
+ const packageName = agent.installCmd?.match(/npm install -g (.+)/)?.[1];
106
100
  notInstalled.push({
107
101
  id: agent.id,
108
102
  name: agent.name,
109
- installCmd,
110
- packageName: agent.packageName,
103
+ installCmd: agent.installCmd ?? (packageName ? `npm install -g ${packageName}` : ''),
104
+ packageName,
111
105
  });
112
106
  }
113
107
  }
@@ -21,29 +21,29 @@ export default function Breadcrumb({ filePath }: { filePath: string }) {
21
21
 
22
22
  if (friendly) {
23
23
  return (
24
- <nav className="flex items-center gap-0.5 text-xs text-muted-foreground flex-wrap">
24
+ <nav className="flex items-center gap-2 text-xs text-muted-foreground flex-wrap">
25
25
  <Link
26
26
  href="/"
27
- className="p-1.5 rounded-md hover:bg-muted/50 hover:text-foreground transition-colors"
27
+ className="p-1.5 rounded-md hover:bg-muted/50 hover:text-foreground transition-colors shrink-0"
28
28
  title="Home"
29
29
  >
30
30
  <Home size={14} />
31
31
  </Link>
32
32
  <ChevronRight size={12} className="text-muted-foreground/50 shrink-0" />
33
- <span className="flex items-center gap-1.5 px-2 py-1 text-foreground font-medium">
33
+ <div className="flex items-center gap-1.5 text-foreground font-medium shrink-0">
34
34
  {friendly.icon}
35
35
  <span>{friendly.getLabel(t)}</span>
36
- </span>
36
+ </div>
37
37
  </nav>
38
38
  );
39
39
  }
40
40
 
41
41
  const parts = filePath.split('/');
42
42
  return (
43
- <nav className="flex items-center gap-0.5 text-xs text-muted-foreground flex-wrap">
43
+ <nav className="flex items-center gap-2 text-xs text-muted-foreground flex-wrap">
44
44
  <Link
45
45
  href="/"
46
- className="p-1.5 rounded-md hover:bg-muted/50 hover:text-foreground transition-colors"
46
+ className="p-1.5 rounded-md hover:bg-muted/50 hover:text-foreground transition-colors shrink-0"
47
47
  title="Home"
48
48
  >
49
49
  <Home size={14} />
@@ -52,19 +52,19 @@ export default function Breadcrumb({ filePath }: { filePath: string }) {
52
52
  const isLast = i === parts.length - 1;
53
53
  const href = '/view/' + parts.slice(0, i + 1).map(encodeURIComponent).join('/');
54
54
  return (
55
- <span key={i} className="flex items-center gap-0.5">
55
+ <div key={i} className="flex items-center gap-2 min-w-0">
56
56
  <ChevronRight size={12} className="text-muted-foreground/50 shrink-0" />
57
57
  {isLast ? (
58
- <span className="flex items-center gap-1.5 px-2 py-1 text-foreground font-medium">
58
+ <div className="flex items-center gap-1.5 text-foreground font-medium shrink-0">
59
59
  <FileTypeIcon name={part} />
60
60
  <span suppressHydrationWarning>{part}</span>
61
- </span>
61
+ </div>
62
62
  ) : (
63
- <Link href={href} className="px-2 py-1 rounded-md hover:bg-muted/50 hover:text-foreground transition-colors truncate max-w-[200px]" title={part}>
63
+ <Link href={href} className="px-2 py-1 rounded-md hover:bg-muted/50 hover:text-foreground transition-colors truncate text-muted-foreground hover:text-foreground" title={part}>
64
64
  <span suppressHydrationWarning>{part}</span>
65
65
  </Link>
66
66
  )}
67
- </span>
67
+ </div>
68
68
  );
69
69
  })}
70
70
  </nav>
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useState } from 'react';
4
- import { usePathname, useSearchParams } from 'next/navigation';
4
+ import { useRouter, usePathname, useSearchParams } from 'next/navigation';
5
5
  import { Globe, Loader2, RefreshCw, Settings } from 'lucide-react';
6
6
  import { useMcpData } from '@/lib/stores/mcp-store';
7
7
  import { useA2aRegistry } from '@/hooks/useA2aRegistry';
@@ -27,6 +27,7 @@ export default function AgentsPanel({
27
27
  }: AgentsPanelProps) {
28
28
  const { t } = useLocale();
29
29
  const p = t.panels.agents;
30
+ const router = useRouter();
30
31
  const mcp = useMcpData();
31
32
  const pathname = usePathname();
32
33
  const searchParams = useSearchParams();
@@ -42,6 +43,10 @@ export default function AgentsPanel({
42
43
  setRefreshing(false);
43
44
  };
44
45
 
46
+ const handleChannelsClick = () => {
47
+ router.push('/agents?tab=channels');
48
+ };
49
+
45
50
  const openAdvancedConfig = () => {
46
51
  window.dispatchEvent(new CustomEvent('mindos:open-settings', { detail: { tab: 'mcp' } }));
47
52
  };
@@ -84,6 +89,8 @@ export default function AgentsPanel({
84
89
  copy={hubCopy}
85
90
  connectedCount={connected.length}
86
91
  mcpEnabled={mcp.status?.connectionMode?.mcp ?? false}
92
+ channelsActive={isChannelsTab}
93
+ onChannelsClick={handleChannelsClick}
87
94
  />
88
95
  );
89
96
 
@@ -50,45 +50,45 @@ export interface ResolvedAgentCommand {
50
50
  enabled: boolean;
51
51
  }
52
52
 
53
+ /* ── Aliases ───────────────────────────────────────────────────────────── */
54
+
55
+ /**
56
+ * Maps alternative agent IDs to their canonical ID in AGENT_DESCRIPTORS.
57
+ * This eliminates full duplicate entries while maintaining backward compatibility.
58
+ */
59
+ export const AGENT_ALIASES: Record<string, string> = {
60
+ 'gemini-cli': 'gemini',
61
+ 'claude-code': 'claude',
62
+ 'claude-acp': 'claude',
63
+ 'codebuddy': 'codebuddy-code',
64
+ 'codex': 'codex-acp',
65
+ 'pi-acp': 'pi',
66
+ };
67
+
68
+ /** Resolve an agent ID to its canonical form (idempotent for canonical IDs). */
69
+ export function resolveAlias(agentId: string): string {
70
+ return AGENT_ALIASES[agentId] ?? agentId;
71
+ }
72
+
53
73
  /* ── Canonical Descriptors ─────────────────────────────────────────────── */
54
74
 
55
75
  /**
56
76
  * All known ACP agents with their detection binary, launch command, and install hint.
57
- * Both detection (`which binary?`) and launch (`spawn cmd args`) read from here.
77
+ * Only canonical entries aliases are handled by AGENT_ALIASES above.
58
78
  */
59
79
  export const AGENT_DESCRIPTORS: Record<string, AcpAgentDescriptor> = {
60
- // Gemini CLI — Google's AI coding agent
61
80
  'gemini': { binary: 'gemini', cmd: 'gemini', args: ['--experimental-acp'], installCmd: 'npm install -g @google/gemini-cli',
62
81
  displayName: 'Gemini CLI',
63
82
  description: 'Google Gemini 驱动的编程智能体。支持多文件编辑、代码审查、调试和项目级重构,原生集成 Google 搜索实时查询技术文档。' },
64
- 'gemini-cli': { binary: 'gemini', cmd: 'gemini', args: ['--experimental-acp'], installCmd: 'npm install -g @google/gemini-cli',
65
- displayName: 'Gemini CLI',
66
- description: 'Google Gemini 驱动的编程智能体。支持多文件编辑、代码审查、调试和项目级重构,原生集成 Google 搜索实时查询技术文档。' },
67
- // Claude Code — Anthropic's AI coding agent
68
83
  'claude': { binary: 'claude', cmd: 'npx', args: ['--yes', '@agentclientprotocol/claude-agent-acp'], installCmd: 'npm install -g @anthropic-ai/claude-code',
69
84
  displayName: 'Claude Code',
70
85
  description: 'Anthropic Claude 驱动的编程智能体。擅长复杂推理、长上下文理解和安全代码生成,支持多文件编辑与 agentic 工作流。' },
71
- 'claude-code': { binary: 'claude', cmd: 'npx', args: ['--yes', '@agentclientprotocol/claude-agent-acp'], installCmd: 'npm install -g @anthropic-ai/claude-code',
72
- displayName: 'Claude Code',
73
- description: 'Anthropic Claude 驱动的编程智能体。擅长复杂推理、长上下文理解和安全代码生成,支持多文件编辑与 agentic 工作流。' },
74
- 'claude-acp': { binary: 'claude', cmd: 'npx', args: ['--yes', '@agentclientprotocol/claude-agent-acp'], installCmd: 'npm install -g @anthropic-ai/claude-code',
75
- displayName: 'Claude Code',
76
- description: 'Anthropic Claude 驱动的编程智能体。擅长复杂推理、长上下文理解和安全代码生成,支持多文件编辑与 agentic 工作流。' },
77
- // CodeBuddy Code — Tencent Cloud's AI coding agent
78
86
  'codebuddy-code': { binary: 'codebuddy', cmd: 'codebuddy', args: ['--acp'], installCmd: 'npm install -g @tencent-ai/codebuddy-code',
79
87
  displayName: 'CodeBuddy Code',
80
88
  description: '腾讯云智能编程助手。基于混元大模型,支持代码补全、生成、审查和多文件重构,深度理解中文语境,适配国内开发生态。' },
81
- 'codebuddy': { binary: 'codebuddy', cmd: 'codebuddy', args: ['--acp'], installCmd: 'npm install -g @tencent-ai/codebuddy-code',
82
- displayName: 'CodeBuddy Code',
83
- description: '腾讯云智能编程助手。基于混元大模型,支持代码补全、生成、审查和多文件重构,深度理解中文语境,适配国内开发生态。' },
84
- // Codex — OpenAI's coding agent
85
89
  'codex-acp': { binary: 'codex', cmd: 'codex', args: [], installCmd: 'npm install -g @openai/codex',
86
90
  displayName: 'Codex',
87
91
  description: 'OpenAI Codex 编程智能体。基于 GPT 系列模型,擅长代码生成、自动化任务和多语言编程支持。' },
88
- 'codex': { binary: 'codex', cmd: 'codex', args: [], installCmd: 'npm install -g @openai/codex',
89
- displayName: 'Codex',
90
- description: 'OpenAI Codex 编程智能体。基于 GPT 系列模型,擅长代码生成、自动化任务和多语言编程支持。' },
91
- // Cursor — AI-first code editor agent
92
92
  'cursor': { binary: 'cursor', cmd: 'cursor', args: [],
93
93
  displayName: 'Cursor',
94
94
  description: 'Cursor AI 编程智能体。AI-first 代码编辑器的 CLI 模式,支持上下文感知的代码编辑、Tab 补全和多文件协同修改。' },
@@ -113,9 +113,6 @@ export const AGENT_DESCRIPTORS: Record<string, AcpAgentDescriptor> = {
113
113
  'pi': { binary: 'pi', cmd: 'pi', args: [],
114
114
  displayName: 'Pi Agent',
115
115
  description: 'Pi Agent 编程智能体。轻量级终端编程助手。' },
116
- 'pi-acp': { binary: 'pi', cmd: 'pi', args: [],
117
- displayName: 'Pi Agent',
118
- description: 'Pi Agent 编程智能体。轻量级终端编程助手。' },
119
116
  'auggie': { binary: 'auggie', cmd: 'auggie', args: [],
120
117
  displayName: 'Auggie',
121
118
  description: 'Augment Code 编程智能体。支持代码理解、生成和全仓库上下文感知。' },
@@ -147,7 +144,7 @@ export function resolveAgentCommand(
147
144
  registryEntry?: AcpRegistryEntry,
148
145
  userOverride?: AcpAgentOverride,
149
146
  ): ResolvedAgentCommand {
150
- const descriptor = AGENT_DESCRIPTORS[agentId];
147
+ const descriptor = AGENT_DESCRIPTORS[resolveAlias(agentId)];
151
148
  const enabled = userOverride?.enabled !== false;
152
149
 
153
150
  // Layer 1: User override
@@ -220,22 +217,47 @@ function registryToCommand(entry: AcpRegistryEntry): { cmd: string; args: string
220
217
 
221
218
  /** Get the binary name for detection (used by detect endpoint). */
222
219
  export function getDescriptorBinary(agentId: string): string | undefined {
223
- return AGENT_DESCRIPTORS[agentId]?.binary;
220
+ return AGENT_DESCRIPTORS[resolveAlias(agentId)]?.binary;
224
221
  }
225
222
 
226
223
  /** Get the install command for UI display. */
227
224
  export function getDescriptorInstallCmd(agentId: string): string | undefined {
228
- return AGENT_DESCRIPTORS[agentId]?.installCmd;
225
+ return AGENT_DESCRIPTORS[resolveAlias(agentId)]?.installCmd;
229
226
  }
230
227
 
231
228
  /** Get curated display name (overrides registry name if available). */
232
229
  export function getDescriptorDisplayName(agentId: string): string | undefined {
233
- return AGENT_DESCRIPTORS[agentId]?.displayName;
230
+ return AGENT_DESCRIPTORS[resolveAlias(agentId)]?.displayName;
234
231
  }
235
232
 
236
233
  /** Get curated description (overrides registry description if available). */
237
234
  export function getDescriptorDescription(agentId: string): string | undefined {
238
- return AGENT_DESCRIPTORS[agentId]?.description;
235
+ return AGENT_DESCRIPTORS[resolveAlias(agentId)]?.description;
236
+ }
237
+
238
+ /* ── Detection ─────────────────────────────────────────────────────────── */
239
+
240
+ /** Agent info needed for local binary detection (no CDN dependency). */
241
+ export interface DetectableAgent {
242
+ id: string;
243
+ name: string;
244
+ binary: string;
245
+ installCmd?: string;
246
+ description?: string;
247
+ }
248
+
249
+ /**
250
+ * Return the canonical list of agents for local detection.
251
+ * Pure local data — no CDN fetch, no async, no network dependency.
252
+ */
253
+ export function getDetectableAgents(): DetectableAgent[] {
254
+ return Object.entries(AGENT_DESCRIPTORS).map(([id, desc]) => ({
255
+ id,
256
+ name: desc.displayName ?? id,
257
+ binary: desc.binary,
258
+ installCmd: desc.installCmd,
259
+ description: desc.description,
260
+ }));
239
261
  }
240
262
 
241
263
  /** Parse and validate acpAgents config from raw settings JSON. */
@@ -3,6 +3,7 @@ export { spawnAcpAgent, sendMessage, sendAndWait, onMessage, onNotification, onR
3
3
  export { createSession, createSessionFromEntry, loadSession, listSessions, prompt, promptStream, cancelPrompt, setMode, setConfigOption, closeSession, getSession, getActiveSessions, closeAllSessions } from './session';
4
4
  export { bridgeA2aToAcp, bridgeAcpResponseToA2a, bridgeAcpUpdatesToA2a } from './bridge';
5
5
  export { acpTools } from './acp-tools';
6
+ export { AGENT_DESCRIPTORS, AGENT_ALIASES, resolveAlias, getDetectableAgents } from './agent-descriptors';
6
7
  export { ACP_ERRORS } from './types';
7
8
  export type {
8
9
  AcpAgentCapabilities,
@@ -38,3 +39,4 @@ export type {
38
39
  AcpTransportType,
39
40
  } from './types';
40
41
  export type { AcpProcess, AcpIncomingRequest, AcpNotification } from './subprocess';
42
+ export type { AcpAgentDescriptor, AcpAgentOverride, ResolvedAgentCommand, DetectableAgent } from './agent-descriptors';
@@ -10,57 +10,31 @@
10
10
  */
11
11
 
12
12
  import type { AcpRegistry, AcpRegistryEntry } from './types';
13
- import { AGENT_DESCRIPTORS, getDescriptorDisplayName, getDescriptorDescription } from './agent-descriptors';
13
+ import { AGENT_DESCRIPTORS, getDescriptorDisplayName, getDescriptorDescription, resolveAlias } from './agent-descriptors';
14
14
 
15
15
  /* ── Constants ─────────────────────────────────────────────────────────── */
16
16
 
17
17
  const REGISTRY_URL = 'https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json';
18
- const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
18
+ const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days — registry updates very rarely
19
19
  const FETCH_TIMEOUT_MS = 10_000;
20
20
 
21
21
  /* ── Built-in Registry (from AGENT_DESCRIPTORS) ────────────────────────── */
22
22
 
23
23
  /**
24
24
  * Generate a baseline registry from the local AGENT_DESCRIPTORS.
25
- * This ensures agents like Gemini CLI, CodeBuddy, Claude etc. are always
26
- * detectable even when CDN is unreachable.
27
- *
28
- * We deduplicate by binary name — e.g. 'gemini' and 'gemini-cli' both map
29
- * to binary 'gemini', so we only keep the canonical entry (shorter ID or
30
- * the one matching the CDN convention).
25
+ * AGENT_DESCRIPTORS contains only canonical entries (aliases are in AGENT_ALIASES),
26
+ * so no deduplication is needed.
31
27
  */
32
28
  function buildBuiltinRegistry(): AcpRegistryEntry[] {
33
- // Preferred IDs per binary — matches what CDN uses
34
- const CANONICAL_IDS: Record<string, string> = {
35
- 'gemini': 'gemini',
36
- 'claude': 'claude-acp',
37
- 'codebuddy': 'codebuddy-code',
38
- 'codex': 'codex-acp',
39
- 'pi': 'pi-acp',
40
- };
41
-
42
- const seen = new Set<string>();
43
- const entries: AcpRegistryEntry[] = [];
44
-
45
- for (const [id, desc] of Object.entries(AGENT_DESCRIPTORS)) {
46
- // Skip alias entries — only keep the canonical ID for each binary
47
- const canonical = CANONICAL_IDS[desc.binary];
48
- if (canonical && canonical !== id) continue;
49
- if (seen.has(desc.binary)) continue;
50
- seen.add(desc.binary);
51
-
52
- entries.push({
53
- id,
54
- name: desc.displayName ?? id,
55
- description: desc.description ?? '',
56
- transport: desc.cmd === 'npx' ? 'npx' : 'stdio',
57
- command: desc.cmd,
58
- args: desc.args,
59
- packageName: desc.installCmd?.match(/npm install -g (.+)/)?.[1],
60
- });
61
- }
62
-
63
- return entries;
29
+ return Object.entries(AGENT_DESCRIPTORS).map(([id, desc]) => ({
30
+ id,
31
+ name: desc.displayName ?? id,
32
+ description: desc.description ?? '',
33
+ transport: (desc.cmd === 'npx' ? 'npx' : 'stdio') as AcpRegistryEntry['transport'],
34
+ command: desc.cmd,
35
+ args: desc.args,
36
+ packageName: desc.installCmd?.match(/npm install -g (.+)/)?.[1],
37
+ }));
64
38
  }
65
39
 
66
40
  let builtinAgents: AcpRegistryEntry[] | null = null;
@@ -78,7 +52,7 @@ let cachedRegistry: AcpRegistry | null = null;
78
52
 
79
53
  /**
80
54
  * Fetch the ACP registry from the CDN and merge with built-in entries.
81
- * Caches for 1 hour. Falls back to built-in registry if CDN is unreachable.
55
+ * Caches for 7 days. Falls back to built-in registry if CDN is unreachable.
82
56
  */
83
57
  export async function fetchAcpRegistry(): Promise<AcpRegistry> {
84
58
  // Return cached if still valid
@@ -119,15 +93,36 @@ export async function fetchAcpRegistry(): Promise<AcpRegistry> {
119
93
  }
120
94
  }
121
95
 
122
- /** Merge built-in and CDN registries. CDN entries win on conflict; built-in fills gaps. */
96
+ /**
97
+ * Merge built-in and CDN registries.
98
+ * - Same ID: keep built-in core fields, supplement with CDN metadata (tags, homepage, version)
99
+ * - CDN alias of built-in entry: skip (avoids duplicate agents in UI)
100
+ * - New CDN-only entry: add as-is
101
+ */
123
102
  function mergeRegistries(builtin: AcpRegistryEntry[], cdn: AcpRegistryEntry[]): AcpRegistryEntry[] {
124
103
  const byId = new Map<string, AcpRegistryEntry>();
125
104
 
126
- // Start with built-in
127
105
  for (const entry of builtin) byId.set(entry.id, entry);
128
106
 
129
- // CDN overwrites / adds
130
- for (const entry of cdn) byId.set(entry.id, entry);
107
+ for (const cdnEntry of cdn) {
108
+ const existing = byId.get(cdnEntry.id);
109
+ const canonicalId = resolveAlias(cdnEntry.id);
110
+ const canonicalExisting = canonicalId !== cdnEntry.id ? byId.get(canonicalId) : undefined;
111
+
112
+ if (existing) {
113
+ // Same ID in both — keep built-in core, supplement with CDN metadata
114
+ byId.set(cdnEntry.id, {
115
+ ...existing,
116
+ tags: cdnEntry.tags ?? existing.tags,
117
+ homepage: cdnEntry.homepage ?? existing.homepage,
118
+ version: cdnEntry.version ?? existing.version,
119
+ });
120
+ } else if (canonicalExisting) {
121
+ // CDN entry is an alias of a built-in entry — skip to avoid duplicates
122
+ } else {
123
+ byId.set(cdnEntry.id, cdnEntry);
124
+ }
125
+ }
131
126
 
132
127
  return Array.from(byId.values());
133
128
  }
@@ -146,11 +141,12 @@ export async function getAcpAgents(): Promise<AcpRegistryEntry[]> {
146
141
  }
147
142
 
148
143
  /**
149
- * Find a specific ACP agent by ID.
144
+ * Find a specific ACP agent by ID (supports alias resolution).
150
145
  */
151
146
  export async function findAcpAgent(id: string): Promise<AcpRegistryEntry | null> {
152
147
  const agents = await getAcpAgents();
153
- return agents.find(a => a.id === id) ?? null;
148
+ const canonical = resolveAlias(id);
149
+ return agents.find(a => a.id === id || a.id === canonical) ?? null;
154
150
  }
155
151
 
156
152
  /**
@@ -36,6 +36,9 @@ const sessions = new Map<string, AcpSession>();
36
36
  const sessionProcesses = new Map<string, AcpProcess>();
37
37
  const autoApprovalCleanups = new Map<string, () => void>();
38
38
 
39
+ const MAX_SESSIONS_PER_AGENT = 3;
40
+ const MAX_TOTAL_SESSIONS = 10;
41
+
39
42
  /* ── Public API — Session Lifecycle ───────────────────────────────────── */
40
43
 
41
44
  /**
@@ -61,11 +64,12 @@ export async function createSessionFromEntry(
61
64
  entry: AcpRegistryEntry,
62
65
  options?: { env?: Record<string, string>; cwd?: string },
63
66
  ): Promise<AcpSession> {
67
+ checkSessionLimits(entry.id);
68
+
64
69
  const proc = spawnAcpAgent(entry, options);
65
70
 
66
- // Install auto-approval BEFORE initialize so any early permission requests
67
- // from the agent don't cause a hang waiting for TTY input.
68
- const unsubApproval = installAutoApproval(proc);
71
+ const sessionCwd = options?.cwd ?? process.cwd();
72
+ const unsubApproval = installAutoApproval(proc, { cwd: sessionCwd });
69
73
 
70
74
  let agentCapabilities: AcpAgentCapabilities | undefined;
71
75
  let authMethods: AcpAuthMethod[] | undefined;
@@ -122,7 +126,7 @@ export async function createSessionFromEntry(
122
126
 
123
127
  try {
124
128
  const newResponse = await sendAndWait(proc, 'session/new', {
125
- cwd: options?.cwd ?? process.cwd(),
129
+ cwd: sessionCwd,
126
130
  mcpServers: [],
127
131
  }, 15_000);
128
132
 
@@ -188,7 +192,8 @@ export async function loadSession(
188
192
  }
189
193
 
190
194
  const proc = spawnAcpAgent(entry, options);
191
- const unsubApproval = installAutoApproval(proc);
195
+ const loadCwd = options?.cwd ?? process.cwd();
196
+ const unsubApproval = installAutoApproval(proc, { cwd: loadCwd });
192
197
 
193
198
  let agentCapabilities: AcpAgentCapabilities | undefined;
194
199
 
@@ -233,7 +238,7 @@ export async function loadSession(
233
238
  try {
234
239
  const loadResponse = await sendAndWait(proc, 'session/load', {
235
240
  sessionId: existingSessionId,
236
- cwd: options?.cwd ?? process.cwd(),
241
+ cwd: loadCwd,
237
242
  mcpServers: [],
238
243
  }, 15_000);
239
244
 
@@ -399,6 +404,8 @@ export async function promptStream(
399
404
  updateSessionState(session, 'active');
400
405
  const wireSessionId = session.agentSessionId ?? sessionId;
401
406
 
407
+ const PROMPT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
408
+
402
409
  return new Promise((resolve, reject) => {
403
410
  let aggregatedText = '';
404
411
  let stopReason: AcpStopReason = 'end_turn';
@@ -412,6 +419,14 @@ export async function promptStream(
412
419
  fn();
413
420
  };
414
421
 
422
+ // ── 0. Timeout guard ──
423
+ const timeoutTimer = setTimeout(() => {
424
+ settle(() => {
425
+ updateSessionState(session, 'error');
426
+ reject(new Error(`Prompt timed out after ${PROMPT_TIMEOUT_MS / 1000}s — no response from agent`));
427
+ });
428
+ }, PROMPT_TIMEOUT_MS);
429
+
415
430
  // ── 1. Notifications: primary streaming channel ──
416
431
  const unsubNotify = onNotification(proc, (notif) => {
417
432
  if (settled) return;
@@ -502,6 +517,7 @@ export async function promptStream(
502
517
  proc.proc.once('exit', onExit);
503
518
 
504
519
  const cleanup = () => {
520
+ clearTimeout(timeoutTimer);
505
521
  unsubNotify();
506
522
  unsubMsg();
507
523
  proc.proc.removeListener('exit', onExit);
@@ -630,9 +646,10 @@ export function getSession(sessionId: string): AcpSession | undefined {
630
646
  }
631
647
 
632
648
  /**
633
- * Get all active sessions.
649
+ * Get all active sessions. Also reaps stale sessions.
634
650
  */
635
651
  export function getActiveSessions(): AcpSession[] {
652
+ reapStaleSessions();
636
653
  return [...sessions.values()];
637
654
  }
638
655
 
@@ -887,6 +904,18 @@ function parseNotificationToUpdate(
887
904
  return base;
888
905
  }
889
906
 
907
+ /* ── Internal — Session limits ─────────────────────────────────────────── */
908
+
909
+ function checkSessionLimits(agentId: string): void {
910
+ if (sessions.size >= MAX_TOTAL_SESSIONS) {
911
+ throw new Error(`Maximum concurrent sessions (${MAX_TOTAL_SESSIONS}) reached. Close existing sessions first.`);
912
+ }
913
+ const agentCount = [...sessions.values()].filter(s => s.agentId === agentId).length;
914
+ if (agentCount >= MAX_SESSIONS_PER_AGENT) {
915
+ throw new Error(`Maximum concurrent sessions for agent "${agentId}" (${MAX_SESSIONS_PER_AGENT}) reached.`);
916
+ }
917
+ }
918
+
890
919
  /* ── Internal — Session reaping ───────────────────────────────────────── */
891
920
 
892
921
  const STALE_SESSION_MS = 30 * 60 * 1000; // 30 minutes
@@ -4,6 +4,8 @@
4
4
  */
5
5
 
6
6
  import { spawn, type ChildProcess } from 'child_process';
7
+ import path from 'path';
8
+ import os from 'os';
7
9
  import type {
8
10
  AcpJsonRpcRequest,
9
11
  AcpJsonRpcResponse,
@@ -362,7 +364,12 @@ export function sendResponse(
362
364
  * Without approval, the agent hangs waiting for TTY input that never comes.
363
365
  * Returns an unsubscribe function.
364
366
  */
365
- export function installAutoApproval(acpProc: AcpProcess): () => void {
367
+ export function installAutoApproval(
368
+ acpProc: AcpProcess,
369
+ options?: { cwd?: string },
370
+ ): () => void {
371
+ const cwd = options?.cwd;
372
+
366
373
  return onRequest(acpProc, (req) => {
367
374
  const method = req.method;
368
375
  const params = (req.params ?? {}) as Record<string, unknown>;
@@ -375,6 +382,10 @@ export function installAutoApproval(acpProc: AcpProcess): () => void {
375
382
  sendResponse(acpProc, req.id, { error: { code: -32602, message: 'path is required' } });
376
383
  return;
377
384
  }
385
+ if (isSensitivePath(filePath)) {
386
+ sendResponse(acpProc, req.id, { error: { code: -32001, message: `Access denied: ${filePath} is a sensitive file` } });
387
+ return;
388
+ }
378
389
  try {
379
390
  const fs = require('fs');
380
391
  const line = typeof params.line === 'number' ? params.line : undefined;
@@ -401,9 +412,12 @@ export function installAutoApproval(acpProc: AcpProcess): () => void {
401
412
  sendResponse(acpProc, req.id, { error: { code: -32602, message: 'path is required' } });
402
413
  return;
403
414
  }
415
+ if (cwd && !isWithinAllowedWritePaths(filePath, cwd)) {
416
+ sendResponse(acpProc, req.id, { error: { code: -32001, message: `Write denied: ${filePath} is outside the working directory` } });
417
+ return;
418
+ }
404
419
  try {
405
420
  const fs = require('fs');
406
- const path = require('path');
407
421
  const dir = path.dirname(filePath);
408
422
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
409
423
  fs.writeFileSync(filePath, content, 'utf-8');
@@ -545,6 +559,30 @@ export function installAutoApproval(acpProc: AcpProcess): () => void {
545
559
  });
546
560
  }
547
561
 
562
+ /* ── Path safety ───────────────────────────────────────────────────────── */
563
+
564
+ const SENSITIVE_PATH_PATTERNS = [
565
+ /[/\\]\.ssh[/\\](id_|config$|authorized_keys|known_hosts)/i,
566
+ /[/\\]\.env(\.[^/\\]*)?$/i,
567
+ /[/\\]credentials\.json$/i,
568
+ /[/\\]\.aws[/\\]credentials$/i,
569
+ /[/\\]\.gnupg[/\\]/i,
570
+ ];
571
+
572
+ function isSensitivePath(filePath: string): boolean {
573
+ const normalized = path.resolve(filePath);
574
+ return SENSITIVE_PATH_PATTERNS.some(p => p.test(normalized));
575
+ }
576
+
577
+ function isWithinAllowedWritePaths(filePath: string, cwd: string): boolean {
578
+ const normalized = path.resolve(filePath);
579
+ const allowedRoots = [cwd, os.tmpdir()];
580
+ return allowedRoots.some(root => {
581
+ const normalizedRoot = path.resolve(root);
582
+ return normalized === normalizedRoot || normalized.startsWith(normalizedRoot + path.sep);
583
+ });
584
+ }
585
+
548
586
  /* ── Terminal management (per ACP process) ─────────────────────────────── */
549
587
 
550
588
  interface TerminalEntry {