c0de-agent 0.0.0 → 1.0.0
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/LICENSE +21 -0
- package/README.md +101 -0
- package/dist/cli/commands/acp.d.ts +19 -0
- package/dist/cli/commands/acp.js +50 -0
- package/dist/cli/commands/chat.d.ts +13 -0
- package/dist/cli/commands/chat.js +26 -0
- package/dist/cli/commands/config.d.ts +17 -0
- package/dist/cli/commands/config.js +77 -0
- package/dist/cli/commands/init.d.ts +9 -0
- package/dist/cli/commands/init.js +14 -0
- package/dist/cli/commands/plugin.d.ts +11 -0
- package/dist/cli/commands/plugin.js +31 -0
- package/dist/cli/commands/serve.d.ts +18 -0
- package/dist/cli/commands/serve.js +35 -0
- package/dist/cli/commands/update.d.ts +24 -0
- package/dist/cli/commands/update.js +40 -0
- package/dist/cli/deps.d.ts +19 -0
- package/dist/cli/deps.js +57 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.js +149 -0
- package/dist/cli/modes/acp.d.ts +18 -0
- package/dist/cli/modes/acp.js +57 -0
- package/dist/cli/modes/print.d.ts +16 -0
- package/dist/cli/modes/print.js +30 -0
- package/dist/cli/parser.d.ts +19 -0
- package/dist/cli/parser.js +28 -0
- package/dist/cli/utils/output.d.ts +11 -0
- package/dist/cli/utils/output.js +23 -0
- package/dist/cli/utils/prompt.d.ts +12 -0
- package/dist/cli/utils/prompt.js +18 -0
- package/dist/core/agent.d.ts +12 -0
- package/dist/core/agent.js +95 -0
- package/dist/core/agents/builtin.d.ts +6 -0
- package/dist/core/agents/builtin.js +76 -0
- package/dist/core/agents/discovery.d.ts +6 -0
- package/dist/core/agents/discovery.js +96 -0
- package/dist/core/agents/index.d.ts +6 -0
- package/dist/core/agents/index.js +4 -0
- package/dist/core/agents/parallel.d.ts +13 -0
- package/dist/core/agents/parallel.js +37 -0
- package/dist/core/agents/registry.d.ts +4 -0
- package/dist/core/agents/registry.js +23 -0
- package/dist/core/agents/types.d.ts +37 -0
- package/dist/core/agents/types.js +1 -0
- package/dist/core/compact.d.ts +11 -0
- package/dist/core/compact.js +31 -0
- package/dist/core/config.d.ts +7 -0
- package/dist/core/config.js +84 -0
- package/dist/core/context.d.ts +23 -0
- package/dist/core/context.js +93 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +11 -0
- package/dist/core/loop.d.ts +25 -0
- package/dist/core/loop.js +651 -0
- package/dist/core/metrics.d.ts +27 -0
- package/dist/core/metrics.js +94 -0
- package/dist/core/prompt-registry.d.ts +21 -0
- package/dist/core/prompt-registry.js +200 -0
- package/dist/core/prompt.d.ts +11 -0
- package/dist/core/prompt.js +13 -0
- package/dist/core/secret.d.ts +12 -0
- package/dist/core/secret.js +51 -0
- package/dist/core/slash.d.ts +15 -0
- package/dist/core/slash.js +114 -0
- package/dist/core/steering.d.ts +8 -0
- package/dist/core/steering.js +15 -0
- package/dist/core/title.d.ts +42 -0
- package/dist/core/title.js +115 -0
- package/dist/core/tool-exec.d.ts +20 -0
- package/dist/core/tool-exec.js +64 -0
- package/dist/core/types.d.ts +95 -0
- package/dist/core/types.js +1 -0
- package/dist/core/worktree.d.ts +24 -0
- package/dist/core/worktree.js +68 -0
- package/dist/dap/index.d.ts +6 -0
- package/dist/dap/index.js +4 -0
- package/dist/dap/protocol.d.ts +40 -0
- package/dist/dap/protocol.js +132 -0
- package/dist/dap/session.d.ts +23 -0
- package/dist/dap/session.js +130 -0
- package/dist/dap/transport.d.ts +8 -0
- package/dist/dap/transport.js +30 -0
- package/dist/dap/types.d.ts +50 -0
- package/dist/dap/types.js +2 -0
- package/dist/db/client.d.ts +22 -0
- package/dist/db/client.js +39 -0
- package/dist/db/index.d.ts +6 -0
- package/dist/db/index.js +4 -0
- package/dist/db/migrate.d.ts +9 -0
- package/dist/db/migrate.js +15 -0
- package/dist/db/schema.d.ts +965 -0
- package/dist/db/schema.js +112 -0
- package/dist/db/types.d.ts +11 -0
- package/dist/db/types.js +1 -0
- package/dist/llm/index.d.ts +13 -0
- package/dist/llm/index.js +8 -0
- package/dist/llm/protocols/openai-compat.d.ts +139 -0
- package/dist/llm/protocols/openai-compat.js +214 -0
- package/dist/llm/protocols/utils/index.d.ts +2 -0
- package/dist/llm/protocols/utils/index.js +2 -0
- package/dist/llm/protocols/utils/lifecycle.d.ts +23 -0
- package/dist/llm/protocols/utils/lifecycle.js +46 -0
- package/dist/llm/protocols/utils/tool-stream.d.ts +46 -0
- package/dist/llm/protocols/utils/tool-stream.js +94 -0
- package/dist/llm/provider-error.d.ts +5 -0
- package/dist/llm/provider-error.js +33 -0
- package/dist/llm/provider.d.ts +32 -0
- package/dist/llm/provider.js +191 -0
- package/dist/llm/registry.d.ts +44 -0
- package/dist/llm/registry.js +107 -0
- package/dist/llm/retry.d.ts +37 -0
- package/dist/llm/retry.js +82 -0
- package/dist/llm/routing.d.ts +36 -0
- package/dist/llm/routing.js +53 -0
- package/dist/llm/schema/errors.d.ts +88 -0
- package/dist/llm/schema/errors.js +33 -0
- package/dist/llm/schema/events.d.ts +135 -0
- package/dist/llm/schema/events.js +35 -0
- package/dist/llm/schema/ids.d.ts +22 -0
- package/dist/llm/schema/ids.js +3 -0
- package/dist/llm/schema/index.d.ts +5 -0
- package/dist/llm/schema/index.js +5 -0
- package/dist/llm/schema/messages.d.ts +97 -0
- package/dist/llm/schema/messages.js +18 -0
- package/dist/llm/schema/options.d.ts +54 -0
- package/dist/llm/schema/options.js +54 -0
- package/dist/llm/token.d.ts +2 -0
- package/dist/llm/token.js +13 -0
- package/dist/llm/transport.d.ts +31 -0
- package/dist/llm/transport.js +165 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +3 -0
- package/dist/plugins/builtin.d.ts +6 -0
- package/dist/plugins/builtin.js +54 -0
- package/dist/plugins/hooks.d.ts +3 -0
- package/dist/plugins/hooks.js +79 -0
- package/dist/plugins/index.d.ts +9 -0
- package/dist/plugins/index.js +8 -0
- package/dist/plugins/init.d.ts +23 -0
- package/dist/plugins/init.js +30 -0
- package/dist/plugins/lifecycle.d.ts +6 -0
- package/dist/plugins/lifecycle.js +74 -0
- package/dist/plugins/loader.d.ts +16 -0
- package/dist/plugins/loader.js +68 -0
- package/dist/plugins/logger.d.ts +4 -0
- package/dist/plugins/logger.js +31 -0
- package/dist/plugins/registry.d.ts +7 -0
- package/dist/plugins/registry.js +24 -0
- package/dist/plugins/types.d.ts +113 -0
- package/dist/plugins/types.js +1 -0
- package/dist/project/detect.d.ts +4 -0
- package/dist/project/detect.js +131 -0
- package/dist/project/index.d.ts +4 -0
- package/dist/project/index.js +2 -0
- package/dist/project/project.d.ts +23 -0
- package/dist/project/project.js +87 -0
- package/dist/project/resolve.d.ts +8 -0
- package/dist/project/resolve.js +81 -0
- package/dist/server/agent-manager.d.ts +32 -0
- package/dist/server/agent-manager.js +54 -0
- package/dist/server/app.d.ts +5 -0
- package/dist/server/app.js +75 -0
- package/dist/server/context.d.ts +20 -0
- package/dist/server/context.js +47 -0
- package/dist/server/dev.d.ts +11 -0
- package/dist/server/dev.js +83 -0
- package/dist/server/index.d.ts +11 -0
- package/dist/server/index.js +7 -0
- package/dist/server/middleware/auth.d.ts +15 -0
- package/dist/server/middleware/auth.js +29 -0
- package/dist/server/middleware/cors.d.ts +16 -0
- package/dist/server/middleware/cors.js +44 -0
- package/dist/server/middleware/error.d.ts +15 -0
- package/dist/server/middleware/error.js +11 -0
- package/dist/server/permission/interactive.d.ts +24 -0
- package/dist/server/permission/interactive.js +56 -0
- package/dist/server/permission/store.d.ts +30 -0
- package/dist/server/permission/store.js +47 -0
- package/dist/server/routes/agent.d.ts +4 -0
- package/dist/server/routes/agent.js +19 -0
- package/dist/server/routes/catalog.d.ts +42 -0
- package/dist/server/routes/catalog.js +194 -0
- package/dist/server/routes/chat.d.ts +8 -0
- package/dist/server/routes/chat.js +282 -0
- package/dist/server/routes/commands.d.ts +5 -0
- package/dist/server/routes/commands.js +17 -0
- package/dist/server/routes/config.d.ts +4 -0
- package/dist/server/routes/config.js +27 -0
- package/dist/server/routes/files.d.ts +4 -0
- package/dist/server/routes/files.js +183 -0
- package/dist/server/routes/filesystem.d.ts +29 -0
- package/dist/server/routes/filesystem.js +108 -0
- package/dist/server/routes/health.d.ts +3 -0
- package/dist/server/routes/health.js +13 -0
- package/dist/server/routes/permissions.d.ts +5 -0
- package/dist/server/routes/permissions.js +23 -0
- package/dist/server/routes/project.d.ts +4 -0
- package/dist/server/routes/project.js +51 -0
- package/dist/server/routes/provider.d.ts +16 -0
- package/dist/server/routes/provider.js +66 -0
- package/dist/server/routes/session.d.ts +4 -0
- package/dist/server/routes/session.js +130 -0
- package/dist/server/routes/tool.d.ts +4 -0
- package/dist/server/routes/tool.js +28 -0
- package/dist/server/routes/update.d.ts +14 -0
- package/dist/server/routes/update.js +49 -0
- package/dist/server/server.d.ts +43 -0
- package/dist/server/server.js +197 -0
- package/dist/server/types.d.ts +73 -0
- package/dist/server/types.js +2 -0
- package/dist/server/util/safe-path.d.ts +7 -0
- package/dist/server/util/safe-path.js +7 -0
- package/dist/session/archive.d.ts +15 -0
- package/dist/session/archive.js +129 -0
- package/dist/session/branch.d.ts +9 -0
- package/dist/session/branch.js +63 -0
- package/dist/session/compaction.d.ts +27 -0
- package/dist/session/compaction.js +192 -0
- package/dist/session/context.d.ts +16 -0
- package/dist/session/context.js +173 -0
- package/dist/session/index.d.ts +10 -0
- package/dist/session/index.js +10 -0
- package/dist/session/message.d.ts +22 -0
- package/dist/session/message.js +145 -0
- package/dist/session/session.d.ts +37 -0
- package/dist/session/session.js +144 -0
- package/dist/session/snapshot.d.ts +11 -0
- package/dist/session/snapshot.js +60 -0
- package/dist/session/squash.d.ts +9 -0
- package/dist/session/squash.js +88 -0
- package/dist/session/token.d.ts +10 -0
- package/dist/session/token.js +33 -0
- package/dist/session/types.d.ts +122 -0
- package/dist/session/types.js +1 -0
- package/dist/shared/index.d.ts +2 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/types/agent.d.ts +202 -0
- package/dist/shared/types/agent.js +1 -0
- package/dist/shared/types/base.d.ts +32 -0
- package/dist/shared/types/base.js +1 -0
- package/dist/shared/types/config.d.ts +104 -0
- package/dist/shared/types/config.js +1 -0
- package/dist/shared/types/index.d.ts +6 -0
- package/dist/shared/types/index.js +1 -0
- package/dist/shared/types/llm.d.ts +122 -0
- package/dist/shared/types/llm.js +1 -0
- package/dist/shared/types/message.d.ts +71 -0
- package/dist/shared/types/message.js +1 -0
- package/dist/shared/types/tool.d.ts +134 -0
- package/dist/shared/types/tool.js +1 -0
- package/dist/shared/utils/path.d.ts +15 -0
- package/dist/shared/utils/path.js +23 -0
- package/dist/shared/utils.d.ts +5 -0
- package/dist/shared/utils.js +10 -0
- package/dist/tools/builtin/bash.d.ts +12 -0
- package/dist/tools/builtin/bash.js +121 -0
- package/dist/tools/builtin/dap.d.ts +23 -0
- package/dist/tools/builtin/dap.js +231 -0
- package/dist/tools/builtin/edit.d.ts +12 -0
- package/dist/tools/builtin/edit.js +151 -0
- package/dist/tools/builtin/glob.d.ts +11 -0
- package/dist/tools/builtin/glob.js +120 -0
- package/dist/tools/builtin/grep.d.ts +6 -0
- package/dist/tools/builtin/grep.js +141 -0
- package/dist/tools/builtin/read.d.ts +6 -0
- package/dist/tools/builtin/read.js +73 -0
- package/dist/tools/builtin/resolvers.d.ts +12 -0
- package/dist/tools/builtin/resolvers.js +79 -0
- package/dist/tools/builtin/task.d.ts +9 -0
- package/dist/tools/builtin/task.js +100 -0
- package/dist/tools/builtin/write.d.ts +6 -0
- package/dist/tools/builtin/write.js +36 -0
- package/dist/tools/builtin/yield.d.ts +9 -0
- package/dist/tools/builtin/yield.js +40 -0
- package/dist/tools/executor.d.ts +9 -0
- package/dist/tools/executor.js +54 -0
- package/dist/tools/hashline/index.d.ts +2 -0
- package/dist/tools/hashline/index.js +3 -0
- package/dist/tools/hashline/patch.d.ts +48 -0
- package/dist/tools/hashline/patch.js +201 -0
- package/dist/tools/index.d.ts +30 -0
- package/dist/tools/index.js +55 -0
- package/dist/tools/permission.d.ts +13 -0
- package/dist/tools/permission.js +39 -0
- package/dist/tools/registry.d.ts +15 -0
- package/dist/tools/registry.js +54 -0
- package/dist/tools/resolver.d.ts +15 -0
- package/dist/tools/resolver.js +47 -0
- package/dist/tools/truncate.d.ts +8 -0
- package/dist/tools/truncate.js +50 -0
- package/dist/tools/types.d.ts +101 -0
- package/dist/tools/types.js +1 -0
- package/dist/tools/validate.d.ts +8 -0
- package/dist/tools/validate.js +130 -0
- package/dist/tools/websearch/fetch.d.ts +11 -0
- package/dist/tools/websearch/fetch.js +21 -0
- package/dist/tools/websearch/index.d.ts +19 -0
- package/dist/tools/websearch/index.js +78 -0
- package/dist/tools/websearch/providers/brave.d.ts +5 -0
- package/dist/tools/websearch/providers/brave.js +53 -0
- package/dist/tools/websearch/providers/duckduckgo.d.ts +3 -0
- package/dist/tools/websearch/providers/duckduckgo.js +75 -0
- package/dist/tools/websearch/providers/tavily.d.ts +11 -0
- package/dist/tools/websearch/providers/tavily.js +89 -0
- package/dist/tools/websearch/types.d.ts +51 -0
- package/dist/tools/websearch/types.js +18 -0
- package/dist/tools/websearch/websearch.d.ts +10 -0
- package/dist/tools/websearch/websearch.js +48 -0
- package/dist/update/index.d.ts +10 -0
- package/dist/update/index.js +6 -0
- package/dist/update/ipc.d.ts +15 -0
- package/dist/update/ipc.js +41 -0
- package/dist/update/scheduler.d.ts +34 -0
- package/dist/update/scheduler.js +57 -0
- package/dist/update/snapshot.d.ts +39 -0
- package/dist/update/snapshot.js +99 -0
- package/dist/update/updater.d.ts +48 -0
- package/dist/update/updater.js +83 -0
- package/dist/update/version.d.ts +18 -0
- package/dist/update/version.js +49 -0
- package/drizzle/0000_magical_nightshade.sql +55 -0
- package/drizzle/0001_chemical_doctor_faustus.sql +13 -0
- package/drizzle/0002_amusing_micromacro.sql +12 -0
- package/drizzle/0003_regular_purifiers.sql +2 -0
- package/drizzle/meta/0000_snapshot.json +448 -0
- package/drizzle/meta/0001_snapshot.json +539 -0
- package/drizzle/meta/0002_snapshot.json +633 -0
- package/drizzle/meta/0003_snapshot.json +645 -0
- package/drizzle/meta/_journal.json +34 -0
- package/package.json +100 -8
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { Hono } from 'hono';
|
|
4
|
+
import { apiError } from '../middleware/error.js';
|
|
5
|
+
/** models.dev API URL,可通过环境变量覆盖。 */
|
|
6
|
+
const MODELS_DEV_URL = process.env.C0DE_MODELS_URL || 'https://models.dev/api.json';
|
|
7
|
+
/** 缓存 TTL:5 分钟。 */
|
|
8
|
+
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
9
|
+
/** 内存缓存。 */
|
|
10
|
+
let memCache = null;
|
|
11
|
+
function getCachePath() {
|
|
12
|
+
const cacheDir = join(process.cwd(), '.c0de', 'cache');
|
|
13
|
+
return join(cacheDir, 'models-dev.json');
|
|
14
|
+
}
|
|
15
|
+
/** 清除内存 + 磁盘缓存(主要用于测试)。 */
|
|
16
|
+
function clearCatalogCache() {
|
|
17
|
+
memCache = null;
|
|
18
|
+
const p = getCachePath();
|
|
19
|
+
if (existsSync(p)) {
|
|
20
|
+
try {
|
|
21
|
+
unlinkSync(p);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// 忽略删除失败
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function readDiskCache() {
|
|
29
|
+
const p = getCachePath();
|
|
30
|
+
if (!existsSync(p))
|
|
31
|
+
return null;
|
|
32
|
+
try {
|
|
33
|
+
const raw = JSON.parse(readFileSync(p, 'utf-8'));
|
|
34
|
+
return raw;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function writeDiskCache(data, fetchedAt) {
|
|
41
|
+
const cacheDir = join(process.cwd(), '.c0de', 'cache');
|
|
42
|
+
if (!existsSync(cacheDir))
|
|
43
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
44
|
+
writeFileSync(getCachePath(), JSON.stringify({ data, fetchedAt }, null, 2), 'utf-8');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 获取 models.dev 目录数据(带缓存)。
|
|
48
|
+
* 优先用内存缓存,其次磁盘缓存,最后发起网络请求。
|
|
49
|
+
*/
|
|
50
|
+
async function fetchCatalog(force = false) {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
if (!force && memCache && now - memCache.fetchedAt < CACHE_TTL_MS) {
|
|
53
|
+
return memCache;
|
|
54
|
+
}
|
|
55
|
+
const disk = readDiskCache();
|
|
56
|
+
if (!force && disk && now - disk.fetchedAt < CACHE_TTL_MS) {
|
|
57
|
+
memCache = disk;
|
|
58
|
+
return disk;
|
|
59
|
+
}
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
const timer = setTimeout(() => controller.abort(), 15_000);
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch(MODELS_DEV_URL, {
|
|
64
|
+
signal: controller.signal,
|
|
65
|
+
headers: { Accept: 'application/json' },
|
|
66
|
+
});
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
throw new Error(`HTTP ${res.status}`);
|
|
69
|
+
}
|
|
70
|
+
const data = (await res.json());
|
|
71
|
+
const fetchedAt = now;
|
|
72
|
+
memCache = { data, fetchedAt };
|
|
73
|
+
writeDiskCache(data, fetchedAt);
|
|
74
|
+
return { data, fetchedAt };
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
// 网络失败时回退到磁盘缓存(即使过期)
|
|
78
|
+
if (disk) {
|
|
79
|
+
memCache = disk;
|
|
80
|
+
return disk;
|
|
81
|
+
}
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** 把内部 Provider 结构转为前端需要的精简列表项。 */
|
|
89
|
+
function toProviderListItem(id, p) {
|
|
90
|
+
return {
|
|
91
|
+
id,
|
|
92
|
+
name: p.name,
|
|
93
|
+
npm: p.npm,
|
|
94
|
+
api: p.api,
|
|
95
|
+
env: p.env,
|
|
96
|
+
doc: p.doc,
|
|
97
|
+
modelCount: Object.keys(p.models).length,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** 把内部 Model 结构转为前端需要的精简列表项。 */
|
|
101
|
+
function toModelItem(m) {
|
|
102
|
+
return {
|
|
103
|
+
id: m.id,
|
|
104
|
+
name: m.name,
|
|
105
|
+
family: m.family,
|
|
106
|
+
reasoning: m.reasoning,
|
|
107
|
+
toolCall: m.tool_call,
|
|
108
|
+
attachment: m.attachment,
|
|
109
|
+
temperature: m.temperature,
|
|
110
|
+
context: m.limit.context,
|
|
111
|
+
output: m.limit.output,
|
|
112
|
+
costInput: m.cost?.input,
|
|
113
|
+
costOutput: m.cost?.output,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function createCatalogRoute(ctx) {
|
|
117
|
+
const app = new Hono();
|
|
118
|
+
void ctx;
|
|
119
|
+
// 列出所有 providers(精简)
|
|
120
|
+
app.get('/providers', async (c) => {
|
|
121
|
+
try {
|
|
122
|
+
const { data } = await fetchCatalog();
|
|
123
|
+
const providers = Object.entries(data)
|
|
124
|
+
.map(([id, p]) => toProviderListItem(id, p))
|
|
125
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
126
|
+
return c.json({ providers });
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
return apiError(c, 502, 'CATALOG_FETCH_FAILED', `Failed to fetch models.dev catalog: ${err instanceof Error ? err.message : String(err)}`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
// 获取指定 provider 的模型列表
|
|
133
|
+
app.get('/providers/:id/models', async (c) => {
|
|
134
|
+
const id = c.req.param('id');
|
|
135
|
+
try {
|
|
136
|
+
const { data } = await fetchCatalog();
|
|
137
|
+
const provider = data[id];
|
|
138
|
+
if (!provider)
|
|
139
|
+
return apiError(c, 404, 'NOT_FOUND', `Provider "${id}" not found`);
|
|
140
|
+
const models = Object.values(provider.models)
|
|
141
|
+
.map(toModelItem)
|
|
142
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
143
|
+
return c.json({
|
|
144
|
+
provider: toProviderListItem(id, provider),
|
|
145
|
+
models,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
return apiError(c, 502, 'CATALOG_FETCH_FAILED', `Failed to fetch models.dev catalog: ${err instanceof Error ? err.message : String(err)}`);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
// 搜索 providers 和 models
|
|
153
|
+
app.get('/search', async (c) => {
|
|
154
|
+
const q = c.req.query('q');
|
|
155
|
+
if (!q)
|
|
156
|
+
return apiError(c, 400, 'BAD_REQUEST', 'Query parameter q is required');
|
|
157
|
+
try {
|
|
158
|
+
const { data } = await fetchCatalog();
|
|
159
|
+
const lower = q.toLowerCase();
|
|
160
|
+
const providers = [];
|
|
161
|
+
const models = [];
|
|
162
|
+
for (const [id, p] of Object.entries(data)) {
|
|
163
|
+
const item = toProviderListItem(id, p);
|
|
164
|
+
const nameMatch = p.name.toLowerCase().includes(lower) || id.toLowerCase().includes(lower);
|
|
165
|
+
if (nameMatch)
|
|
166
|
+
providers.push(item);
|
|
167
|
+
for (const m of Object.values(p.models)) {
|
|
168
|
+
const modelItem = toModelItem(m);
|
|
169
|
+
const modelMatch = m.name.toLowerCase().includes(lower) ||
|
|
170
|
+
m.id.toLowerCase().includes(lower) ||
|
|
171
|
+
m.family?.toLowerCase().includes(lower);
|
|
172
|
+
if (modelMatch)
|
|
173
|
+
models.push({ ...item, model: modelItem });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return c.json({ providers, models });
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
return apiError(c, 502, 'CATALOG_FETCH_FAILED', `Failed to fetch models.dev catalog: ${err instanceof Error ? err.message : String(err)}`);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
// 刷新缓存
|
|
183
|
+
app.post('/refresh', async (c) => {
|
|
184
|
+
try {
|
|
185
|
+
await fetchCatalog(true);
|
|
186
|
+
return c.json({ refreshed: true });
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
return apiError(c, 502, 'CATALOG_FETCH_FAILED', `Failed to refresh models.dev catalog: ${err instanceof Error ? err.message : String(err)}`);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
return app;
|
|
193
|
+
}
|
|
194
|
+
export { clearCatalogCache, createCatalogRoute, fetchCatalog };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import type { ServerContext } from '../types.js';
|
|
3
|
+
/** 按 session.projectId 解析 agent 工作目录;无项目回退 ctx.cwd。 */
|
|
4
|
+
export declare function resolveAgentCwd(ctx: ServerContext, session: {
|
|
5
|
+
projectId: string | null;
|
|
6
|
+
}): Promise<string>;
|
|
7
|
+
declare function createChatRoute(ctx: ServerContext): Hono;
|
|
8
|
+
export { createChatRoute };
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { streamSSE } from 'hono/streaming';
|
|
4
|
+
import { createAgent, runAgent } from '../../core/agent.js';
|
|
5
|
+
import { createSlashRegistry, parseSlashInput } from '../../core/slash.js';
|
|
6
|
+
import { getProject } from '../../project/project.js';
|
|
7
|
+
import { getLLMSegments, getSession, updateSessionLastRun } from '../../session/session.js';
|
|
8
|
+
import { upsertFileSnapshot } from '../../session/snapshot.js';
|
|
9
|
+
import { autoAllowChecker } from '../../tools/permission.js';
|
|
10
|
+
import { listTools } from '../../tools/registry.js';
|
|
11
|
+
import { apiError } from '../middleware/error.js';
|
|
12
|
+
import { createInteractivePermissionChecker } from '../permission/interactive.js';
|
|
13
|
+
import { safeResolve } from '../util/safe-path.js';
|
|
14
|
+
/** 按 session.projectId 解析 agent 工作目录;无项目回退 ctx.cwd。 */
|
|
15
|
+
export async function resolveAgentCwd(ctx, session) {
|
|
16
|
+
if (!session.projectId)
|
|
17
|
+
return ctx.cwd;
|
|
18
|
+
const project = await getProject(ctx.db, session.projectId);
|
|
19
|
+
return project?.worktree ?? ctx.cwd;
|
|
20
|
+
}
|
|
21
|
+
function createChatRoute(ctx) {
|
|
22
|
+
const app = new Hono();
|
|
23
|
+
// POST / — SSE 流式聊天
|
|
24
|
+
app.post('/', async (c) => {
|
|
25
|
+
const body = await c.req.json().catch(() => ({}));
|
|
26
|
+
const sessionId = body.sessionId;
|
|
27
|
+
const message = body.message;
|
|
28
|
+
if (!sessionId || !message) {
|
|
29
|
+
return apiError(c, 400, 'BAD_REQUEST', 'sessionId and message are required');
|
|
30
|
+
}
|
|
31
|
+
let session;
|
|
32
|
+
try {
|
|
33
|
+
session = await getSession(ctx.db, sessionId);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return apiError(c, 404, 'NOT_FOUND', 'Session not found');
|
|
37
|
+
}
|
|
38
|
+
if (!session) {
|
|
39
|
+
return apiError(c, 404, 'NOT_FOUND', 'Session not found');
|
|
40
|
+
}
|
|
41
|
+
// 提前解析 cwd(slash 拦截与 agent 路径都需使用)
|
|
42
|
+
const cwd = await resolveAgentCwd(ctx, session);
|
|
43
|
+
// 斜杠命令拦截
|
|
44
|
+
const parsed = parseSlashInput(message);
|
|
45
|
+
if (parsed) {
|
|
46
|
+
const registry = createSlashRegistry();
|
|
47
|
+
const cmd = registry.get(parsed.name);
|
|
48
|
+
if (cmd) {
|
|
49
|
+
const commandCtx = {
|
|
50
|
+
cwd,
|
|
51
|
+
config: ctx.config,
|
|
52
|
+
// 内置斜杠命令(/clear、/fork、/config)仅需 db + config;
|
|
53
|
+
// permission/toolRegistry/llmRegistry 不会触发,用 autoAllow 凑齐类型。
|
|
54
|
+
deps: {
|
|
55
|
+
db: ctx.db,
|
|
56
|
+
config: ctx.config,
|
|
57
|
+
cwd,
|
|
58
|
+
permission: autoAllowChecker,
|
|
59
|
+
toolRegistry: ctx.toolRegistry,
|
|
60
|
+
llmRegistry: ctx.llmRegistry,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
return streamSSE(c, async (stream) => {
|
|
64
|
+
try {
|
|
65
|
+
const result = await cmd.execute(parsed.args, commandCtx);
|
|
66
|
+
if (result._tag === 'error') {
|
|
67
|
+
await stream.writeSSE({
|
|
68
|
+
event: 'error',
|
|
69
|
+
data: JSON.stringify({
|
|
70
|
+
_tag: 'error',
|
|
71
|
+
error: { _tag: 'unexpected', message: result.message },
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const text = result._tag === 'success' ? result.message : result.text;
|
|
77
|
+
await stream.writeSSE({
|
|
78
|
+
event: 'text_delta',
|
|
79
|
+
data: JSON.stringify({ _tag: 'text_delta', text }),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
await stream.writeSSE({ event: 'done', data: JSON.stringify({ _tag: 'done' }) });
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
await stream.writeSSE({
|
|
86
|
+
event: 'error',
|
|
87
|
+
data: JSON.stringify({
|
|
88
|
+
_tag: 'error',
|
|
89
|
+
error: { _tag: 'unexpected', message: String(e) },
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// 未知斜杠命令:回退为正常消息发给 agent
|
|
96
|
+
}
|
|
97
|
+
// 构建多模态 user content:文本在前,images(dataURL base64)在后
|
|
98
|
+
const userContent = [{ _tag: 'text', text: message }];
|
|
99
|
+
const images = body.images;
|
|
100
|
+
if (images?.length) {
|
|
101
|
+
for (const img of images) {
|
|
102
|
+
userContent.push({ _tag: 'image', mediaType: img.mediaType, data: img.data });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// @agent 调用 subagent:在消息前注入指令(复用 task 工具派生)
|
|
106
|
+
const mentionedAgents = body.agents ?? [];
|
|
107
|
+
if (mentionedAgents.length > 0) {
|
|
108
|
+
const valid = mentionedAgents
|
|
109
|
+
.map((n) => ctx.agentRegistry.get(n))
|
|
110
|
+
.filter((d) => Boolean(d && d.mode !== 'primary'));
|
|
111
|
+
if (valid.length > 0) {
|
|
112
|
+
const names = valid.map((d) => d.name).join(', ');
|
|
113
|
+
const first = userContent[0];
|
|
114
|
+
if (first && first._tag === 'text') {
|
|
115
|
+
first.text = `[User requested subagent(s): ${names}]\n\n${first.text}`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// @文件上下文:读取文件内容写入快照,后续 getSessionContext→injectSnapshots 自动注入。
|
|
120
|
+
// 路径越界或读取失败静默跳过,不阻塞主对话流。
|
|
121
|
+
const files = body.files;
|
|
122
|
+
if (files?.length) {
|
|
123
|
+
for (const p of files) {
|
|
124
|
+
const resolved = safeResolve(cwd, p);
|
|
125
|
+
if (!resolved)
|
|
126
|
+
continue;
|
|
127
|
+
try {
|
|
128
|
+
const content = await readFile(resolved, 'utf-8');
|
|
129
|
+
await upsertFileSnapshot(ctx.db, sessionId, p, content);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// 文件读取失败静默跳过
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const provider = body.provider ?? ctx.config.defaultProvider;
|
|
137
|
+
const model = body.model ?? ctx.config.defaultModel;
|
|
138
|
+
// 前端 ToolToggle 全选时不传 tools(undefined),语义为「启用全部注册工具」。
|
|
139
|
+
// 显式传 [] 才是禁用全部。config.tools.enabled 仅 CLI 模式使用,此处不回退它,
|
|
140
|
+
// 避免配置里 enabled:[] 把 Web 全选误降级为无工具(LLM 无法 function call)。
|
|
141
|
+
const tools = body.tools ??
|
|
142
|
+
listTools(ctx.toolRegistry, { config: {}, cwd }).map((t) => t.name);
|
|
143
|
+
// primary agent 解析(spec: agent-frontend-switching §4.3)
|
|
144
|
+
const agentName = body.agent ?? 'default';
|
|
145
|
+
const agentDef = ctx.agentRegistry.get(agentName);
|
|
146
|
+
if (!agentDef || agentDef.mode === 'subagent') {
|
|
147
|
+
return apiError(c, 400, 'INVALID_AGENT', `Unknown or non-primary agent: ${agentName}`);
|
|
148
|
+
}
|
|
149
|
+
// agent def 覆盖:tools(plan 限只读)、model(可选)
|
|
150
|
+
const resolvedTools = agentDef.tools ?? tools;
|
|
151
|
+
const resolvedModel = agentDef.model ?? model;
|
|
152
|
+
// 分段预检:切换 provider/model/tools 将开新段(前缀失效→缓存 miss),
|
|
153
|
+
// 需用户显式确认(confirmSegmentBreak)。首轮无活跃段时跳过。
|
|
154
|
+
const existingRun = ctx.agentManager.get(sessionId);
|
|
155
|
+
const segs = existingRun ? existingRun.state.segments : await getLLMSegments(ctx.db, sessionId);
|
|
156
|
+
const active = segs[segs.length - 1];
|
|
157
|
+
if (active) {
|
|
158
|
+
const reqTools = new Set(resolvedTools);
|
|
159
|
+
const segTools = new Set(active.tools.map((t) => t.name));
|
|
160
|
+
const toolsDiffer = reqTools.size !== segTools.size || [...reqTools].some((t) => !segTools.has(t));
|
|
161
|
+
const modelDiffer = active.provider !== provider || active.model !== resolvedModel;
|
|
162
|
+
// 旧段 agentName undefined 视为 'default'
|
|
163
|
+
const agentDiffer = (active.agentName ?? 'default') !== agentName;
|
|
164
|
+
const confirmed = body.confirmSegmentBreak === true;
|
|
165
|
+
if ((modelDiffer || toolsDiffer || agentDiffer) && !confirmed) {
|
|
166
|
+
return apiError(c, 409, 'SEGMENT_BREAK_REQUIRED', '切换模型/工具将开始新的上下文段(缓存失效),需用户确认', {
|
|
167
|
+
activeSegment: {
|
|
168
|
+
provider: active.provider,
|
|
169
|
+
model: active.model,
|
|
170
|
+
tools: active.tools.map((t) => t.name),
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return streamSSE(c, async (stream) => {
|
|
176
|
+
// 权限检查器:ask 权限通过 SSE 通知前端,阻塞等待确认
|
|
177
|
+
const permissionChecker = createInteractivePermissionChecker(ctx.permissionStore, {
|
|
178
|
+
getMode: () => ctx.permissionMode,
|
|
179
|
+
onPermissionRequired: async (req) => {
|
|
180
|
+
await stream.writeSSE({
|
|
181
|
+
event: 'permission_required',
|
|
182
|
+
data: JSON.stringify({ _tag: 'permission_required', ...req }),
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
// cwd 已在斜杠拦截前解析,此处直接复用
|
|
187
|
+
// 构建 agent 依赖(注入测试用 chatStream)
|
|
188
|
+
const deps = {
|
|
189
|
+
db: ctx.db,
|
|
190
|
+
llmRegistry: ctx.llmRegistry,
|
|
191
|
+
toolRegistry: ctx.toolRegistry,
|
|
192
|
+
urlRegistry: ctx.urlRegistry,
|
|
193
|
+
hookRunner: ctx.hookRunner,
|
|
194
|
+
permission: permissionChecker,
|
|
195
|
+
config: ctx.config,
|
|
196
|
+
agentRegistry: ctx.agentRegistry,
|
|
197
|
+
cwd,
|
|
198
|
+
...(ctx.chatStream ? { chatStream: ctx.chatStream } : {}),
|
|
199
|
+
};
|
|
200
|
+
const agentConfig = {
|
|
201
|
+
provider,
|
|
202
|
+
model: resolvedModel,
|
|
203
|
+
tools: resolvedTools,
|
|
204
|
+
plugins: ctx.config.plugins.enabled,
|
|
205
|
+
agentName,
|
|
206
|
+
...(agentDef.systemPrompt ? { agentRolePrompt: agentDef.systemPrompt } : {}),
|
|
207
|
+
};
|
|
208
|
+
const state = await createAgent(session, agentConfig, deps);
|
|
209
|
+
// 持久化 run 状态:status='running' 写入 DB。服务重启后此字段仍为 'running'
|
|
210
|
+
// 而进程无活跃 run → 检测为 interrupted。finally 中标记 'completed'。
|
|
211
|
+
const runStartedAt = Date.now();
|
|
212
|
+
await updateSessionLastRun(ctx.db, sessionId, {
|
|
213
|
+
status: 'running',
|
|
214
|
+
...(agentName !== 'default' ? { agentName } : {}),
|
|
215
|
+
provider,
|
|
216
|
+
model: resolvedModel,
|
|
217
|
+
startedAt: runStartedAt,
|
|
218
|
+
});
|
|
219
|
+
ctx.agentManager.register({ sessionId, state, deps });
|
|
220
|
+
// 客户端断开时中止 agent
|
|
221
|
+
stream.onAbort(() => {
|
|
222
|
+
ctx.agentManager.abort(sessionId);
|
|
223
|
+
});
|
|
224
|
+
try {
|
|
225
|
+
for await (const event of runAgent(state, userContent, deps)) {
|
|
226
|
+
await stream.writeSSE({
|
|
227
|
+
event: event._tag,
|
|
228
|
+
data: JSON.stringify(event),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
const message = err instanceof Error
|
|
234
|
+
? err.message
|
|
235
|
+
: typeof err === 'object' && err !== null && 'message' in err
|
|
236
|
+
? String(err.message)
|
|
237
|
+
: String(err);
|
|
238
|
+
await stream.writeSSE({
|
|
239
|
+
event: 'error',
|
|
240
|
+
data: JSON.stringify({
|
|
241
|
+
_tag: 'error',
|
|
242
|
+
error: {
|
|
243
|
+
_tag: 'unexpected',
|
|
244
|
+
message,
|
|
245
|
+
},
|
|
246
|
+
}),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
// 无论正常完成、错误还是 abort,只要服务还活着就标记 completed。
|
|
251
|
+
// 只有服务崩溃/重启才会留下 status='running' → 下次加载检测为 interrupted。
|
|
252
|
+
await updateSessionLastRun(ctx.db, sessionId, {
|
|
253
|
+
status: 'completed',
|
|
254
|
+
...(agentName !== 'default' ? { agentName } : {}),
|
|
255
|
+
provider,
|
|
256
|
+
model: resolvedModel,
|
|
257
|
+
startedAt: runStartedAt,
|
|
258
|
+
}).catch(() => { });
|
|
259
|
+
ctx.agentManager.unregister(sessionId);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
// 控制端点
|
|
264
|
+
app.post('/abort', async (c) => {
|
|
265
|
+
const { sessionId } = await c.req.json();
|
|
266
|
+
return c.json({ aborted: ctx.agentManager.abort(sessionId) });
|
|
267
|
+
});
|
|
268
|
+
app.post('/pause', async (c) => {
|
|
269
|
+
const { sessionId } = await c.req.json();
|
|
270
|
+
return c.json({ paused: ctx.agentManager.pause(sessionId) });
|
|
271
|
+
});
|
|
272
|
+
app.post('/resume', async (c) => {
|
|
273
|
+
const { sessionId } = await c.req.json();
|
|
274
|
+
return c.json({ resumed: ctx.agentManager.resume(sessionId) });
|
|
275
|
+
});
|
|
276
|
+
app.post('/steer', async (c) => {
|
|
277
|
+
const body = await c.req.json();
|
|
278
|
+
return c.json({ steered: ctx.agentManager.steer(body.sessionId, body.message) });
|
|
279
|
+
});
|
|
280
|
+
return app;
|
|
281
|
+
}
|
|
282
|
+
export { createChatRoute };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { createSlashRegistry } from '../../core/slash.js';
|
|
3
|
+
/** GET / — 返回内置斜杠命令列表(name/description/argsHint)。 */
|
|
4
|
+
function createCommandsRoute(_ctx) {
|
|
5
|
+
const app = new Hono();
|
|
6
|
+
app.get('/', (c) => {
|
|
7
|
+
const registry = createSlashRegistry();
|
|
8
|
+
const commands = registry.list().map((cmd) => ({
|
|
9
|
+
name: cmd.name,
|
|
10
|
+
description: cmd.description,
|
|
11
|
+
...(cmd.argsHint ? { argsHint: cmd.argsHint } : {}),
|
|
12
|
+
}));
|
|
13
|
+
return c.json({ commands });
|
|
14
|
+
});
|
|
15
|
+
return app;
|
|
16
|
+
}
|
|
17
|
+
export { createCommandsRoute };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { mergeConfig, saveConfig } from '../../core/config.js';
|
|
3
|
+
import { encryptSecret, isEncryptedSecret } from '../../core/secret.js';
|
|
4
|
+
import { syncRegistryFromConfig } from '../server.js';
|
|
5
|
+
function createConfigRoute(ctx) {
|
|
6
|
+
const app = new Hono();
|
|
7
|
+
app.get('/', (c) => {
|
|
8
|
+
return c.json(ctx.config);
|
|
9
|
+
});
|
|
10
|
+
app.patch('/', async (c) => {
|
|
11
|
+
const patch = (await c.req.json());
|
|
12
|
+
// spec §24.2:provider apiKey 落盘前加密,明文不持久化。
|
|
13
|
+
// 已加密(enc: 前缀)或无 apiKey 的透传。
|
|
14
|
+
if (Array.isArray(patch.providers)) {
|
|
15
|
+
patch.providers = patch.providers.map((p) => p.apiKey && !isEncryptedSecret(p.apiKey) ? { ...p, apiKey: encryptSecret(p.apiKey) } : p);
|
|
16
|
+
}
|
|
17
|
+
ctx.config = mergeConfig(ctx.config, patch);
|
|
18
|
+
// providers 变更后原地同步 registry,使运行中的连接立即生效
|
|
19
|
+
syncRegistryFromConfig(ctx.llmRegistry, ctx.config);
|
|
20
|
+
await saveConfig(ctx.config, 'project', ctx.cwd).catch(() => {
|
|
21
|
+
// 保存失败不影响内存配置
|
|
22
|
+
});
|
|
23
|
+
return c.json(ctx.config);
|
|
24
|
+
});
|
|
25
|
+
return app;
|
|
26
|
+
}
|
|
27
|
+
export { createConfigRoute };
|