@worldzb/agent-sync 1.4.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/README.md +176 -0
- package/dist/agents/abstract-file-adapter.js +53 -0
- package/dist/agents/claude-adapter.js +154 -0
- package/dist/agents/codex-adapter.js +139 -0
- package/dist/agents/jsonl.js +26 -0
- package/dist/agents/opencode-adapter.js +193 -0
- package/dist/agents/process-runner.js +33 -0
- package/dist/agents/registry.js +23 -0
- package/dist/agents/types.js +1 -0
- package/dist/claude/settings.js +52 -0
- package/dist/cli/create-program.js +32 -0
- package/dist/commands/add.js +70 -0
- package/dist/commands/agents.js +16 -0
- package/dist/commands/context.js +1 -0
- package/dist/commands/current.js +32 -0
- package/dist/commands/delete.js +47 -0
- package/dist/commands/history.js +155 -0
- package/dist/commands/integrations.js +77 -0
- package/dist/commands/list.js +31 -0
- package/dist/commands/migrate.js +42 -0
- package/dist/commands/sessions.js +52 -0
- package/dist/commands/switch.js +93 -0
- package/dist/config/config-data.js +85 -0
- package/dist/config/config-repository.js +29 -0
- package/dist/config/paths.js +16 -0
- package/dist/config/types.js +1 -0
- package/dist/history/session-loader-worker.js +23 -0
- package/dist/history/session-scope.js +10 -0
- package/dist/history/session-service.js +29 -0
- package/dist/history/ui/formatters.js +37 -0
- package/dist/history/ui/history-app.js +262 -0
- package/dist/history/ui/key-hints.js +6 -0
- package/dist/history/ui/navigation.js +11 -0
- package/dist/history/ui/session-details.js +8 -0
- package/dist/history/ui/session-list.js +16 -0
- package/dist/history/ui/theme.js +14 -0
- package/dist/history/ui/viewport.js +13 -0
- package/dist/index.js +27 -0
- package/dist/migration/migration-service.js +55 -0
- package/dist/migration/prompt-renderer.js +27 -0
- package/dist/migration/transcript-normalizer.js +53 -0
- package/dist/sessions/managed-session-repository.js +56 -0
- package/dist/sessions/session-launcher.js +63 -0
- package/dist/sessions/session-monitor.js +16 -0
- package/dist/ui/output.js +13 -0
- package/package.json +52 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { AbstractFileAdapter } from './abstract-file-adapter.js';
|
|
4
|
+
import { asString, isRecord } from './jsonl.js';
|
|
5
|
+
import { findExecutable, runCommand } from './process-runner.js';
|
|
6
|
+
import { normalizeMessage } from '../migration/transcript-normalizer.js';
|
|
7
|
+
export class OpenCodeAdapter extends AbstractFileAdapter {
|
|
8
|
+
homeDirectory;
|
|
9
|
+
id = 'opencode';
|
|
10
|
+
name = 'OpenCode';
|
|
11
|
+
command = 'opencode';
|
|
12
|
+
historyRoot;
|
|
13
|
+
constructor(homeDirectory) {
|
|
14
|
+
super();
|
|
15
|
+
this.homeDirectory = homeDirectory;
|
|
16
|
+
this.historyRoot = path.join(homeDirectory, '.local', 'share', 'opencode');
|
|
17
|
+
}
|
|
18
|
+
listSessions() {
|
|
19
|
+
try {
|
|
20
|
+
const value = JSON.parse(this.execute(['session', 'list', '--format', 'json']));
|
|
21
|
+
const sessions = toSessions(value, this.id);
|
|
22
|
+
return sessions.length ? sessions : this.listSessionsFromDatabase();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return this.listSessionsFromDatabase();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
listSessionsFromDatabase() {
|
|
29
|
+
const sqlite = findExecutable('sqlite3');
|
|
30
|
+
const database = path.join(this.historyRoot, 'opencode.db');
|
|
31
|
+
if (!sqlite || !fs.existsSync(database))
|
|
32
|
+
return [];
|
|
33
|
+
try {
|
|
34
|
+
const output = runCommand(sqlite, [
|
|
35
|
+
'-json',
|
|
36
|
+
'-readonly',
|
|
37
|
+
database,
|
|
38
|
+
'SELECT id, title, directory, time_updated FROM session ORDER BY time_updated DESC;',
|
|
39
|
+
]);
|
|
40
|
+
return toSessions(JSON.parse(output), this.id);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
readTranscript(session) {
|
|
47
|
+
try {
|
|
48
|
+
const value = JSON.parse(this.execute(['export', session.id]));
|
|
49
|
+
const messages = collectMessages(value);
|
|
50
|
+
return {
|
|
51
|
+
source: session,
|
|
52
|
+
messages,
|
|
53
|
+
warnings: messages.length ? [] : ['OpenCode 未返回可迁移的消息内容。'],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
return {
|
|
58
|
+
source: session,
|
|
59
|
+
messages: [],
|
|
60
|
+
warnings: [error instanceof Error ? error.message : '无法导出 OpenCode 会话。'],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
createResumeLaunch(session) {
|
|
65
|
+
return { command: [this.command, '--session', session.id], cwd: session.cwd || process.cwd() };
|
|
66
|
+
}
|
|
67
|
+
createNewLaunch(input) {
|
|
68
|
+
const prompt = input.prompt
|
|
69
|
+
? `${input.prompt}${input.assetDirectory ? `\n\n迁移附件目录:${input.assetDirectory}` : ''}`
|
|
70
|
+
: undefined;
|
|
71
|
+
return { command: [this.command, ...(prompt ? ['--prompt', prompt] : [])], cwd: input.cwd || process.cwd() };
|
|
72
|
+
}
|
|
73
|
+
deleteSession(session) {
|
|
74
|
+
this.execute(['session', 'delete', session.id], session.cwd);
|
|
75
|
+
}
|
|
76
|
+
integrationRoots(project) {
|
|
77
|
+
return [
|
|
78
|
+
{ directory: path.join(this.homeDirectory, '.config', 'opencode'), scope: 'user' },
|
|
79
|
+
...(project ? [{ directory: path.join(project, '.opencode'), scope: 'project' }] : []),
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
listIntegrations(project) {
|
|
83
|
+
return [...super.listIntegrations(project), ...this.listLocalPlugins(project), ...this.listConfiguredItems(), ...this.listMcp()];
|
|
84
|
+
}
|
|
85
|
+
installPlugin(plugin, scope) {
|
|
86
|
+
this.execute(['plugin', plugin, ...(scope === 'user' ? ['--global'] : [])]);
|
|
87
|
+
}
|
|
88
|
+
removeIntegration(item) {
|
|
89
|
+
if (item.kind === 'mcp' || (item.kind === 'plugin' && item.location.endsWith('.json'))) {
|
|
90
|
+
throw new Error('当前 OpenCode 版本未提供该资源的安全移除命令;请在配置文件中手动移除。');
|
|
91
|
+
}
|
|
92
|
+
this.removeLocalPath(item.location);
|
|
93
|
+
}
|
|
94
|
+
listLocalPlugins(project) {
|
|
95
|
+
const roots = [
|
|
96
|
+
{ directory: path.join(this.homeDirectory, '.config', 'opencode', 'plugins'), scope: 'user' },
|
|
97
|
+
...(project ? [{ directory: path.join(project, '.opencode', 'plugins'), scope: 'project' }] : []),
|
|
98
|
+
];
|
|
99
|
+
return roots.flatMap(({ directory, scope }) => {
|
|
100
|
+
if (!fs.existsSync(directory))
|
|
101
|
+
return [];
|
|
102
|
+
return fs.readdirSync(directory, { withFileTypes: true })
|
|
103
|
+
.filter((entry) => !entry.name.startsWith('.') && (entry.isDirectory() || entry.isFile()))
|
|
104
|
+
.map((entry) => ({
|
|
105
|
+
agent: this.id,
|
|
106
|
+
kind: 'plugin',
|
|
107
|
+
name: entry.name,
|
|
108
|
+
scope,
|
|
109
|
+
location: path.join(directory, entry.name),
|
|
110
|
+
removable: true,
|
|
111
|
+
}));
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
listConfiguredItems() {
|
|
115
|
+
const file = path.join(this.homeDirectory, '.config', 'opencode', 'opencode.json');
|
|
116
|
+
if (!fs.existsSync(file))
|
|
117
|
+
return [];
|
|
118
|
+
try {
|
|
119
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
120
|
+
if (!isRecord(parsed))
|
|
121
|
+
return [];
|
|
122
|
+
const plugins = Array.isArray(parsed.plugin) ? parsed.plugin : [];
|
|
123
|
+
const pluginItems = plugins.filter((plugin) => typeof plugin === 'string').map((name) => ({
|
|
124
|
+
agent: this.id,
|
|
125
|
+
kind: 'plugin',
|
|
126
|
+
name,
|
|
127
|
+
scope: 'user',
|
|
128
|
+
location: file,
|
|
129
|
+
removable: true,
|
|
130
|
+
}));
|
|
131
|
+
const mcp = isRecord(parsed.mcp) ? Object.keys(parsed.mcp).map((name) => ({
|
|
132
|
+
agent: this.id,
|
|
133
|
+
kind: 'mcp',
|
|
134
|
+
name,
|
|
135
|
+
scope: 'user',
|
|
136
|
+
location: file,
|
|
137
|
+
removable: false,
|
|
138
|
+
})) : [];
|
|
139
|
+
return [...pluginItems, ...mcp];
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
listMcp() {
|
|
146
|
+
try {
|
|
147
|
+
const output = this.execute(['mcp', 'list']);
|
|
148
|
+
return output.split('\n')
|
|
149
|
+
.map((line) => line.trim().split(/\s+/)[0])
|
|
150
|
+
.filter((name) => Boolean(name) && !name.toLowerCase().startsWith('name'))
|
|
151
|
+
.map((name) => ({ agent: this.id, kind: 'mcp', name, scope: 'user', location: 'opencode mcp', removable: false }));
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function toSessions(value, agent) {
|
|
159
|
+
const rows = Array.isArray(value) ? value : isRecord(value) && Array.isArray(value.sessions) ? value.sessions : [];
|
|
160
|
+
return rows.filter(isRecord).flatMap((row) => {
|
|
161
|
+
const id = asString(row.id);
|
|
162
|
+
if (!id)
|
|
163
|
+
return [];
|
|
164
|
+
const time = isRecord(row.time) ? row.time : {};
|
|
165
|
+
return [{
|
|
166
|
+
agent,
|
|
167
|
+
id,
|
|
168
|
+
title: asString(row.title) || asString(row.slug) || '未命名 OpenCode 会话',
|
|
169
|
+
cwd: asString(row.directory) || asString(row.path) || process.cwd(),
|
|
170
|
+
updatedAt: toIso(row.time_updated ?? time.updated ?? time.updatedAt),
|
|
171
|
+
sourcePath: id,
|
|
172
|
+
}];
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function collectMessages(value) {
|
|
176
|
+
if (Array.isArray(value))
|
|
177
|
+
return value.flatMap(collectMessages);
|
|
178
|
+
if (!isRecord(value))
|
|
179
|
+
return [];
|
|
180
|
+
const direct = normalizeMessage(value);
|
|
181
|
+
if (direct)
|
|
182
|
+
return [direct];
|
|
183
|
+
return Object.values(value).flatMap(collectMessages);
|
|
184
|
+
}
|
|
185
|
+
function toIso(value) {
|
|
186
|
+
if (typeof value === 'number')
|
|
187
|
+
return new Date(value).toISOString();
|
|
188
|
+
if (typeof value === 'string') {
|
|
189
|
+
const date = new Date(value);
|
|
190
|
+
return Number.isNaN(date.valueOf()) ? new Date(0).toISOString() : date.toISOString();
|
|
191
|
+
}
|
|
192
|
+
return new Date(0).toISOString();
|
|
193
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
export function findExecutable(name) {
|
|
5
|
+
const paths = (process.env.PATH || '').split(path.delimiter);
|
|
6
|
+
const extensions = process.platform === 'win32' ? ['', '.cmd', '.exe', '.bat'] : [''];
|
|
7
|
+
return paths
|
|
8
|
+
.flatMap((directory) => extensions.map((extension) => path.join(directory, `${name}${extension}`)))
|
|
9
|
+
.find((candidate) => fs.existsSync(candidate));
|
|
10
|
+
}
|
|
11
|
+
export function runCommand(command, args, cwd) {
|
|
12
|
+
const result = spawnSync(command, args, {
|
|
13
|
+
cwd,
|
|
14
|
+
encoding: 'utf8',
|
|
15
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
16
|
+
timeout: 30_000,
|
|
17
|
+
});
|
|
18
|
+
if (result.error) {
|
|
19
|
+
throw new Error(`无法执行 ${command}:${result.error.message}`);
|
|
20
|
+
}
|
|
21
|
+
if (result.status !== 0) {
|
|
22
|
+
throw new Error(`${command} 执行失败:${(result.stderr || result.stdout || '未知错误').trim()}`);
|
|
23
|
+
}
|
|
24
|
+
return result.stdout.trim();
|
|
25
|
+
}
|
|
26
|
+
export function getVersion(command) {
|
|
27
|
+
try {
|
|
28
|
+
return runCommand(command, ['--version']).split('\n')[0]?.trim();
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ClaudeAdapter } from './claude-adapter.js';
|
|
2
|
+
import { CodexAdapter } from './codex-adapter.js';
|
|
3
|
+
import { OpenCodeAdapter } from './opencode-adapter.js';
|
|
4
|
+
export class AgentRegistry {
|
|
5
|
+
adapters;
|
|
6
|
+
constructor(homeDirectory) {
|
|
7
|
+
const adapters = [
|
|
8
|
+
new ClaudeAdapter(homeDirectory),
|
|
9
|
+
new CodexAdapter(homeDirectory),
|
|
10
|
+
new OpenCodeAdapter(homeDirectory),
|
|
11
|
+
];
|
|
12
|
+
this.adapters = new Map(adapters.map((adapter) => [adapter.id, adapter]));
|
|
13
|
+
}
|
|
14
|
+
all() {
|
|
15
|
+
return [...this.adapters.values()];
|
|
16
|
+
}
|
|
17
|
+
get(id) {
|
|
18
|
+
const adapter = this.adapters.get(id);
|
|
19
|
+
if (!adapter)
|
|
20
|
+
throw new Error(`不支持的 Agent:${id}`);
|
|
21
|
+
return adapter;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const AGENT_IDS = ['claude', 'codex', 'opencode'];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export function readClaudeSettings(filePath) {
|
|
4
|
+
if (!fs.existsSync(filePath)) {
|
|
5
|
+
return {};
|
|
6
|
+
}
|
|
7
|
+
try {
|
|
8
|
+
const value = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
9
|
+
if (!isRecord(value)) {
|
|
10
|
+
throw new Error('settings.json 必须是对象。');
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
throw new Error(`无法读取 Claude 设置文件:${filePath}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function writeClaudeSettings(filePath, settings) {
|
|
19
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
20
|
+
fs.writeFileSync(filePath, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
|
|
21
|
+
}
|
|
22
|
+
export function applyDefaultConfig(settings, config) {
|
|
23
|
+
const env = isRecord(settings.env) ? settings.env : {};
|
|
24
|
+
return {
|
|
25
|
+
...settings,
|
|
26
|
+
env: {
|
|
27
|
+
...env,
|
|
28
|
+
ANTHROPIC_AUTH_TOKEN: config.apiKey,
|
|
29
|
+
ANTHROPIC_BASE_URL: config.baseUrl,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function findActiveConfig(configs, settings) {
|
|
34
|
+
const authToken = settings.env?.ANTHROPIC_AUTH_TOKEN;
|
|
35
|
+
const baseUrl = settings.env?.ANTHROPIC_BASE_URL;
|
|
36
|
+
if (typeof authToken !== 'string' || typeof baseUrl !== 'string') {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
return configs.find((config) => config.apiKey === authToken && config.baseUrl === baseUrl);
|
|
40
|
+
}
|
|
41
|
+
export function createTemporaryExports(config) {
|
|
42
|
+
return [
|
|
43
|
+
`export ANTHROPIC_API_KEY=${shellQuote(config.apiKey)}`,
|
|
44
|
+
`export ANTHROPIC_BASE_URL=${shellQuote(config.baseUrl)}`,
|
|
45
|
+
].join('\n');
|
|
46
|
+
}
|
|
47
|
+
export function shellQuote(value) {
|
|
48
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
49
|
+
}
|
|
50
|
+
function isRecord(value) {
|
|
51
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
52
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { registerAddCommand } from '../commands/add.js';
|
|
4
|
+
import { registerAgentsCommand } from '../commands/agents.js';
|
|
5
|
+
import { registerCurrentCommand } from '../commands/current.js';
|
|
6
|
+
import { registerDeleteCommand } from '../commands/delete.js';
|
|
7
|
+
import { registerHistoryCommand } from '../commands/history.js';
|
|
8
|
+
import { registerIntegrationsCommand } from '../commands/integrations.js';
|
|
9
|
+
import { registerListCommand } from '../commands/list.js';
|
|
10
|
+
import { registerMigrateCommand } from '../commands/migrate.js';
|
|
11
|
+
import { registerSessionsCommand } from '../commands/sessions.js';
|
|
12
|
+
import { registerSwitchCommand } from '../commands/switch.js';
|
|
13
|
+
export function createProgram(context) {
|
|
14
|
+
const program = new Command();
|
|
15
|
+
program
|
|
16
|
+
.name('zmai')
|
|
17
|
+
.description('🤖 多 Agent 会话与 Claude API 配置管理工具')
|
|
18
|
+
.version('1.3.0')
|
|
19
|
+
.addHelpText('beforeAll', chalk.cyan.bold('\n╔══════════════════════════════════════╗\n║ ZMAI Agent Workspace ║\n╚══════════════════════════════════════╝\n'))
|
|
20
|
+
.addHelpText('after', chalk.gray('\n💡 历史会话:zmai history\n💡 Agent 扫描:zmai agents\n💡 集成管理:zmai integrations\n'));
|
|
21
|
+
registerAgentsCommand(program, context);
|
|
22
|
+
registerHistoryCommand(program, context);
|
|
23
|
+
registerSessionsCommand(program, context);
|
|
24
|
+
registerMigrateCommand(program, context);
|
|
25
|
+
registerIntegrationsCommand(program, context);
|
|
26
|
+
registerAddCommand(program, context);
|
|
27
|
+
registerListCommand(program, context);
|
|
28
|
+
registerSwitchCommand(program, context);
|
|
29
|
+
registerDeleteCommand(program, context);
|
|
30
|
+
registerCurrentCommand(program, context);
|
|
31
|
+
return program;
|
|
32
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { cancel, confirm, intro, isCancel, note, outro, text } from '@clack/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { addConfig, createApiConfig } from '../config/config-data.js';
|
|
4
|
+
import { maskApiKey, printSuccess } from '../ui/output.js';
|
|
5
|
+
export function registerAddCommand(program, context) {
|
|
6
|
+
program
|
|
7
|
+
.command('add')
|
|
8
|
+
.description('➕ 添加新的 Claude API 配置')
|
|
9
|
+
.option('-i, --interactive', '交互式添加(推荐)')
|
|
10
|
+
.option('-n, --name <name>', '配置名称')
|
|
11
|
+
.option('-k, --key <key>', 'API Key')
|
|
12
|
+
.option('-u, --url <url>', 'Base URL')
|
|
13
|
+
.action(async (options) => {
|
|
14
|
+
if (options.interactive || !options.name || !options.key) {
|
|
15
|
+
await interactiveAdd(context);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
saveConfig(context, createApiConfig({
|
|
19
|
+
name: options.name,
|
|
20
|
+
apiKey: options.key,
|
|
21
|
+
baseUrl: options.url,
|
|
22
|
+
createdAt: new Date().toISOString(),
|
|
23
|
+
}));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async function interactiveAdd(context) {
|
|
27
|
+
console.clear();
|
|
28
|
+
intro(chalk.cyan.bold('🤖 Claude API Switcher - 添加新配置'));
|
|
29
|
+
const existingNames = context.repository.read().configs.map((config) => config.name);
|
|
30
|
+
const name = await text({
|
|
31
|
+
message: '📝 配置名称',
|
|
32
|
+
placeholder: '例如: 官方 API、代理 API',
|
|
33
|
+
validate: (value) => !value ? '请输入配置名称' : existingNames.includes(value) ? `配置 "${value}" 已存在` : undefined,
|
|
34
|
+
});
|
|
35
|
+
if (isCancel(name))
|
|
36
|
+
return void cancel('操作已取消');
|
|
37
|
+
const apiKey = await text({
|
|
38
|
+
message: '🔑 API Key',
|
|
39
|
+
placeholder: 'sk-ant-api03-xxx',
|
|
40
|
+
validate: (value) => !value ? '请输入 API Key' : value.length < 10 ? 'API Key 格式不正确' : undefined,
|
|
41
|
+
});
|
|
42
|
+
if (isCancel(apiKey))
|
|
43
|
+
return void cancel('操作已取消');
|
|
44
|
+
const baseUrl = await text({
|
|
45
|
+
message: '🌐 Base URL',
|
|
46
|
+
initialValue: 'https://api.anthropic.com',
|
|
47
|
+
validate: (value) => {
|
|
48
|
+
try {
|
|
49
|
+
new URL(value);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return 'URL 格式不正确';
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
if (isCancel(baseUrl))
|
|
58
|
+
return void cancel('操作已取消');
|
|
59
|
+
const config = createApiConfig({ name, apiKey, baseUrl, createdAt: new Date().toISOString() });
|
|
60
|
+
note(`名称: ${config.name}\nAPI Key: ${maskApiKey(config.apiKey)}\nBase URL: ${config.baseUrl}`, '配置预览');
|
|
61
|
+
const accepted = await confirm({ message: '确认添加此配置?', initialValue: true });
|
|
62
|
+
if (isCancel(accepted) || !accepted)
|
|
63
|
+
return void cancel('已取消添加');
|
|
64
|
+
saveConfig(context, config);
|
|
65
|
+
outro(chalk.green('✅ 配置添加成功!'));
|
|
66
|
+
}
|
|
67
|
+
function saveConfig(context, config) {
|
|
68
|
+
context.repository.write(addConfig(context.repository.read(), config));
|
|
69
|
+
printSuccess(`配置 "${config.name}" 已添加!`);
|
|
70
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export function registerAgentsCommand(program, context) {
|
|
3
|
+
program
|
|
4
|
+
.command('agents')
|
|
5
|
+
.description('🔎 扫描已安装的 Agent 工具')
|
|
6
|
+
.action(() => {
|
|
7
|
+
context.agents.all().map((agent) => agent.discover()).forEach((agent) => {
|
|
8
|
+
const status = agent.installed ? chalk.green('已安装') : chalk.gray('未安装');
|
|
9
|
+
console.log(`${agent.installed ? '🟢' : '⚪'} ${chalk.bold(agent.name)} ${status}`);
|
|
10
|
+
console.log(` 命令: ${agent.executable || '未在 PATH 中找到'}`);
|
|
11
|
+
console.log(` 版本: ${agent.version || '未知'}`);
|
|
12
|
+
console.log(` 历史目录: ${agent.historyRoot}`);
|
|
13
|
+
console.log(` 能力: ${agent.capabilities.join('、')}`);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { findActiveConfig, readClaudeSettings } from '../claude/settings.js';
|
|
3
|
+
import { maskApiKey } from '../ui/output.js';
|
|
4
|
+
export function registerCurrentCommand(program, context) {
|
|
5
|
+
program
|
|
6
|
+
.command('current')
|
|
7
|
+
.description('👀 查看当前 Claude 配置')
|
|
8
|
+
.action(() => showCurrentConfig(context));
|
|
9
|
+
}
|
|
10
|
+
function showCurrentConfig(context) {
|
|
11
|
+
const data = context.repository.read();
|
|
12
|
+
const settings = readClaudeSettings(context.claudeSettingsFile);
|
|
13
|
+
const activeConfig = findActiveConfig(data.configs, settings);
|
|
14
|
+
const authToken = settings.env?.ANTHROPIC_AUTH_TOKEN;
|
|
15
|
+
const baseUrl = settings.env?.ANTHROPIC_BASE_URL;
|
|
16
|
+
console.log(chalk.hex('#7C3AED').bold('\n🟣 Claude Code 当前配置'));
|
|
17
|
+
console.log(chalk.gray('━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
18
|
+
if (activeConfig) {
|
|
19
|
+
console.log(`名称: ${activeConfig.name}`);
|
|
20
|
+
console.log(`API Key: ${maskApiKey(activeConfig.apiKey)}`);
|
|
21
|
+
console.log(`Base URL: ${activeConfig.baseUrl}`);
|
|
22
|
+
}
|
|
23
|
+
else if (typeof authToken === 'string') {
|
|
24
|
+
console.log(`API Key: ${maskApiKey(authToken)}`);
|
|
25
|
+
console.log(`Base URL: ${typeof baseUrl === 'string' ? baseUrl : '(默认)'}`);
|
|
26
|
+
console.log(chalk.gray('(未匹配到已保存的配置)'));
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
console.log(chalk.yellow('未配置'));
|
|
30
|
+
}
|
|
31
|
+
console.log('');
|
|
32
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { cancel, confirm, intro, isCancel, outro, select } from '@clack/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { deleteConfig } from '../config/config-data.js';
|
|
4
|
+
import { maskApiKey, printSuccess } from '../ui/output.js';
|
|
5
|
+
export function registerDeleteCommand(program, context) {
|
|
6
|
+
program
|
|
7
|
+
.command('delete')
|
|
8
|
+
.alias('rm')
|
|
9
|
+
.description('🗑️ 删除配置')
|
|
10
|
+
.option('-i, --interactive', '交互式删除')
|
|
11
|
+
.option('-n, --name <name>', '配置名称')
|
|
12
|
+
.action(async (options) => {
|
|
13
|
+
if (options.interactive || !options.name) {
|
|
14
|
+
await interactiveDelete(context);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
removeConfig(context, options.name);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async function interactiveDelete(context) {
|
|
21
|
+
console.clear();
|
|
22
|
+
intro(chalk.cyan.bold('🗑️ Claude API Switcher - 删除配置'));
|
|
23
|
+
const data = context.repository.read();
|
|
24
|
+
if (data.configs.length === 0) {
|
|
25
|
+
return void outro(chalk.yellow('暂无配置。'));
|
|
26
|
+
}
|
|
27
|
+
const selected = await select({
|
|
28
|
+
message: '选择要删除的配置',
|
|
29
|
+
options: data.configs.map((config) => ({
|
|
30
|
+
value: config.name,
|
|
31
|
+
label: `${config.name}${data.current === config.name ? ' (当前默认)' : ''}`,
|
|
32
|
+
hint: maskApiKey(config.apiKey),
|
|
33
|
+
})),
|
|
34
|
+
});
|
|
35
|
+
if (isCancel(selected))
|
|
36
|
+
return void cancel('操作已取消');
|
|
37
|
+
const selectedName = String(selected);
|
|
38
|
+
const accepted = await confirm({ message: `确认删除配置 "${selectedName}"?`, initialValue: false });
|
|
39
|
+
if (isCancel(accepted) || !accepted)
|
|
40
|
+
return void cancel('已取消删除');
|
|
41
|
+
removeConfig(context, selectedName);
|
|
42
|
+
outro(chalk.green('✅ 配置已删除!'));
|
|
43
|
+
}
|
|
44
|
+
function removeConfig(context, name) {
|
|
45
|
+
context.repository.write(deleteConfig(context.repository.read(), name));
|
|
46
|
+
printSuccess(`配置 "${name}" 已删除!`);
|
|
47
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { Worker } from 'node:worker_threads';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { cancel, confirm, isCancel } from '@clack/prompts';
|
|
6
|
+
import { render } from 'ink';
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { pageSessions } from '../history/session-service.js';
|
|
10
|
+
import { HistoryApp } from '../history/ui/history-app.js';
|
|
11
|
+
export function registerHistoryCommand(program, context) {
|
|
12
|
+
program
|
|
13
|
+
.command('history')
|
|
14
|
+
.description('🕘 查看所有 Agent 的历史会话')
|
|
15
|
+
.option('-a, --agent <agent>', '筛选 Agent:claude、codex、opencode')
|
|
16
|
+
.option('-p, --page <page>', '页码', '1')
|
|
17
|
+
.option('--page-size <size>', '每页数量', '20')
|
|
18
|
+
.option('--plain', '以非交互文本格式输出')
|
|
19
|
+
.action(async (options) => {
|
|
20
|
+
validateAgent(options.agent);
|
|
21
|
+
const page = parsePositiveInteger(options.page, 1, 'page');
|
|
22
|
+
const pageSize = parsePositiveInteger(options.pageSize, 20, 'page-size');
|
|
23
|
+
if (options.plain || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
24
|
+
printHistoryPage(loadSessions(context), options.agent, page, pageSize);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
await runInkHistory(context, options.agent, pageSize);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async function runInkHistory(context, agent, pageSize) {
|
|
31
|
+
let foregroundLaunch;
|
|
32
|
+
let clearScreen = () => { };
|
|
33
|
+
const app = render(_jsx(HistoryApp, { agent: agent, pageSize: pageSize, currentDirectory: process.cwd(), agents: context.agents.all().map((adapter) => ({
|
|
34
|
+
id: adapter.id,
|
|
35
|
+
name: adapter.name,
|
|
36
|
+
installed: adapter.discover().installed,
|
|
37
|
+
})), loadSessions: (onProgress) => loadSessionsInWorker(onProgress), onResume: async (session) => resumeSession(context, session), onMigrate: async (session, target) => migrateAndLaunch(context, session, target, true), onDelete: async (session) => deleteSession(context, session), onForegroundLaunch: (spec) => { foregroundLaunch = spec; }, onClearScreen: () => clearScreen() }));
|
|
38
|
+
clearScreen = app.clear;
|
|
39
|
+
try {
|
|
40
|
+
await app.waitUntilExit();
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
app.unmount();
|
|
44
|
+
}
|
|
45
|
+
if (foregroundLaunch)
|
|
46
|
+
launchInCurrentTerminal(foregroundLaunch);
|
|
47
|
+
}
|
|
48
|
+
function loadSessionsInWorker(onProgress) {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const worker = new Worker(fileURLToPath(new URL('../history/session-loader-worker.js', import.meta.url)));
|
|
51
|
+
worker.on('message', (message) => {
|
|
52
|
+
if (!isWorkerMessage(message))
|
|
53
|
+
return;
|
|
54
|
+
if (message.type === 'progress') {
|
|
55
|
+
onProgress(message.message);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (message.type === 'complete') {
|
|
59
|
+
resolve(message.sessions);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
reject(new Error(message.message));
|
|
63
|
+
});
|
|
64
|
+
worker.on('error', reject);
|
|
65
|
+
worker.on('exit', (code) => {
|
|
66
|
+
if (code !== 0)
|
|
67
|
+
reject(new Error(`历史扫描进程异常退出:${code}`));
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function isWorkerMessage(value) {
|
|
72
|
+
return typeof value === 'object' && value !== null && typeof value.type === 'string';
|
|
73
|
+
}
|
|
74
|
+
function loadSessionsDeferred(context) {
|
|
75
|
+
return new Promise((resolve) => setTimeout(() => resolve(loadSessions(context)), 0));
|
|
76
|
+
}
|
|
77
|
+
function loadSessions(context) {
|
|
78
|
+
return context.agents.all().flatMap((adapter) => adapter.listSessions());
|
|
79
|
+
}
|
|
80
|
+
function printHistoryPage(contextSessions, agent, pageNumber, pageSize) {
|
|
81
|
+
const page = pageSessions(contextSessions, { page: pageNumber, pageSize, ...(agent ? { agent } : {}) });
|
|
82
|
+
console.log(chalk.bold(`\n🕘 历史会话:第 ${page.page}/${page.totalPages} 页,共 ${page.total} 条\n`));
|
|
83
|
+
if (!page.items.length) {
|
|
84
|
+
console.log(chalk.yellow('没有找到会话。'));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
page.items.forEach((session) => {
|
|
88
|
+
console.log(`${chalk.cyan(`[${session.agent}]`)} ${chalk.bold(session.title)}`);
|
|
89
|
+
console.log(chalk.gray(` ${session.cwd} · ${session.updatedAt} · ${session.id}`));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async function resumeSession(context, session) {
|
|
93
|
+
return {
|
|
94
|
+
message: `正在当前终端启动 ${session.agent} 会话。`,
|
|
95
|
+
launch: context.agents.get(session.agent).createResumeLaunch(session),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
async function deleteSession(context, session) {
|
|
99
|
+
context.agents.get(session.agent).deleteSession(session);
|
|
100
|
+
return { message: `已删除 ${session.agent} 会话:${session.title}` };
|
|
101
|
+
}
|
|
102
|
+
export async function migrateAndLaunch(context, session, target, confirmed = false) {
|
|
103
|
+
const prepared = context.migrationService.prepare(context.agents.get(session.agent), session, target);
|
|
104
|
+
if (!confirmed) {
|
|
105
|
+
const accepted = await confirm({
|
|
106
|
+
message: `将 ${session.agent} 会话迁移到 ${target},创建新会话并保留原会话?附件:${prepared.attachmentCount} 个。`,
|
|
107
|
+
initialValue: true,
|
|
108
|
+
});
|
|
109
|
+
if (isCancel(accepted) || !accepted) {
|
|
110
|
+
cancel('已取消迁移');
|
|
111
|
+
return { message: '已取消迁移。' };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
copyHistoryToClipboard(prepared.prompt);
|
|
115
|
+
const spec = context.agents.get(target).createNewLaunch({
|
|
116
|
+
cwd: session.cwd,
|
|
117
|
+
prompt: prepared.prompt,
|
|
118
|
+
assetDirectory: prepared.assetDirectory,
|
|
119
|
+
});
|
|
120
|
+
const warnings = prepared.warnings.length ? `\n迁移提示:${prepared.warnings.join(';')}` : '';
|
|
121
|
+
return {
|
|
122
|
+
message: `历史记录已自动导入 ${target} 新会话,并复制到剪贴板;迁移文件保留在 ${prepared.directory}。${warnings}`,
|
|
123
|
+
launch: spec,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function copyHistoryToClipboard(history) {
|
|
127
|
+
const clipboard = process.platform === 'darwin' ? 'pbcopy' : process.platform === 'win32' ? 'clip' : 'xclip';
|
|
128
|
+
const args = process.platform === 'linux' ? ['-selection', 'clipboard'] : [];
|
|
129
|
+
const result = spawnSync(clipboard, args, { input: history, encoding: 'utf8' });
|
|
130
|
+
if (result.error || result.status !== 0) {
|
|
131
|
+
throw new Error('无法复制历史记录到剪贴板。');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function launchInCurrentTerminal(spec) {
|
|
135
|
+
const [command, ...args] = spec.command;
|
|
136
|
+
if (!command)
|
|
137
|
+
throw new Error('启动命令为空。');
|
|
138
|
+
const result = spawnSync(command, args, { cwd: spec.cwd, stdio: 'inherit' });
|
|
139
|
+
if (result.error)
|
|
140
|
+
throw new Error(`无法启动 ${command}:${result.error.message}`);
|
|
141
|
+
if (result.status && result.status !== 0)
|
|
142
|
+
process.exitCode = result.status;
|
|
143
|
+
}
|
|
144
|
+
function parsePositiveInteger(value, fallback, option) {
|
|
145
|
+
const parsed = Number(value ?? fallback);
|
|
146
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
147
|
+
throw new Error(`--${option} 必须为大于 0 的整数。`);
|
|
148
|
+
}
|
|
149
|
+
return parsed;
|
|
150
|
+
}
|
|
151
|
+
function validateAgent(agent) {
|
|
152
|
+
if (agent && !['claude', 'codex', 'opencode'].includes(agent)) {
|
|
153
|
+
throw new Error('Agent 必须为 claude、codex 或 opencode。');
|
|
154
|
+
}
|
|
155
|
+
}
|