@geminilight/mindos 0.6.29 → 0.6.31
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.
- package/README.md +10 -4
- package/README_zh.md +10 -4
- package/app/app/api/acp/config/route.ts +82 -0
- package/app/app/api/acp/detect/route.ts +71 -48
- package/app/app/api/acp/install/route.ts +51 -0
- package/app/app/api/acp/session/route.ts +141 -11
- package/app/app/api/ask/route.ts +126 -18
- package/app/app/api/export/route.ts +105 -0
- package/app/app/api/workflows/route.ts +156 -0
- package/app/app/globals.css +2 -2
- package/app/app/page.tsx +7 -2
- package/app/app/trash/page.tsx +7 -0
- package/app/app/view/[...path]/ViewPageClient.tsx +234 -2
- package/app/components/ActivityBar.tsx +12 -4
- package/app/components/AskModal.tsx +4 -1
- package/app/components/ExportModal.tsx +220 -0
- package/app/components/FileTree.tsx +42 -11
- package/app/components/HomeContent.tsx +92 -20
- package/app/components/MarkdownView.tsx +45 -10
- package/app/components/Panel.tsx +1 -0
- package/app/components/RightAskPanel.tsx +5 -1
- package/app/components/Sidebar.tsx +10 -1
- package/app/components/SidebarLayout.tsx +6 -0
- package/app/components/TrashPageClient.tsx +263 -0
- package/app/components/agents/AgentDetailContent.tsx +263 -47
- package/app/components/agents/AgentsContentPage.tsx +11 -0
- package/app/components/agents/AgentsPanelA2aTab.tsx +285 -46
- package/app/components/agents/AgentsPanelSessionsTab.tsx +166 -0
- package/app/components/agents/agents-content-model.ts +2 -2
- package/app/components/ask/AgentSelectorCapsule.tsx +218 -0
- package/app/components/ask/AskContent.tsx +197 -239
- package/app/components/ask/FileChip.tsx +82 -17
- package/app/components/ask/MentionPopover.tsx +21 -3
- package/app/components/ask/MessageList.tsx +30 -9
- package/app/components/ask/SlashCommandPopover.tsx +21 -3
- package/app/components/ask/ToolCallBlock.tsx +102 -18
- package/app/components/changes/ChangesContentPage.tsx +58 -14
- package/app/components/explore/ExploreContent.tsx +4 -7
- package/app/components/explore/UseCaseCard.tsx +18 -1
- package/app/components/explore/use-cases.generated.ts +76 -0
- package/app/components/explore/use-cases.yaml +185 -0
- package/app/components/panels/AgentsPanel.tsx +1 -0
- package/app/components/panels/AgentsPanelHubNav.tsx +9 -2
- package/app/components/panels/DiscoverPanel.tsx +1 -1
- package/app/components/panels/WorkflowsPanel.tsx +206 -0
- package/app/components/renderers/workflow-yaml/StepEditor.tsx +164 -0
- package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +211 -0
- package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +269 -0
- package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +126 -0
- package/app/components/renderers/workflow-yaml/execution.ts +229 -0
- package/app/components/renderers/workflow-yaml/index.ts +6 -0
- package/app/components/renderers/workflow-yaml/manifest.ts +21 -0
- package/app/components/renderers/workflow-yaml/parser.ts +172 -0
- package/app/components/renderers/workflow-yaml/selectors.tsx +574 -0
- package/app/components/renderers/workflow-yaml/serializer.ts +56 -0
- package/app/components/renderers/workflow-yaml/types.ts +46 -0
- package/app/components/settings/AiTab.tsx +191 -174
- package/app/components/settings/AppearanceTab.tsx +168 -77
- package/app/components/settings/KnowledgeTab.tsx +131 -136
- package/app/components/settings/McpTab.tsx +11 -11
- package/app/components/settings/Primitives.tsx +60 -0
- package/app/components/settings/SettingsContent.tsx +15 -8
- package/app/components/settings/SyncTab.tsx +12 -12
- package/app/components/settings/UninstallTab.tsx +8 -18
- package/app/components/settings/UpdateTab.tsx +82 -82
- package/app/components/settings/types.ts +17 -8
- package/app/hooks/useAcpConfig.ts +96 -0
- package/app/hooks/useAcpDetection.ts +69 -14
- package/app/hooks/useAcpRegistry.ts +46 -11
- package/app/hooks/useAskModal.ts +12 -5
- package/app/hooks/useAskPanel.ts +8 -5
- package/app/hooks/useAskSession.ts +19 -2
- package/app/hooks/useImageUpload.ts +152 -0
- package/app/lib/acp/acp-tools.ts +3 -1
- package/app/lib/acp/agent-descriptors.ts +274 -0
- package/app/lib/acp/bridge.ts +6 -0
- package/app/lib/acp/index.ts +20 -4
- package/app/lib/acp/registry.ts +74 -7
- package/app/lib/acp/session.ts +490 -28
- package/app/lib/acp/subprocess.ts +307 -21
- package/app/lib/acp/types.ts +158 -20
- package/app/lib/actions.ts +57 -3
- package/app/lib/agent/model.ts +18 -3
- package/app/lib/agent/stream-consumer.ts +18 -0
- package/app/lib/agent/to-agent-messages.ts +25 -2
- package/app/lib/agent/tools.ts +56 -9
- package/app/lib/core/export.ts +116 -0
- package/app/lib/core/trash.ts +241 -0
- package/app/lib/fs.ts +47 -0
- package/app/lib/hooks/usePinnedFiles.ts +90 -0
- package/app/lib/i18n/generated/explore-i18n.generated.ts +138 -0
- package/app/lib/i18n/index.ts +3 -0
- package/app/lib/i18n/modules/knowledge.ts +124 -6
- package/app/lib/i18n/modules/navigation.ts +2 -0
- package/app/lib/i18n/modules/onboarding.ts +2 -134
- package/app/lib/i18n/modules/panels.ts +146 -2
- package/app/lib/i18n/modules/settings.ts +12 -0
- package/app/lib/pi-integration/skills.ts +21 -6
- package/app/lib/renderers/index.ts +2 -2
- package/app/lib/settings.ts +10 -0
- package/app/lib/types.ts +12 -1
- package/app/next-env.d.ts +1 -1
- package/app/package.json +11 -3
- package/app/scripts/generate-explore.ts +145 -0
- package/package.json +1 -1
- package/templates/en/.mindos/workflows/Sprint Release.flow.yaml +130 -0
- package/templates/zh/.mindos/workflows//345/221/250/350/277/255/344/273/243/346/243/200/346/237/245.flow.yaml +84 -0
- package/app/components/explore/use-cases.ts +0 -58
- package/app/components/renderers/workflow/WorkflowRenderer.tsx +0 -409
- package/app/components/renderers/workflow/manifest.ts +0 -14
package/README.md
CHANGED
|
@@ -162,11 +162,15 @@ npx skills add https://github.com/GeminiLight/MindOS --skill mindos-zh -g -y #
|
|
|
162
162
|
|
|
163
163
|
- **GUI Workbench**: browse, edit, search notes with unified search + AI entry (`⌘K` / `⌘/`), designed for human-AI co-creation.
|
|
164
164
|
- **Built-in Agent Assistant**: converse with the knowledge base in context; edits seamlessly capture human-curated knowledge.
|
|
165
|
-
- **
|
|
165
|
+
- **One-Click Import**: drag-and-drop files with Inline AI Organize — auto-analyzes, categorizes, and writes into the knowledge base with progress tracking and undo support.
|
|
166
|
+
- **Guided Onboarding**: step-by-step first-run experience that helps new users set up their knowledge base and connect their first Agent.
|
|
167
|
+
- **Plugin Extensions**: multiple built-in renderer plugins — TODO Board, CSV Views, Wiki Graph, Timeline, Workflow Editor, Agent Inspector, and more.
|
|
166
168
|
|
|
167
169
|
**For Agents**
|
|
168
170
|
|
|
169
|
-
- **MCP Server + Skills**: stdio + HTTP dual transport, full-lineup Agent compatible (
|
|
171
|
+
- **MCP Server + Skills**: stdio + HTTP dual transport, full-lineup Agent compatible (Claude Code, Cursor, Gemini CLI, etc.). Zero-config access.
|
|
172
|
+
- **ACP / A2A Protocols**: Agent Communication Protocol for inter-agent discovery, delegation, and orchestration. Phase 1 live with Agent Card discovery + JSON-RPC messaging.
|
|
173
|
+
- **Workflow Orchestration**: YAML-based workflow editor with step execution engine — define, edit, and run multi-step agent workflows visually.
|
|
170
174
|
- **Structured Templates**: pre-set directory structures for Profiles, Workflows, Configurations, etc., to jumpstart personal context.
|
|
171
175
|
- **Agent-Ready Docs**: everyday notes naturally double as high-quality executable Agent commands — no format conversion needed, write and dispatch.
|
|
172
176
|
|
|
@@ -175,15 +179,17 @@ npx skills add https://github.com/GeminiLight/MindOS --skill mindos-zh -g -y #
|
|
|
175
179
|
- **Security**: Bearer Token auth, path sandboxing, INSTRUCTION.md write-protection, atomic writes.
|
|
176
180
|
- **Knowledge Graph**: dynamically parses and visualizes inter-file references and dependencies.
|
|
177
181
|
- **Backlinks View**: displays all files that reference the current file, helping you understand how a note fits into the knowledge network.
|
|
182
|
+
- **Agent Inspector**: renders Agent operation logs as a filterable timeline to audit every tool call in detail.
|
|
178
183
|
- **Git Time Machine**: Git auto-sync (commit/push/pull), records every edit by both humans and Agents. One-click rollback, cross-device sync.
|
|
179
184
|
- **Desktop App**: native macOS/Windows/Linux app with system tray, auto-start, and local process management.
|
|
180
185
|
|
|
181
186
|
<details>
|
|
182
187
|
<summary><strong>Coming Soon</strong></summary>
|
|
183
188
|
|
|
184
|
-
- [ ] ACP (Agent Communication Protocol): connect external Agents (e.g., Claude Code, Cursor) and turn the knowledge base into a multi-Agent collaboration hub
|
|
185
189
|
- [ ] Deep RAG integration: retrieval-augmented generation grounded in your knowledge base for more accurate, context-aware AI responses
|
|
186
|
-
- [ ]
|
|
190
|
+
- [ ] ACP / A2A Phase 2: deep multi-agent collaboration with task delegation, shared context, and workflow chaining
|
|
191
|
+
- [ ] Experience Compiler: auto-extract corrections and preferences from agent interactions into reusable Skills/SOPs
|
|
192
|
+
- [ ] Knowledge Health Dashboard: visualize cognitive compound metrics — rules accumulated, agent reuse count, knowledge freshness
|
|
187
193
|
|
|
188
194
|
</details>
|
|
189
195
|
|
package/README_zh.md
CHANGED
|
@@ -162,11 +162,15 @@ npx skills add https://github.com/GeminiLight/MindOS --skill mindos-zh -g -y #
|
|
|
162
162
|
|
|
163
163
|
- **GUI 工作台**:浏览、编辑、搜索笔记,统一搜索 + AI 入口(`⌘K` / `⌘/`),专为人机共创设计。
|
|
164
164
|
- **内置 Agent 助手**:在上下文中与知识库对话,编辑无缝沉淀为可管理知识。
|
|
165
|
-
-
|
|
165
|
+
- **一键导入**:拖拽文件即可导入,Inline AI Organize 自动分析、分类、写入知识库,支持进度追踪和撤销。
|
|
166
|
+
- **新手引导**:首次使用的分步引导体验,帮助新用户快速搭建知识库并连接第一个 Agent。
|
|
167
|
+
- **插件扩展**:多种内置渲染器插件——TODO Board、CSV Views、Wiki Graph、Timeline、Workflow Editor、Agent Inspector 等。
|
|
166
168
|
|
|
167
169
|
**Agent 侧**
|
|
168
170
|
|
|
169
|
-
- **MCP Server + Skills**:stdio + HTTP 双传输,全阵容 Agent 兼容(
|
|
171
|
+
- **MCP Server + Skills**:stdio + HTTP 双传输,全阵容 Agent 兼容(Claude Code, Cursor, Gemini CLI 等),零配置接入。
|
|
172
|
+
- **ACP / A2A 协议**:Agent 间通信协议,支持 Agent 发现、任务委派与编排。Phase 1 已上线:Agent Card 发现 + JSON-RPC 消息通信。
|
|
173
|
+
- **Workflow 编排**:基于 YAML 的可视化工作流编辑器 + 步骤执行引擎,定义、编辑、运行多步 Agent 工作流。
|
|
170
174
|
- **结构化模板**:预置 Profile、Workflows、Configurations 等目录骨架,快速冷启动个人 Context。
|
|
171
175
|
- **笔记即指令**:日常笔记天然就是 Agent 可直接执行的高质量指令——无需额外格式转换,写下即可调度。
|
|
172
176
|
|
|
@@ -175,15 +179,17 @@ npx skills add https://github.com/GeminiLight/MindOS --skill mindos-zh -g -y #
|
|
|
175
179
|
- **安全防线**:Bearer Token 认证、路径沙箱、INSTRUCTION.md 写保护、原子写入。
|
|
176
180
|
- **知识图谱**:动态解析并可视化文件间的引用与依赖关系。
|
|
177
181
|
- **反向链接视图**:展示所有引用当前文件的反向链接,理解笔记在知识网络中的位置。
|
|
182
|
+
- **Agent 审计面板**:将 Agent 操作日志渲染为可筛选的时间线,审查每次工具调用的详情。
|
|
178
183
|
- **Git 时光机**:Git 自动同步(commit/push/pull),记录人类与 Agent 的每次编辑历史,一键回滚,跨设备同步。
|
|
179
184
|
- **桌面客户端**:原生 macOS/Windows/Linux 应用,系统托盘、开机自启、本地进程管理。
|
|
180
185
|
|
|
181
186
|
<details>
|
|
182
187
|
<summary><strong>即将到来</strong></summary>
|
|
183
188
|
|
|
184
|
-
- [ ] ACP(Agent Communication Protocol):连接外部 Agent(如 Claude Code、Cursor),让知识库成为多 Agent 协作的中枢
|
|
185
189
|
- [ ] RAG 深度集成:基于知识库内容的检索增强生成,让 AI 回答更精准、更有上下文
|
|
186
|
-
- [ ]
|
|
190
|
+
- [ ] ACP / A2A Phase 2:深度多 Agent 协作,支持任务委派、共享上下文、工作流链式执行
|
|
191
|
+
- [ ] 经验编译器:从 Agent 交互中自动提取纠正和偏好,沉淀为可复用的 Skills/SOP
|
|
192
|
+
- [ ] 知识库健康度仪表盘:可视化认知复利指标——已积累规则数、Agent 复用次数、知识新鲜度
|
|
187
193
|
|
|
188
194
|
</details>
|
|
189
195
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
|
|
3
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
4
|
+
import { readSettings, writeSettings } from '@/lib/settings';
|
|
5
|
+
import type { AcpAgentOverride } from '@/lib/acp/agent-descriptors';
|
|
6
|
+
|
|
7
|
+
/** GET /api/acp/config — Returns all per-agent ACP overrides from settings. */
|
|
8
|
+
export async function GET() {
|
|
9
|
+
const settings = readSettings();
|
|
10
|
+
return NextResponse.json({ agents: settings.acpAgents ?? {} });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** POST /api/acp/config — Save a per-agent override. Body: { agentId, config } */
|
|
14
|
+
export async function POST(req: NextRequest) {
|
|
15
|
+
let body: { agentId?: string; config?: AcpAgentOverride };
|
|
16
|
+
try {
|
|
17
|
+
body = await req.json();
|
|
18
|
+
} catch {
|
|
19
|
+
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { agentId, config } = body;
|
|
23
|
+
if (!agentId || typeof agentId !== 'string') {
|
|
24
|
+
return NextResponse.json({ error: 'agentId is required' }, { status: 400 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const settings = readSettings();
|
|
28
|
+
const existing = settings.acpAgents ?? {};
|
|
29
|
+
|
|
30
|
+
if (config && typeof config === 'object') {
|
|
31
|
+
// Sanitize the override
|
|
32
|
+
const sanitized: AcpAgentOverride = {};
|
|
33
|
+
if (typeof config.command === 'string' && config.command.trim()) {
|
|
34
|
+
sanitized.command = config.command.trim();
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(config.args)) {
|
|
37
|
+
sanitized.args = config.args.filter((a): a is string => typeof a === 'string');
|
|
38
|
+
}
|
|
39
|
+
if (config.env && typeof config.env === 'object' && !Array.isArray(config.env)) {
|
|
40
|
+
const env: Record<string, string> = {};
|
|
41
|
+
for (const [k, v] of Object.entries(config.env)) {
|
|
42
|
+
if (typeof v === 'string') env[k] = v;
|
|
43
|
+
}
|
|
44
|
+
if (Object.keys(env).length > 0) sanitized.env = env;
|
|
45
|
+
}
|
|
46
|
+
if (typeof config.enabled === 'boolean') {
|
|
47
|
+
sanitized.enabled = config.enabled;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
existing[agentId] = sanitized;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
settings.acpAgents = existing;
|
|
54
|
+
writeSettings(settings);
|
|
55
|
+
return NextResponse.json({ ok: true, agents: settings.acpAgents });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** DELETE /api/acp/config — Reset a single agent to defaults. Body: { agentId } */
|
|
59
|
+
export async function DELETE(req: NextRequest) {
|
|
60
|
+
let body: { agentId?: string };
|
|
61
|
+
try {
|
|
62
|
+
body = await req.json();
|
|
63
|
+
} catch {
|
|
64
|
+
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { agentId } = body;
|
|
68
|
+
if (!agentId || typeof agentId !== 'string') {
|
|
69
|
+
return NextResponse.json({ error: 'agentId is required' }, { status: 400 });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const settings = readSettings();
|
|
73
|
+
if (settings.acpAgents) {
|
|
74
|
+
delete settings.acpAgents[agentId];
|
|
75
|
+
if (Object.keys(settings.acpAgents).length === 0) {
|
|
76
|
+
settings.acpAgents = undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
writeSettings(settings);
|
|
81
|
+
return NextResponse.json({ ok: true, agents: settings.acpAgents ?? {} });
|
|
82
|
+
}
|
|
@@ -1,87 +1,110 @@
|
|
|
1
1
|
export const dynamic = 'force-dynamic';
|
|
2
2
|
|
|
3
3
|
import { NextResponse } from 'next/server';
|
|
4
|
-
import {
|
|
4
|
+
import { exec } from 'child_process';
|
|
5
5
|
import { getAcpAgents } from '@/lib/acp/registry';
|
|
6
|
+
import { getDescriptorBinary, getDescriptorInstallCmd, resolveAgentCommand } from '@/lib/acp/agent-descriptors';
|
|
7
|
+
import { readSettings } from '@/lib/settings';
|
|
6
8
|
|
|
7
|
-
/* ──
|
|
8
|
-
|
|
9
|
-
const AGENT_BINARY_MAP: Record<string, string> = {
|
|
10
|
-
claude: 'claude',
|
|
11
|
-
'claude-code': 'claude',
|
|
12
|
-
gemini: 'gemini',
|
|
13
|
-
'gemini-cli': 'gemini',
|
|
14
|
-
codex: 'codex',
|
|
15
|
-
cursor: 'cursor',
|
|
16
|
-
cline: 'cline',
|
|
17
|
-
goose: 'goose',
|
|
18
|
-
opencode: 'opencode',
|
|
19
|
-
kilo: 'kilo',
|
|
20
|
-
codebuddy: 'codebuddy',
|
|
21
|
-
openclaw: 'openclaw',
|
|
22
|
-
pi: 'pi',
|
|
23
|
-
augment: 'auggie',
|
|
24
|
-
iflow: 'iflow',
|
|
25
|
-
kimi: 'kimi',
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const INSTALL_COMMANDS: Record<string, string> = {
|
|
29
|
-
claude: 'npm install -g @anthropic-ai/claude-code',
|
|
30
|
-
'claude-code': 'npm install -g @anthropic-ai/claude-code',
|
|
31
|
-
gemini: 'npm install -g @anthropic-ai/claude-code && npx @anthropic-ai/claude-code',
|
|
32
|
-
'gemini-cli': 'npm install -g @anthropic-ai/claude-code',
|
|
33
|
-
codex: 'npm install -g @openai/codex',
|
|
34
|
-
goose: 'pip install goose-ai',
|
|
35
|
-
opencode: 'go install github.com/opencode-ai/opencode@latest',
|
|
36
|
-
codebuddy: 'npm install -g @anthropic-ai/claude-code',
|
|
37
|
-
};
|
|
9
|
+
/* ── Types ─────────────────────────────────────────────────────────────── */
|
|
38
10
|
|
|
39
11
|
interface InstalledAgent {
|
|
40
12
|
id: string;
|
|
41
13
|
name: string;
|
|
42
14
|
binaryPath: string;
|
|
15
|
+
resolvedCommand: {
|
|
16
|
+
cmd: string;
|
|
17
|
+
args: string[];
|
|
18
|
+
source: 'user-override' | 'descriptor' | 'registry';
|
|
19
|
+
};
|
|
43
20
|
}
|
|
44
21
|
|
|
45
22
|
interface NotInstalledAgent {
|
|
46
23
|
id: string;
|
|
47
24
|
name: string;
|
|
48
25
|
installCmd: string;
|
|
26
|
+
packageName?: string;
|
|
49
27
|
}
|
|
50
28
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
29
|
+
/* ── Server-side detection cache (5 min TTL) ──────────────────────────── */
|
|
30
|
+
|
|
31
|
+
const DETECT_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
32
|
+
let detectCache: { data: { installed: InstalledAgent[]; notInstalled: NotInstalledAgent[] }; ts: number } | null = null;
|
|
33
|
+
|
|
34
|
+
/* ── Async detection helpers ──────────────────────────────────────────── */
|
|
35
|
+
|
|
36
|
+
function whichAsync(binary: string): Promise<string | null> {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
const child = exec(`which ${binary}`, { encoding: 'utf-8', timeout: 2000 }, (err, stdout) => {
|
|
39
|
+
resolve(err ? null : (stdout.trim() || null));
|
|
40
|
+
});
|
|
41
|
+
child.on('error', () => resolve(null));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Batch `which` for multiple unique binaries in a single call.
|
|
47
|
+
* Returns a Map<binary, path | null>.
|
|
48
|
+
*/
|
|
49
|
+
async function whichBatch(binaries: string[]): Promise<Map<string, string | null>> {
|
|
50
|
+
const unique = [...new Set(binaries)];
|
|
51
|
+
if (unique.length === 0) return new Map();
|
|
52
|
+
|
|
53
|
+
const results = await Promise.all(unique.map(async (bin) => {
|
|
54
|
+
const path = await whichAsync(bin);
|
|
55
|
+
return [bin, path] as const;
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
return new Map(results);
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
/* ── Route handler ─────────────────────────────────────────────────────── */
|
|
62
|
+
|
|
63
|
+
export async function GET(req: Request) {
|
|
60
64
|
try {
|
|
65
|
+
const force = new URL(req.url).searchParams.get('force') === '1';
|
|
66
|
+
if (!force && detectCache && Date.now() - detectCache.ts < DETECT_CACHE_TTL_MS) {
|
|
67
|
+
return NextResponse.json(detectCache.data);
|
|
68
|
+
}
|
|
69
|
+
|
|
61
70
|
const agents = await getAcpAgents();
|
|
71
|
+
const settings = readSettings();
|
|
72
|
+
|
|
73
|
+
const binaryNames = agents.map((a) => getDescriptorBinary(a.id)).filter(Boolean) as string[];
|
|
74
|
+
const whichMap = await whichBatch(binaryNames);
|
|
75
|
+
|
|
62
76
|
const installed: InstalledAgent[] = [];
|
|
63
77
|
const notInstalled: NotInstalledAgent[] = [];
|
|
64
78
|
|
|
65
79
|
for (const agent of agents) {
|
|
66
|
-
const binary =
|
|
67
|
-
|
|
68
|
-
notInstalled.push({ id: agent.id, name: agent.name, installCmd: '' });
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
80
|
+
const binary = getDescriptorBinary(agent.id);
|
|
81
|
+
const binaryPath = binary ? (whichMap.get(binary) ?? null) : null;
|
|
71
82
|
|
|
72
|
-
const binaryPath = whichBinary(binary);
|
|
73
83
|
if (binaryPath) {
|
|
74
|
-
|
|
84
|
+
const userOverride = settings.acpAgents?.[agent.id];
|
|
85
|
+
const resolved = resolveAgentCommand(agent.id, agent, userOverride);
|
|
86
|
+
installed.push({
|
|
87
|
+
id: agent.id,
|
|
88
|
+
name: agent.name,
|
|
89
|
+
binaryPath,
|
|
90
|
+
resolvedCommand: { cmd: resolved.cmd, args: resolved.args, source: resolved.source },
|
|
91
|
+
});
|
|
75
92
|
} else {
|
|
93
|
+
const installCmd =
|
|
94
|
+
getDescriptorInstallCmd(agent.id) ??
|
|
95
|
+
(agent.packageName ? `npm install -g ${agent.packageName}` : '');
|
|
76
96
|
notInstalled.push({
|
|
77
97
|
id: agent.id,
|
|
78
98
|
name: agent.name,
|
|
79
|
-
installCmd
|
|
99
|
+
installCmd,
|
|
100
|
+
packageName: agent.packageName,
|
|
80
101
|
});
|
|
81
102
|
}
|
|
82
103
|
}
|
|
83
104
|
|
|
84
|
-
|
|
105
|
+
const data = { installed, notInstalled };
|
|
106
|
+
detectCache = { data, ts: Date.now() };
|
|
107
|
+
return NextResponse.json(data);
|
|
85
108
|
} catch (err) {
|
|
86
109
|
return NextResponse.json(
|
|
87
110
|
{ error: (err as Error).message, installed: [], notInstalled: [] },
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
|
|
3
|
+
import { NextResponse } from 'next/server';
|
|
4
|
+
import { exec } from 'child_process';
|
|
5
|
+
|
|
6
|
+
interface InstallRequest {
|
|
7
|
+
agentId: string;
|
|
8
|
+
packageName: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function POST(request: Request) {
|
|
12
|
+
try {
|
|
13
|
+
const body = (await request.json()) as InstallRequest;
|
|
14
|
+
|
|
15
|
+
if (!body.agentId || !body.packageName) {
|
|
16
|
+
return NextResponse.json(
|
|
17
|
+
{ error: 'agentId and packageName are required' },
|
|
18
|
+
{ status: 400 },
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Validate packageName to prevent command injection
|
|
23
|
+
// Allow scoped packages like @scope/name and plain names
|
|
24
|
+
if (!/^(@[\w-]+\/)?[\w][\w./-]*$/.test(body.packageName)) {
|
|
25
|
+
return NextResponse.json(
|
|
26
|
+
{ error: 'Invalid package name' },
|
|
27
|
+
{ status: 400 },
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Run npm install -g in background (fire-and-forget for the HTTP response)
|
|
32
|
+
const child = exec(
|
|
33
|
+
`npm install -g ${body.packageName}`,
|
|
34
|
+
{ timeout: 120_000 },
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Detach so the process continues after response
|
|
38
|
+
child.unref();
|
|
39
|
+
|
|
40
|
+
return NextResponse.json({
|
|
41
|
+
status: 'installing',
|
|
42
|
+
agentId: body.agentId,
|
|
43
|
+
packageName: body.packageName,
|
|
44
|
+
});
|
|
45
|
+
} catch (err) {
|
|
46
|
+
return NextResponse.json(
|
|
47
|
+
{ error: (err as Error).message },
|
|
48
|
+
{ status: 500 },
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
export const dynamic = 'force-dynamic';
|
|
2
2
|
|
|
3
3
|
import { NextResponse } from 'next/server';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createSession,
|
|
6
|
+
loadSession,
|
|
7
|
+
listSessions,
|
|
8
|
+
closeSession,
|
|
9
|
+
prompt,
|
|
10
|
+
cancelPrompt,
|
|
11
|
+
setMode,
|
|
12
|
+
setConfigOption,
|
|
13
|
+
getSession,
|
|
14
|
+
getActiveSessions,
|
|
15
|
+
} from '@/lib/acp/session';
|
|
5
16
|
|
|
6
17
|
export async function GET() {
|
|
7
18
|
try {
|
|
@@ -15,20 +26,139 @@ export async function GET() {
|
|
|
15
26
|
}
|
|
16
27
|
}
|
|
17
28
|
|
|
29
|
+
/**
|
|
30
|
+
* POST /api/acp/session
|
|
31
|
+
* Action-based dispatch for all ACP session operations.
|
|
32
|
+
*
|
|
33
|
+
* Actions:
|
|
34
|
+
* create — { action: "create", agentId, env?, cwd?, prompt? }
|
|
35
|
+
* load — { action: "load", agentId, sessionId, env?, cwd? }
|
|
36
|
+
* prompt — { action: "prompt", sessionId, text }
|
|
37
|
+
* cancel — { action: "cancel", sessionId }
|
|
38
|
+
* set_mode — { action: "set_mode", sessionId, modeId }
|
|
39
|
+
* set_config — { action: "set_config", sessionId, configId, value }
|
|
40
|
+
* list_sessions — { action: "list_sessions", sessionId, cursor?, cwd? }
|
|
41
|
+
*
|
|
42
|
+
* Legacy (no action field): treated as "create" for backwards compat.
|
|
43
|
+
*/
|
|
18
44
|
export async function POST(req: Request) {
|
|
19
45
|
try {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
46
|
+
const body = await req.json();
|
|
47
|
+
const action = body.action ?? 'create';
|
|
48
|
+
|
|
49
|
+
switch (action) {
|
|
50
|
+
case 'create': {
|
|
51
|
+
const { agentId, env, cwd, prompt: promptText } = body as {
|
|
52
|
+
agentId?: string;
|
|
53
|
+
env?: Record<string, string>;
|
|
54
|
+
cwd?: string;
|
|
55
|
+
prompt?: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (!agentId || typeof agentId !== 'string') {
|
|
59
|
+
return NextResponse.json({ error: 'agentId is required' }, { status: 400 });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const session = await createSession(agentId, { env, cwd });
|
|
63
|
+
|
|
64
|
+
// One-shot mode: send prompt, get response, close session
|
|
65
|
+
if (promptText && typeof promptText === 'string') {
|
|
66
|
+
try {
|
|
67
|
+
const response = await prompt(session.id, promptText);
|
|
68
|
+
await closeSession(session.id).catch(() => {});
|
|
69
|
+
return NextResponse.json({ session, response });
|
|
70
|
+
} catch (promptErr) {
|
|
71
|
+
await closeSession(session.id).catch(() => {});
|
|
72
|
+
return NextResponse.json(
|
|
73
|
+
{ error: `Prompt failed: ${(promptErr as Error).message}`, session },
|
|
74
|
+
{ status: 500 },
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return NextResponse.json({ session });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
case 'load': {
|
|
83
|
+
const { agentId, sessionId, env, cwd } = body as {
|
|
84
|
+
agentId?: string;
|
|
85
|
+
sessionId?: string;
|
|
86
|
+
env?: Record<string, string>;
|
|
87
|
+
cwd?: string;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (!agentId || typeof agentId !== 'string') {
|
|
91
|
+
return NextResponse.json({ error: 'agentId is required' }, { status: 400 });
|
|
92
|
+
}
|
|
93
|
+
if (!sessionId || typeof sessionId !== 'string') {
|
|
94
|
+
return NextResponse.json({ error: 'sessionId is required' }, { status: 400 });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const session = await loadSession(agentId, sessionId, { env, cwd });
|
|
98
|
+
return NextResponse.json({ session });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
case 'prompt': {
|
|
102
|
+
const { sessionId, text } = body as { sessionId?: string; text?: string };
|
|
103
|
+
|
|
104
|
+
if (!sessionId || typeof sessionId !== 'string') {
|
|
105
|
+
return NextResponse.json({ error: 'sessionId is required' }, { status: 400 });
|
|
106
|
+
}
|
|
107
|
+
if (!text || typeof text !== 'string') {
|
|
108
|
+
return NextResponse.json({ error: 'text is required' }, { status: 400 });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const response = await prompt(sessionId, text);
|
|
112
|
+
return NextResponse.json({ response });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
case 'cancel': {
|
|
116
|
+
const { sessionId } = body as { sessionId?: string };
|
|
117
|
+
if (!sessionId || typeof sessionId !== 'string') {
|
|
118
|
+
return NextResponse.json({ error: 'sessionId is required' }, { status: 400 });
|
|
119
|
+
}
|
|
24
120
|
|
|
25
|
-
|
|
26
|
-
|
|
121
|
+
await cancelPrompt(sessionId);
|
|
122
|
+
return NextResponse.json({ ok: true });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
case 'set_mode': {
|
|
126
|
+
const { sessionId, modeId } = body as { sessionId?: string; modeId?: string };
|
|
127
|
+
if (!sessionId || !modeId) {
|
|
128
|
+
return NextResponse.json({ error: 'sessionId and modeId are required' }, { status: 400 });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await setMode(sessionId, modeId);
|
|
132
|
+
return NextResponse.json({ ok: true });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
case 'set_config': {
|
|
136
|
+
const { sessionId, configId, value } = body as { sessionId?: string; configId?: string; value?: string };
|
|
137
|
+
if (!sessionId || !configId || value === undefined) {
|
|
138
|
+
return NextResponse.json({ error: 'sessionId, configId, and value are required' }, { status: 400 });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const configOptions = await setConfigOption(sessionId, configId, String(value));
|
|
142
|
+
return NextResponse.json({ configOptions });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
case 'list_sessions': {
|
|
146
|
+
const { sessionId, cursor, cwd } = body as { sessionId?: string; cursor?: string; cwd?: string };
|
|
147
|
+
if (!sessionId || typeof sessionId !== 'string') {
|
|
148
|
+
return NextResponse.json({ error: 'sessionId is required' }, { status: 400 });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const result = await listSessions(sessionId, { cursor, cwd });
|
|
152
|
+
return NextResponse.json(result);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
default:
|
|
156
|
+
return NextResponse.json({ error: `Unknown action: ${action}` }, { status: 400 });
|
|
157
|
+
}
|
|
27
158
|
} catch (err) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
);
|
|
159
|
+
const msg = (err as Error).message;
|
|
160
|
+
const status = msg.includes('not found') ? 404 : msg.includes('not support') ? 501 : 500;
|
|
161
|
+
return NextResponse.json({ error: msg }, { status });
|
|
32
162
|
}
|
|
33
163
|
}
|
|
34
164
|
|