codexmate 0.0.23 → 0.0.25
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 +32 -9
- package/README.zh.md +33 -9
- package/cli/auth-profiles.js +23 -7
- package/cli/builtin-proxy.js +35 -0
- package/cli/claude-proxy.js +24 -0
- package/cli/doctor-core.js +903 -0
- package/cli/import-skills-url.js +356 -0
- package/cli/openai-bridge.js +51 -4
- package/cli/session-usage.js +8 -2
- package/cli.js +1921 -399
- package/lib/automation.js +404 -0
- package/lib/cli-models-utils.js +0 -40
- package/lib/cli-network-utils.js +28 -2
- package/lib/cli-path-utils.js +21 -5
- package/lib/cli-sessions.js +32 -1
- package/lib/download-artifacts.js +17 -2
- package/lib/mcp-stdio.js +13 -0
- package/package.json +3 -3
- package/plugins/README.md +20 -0
- package/plugins/README.zh-CN.md +20 -0
- package/plugins/prompt-templates/comment-polish/index.mjs +25 -0
- package/plugins/prompt-templates/computed.mjs +253 -0
- package/plugins/prompt-templates/index.mjs +8 -0
- package/plugins/prompt-templates/manifest.mjs +15 -0
- package/plugins/prompt-templates/methods.mjs +619 -0
- package/plugins/prompt-templates/overview.mjs +90 -0
- package/plugins/prompt-templates/ownership.mjs +19 -0
- package/plugins/prompt-templates/rule-ack/index.mjs +21 -0
- package/plugins/prompt-templates/storage.mjs +64 -0
- package/plugins/registry.mjs +16 -0
- package/web-ui/app.js +21 -35
- package/web-ui/index.html +4 -3
- package/web-ui/logic.sessions.mjs +2 -2
- package/web-ui/modules/app.computed.dashboard.mjs +24 -22
- package/web-ui/modules/app.computed.main-tabs.mjs +3 -0
- package/web-ui/modules/app.computed.session.mjs +17 -0
- package/web-ui/modules/app.methods.agents.mjs +91 -3
- package/web-ui/modules/app.methods.codex-config.mjs +153 -164
- package/web-ui/modules/app.methods.install.mjs +28 -0
- package/web-ui/modules/app.methods.navigation.mjs +34 -1
- package/web-ui/modules/app.methods.runtime.mjs +24 -2
- package/web-ui/modules/app.methods.session-actions.mjs +8 -1
- package/web-ui/modules/app.methods.session-browser.mjs +37 -6
- package/web-ui/modules/app.methods.session-trash.mjs +4 -2
- package/web-ui/modules/config-mode.computed.mjs +1 -3
- package/web-ui/modules/i18n.dict.mjs +2055 -0
- package/web-ui/modules/i18n.mjs +2 -1769
- package/web-ui/partials/index/layout-header.html +48 -34
- package/web-ui/partials/index/modal-config-template-agents.html +3 -4
- package/web-ui/partials/index/modal-health-check.html +33 -60
- package/web-ui/partials/index/panel-config-claude.html +35 -15
- package/web-ui/partials/index/panel-config-codex.html +47 -19
- package/web-ui/partials/index/panel-config-openclaw.html +8 -3
- package/web-ui/partials/index/panel-dashboard.html +186 -0
- package/web-ui/partials/index/panel-docs.html +1 -1
- package/web-ui/partials/index/panel-market.html +3 -0
- package/web-ui/partials/index/panel-orchestration.html +3 -0
- package/web-ui/partials/index/panel-plugins.html +16 -10
- package/web-ui/partials/index/panel-sessions.html +8 -3
- package/web-ui/partials/index/panel-settings.html +1 -1
- package/web-ui/partials/index/panel-usage.html +9 -1
- package/web-ui/res/logo-pack.webp +0 -0
- package/web-ui/styles/controls-forms.css +58 -4
- package/web-ui/styles/dashboard.css +274 -0
- package/web-ui/styles/layout-shell.css +3 -2
- package/web-ui/styles/responsive.css +0 -2
- package/web-ui/styles/sessions-list.css +5 -7
- package/web-ui/styles/sessions-toolbar-trash.css +4 -4
- package/web-ui/styles/sessions-usage.css +33 -0
- package/web-ui/styles.css +1 -0
- package/res/logo.png +0 -0
- /package/{res → web-ui/res}/json5.min.js +0 -0
- /package/{res → web-ui/res}/vue.global.prod.js +0 -0
package/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ const yauzl = require('yauzl');
|
|
|
10
10
|
const { exec, execSync, spawn, spawnSync } = require('child_process');
|
|
11
11
|
const http = require('http');
|
|
12
12
|
const https = require('https');
|
|
13
|
+
const net = require('net');
|
|
13
14
|
const readline = require('readline');
|
|
14
15
|
const {
|
|
15
16
|
expandHomePath,
|
|
@@ -42,8 +43,9 @@ const {
|
|
|
42
43
|
extractModelNames,
|
|
43
44
|
hasModelsListPayload,
|
|
44
45
|
buildModelsCacheKey,
|
|
46
|
+
buildApiProbeUrlCandidates,
|
|
45
47
|
buildModelProbeSpec,
|
|
46
|
-
|
|
48
|
+
buildModelProbeSpecs,
|
|
47
49
|
extractModelResponseText,
|
|
48
50
|
normalizeWireApi,
|
|
49
51
|
getSupplementalModelsForBaseUrl,
|
|
@@ -73,7 +75,16 @@ const {
|
|
|
73
75
|
validateTaskPlan,
|
|
74
76
|
executeTaskPlan
|
|
75
77
|
} = require('./lib/task-orchestrator');
|
|
78
|
+
const {
|
|
79
|
+
readAutomationConfig,
|
|
80
|
+
matchAutomationRule,
|
|
81
|
+
buildAutomationEventKey,
|
|
82
|
+
isCronMatch,
|
|
83
|
+
dispatchAutomationNotifiers,
|
|
84
|
+
formatTaskRunNotificationPayload
|
|
85
|
+
} = require('./lib/automation');
|
|
76
86
|
const { buildConfigHealthReport: buildConfigHealthReportCore } = require('./cli/config-health');
|
|
87
|
+
const { buildDoctorReport, buildDoctorLegacyPayload, renderDoctorMarkdown } = require('./cli/doctor-core');
|
|
77
88
|
const {
|
|
78
89
|
createAuthProfileController
|
|
79
90
|
} = require('./cli/auth-profiles');
|
|
@@ -125,6 +136,7 @@ const {
|
|
|
125
136
|
deleteSkills,
|
|
126
137
|
deleteCodexSkills
|
|
127
138
|
} = require('./cli/skills');
|
|
139
|
+
const { cmdImportSkills: cmdImportSkillsFromUrl } = require('./cli/import-skills-url');
|
|
128
140
|
const {
|
|
129
141
|
getFileStatSafe,
|
|
130
142
|
isBootstrapLikeText,
|
|
@@ -182,6 +194,10 @@ const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
|
182
194
|
const CLAUDE_SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
|
|
183
195
|
const CLAUDE_MD_FILE_NAME = 'CLAUDE.md';
|
|
184
196
|
const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
|
|
197
|
+
const CODEBUDDY_DIR = path.join(os.homedir(), '.codebuddy');
|
|
198
|
+
const CODEBUDDY_PROJECTS_DIR = path.join(CODEBUDDY_DIR, 'projects');
|
|
199
|
+
const GEMINI_DIR = path.join(os.homedir(), '.gemini');
|
|
200
|
+
const GEMINI_TMP_DIR = path.join(GEMINI_DIR, 'tmp');
|
|
185
201
|
const RECENT_CONFIGS_FILE = path.join(CONFIG_DIR, 'recent-configs.json');
|
|
186
202
|
const WORKFLOW_DEFINITIONS_FILE = path.join(CONFIG_DIR, 'codexmate-workflows.json');
|
|
187
203
|
const WORKFLOW_RUNS_FILE = path.join(CONFIG_DIR, 'codexmate-workflow-runs.jsonl');
|
|
@@ -189,6 +205,8 @@ const TASK_QUEUE_FILE = path.join(CONFIG_DIR, 'codexmate-task-queue.json');
|
|
|
189
205
|
const TASK_RUNS_FILE = path.join(CONFIG_DIR, 'codexmate-task-runs.jsonl');
|
|
190
206
|
const TASK_RUN_DETAILS_DIR = path.join(CONFIG_DIR, 'codexmate-task-runs');
|
|
191
207
|
const TASK_QUEUE_WORKER_FILE = path.join(CONFIG_DIR, 'codexmate-task-queue-worker.json');
|
|
208
|
+
const TASK_ARTIFACTS_DIR = path.join(CONFIG_DIR, 'codexmate-task-artifacts');
|
|
209
|
+
const AUTOMATION_CONFIG_FILE = path.join(CONFIG_DIR, 'codexmate-automation.json');
|
|
192
210
|
const DEFAULT_CLAUDE_MODEL = 'glm-4.7';
|
|
193
211
|
const DEFAULT_MODEL_CONTEXT_WINDOW = 190000;
|
|
194
212
|
const DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT = 185000;
|
|
@@ -259,6 +277,18 @@ const CLI_INSTALL_TARGETS = Object.freeze([
|
|
|
259
277
|
packageName: '@anthropic-ai/claude-code',
|
|
260
278
|
bins: ['claude']
|
|
261
279
|
},
|
|
280
|
+
{
|
|
281
|
+
id: 'codebuddy',
|
|
282
|
+
name: 'CodeBuddy Code',
|
|
283
|
+
packageName: '@tencent-ai/codebuddy-code',
|
|
284
|
+
bins: ['codebuddy']
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
id: 'gemini',
|
|
288
|
+
name: 'Gemini CLI',
|
|
289
|
+
packageName: '@google/gemini-cli',
|
|
290
|
+
bins: ['gemini']
|
|
291
|
+
},
|
|
262
292
|
{
|
|
263
293
|
id: 'codex',
|
|
264
294
|
name: 'Codex CLI',
|
|
@@ -272,7 +302,7 @@ const HTTPS_KEEP_ALIVE_AGENT = new https.Agent({ keepAlive: true });
|
|
|
272
302
|
|
|
273
303
|
const openaiBridgeHandler = createOpenaiBridgeHttpHandler({
|
|
274
304
|
settingsFile: OPENAI_BRIDGE_SETTINGS_FILE,
|
|
275
|
-
expectedToken: '
|
|
305
|
+
expectedToken: typeof process.env.CODEXMATE_HTTP_TOKEN === 'string' ? process.env.CODEXMATE_HTTP_TOKEN.trim() : '',
|
|
276
306
|
maxBodySize: MAX_API_BODY_SIZE,
|
|
277
307
|
httpAgent: HTTP_KEEP_ALIVE_AGENT,
|
|
278
308
|
httpsAgent: HTTPS_KEEP_ALIVE_AGENT
|
|
@@ -556,7 +586,9 @@ let g_sessionListCache = new Map();
|
|
|
556
586
|
let g_sessionInventoryCache = new Map();
|
|
557
587
|
let g_sessionFileLookupCache = {
|
|
558
588
|
codex: new Map(),
|
|
559
|
-
claude: new Map()
|
|
589
|
+
claude: new Map(),
|
|
590
|
+
gemini: new Map(),
|
|
591
|
+
codebuddy: new Map()
|
|
560
592
|
};
|
|
561
593
|
let g_exactMessageCountCache = new Map();
|
|
562
594
|
let g_modelsCache = new Map();
|
|
@@ -1251,6 +1283,31 @@ function getClaudeProjectsDir() {
|
|
|
1251
1283
|
return resolveExistingDir(candidates, CLAUDE_PROJECTS_DIR);
|
|
1252
1284
|
}
|
|
1253
1285
|
|
|
1286
|
+
function getGeminiTmpDir() {
|
|
1287
|
+
const candidates = [];
|
|
1288
|
+
const envGeminiHome = process.env.GEMINI_HOME;
|
|
1289
|
+
if (envGeminiHome) {
|
|
1290
|
+
candidates.push(path.join(envGeminiHome, 'tmp'));
|
|
1291
|
+
}
|
|
1292
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME;
|
|
1293
|
+
if (xdgConfig) {
|
|
1294
|
+
candidates.push(path.join(xdgConfig, 'gemini', 'tmp'));
|
|
1295
|
+
}
|
|
1296
|
+
candidates.push(path.join(os.homedir(), '.config', 'gemini', 'tmp'));
|
|
1297
|
+
candidates.push(GEMINI_TMP_DIR);
|
|
1298
|
+
return resolveExistingDir(candidates, GEMINI_TMP_DIR);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function getCodeBuddyProjectsDir() {
|
|
1302
|
+
const candidates = [];
|
|
1303
|
+
const envHome = process.env.CODEBUDDY_CODE_HOME_DIR || process.env.CODEBUDDY_HOME;
|
|
1304
|
+
if (envHome) {
|
|
1305
|
+
candidates.push(path.join(envHome, 'projects'));
|
|
1306
|
+
}
|
|
1307
|
+
candidates.push(CODEBUDDY_PROJECTS_DIR);
|
|
1308
|
+
return resolveExistingDir(candidates, CODEBUDDY_PROJECTS_DIR);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1254
1311
|
function readModelsCacheEntry(cacheKey) {
|
|
1255
1312
|
if (!cacheKey) return null;
|
|
1256
1313
|
const entry = g_modelsCache.get(cacheKey);
|
|
@@ -2498,6 +2555,19 @@ function countConversationMessagesInRecords(records, source) {
|
|
|
2498
2555
|
}
|
|
2499
2556
|
continue;
|
|
2500
2557
|
}
|
|
2558
|
+
if (source === 'codebuddy') {
|
|
2559
|
+
if (record && record.type === 'message') {
|
|
2560
|
+
const role = normalizeRole(record.role);
|
|
2561
|
+
if (role === 'assistant' || role === 'user' || role === 'system') {
|
|
2562
|
+
const content = record.message?.content ?? record.content ?? '';
|
|
2563
|
+
messages.push({
|
|
2564
|
+
role,
|
|
2565
|
+
text: extractMessageText(content)
|
|
2566
|
+
});
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
continue;
|
|
2570
|
+
}
|
|
2501
2571
|
|
|
2502
2572
|
const role = normalizeRole(record.type);
|
|
2503
2573
|
if (role === 'assistant' || role === 'user' || role === 'system') {
|
|
@@ -2519,6 +2589,28 @@ async function countConversationMessagesInFile(filePath, source) {
|
|
|
2519
2589
|
return cached;
|
|
2520
2590
|
}
|
|
2521
2591
|
|
|
2592
|
+
if (source === 'gemini') {
|
|
2593
|
+
let json;
|
|
2594
|
+
try {
|
|
2595
|
+
json = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
2596
|
+
} catch (_) {
|
|
2597
|
+
json = null;
|
|
2598
|
+
}
|
|
2599
|
+
const rawMessages = json && Array.isArray(json.messages) ? json.messages : [];
|
|
2600
|
+
const messages = [];
|
|
2601
|
+
for (const entry of rawMessages) {
|
|
2602
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
2603
|
+
const role = normalizeGeminiMessageRole(entry.type);
|
|
2604
|
+
if (!role) continue;
|
|
2605
|
+
const text = extractMessageText(extractGeminiMessageText(entry.content ?? entry.message ?? entry.text));
|
|
2606
|
+
if (!text && role !== 'system') continue;
|
|
2607
|
+
messages.push({ role, text });
|
|
2608
|
+
}
|
|
2609
|
+
const safeCount = removeLeadingSystemMessage(messages).length;
|
|
2610
|
+
writeExactMessageCountCache(filePath, source, safeCount, fileStat);
|
|
2611
|
+
return safeCount;
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2522
2614
|
let stream;
|
|
2523
2615
|
let rl;
|
|
2524
2616
|
let messageCount = 0;
|
|
@@ -2546,6 +2638,15 @@ async function countConversationMessagesInFile(filePath, source) {
|
|
|
2546
2638
|
role = normalizeRole(record.payload.role);
|
|
2547
2639
|
text = extractMessageText(record.payload.content);
|
|
2548
2640
|
}
|
|
2641
|
+
} else if (source === 'codebuddy') {
|
|
2642
|
+
if (record && record.type === 'message') {
|
|
2643
|
+
role = normalizeRole(record.role);
|
|
2644
|
+
if (role === 'assistant' || role === 'user' || role === 'system') {
|
|
2645
|
+
text = extractMessageText(record.message?.content ?? record.content ?? '');
|
|
2646
|
+
} else {
|
|
2647
|
+
role = '';
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2549
2650
|
} else {
|
|
2550
2651
|
role = normalizeRole(record.type);
|
|
2551
2652
|
if (role === 'assistant' || role === 'user' || role === 'system') {
|
|
@@ -2708,7 +2809,13 @@ async function resolveSessionTrashEntryExactMessageCount(entry) {
|
|
|
2708
2809
|
}
|
|
2709
2810
|
|
|
2710
2811
|
async function hydrateSessionTrashEntries(entries, options = {}) {
|
|
2711
|
-
const source = options.source === 'claude'
|
|
2812
|
+
const source = options.source === 'claude'
|
|
2813
|
+
? 'claude'
|
|
2814
|
+
: (options.source === 'codex'
|
|
2815
|
+
? 'codex'
|
|
2816
|
+
: (options.source === 'gemini'
|
|
2817
|
+
? 'gemini'
|
|
2818
|
+
: (options.source === 'codebuddy' ? 'codebuddy' : 'all')));
|
|
2712
2819
|
const hydratedEntries = await mapWithConcurrency(Array.isArray(entries) ? entries : [], 8, async (entry) => {
|
|
2713
2820
|
const normalizedEntry = normalizeSessionTrashEntry(entry);
|
|
2714
2821
|
if (!normalizedEntry) {
|
|
@@ -2717,7 +2824,7 @@ async function hydrateSessionTrashEntries(entries, options = {}) {
|
|
|
2717
2824
|
return await resolveSessionTrashEntryExactMessageCount(normalizedEntry);
|
|
2718
2825
|
});
|
|
2719
2826
|
|
|
2720
|
-
if (source === 'codex' || source === 'claude') {
|
|
2827
|
+
if (source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy') {
|
|
2721
2828
|
return hydratedEntries.filter((entry) => entry.source === source);
|
|
2722
2829
|
}
|
|
2723
2830
|
return hydratedEntries;
|
|
@@ -2731,7 +2838,11 @@ async function hydrateSessionItemsExactMessageCount(items) {
|
|
|
2731
2838
|
if (item.__messageCountExact === true) {
|
|
2732
2839
|
return item;
|
|
2733
2840
|
}
|
|
2734
|
-
const source = item.source === 'claude'
|
|
2841
|
+
const source = item.source === 'claude'
|
|
2842
|
+
? 'claude'
|
|
2843
|
+
: (item.source === 'codex'
|
|
2844
|
+
? 'codex'
|
|
2845
|
+
: (item.source === 'gemini' ? 'gemini' : (item.source === 'codebuddy' ? 'codebuddy' : '')));
|
|
2735
2846
|
const filePath = typeof item.filePath === 'string' ? item.filePath : '';
|
|
2736
2847
|
if (!source || !filePath || !fs.existsSync(filePath)) {
|
|
2737
2848
|
return item;
|
|
@@ -2958,6 +3069,54 @@ async function scanSessionContentForQuery(session, tokens, options = {}) {
|
|
|
2958
3069
|
? Math.max(1024, rawMaxBytes)
|
|
2959
3070
|
: 0;
|
|
2960
3071
|
const state = createSessionQueryScanState(tokens, options);
|
|
3072
|
+
if (session.source === 'gemini') {
|
|
3073
|
+
if (state.roleFilter !== 'all') {
|
|
3074
|
+
let json;
|
|
3075
|
+
try {
|
|
3076
|
+
json = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
3077
|
+
} catch (_) {
|
|
3078
|
+
json = null;
|
|
3079
|
+
}
|
|
3080
|
+
const rawMessages = json && Array.isArray(json.messages) ? json.messages : [];
|
|
3081
|
+
for (const entry of rawMessages) {
|
|
3082
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
3083
|
+
const role = normalizeGeminiMessageRole(entry.type);
|
|
3084
|
+
if (!role) continue;
|
|
3085
|
+
const text = extractMessageText(extractGeminiMessageText(entry.content ?? entry.message ?? entry.text));
|
|
3086
|
+
if (!text) continue;
|
|
3087
|
+
if (consumeSessionQueryMessage(state, { role, text })) {
|
|
3088
|
+
break;
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
return buildSessionQueryScanResult(state);
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
let text = '';
|
|
3095
|
+
try {
|
|
3096
|
+
const stat = fs.statSync(filePath);
|
|
3097
|
+
const targetBytes = maxBytes > 0 ? Math.min(maxBytes, stat.size || 0) : Math.min(stat.size || 0, 512 * 1024);
|
|
3098
|
+
const fd = fs.openSync(filePath, 'r');
|
|
3099
|
+
const buf = Buffer.alloc(targetBytes);
|
|
3100
|
+
const bytes = fs.readSync(fd, buf, 0, targetBytes, 0);
|
|
3101
|
+
fs.closeSync(fd);
|
|
3102
|
+
text = bytes > 0 ? buf.slice(0, bytes).toString('utf-8') : '';
|
|
3103
|
+
} catch (_) {
|
|
3104
|
+
try {
|
|
3105
|
+
text = fs.readFileSync(filePath, 'utf-8');
|
|
3106
|
+
} catch (_) {
|
|
3107
|
+
text = '';
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
if (!matchTokensInText(text, state.tokens, state.mode)) {
|
|
3112
|
+
return buildSessionQueryScanResult(state);
|
|
3113
|
+
}
|
|
3114
|
+
state.count = 1;
|
|
3115
|
+
if (state.snippetLimit > 0) {
|
|
3116
|
+
state.snippets.push(truncateText(text));
|
|
3117
|
+
}
|
|
3118
|
+
return buildSessionQueryScanResult(state);
|
|
3119
|
+
}
|
|
2961
3120
|
let stream;
|
|
2962
3121
|
let rl;
|
|
2963
3122
|
try {
|
|
@@ -3240,7 +3399,9 @@ function getSessionInventoryCache(cacheKey, forceRefresh = false) {
|
|
|
3240
3399
|
}
|
|
3241
3400
|
|
|
3242
3401
|
function registerSessionFileLookupEntries(source, sessions = []) {
|
|
3243
|
-
const normalizedSource = source === 'claude'
|
|
3402
|
+
const normalizedSource = source === 'claude' || source === 'gemini' || source === 'codebuddy'
|
|
3403
|
+
? source
|
|
3404
|
+
: 'codex';
|
|
3244
3405
|
const store = g_sessionFileLookupCache[normalizedSource];
|
|
3245
3406
|
if (!(store instanceof Map) || !Array.isArray(sessions)) {
|
|
3246
3407
|
return;
|
|
@@ -3279,7 +3440,9 @@ function setSessionInventoryCache(cacheKey, source, value) {
|
|
|
3279
3440
|
}
|
|
3280
3441
|
|
|
3281
3442
|
function listSessionInventoryBySource(source, limit, scanOptions = {}, options = {}) {
|
|
3282
|
-
const normalizedSource = source === 'claude'
|
|
3443
|
+
const normalizedSource = source === 'claude' || source === 'gemini' || source === 'codebuddy'
|
|
3444
|
+
? source
|
|
3445
|
+
: 'codex';
|
|
3283
3446
|
const forceRefresh = !!options.forceRefresh;
|
|
3284
3447
|
const cacheKey = buildSessionInventoryCacheKey(normalizedSource, limit, scanOptions);
|
|
3285
3448
|
const cached = getSessionInventoryCache(cacheKey, forceRefresh);
|
|
@@ -3289,7 +3452,11 @@ function listSessionInventoryBySource(source, limit, scanOptions = {}, options =
|
|
|
3289
3452
|
|
|
3290
3453
|
const sessions = normalizedSource === 'claude'
|
|
3291
3454
|
? listClaudeSessions(limit, scanOptions)
|
|
3292
|
-
:
|
|
3455
|
+
: (normalizedSource === 'gemini'
|
|
3456
|
+
? listGeminiSessions(limit, scanOptions)
|
|
3457
|
+
: (normalizedSource === 'codebuddy'
|
|
3458
|
+
? listCodeBuddySessions(limit, scanOptions)
|
|
3459
|
+
: listCodexSessions(limit, scanOptions)));
|
|
3293
3460
|
setSessionInventoryCache(cacheKey, normalizedSource, sessions);
|
|
3294
3461
|
return sessions;
|
|
3295
3462
|
}
|
|
@@ -3299,7 +3466,9 @@ function invalidateSessionListCache() {
|
|
|
3299
3466
|
g_sessionInventoryCache.clear();
|
|
3300
3467
|
g_sessionFileLookupCache = {
|
|
3301
3468
|
codex: new Map(),
|
|
3302
|
-
claude: new Map()
|
|
3469
|
+
claude: new Map(),
|
|
3470
|
+
gemini: new Map(),
|
|
3471
|
+
codebuddy: new Map()
|
|
3303
3472
|
};
|
|
3304
3473
|
}
|
|
3305
3474
|
|
|
@@ -3891,88 +4060,399 @@ function parseClaudeSessionSummary(filePath, options = {}) {
|
|
|
3891
4060
|
};
|
|
3892
4061
|
}
|
|
3893
4062
|
|
|
3894
|
-
function
|
|
3895
|
-
const codexSessionsDir = getCodexSessionsDir();
|
|
3896
|
-
const scanFactor = Number.isFinite(Number(options.scanFactor))
|
|
3897
|
-
? Math.max(1, Number(options.scanFactor))
|
|
3898
|
-
: SESSION_SCAN_FACTOR;
|
|
3899
|
-
const minFiles = Number.isFinite(Number(options.minFiles))
|
|
3900
|
-
? Math.max(1, Number(options.minFiles))
|
|
3901
|
-
: Math.min(SESSION_SCAN_MIN_FILES, MAX_SESSION_LIST_SIZE * SESSION_SCAN_FACTOR);
|
|
3902
|
-
const targetCount = Number.isFinite(Number(options.targetCount))
|
|
3903
|
-
? Math.max(1, Math.floor(Number(options.targetCount)))
|
|
3904
|
-
: Math.max(1, Math.floor(limit * scanFactor));
|
|
3905
|
-
const scanCount = Number.isFinite(Number(options.scanCount))
|
|
3906
|
-
? Math.max(targetCount, Math.floor(Number(options.scanCount)))
|
|
3907
|
-
: Math.max(targetCount, minFiles);
|
|
3908
|
-
const maxFilesScanned = Number.isFinite(Number(options.maxFilesScanned))
|
|
3909
|
-
? Math.max(scanCount, Math.floor(Number(options.maxFilesScanned)))
|
|
3910
|
-
: Math.max(scanCount * 2, minFiles);
|
|
4063
|
+
function parseCodeBuddySessionSummary(filePath, options = {}) {
|
|
3911
4064
|
const summaryReadBytes = Number.isFinite(Number(options.summaryReadBytes))
|
|
3912
4065
|
? Math.max(1024, Math.floor(Number(options.summaryReadBytes)))
|
|
3913
4066
|
: SESSION_SUMMARY_READ_BYTES;
|
|
3914
4067
|
const titleReadBytes = Number.isFinite(Number(options.titleReadBytes))
|
|
3915
4068
|
? Math.max(1024, Math.floor(Number(options.titleReadBytes)))
|
|
3916
4069
|
: SESSION_TITLE_READ_BYTES;
|
|
3917
|
-
const
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
}
|
|
3921
|
-
const sessions = [];
|
|
4070
|
+
const records = parseJsonlHeadRecords(filePath, summaryReadBytes);
|
|
4071
|
+
if (records.length === 0) {
|
|
4072
|
+
return null;
|
|
4073
|
+
}
|
|
3922
4074
|
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
4075
|
+
let stat;
|
|
4076
|
+
try {
|
|
4077
|
+
stat = fs.statSync(filePath);
|
|
4078
|
+
} catch (_) {
|
|
4079
|
+
return null;
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
let sessionId = path.basename(filePath, '.jsonl');
|
|
4083
|
+
let cwd = '';
|
|
4084
|
+
let firstPrompt = '';
|
|
4085
|
+
let messageCount = 0;
|
|
4086
|
+
let totalTokens = 0;
|
|
4087
|
+
let contextWindow = 0;
|
|
4088
|
+
let inputTokens = 0;
|
|
4089
|
+
let cachedInputTokens = 0;
|
|
4090
|
+
let outputTokens = 0;
|
|
4091
|
+
let reasoningOutputTokens = 0;
|
|
4092
|
+
let provider = 'codebuddy';
|
|
4093
|
+
let model = '';
|
|
4094
|
+
const models = [];
|
|
4095
|
+
const usageState = { totalTokens, contextWindow, inputTokens, cachedInputTokens, outputTokens, reasoningOutputTokens };
|
|
4096
|
+
const previewMessages = [];
|
|
4097
|
+
let createdAt = '';
|
|
4098
|
+
let updatedAt = stat.mtime.toISOString();
|
|
4099
|
+
|
|
4100
|
+
for (const record of records) {
|
|
4101
|
+
if (!createdAt && record && record.timestamp) {
|
|
4102
|
+
createdAt = toIsoTime(record.timestamp, createdAt);
|
|
4103
|
+
}
|
|
4104
|
+
if (record && record.timestamp) {
|
|
4105
|
+
updatedAt = updateLatestIso(updatedAt, record.timestamp);
|
|
3930
4106
|
}
|
|
3931
4107
|
|
|
3932
|
-
|
|
3933
|
-
|
|
4108
|
+
applySessionUsageSummaryFromRecord(usageState, record, 'codebuddy');
|
|
4109
|
+
totalTokens = usageState.totalTokens || 0;
|
|
4110
|
+
contextWindow = usageState.contextWindow || 0;
|
|
4111
|
+
inputTokens = usageState.inputTokens || 0;
|
|
4112
|
+
cachedInputTokens = usageState.cachedInputTokens || 0;
|
|
4113
|
+
outputTokens = usageState.outputTokens || 0;
|
|
4114
|
+
reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
|
|
4115
|
+
|
|
4116
|
+
if (record && typeof record.sessionId === 'string' && record.sessionId.trim()) {
|
|
4117
|
+
sessionId = record.sessionId.trim();
|
|
4118
|
+
}
|
|
4119
|
+
if (!cwd && record && typeof record.cwd === 'string' && record.cwd.trim()) {
|
|
4120
|
+
cwd = record.cwd.trim();
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
provider = readExplicitSessionProviderFromRecord(record) || provider;
|
|
4124
|
+
const recordModels = readSessionModelsFromRecord(record);
|
|
4125
|
+
for (const recordModel of recordModels) {
|
|
4126
|
+
if (!models.includes(recordModel)) {
|
|
4127
|
+
models.push(recordModel);
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
model = recordModels[0] || model;
|
|
4131
|
+
|
|
4132
|
+
if (record && record.type === 'message') {
|
|
4133
|
+
const role = normalizeRole(record.role);
|
|
4134
|
+
if (role === 'assistant' || role === 'user' || role === 'system') {
|
|
4135
|
+
const content = record.message?.content ?? record.content ?? '';
|
|
4136
|
+
previewMessages.push({
|
|
4137
|
+
role,
|
|
4138
|
+
text: extractMessageText(content)
|
|
4139
|
+
});
|
|
4140
|
+
}
|
|
3934
4141
|
}
|
|
3935
4142
|
}
|
|
3936
4143
|
|
|
3937
|
-
|
|
4144
|
+
const tailRecords = parseJsonlTailRecords(filePath, summaryReadBytes);
|
|
4145
|
+
for (const record of tailRecords) {
|
|
4146
|
+
applySessionUsageSummaryFromRecord(usageState, record, 'codebuddy');
|
|
4147
|
+
totalTokens = usageState.totalTokens || 0;
|
|
4148
|
+
contextWindow = usageState.contextWindow || 0;
|
|
4149
|
+
inputTokens = usageState.inputTokens || 0;
|
|
4150
|
+
cachedInputTokens = usageState.cachedInputTokens || 0;
|
|
4151
|
+
outputTokens = usageState.outputTokens || 0;
|
|
4152
|
+
reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
|
|
4153
|
+
provider = readExplicitSessionProviderFromRecord(record) || provider;
|
|
4154
|
+
const recordModels = readSessionModelsFromRecord(record);
|
|
4155
|
+
for (const recordModel of recordModels) {
|
|
4156
|
+
if (!models.includes(recordModel)) {
|
|
4157
|
+
models.push(recordModel);
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
model = recordModels[0] || model;
|
|
4161
|
+
}
|
|
4162
|
+
|
|
4163
|
+
const filteredPreviewMessages = removeLeadingSystemMessage(previewMessages);
|
|
4164
|
+
messageCount = filteredPreviewMessages.length;
|
|
4165
|
+
const firstUser = filteredPreviewMessages.find(item => item.role === 'user' && item.text);
|
|
4166
|
+
if (firstUser) {
|
|
4167
|
+
firstPrompt = truncateText(firstUser.text);
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4170
|
+
if (!firstPrompt) {
|
|
4171
|
+
const titleRecords = parseJsonlHeadRecords(filePath, titleReadBytes);
|
|
4172
|
+
const titleMessages = [];
|
|
4173
|
+
for (const record of titleRecords) {
|
|
4174
|
+
if (record && record.type === 'message') {
|
|
4175
|
+
const role = normalizeRole(record.role);
|
|
4176
|
+
if (role === 'assistant' || role === 'user' || role === 'system') {
|
|
4177
|
+
const content = record.message?.content ?? record.content ?? '';
|
|
4178
|
+
titleMessages.push({
|
|
4179
|
+
role,
|
|
4180
|
+
text: extractMessageText(content)
|
|
4181
|
+
});
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4186
|
+
const filteredTitleMessages = removeLeadingSystemMessage(titleMessages);
|
|
4187
|
+
const titleUser = filteredTitleMessages.find(item => item.role === 'user' && item.text);
|
|
4188
|
+
if (titleUser) {
|
|
4189
|
+
firstPrompt = truncateText(titleUser.text);
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
|
|
4193
|
+
messageCount = Math.max(0, messageCount);
|
|
4194
|
+
|
|
4195
|
+
return {
|
|
4196
|
+
source: 'codebuddy',
|
|
4197
|
+
sourceLabel: 'CodeBuddy Code',
|
|
4198
|
+
provider,
|
|
4199
|
+
model,
|
|
4200
|
+
models,
|
|
4201
|
+
sessionId,
|
|
4202
|
+
title: firstPrompt || sessionId,
|
|
4203
|
+
cwd,
|
|
4204
|
+
createdAt,
|
|
4205
|
+
updatedAt,
|
|
4206
|
+
messageCount,
|
|
4207
|
+
totalTokens,
|
|
4208
|
+
contextWindow,
|
|
4209
|
+
inputTokens,
|
|
4210
|
+
cachedInputTokens,
|
|
4211
|
+
outputTokens,
|
|
4212
|
+
reasoningOutputTokens,
|
|
4213
|
+
__messageCountExact: isSessionSummaryMessageCountExact(stat, summaryReadBytes),
|
|
4214
|
+
filePath,
|
|
4215
|
+
keywords: [],
|
|
4216
|
+
capabilities: { code: true }
|
|
4217
|
+
};
|
|
3938
4218
|
}
|
|
3939
4219
|
|
|
3940
|
-
function
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
4220
|
+
function extractGeminiMessageText(content) {
|
|
4221
|
+
if (typeof content === 'string') {
|
|
4222
|
+
return content;
|
|
4223
|
+
}
|
|
4224
|
+
if (Array.isArray(content)) {
|
|
4225
|
+
const parts = [];
|
|
4226
|
+
for (const item of content) {
|
|
4227
|
+
if (!item) continue;
|
|
4228
|
+
if (typeof item === 'string') {
|
|
4229
|
+
parts.push(item);
|
|
4230
|
+
continue;
|
|
4231
|
+
}
|
|
4232
|
+
if (typeof item.text === 'string' && item.text.trim()) {
|
|
4233
|
+
parts.push(item.text);
|
|
4234
|
+
continue;
|
|
4235
|
+
}
|
|
4236
|
+
if (typeof item.content === 'string' && item.content.trim()) {
|
|
4237
|
+
parts.push(item.content);
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
4240
|
+
return parts.filter(Boolean).join('\n');
|
|
4241
|
+
}
|
|
4242
|
+
if (content && typeof content === 'object') {
|
|
4243
|
+
if (typeof content.text === 'string') {
|
|
4244
|
+
return content.text;
|
|
4245
|
+
}
|
|
4246
|
+
if (typeof content.content === 'string') {
|
|
4247
|
+
return content.content;
|
|
4248
|
+
}
|
|
4249
|
+
if (Array.isArray(content.parts)) {
|
|
4250
|
+
return extractGeminiMessageText(content.parts);
|
|
4251
|
+
}
|
|
4252
|
+
if (Array.isArray(content.content)) {
|
|
4253
|
+
return extractGeminiMessageText(content.content);
|
|
4254
|
+
}
|
|
3944
4255
|
}
|
|
4256
|
+
return '';
|
|
4257
|
+
}
|
|
3945
4258
|
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
const scanCount = Number.isFinite(Number(options.scanCount))
|
|
3956
|
-
? Math.max(targetCount, Math.floor(Number(options.scanCount)))
|
|
3957
|
-
: Math.max(targetCount, minFiles);
|
|
3958
|
-
const maxFilesScanned = Number.isFinite(Number(options.maxFilesScanned))
|
|
3959
|
-
? Math.max(scanCount, Math.floor(Number(options.maxFilesScanned)))
|
|
3960
|
-
: Math.max(scanCount * 2, minFiles);
|
|
4259
|
+
function normalizeGeminiMessageRole(type) {
|
|
4260
|
+
const t = typeof type === 'string' ? type.trim().toLowerCase() : '';
|
|
4261
|
+
if (t === 'user') return 'user';
|
|
4262
|
+
if (t === 'gemini' || t === 'assistant' || t === 'model') return 'assistant';
|
|
4263
|
+
if (t === 'system' || t === 'info' || t === 'warning' || t === 'error') return 'system';
|
|
4264
|
+
return '';
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4267
|
+
function parseGeminiSessionSummary(filePath, options = {}) {
|
|
3961
4268
|
const summaryReadBytes = Number.isFinite(Number(options.summaryReadBytes))
|
|
3962
4269
|
? Math.max(1024, Math.floor(Number(options.summaryReadBytes)))
|
|
3963
4270
|
: SESSION_SUMMARY_READ_BYTES;
|
|
3964
4271
|
const titleReadBytes = Number.isFinite(Number(options.titleReadBytes))
|
|
3965
4272
|
? Math.max(1024, Math.floor(Number(options.titleReadBytes)))
|
|
3966
4273
|
: SESSION_TITLE_READ_BYTES;
|
|
3967
|
-
|
|
3968
|
-
const sessions = [];
|
|
3969
|
-
let projectDirs = [];
|
|
4274
|
+
let stat;
|
|
3970
4275
|
try {
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
}
|
|
3975
|
-
|
|
4276
|
+
stat = fs.statSync(filePath);
|
|
4277
|
+
} catch (_) {
|
|
4278
|
+
return null;
|
|
4279
|
+
}
|
|
4280
|
+
|
|
4281
|
+
const fileName = path.basename(filePath);
|
|
4282
|
+
const projectHash = path.basename(path.dirname(path.dirname(filePath)));
|
|
4283
|
+
let sessionId = path.basename(filePath, '.json');
|
|
4284
|
+
let createdAt = '';
|
|
4285
|
+
let updatedAt = stat.mtime.toISOString();
|
|
4286
|
+
let provider = 'gemini';
|
|
4287
|
+
let model = '';
|
|
4288
|
+
const models = [];
|
|
4289
|
+
let firstPrompt = '';
|
|
4290
|
+
let messageCount = 0;
|
|
4291
|
+
|
|
4292
|
+
let headText = '';
|
|
4293
|
+
try {
|
|
4294
|
+
const fd = fs.openSync(filePath, 'r');
|
|
4295
|
+
const buf = Buffer.alloc(summaryReadBytes);
|
|
4296
|
+
const bytes = fs.readSync(fd, buf, 0, summaryReadBytes, 0);
|
|
4297
|
+
fs.closeSync(fd);
|
|
4298
|
+
headText = bytes > 0 ? buf.slice(0, bytes).toString('utf-8') : '';
|
|
4299
|
+
} catch (_) {
|
|
4300
|
+
headText = '';
|
|
4301
|
+
}
|
|
4302
|
+
|
|
4303
|
+
if (headText) {
|
|
4304
|
+
const sessionIdMatch = headText.match(/"sessionId"\s*:\s*"([^"]+)"/);
|
|
4305
|
+
if (sessionIdMatch) {
|
|
4306
|
+
sessionId = sessionIdMatch[1] || sessionId;
|
|
4307
|
+
}
|
|
4308
|
+
const startMatch = headText.match(/"startTime"\s*:\s*"([^"]+)"/);
|
|
4309
|
+
if (startMatch) {
|
|
4310
|
+
createdAt = toIsoTime(startMatch[1], createdAt);
|
|
4311
|
+
}
|
|
4312
|
+
const updatedMatch = headText.match(/"lastUpdated"\s*:\s*"([^"]+)"/);
|
|
4313
|
+
if (updatedMatch) {
|
|
4314
|
+
updatedAt = toIsoTime(updatedMatch[1], updatedAt);
|
|
4315
|
+
}
|
|
4316
|
+
const modelMatch = headText.match(/"model"\s*:\s*"([^"]+)"/);
|
|
4317
|
+
if (modelMatch && modelMatch[1]) {
|
|
4318
|
+
model = modelMatch[1];
|
|
4319
|
+
models.push(model);
|
|
4320
|
+
}
|
|
4321
|
+
const summaryMatch = headText.match(/"summary"\s*:\s*"([^"]+)"/);
|
|
4322
|
+
if (summaryMatch && summaryMatch[1]) {
|
|
4323
|
+
firstPrompt = truncateText(summaryMatch[1]);
|
|
4324
|
+
}
|
|
4325
|
+
if (!firstPrompt) {
|
|
4326
|
+
const userIdx = headText.search(/"type"\s*:\s*"user"/);
|
|
4327
|
+
if (userIdx >= 0) {
|
|
4328
|
+
const slice = headText.slice(userIdx, Math.min(headText.length, userIdx + titleReadBytes));
|
|
4329
|
+
const contentStringMatch = slice.match(/"content"\s*:\s*"((?:\\\\.|[^\"\\\\])*)"/);
|
|
4330
|
+
const textMatch = slice.match(/"text"\s*:\s*"((?:\\\\.|[^\"\\\\])*)"/);
|
|
4331
|
+
const raw = (contentStringMatch && contentStringMatch[1]) || (textMatch && textMatch[1]) || '';
|
|
4332
|
+
if (raw) {
|
|
4333
|
+
try {
|
|
4334
|
+
firstPrompt = truncateText(JSON.parse(`"${raw}"`));
|
|
4335
|
+
} catch (_) {
|
|
4336
|
+
firstPrompt = truncateText(raw);
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
}
|
|
4342
|
+
|
|
4343
|
+
if (!createdAt) {
|
|
4344
|
+
createdAt = stat.mtime.toISOString();
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4347
|
+
const cwd = projectHash ? path.join(getGeminiTmpDir(), projectHash) : '';
|
|
4348
|
+
|
|
4349
|
+
return {
|
|
4350
|
+
source: 'gemini',
|
|
4351
|
+
sourceLabel: 'Gemini CLI',
|
|
4352
|
+
provider,
|
|
4353
|
+
model,
|
|
4354
|
+
models,
|
|
4355
|
+
sessionId,
|
|
4356
|
+
title: firstPrompt || sessionId || fileName,
|
|
4357
|
+
cwd,
|
|
4358
|
+
createdAt,
|
|
4359
|
+
updatedAt,
|
|
4360
|
+
messageCount,
|
|
4361
|
+
totalTokens: 0,
|
|
4362
|
+
contextWindow: 0,
|
|
4363
|
+
inputTokens: 0,
|
|
4364
|
+
cachedInputTokens: 0,
|
|
4365
|
+
outputTokens: 0,
|
|
4366
|
+
reasoningOutputTokens: 0,
|
|
4367
|
+
__messageCountExact: false,
|
|
4368
|
+
filePath,
|
|
4369
|
+
keywords: [],
|
|
4370
|
+
capabilities: { code: true }
|
|
4371
|
+
};
|
|
4372
|
+
}
|
|
4373
|
+
|
|
4374
|
+
function listCodexSessions(limit, options = {}) {
|
|
4375
|
+
const codexSessionsDir = getCodexSessionsDir();
|
|
4376
|
+
const scanFactor = Number.isFinite(Number(options.scanFactor))
|
|
4377
|
+
? Math.max(1, Number(options.scanFactor))
|
|
4378
|
+
: SESSION_SCAN_FACTOR;
|
|
4379
|
+
const minFiles = Number.isFinite(Number(options.minFiles))
|
|
4380
|
+
? Math.max(1, Number(options.minFiles))
|
|
4381
|
+
: Math.min(SESSION_SCAN_MIN_FILES, MAX_SESSION_LIST_SIZE * SESSION_SCAN_FACTOR);
|
|
4382
|
+
const targetCount = Number.isFinite(Number(options.targetCount))
|
|
4383
|
+
? Math.max(1, Math.floor(Number(options.targetCount)))
|
|
4384
|
+
: Math.max(1, Math.floor(limit * scanFactor));
|
|
4385
|
+
const scanCount = Number.isFinite(Number(options.scanCount))
|
|
4386
|
+
? Math.max(targetCount, Math.floor(Number(options.scanCount)))
|
|
4387
|
+
: Math.max(targetCount, minFiles);
|
|
4388
|
+
const maxFilesScanned = Number.isFinite(Number(options.maxFilesScanned))
|
|
4389
|
+
? Math.max(scanCount, Math.floor(Number(options.maxFilesScanned)))
|
|
4390
|
+
: Math.max(scanCount * 2, minFiles);
|
|
4391
|
+
const summaryReadBytes = Number.isFinite(Number(options.summaryReadBytes))
|
|
4392
|
+
? Math.max(1024, Math.floor(Number(options.summaryReadBytes)))
|
|
4393
|
+
: SESSION_SUMMARY_READ_BYTES;
|
|
4394
|
+
const titleReadBytes = Number.isFinite(Number(options.titleReadBytes))
|
|
4395
|
+
? Math.max(1024, Math.floor(Number(options.titleReadBytes)))
|
|
4396
|
+
: SESSION_TITLE_READ_BYTES;
|
|
4397
|
+
const files = collectRecentJsonlFiles(codexSessionsDir, {
|
|
4398
|
+
returnCount: scanCount,
|
|
4399
|
+
maxFilesScanned
|
|
4400
|
+
});
|
|
4401
|
+
const sessions = [];
|
|
4402
|
+
|
|
4403
|
+
for (const filePath of files) {
|
|
4404
|
+
const summary = parseCodexSessionSummary(filePath, {
|
|
4405
|
+
summaryReadBytes,
|
|
4406
|
+
titleReadBytes
|
|
4407
|
+
});
|
|
4408
|
+
if (summary) {
|
|
4409
|
+
sessions.push(summary);
|
|
4410
|
+
}
|
|
4411
|
+
|
|
4412
|
+
if (sessions.length >= targetCount) {
|
|
4413
|
+
break;
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
4416
|
+
|
|
4417
|
+
return mergeAndLimitSessions(sessions, limit);
|
|
4418
|
+
}
|
|
4419
|
+
|
|
4420
|
+
function listClaudeSessions(limit, options = {}) {
|
|
4421
|
+
const claudeProjectsDir = getClaudeProjectsDir();
|
|
4422
|
+
if (!fs.existsSync(claudeProjectsDir)) {
|
|
4423
|
+
return [];
|
|
4424
|
+
}
|
|
4425
|
+
|
|
4426
|
+
const scanFactor = Number.isFinite(Number(options.scanFactor))
|
|
4427
|
+
? Math.max(1, Number(options.scanFactor))
|
|
4428
|
+
: SESSION_SCAN_FACTOR;
|
|
4429
|
+
const minFiles = Number.isFinite(Number(options.minFiles))
|
|
4430
|
+
? Math.max(1, Number(options.minFiles))
|
|
4431
|
+
: Math.min(SESSION_SCAN_MIN_FILES, MAX_SESSION_LIST_SIZE * SESSION_SCAN_FACTOR);
|
|
4432
|
+
const targetCount = Number.isFinite(Number(options.targetCount))
|
|
4433
|
+
? Math.max(1, Math.floor(Number(options.targetCount)))
|
|
4434
|
+
: Math.max(1, Math.floor(limit * scanFactor));
|
|
4435
|
+
const scanCount = Number.isFinite(Number(options.scanCount))
|
|
4436
|
+
? Math.max(targetCount, Math.floor(Number(options.scanCount)))
|
|
4437
|
+
: Math.max(targetCount, minFiles);
|
|
4438
|
+
const maxFilesScanned = Number.isFinite(Number(options.maxFilesScanned))
|
|
4439
|
+
? Math.max(scanCount, Math.floor(Number(options.maxFilesScanned)))
|
|
4440
|
+
: Math.max(scanCount * 2, minFiles);
|
|
4441
|
+
const summaryReadBytes = Number.isFinite(Number(options.summaryReadBytes))
|
|
4442
|
+
? Math.max(1024, Math.floor(Number(options.summaryReadBytes)))
|
|
4443
|
+
: SESSION_SUMMARY_READ_BYTES;
|
|
4444
|
+
const titleReadBytes = Number.isFinite(Number(options.titleReadBytes))
|
|
4445
|
+
? Math.max(1024, Math.floor(Number(options.titleReadBytes)))
|
|
4446
|
+
: SESSION_TITLE_READ_BYTES;
|
|
4447
|
+
|
|
4448
|
+
const sessions = [];
|
|
4449
|
+
let projectDirs = [];
|
|
4450
|
+
try {
|
|
4451
|
+
projectDirs = fs.readdirSync(claudeProjectsDir, { withFileTypes: true })
|
|
4452
|
+
.filter(entry => entry.isDirectory())
|
|
4453
|
+
.map(entry => path.join(claudeProjectsDir, entry.name));
|
|
4454
|
+
} catch (e) {
|
|
4455
|
+
projectDirs = [];
|
|
3976
4456
|
}
|
|
3977
4457
|
|
|
3978
4458
|
for (const projectDir of projectDirs) {
|
|
@@ -4141,8 +4621,144 @@ function listClaudeSessions(limit, options = {}) {
|
|
|
4141
4621
|
return mergeAndLimitSessions(sessions, limit);
|
|
4142
4622
|
}
|
|
4143
4623
|
|
|
4624
|
+
function listGeminiSessions(limit, options = {}) {
|
|
4625
|
+
const geminiTmpDir = getGeminiTmpDir();
|
|
4626
|
+
if (!fs.existsSync(geminiTmpDir)) {
|
|
4627
|
+
return [];
|
|
4628
|
+
}
|
|
4629
|
+
|
|
4630
|
+
const scanFactor = Number.isFinite(Number(options.scanFactor))
|
|
4631
|
+
? Math.max(1, Number(options.scanFactor))
|
|
4632
|
+
: SESSION_SCAN_FACTOR;
|
|
4633
|
+
const minFiles = Number.isFinite(Number(options.minFiles))
|
|
4634
|
+
? Math.max(1, Number(options.minFiles))
|
|
4635
|
+
: Math.min(SESSION_SCAN_MIN_FILES, MAX_SESSION_LIST_SIZE * SESSION_SCAN_FACTOR);
|
|
4636
|
+
const targetCount = Number.isFinite(Number(options.targetCount))
|
|
4637
|
+
? Math.max(1, Math.floor(Number(options.targetCount)))
|
|
4638
|
+
: Math.max(1, Math.floor(limit * scanFactor));
|
|
4639
|
+
const scanCount = Number.isFinite(Number(options.scanCount))
|
|
4640
|
+
? Math.max(targetCount, Math.floor(Number(options.scanCount)))
|
|
4641
|
+
: Math.max(targetCount, minFiles);
|
|
4642
|
+
const maxFilesScanned = Number.isFinite(Number(options.maxFilesScanned))
|
|
4643
|
+
? Math.max(scanCount, Math.floor(Number(options.maxFilesScanned)))
|
|
4644
|
+
: Math.max(scanCount * 2, minFiles);
|
|
4645
|
+
const summaryReadBytes = Number.isFinite(Number(options.summaryReadBytes))
|
|
4646
|
+
? Math.max(1024, Math.floor(Number(options.summaryReadBytes)))
|
|
4647
|
+
: SESSION_SUMMARY_READ_BYTES;
|
|
4648
|
+
const titleReadBytes = Number.isFinite(Number(options.titleReadBytes))
|
|
4649
|
+
? Math.max(1024, Math.floor(Number(options.titleReadBytes)))
|
|
4650
|
+
: SESSION_TITLE_READ_BYTES;
|
|
4651
|
+
|
|
4652
|
+
const sessions = [];
|
|
4653
|
+
const filesMeta = [];
|
|
4654
|
+
let scanned = 0;
|
|
4655
|
+
let projectDirs = [];
|
|
4656
|
+
try {
|
|
4657
|
+
projectDirs = fs.readdirSync(geminiTmpDir, { withFileTypes: true })
|
|
4658
|
+
.filter(entry => entry.isDirectory())
|
|
4659
|
+
.map(entry => path.join(geminiTmpDir, entry.name));
|
|
4660
|
+
} catch (_) {
|
|
4661
|
+
projectDirs = [];
|
|
4662
|
+
}
|
|
4663
|
+
|
|
4664
|
+
for (const projectDir of projectDirs) {
|
|
4665
|
+
const chatsDir = path.join(projectDir, 'chats');
|
|
4666
|
+
if (!fs.existsSync(chatsDir)) {
|
|
4667
|
+
continue;
|
|
4668
|
+
}
|
|
4669
|
+
let entries = [];
|
|
4670
|
+
try {
|
|
4671
|
+
entries = fs.readdirSync(chatsDir, { withFileTypes: true });
|
|
4672
|
+
} catch (_) {
|
|
4673
|
+
entries = [];
|
|
4674
|
+
}
|
|
4675
|
+
for (const entry of entries) {
|
|
4676
|
+
if (!entry.isFile() || !entry.name.endsWith('.json')) {
|
|
4677
|
+
continue;
|
|
4678
|
+
}
|
|
4679
|
+
const fullPath = path.join(chatsDir, entry.name);
|
|
4680
|
+
try {
|
|
4681
|
+
const stat = fs.statSync(fullPath);
|
|
4682
|
+
filesMeta.push({ filePath: fullPath, mtimeMs: stat.mtimeMs || 0 });
|
|
4683
|
+
} catch (_) {}
|
|
4684
|
+
scanned += 1;
|
|
4685
|
+
if (scanned >= maxFilesScanned) {
|
|
4686
|
+
break;
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
4689
|
+
if (scanned >= maxFilesScanned) {
|
|
4690
|
+
break;
|
|
4691
|
+
}
|
|
4692
|
+
}
|
|
4693
|
+
|
|
4694
|
+
filesMeta.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
4695
|
+
for (const item of filesMeta.slice(0, scanCount)) {
|
|
4696
|
+
const summary = parseGeminiSessionSummary(item.filePath, { summaryReadBytes, titleReadBytes });
|
|
4697
|
+
if (summary) {
|
|
4698
|
+
sessions.push(summary);
|
|
4699
|
+
}
|
|
4700
|
+
if (sessions.length >= targetCount) {
|
|
4701
|
+
break;
|
|
4702
|
+
}
|
|
4703
|
+
}
|
|
4704
|
+
|
|
4705
|
+
return mergeAndLimitSessions(sessions, limit);
|
|
4706
|
+
}
|
|
4707
|
+
|
|
4708
|
+
function listCodeBuddySessions(limit, options = {}) {
|
|
4709
|
+
const projectsDir = getCodeBuddyProjectsDir();
|
|
4710
|
+
if (!fs.existsSync(projectsDir)) {
|
|
4711
|
+
return [];
|
|
4712
|
+
}
|
|
4713
|
+
|
|
4714
|
+
const scanFactor = Number.isFinite(Number(options.scanFactor))
|
|
4715
|
+
? Math.max(1, Number(options.scanFactor))
|
|
4716
|
+
: SESSION_SCAN_FACTOR;
|
|
4717
|
+
const minFiles = Number.isFinite(Number(options.minFiles))
|
|
4718
|
+
? Math.max(1, Number(options.minFiles))
|
|
4719
|
+
: Math.min(SESSION_SCAN_MIN_FILES, MAX_SESSION_LIST_SIZE * SESSION_SCAN_FACTOR);
|
|
4720
|
+
const targetCount = Number.isFinite(Number(options.targetCount))
|
|
4721
|
+
? Math.max(1, Math.floor(Number(options.targetCount)))
|
|
4722
|
+
: Math.max(1, Math.floor(limit * scanFactor));
|
|
4723
|
+
const scanCount = Number.isFinite(Number(options.scanCount))
|
|
4724
|
+
? Math.max(targetCount, Math.floor(Number(options.scanCount)))
|
|
4725
|
+
: Math.max(targetCount, minFiles);
|
|
4726
|
+
const maxFilesScanned = Number.isFinite(Number(options.maxFilesScanned))
|
|
4727
|
+
? Math.max(scanCount, Math.floor(Number(options.maxFilesScanned)))
|
|
4728
|
+
: Math.max(scanCount * 2, minFiles);
|
|
4729
|
+
const summaryReadBytes = Number.isFinite(Number(options.summaryReadBytes))
|
|
4730
|
+
? Math.max(1024, Math.floor(Number(options.summaryReadBytes)))
|
|
4731
|
+
: SESSION_SUMMARY_READ_BYTES;
|
|
4732
|
+
const titleReadBytes = Number.isFinite(Number(options.titleReadBytes))
|
|
4733
|
+
? Math.max(1024, Math.floor(Number(options.titleReadBytes)))
|
|
4734
|
+
: SESSION_TITLE_READ_BYTES;
|
|
4735
|
+
|
|
4736
|
+
const files = collectRecentJsonlFiles(projectsDir, {
|
|
4737
|
+
returnCount: scanCount,
|
|
4738
|
+
maxFilesScanned,
|
|
4739
|
+
ignoreSubPath: `${path.sep}subagents${path.sep}`
|
|
4740
|
+
});
|
|
4741
|
+
const sessions = [];
|
|
4742
|
+
for (const filePath of files) {
|
|
4743
|
+
if (path.basename(filePath) === 'history.jsonl') {
|
|
4744
|
+
continue;
|
|
4745
|
+
}
|
|
4746
|
+
const summary = parseCodeBuddySessionSummary(filePath, {
|
|
4747
|
+
summaryReadBytes,
|
|
4748
|
+
titleReadBytes
|
|
4749
|
+
});
|
|
4750
|
+
if (summary) {
|
|
4751
|
+
sessions.push(summary);
|
|
4752
|
+
}
|
|
4753
|
+
if (sessions.length >= targetCount) {
|
|
4754
|
+
break;
|
|
4755
|
+
}
|
|
4756
|
+
}
|
|
4757
|
+
return mergeAndLimitSessions(sessions, limit);
|
|
4758
|
+
}
|
|
4759
|
+
|
|
4144
4760
|
async function listAllSessions(params = {}) {
|
|
4145
|
-
const source = params.source === 'codex' || params.source === 'claude'
|
|
4761
|
+
const source = params.source === 'codex' || params.source === 'claude' || params.source === 'gemini' || params.source === 'codebuddy'
|
|
4146
4762
|
? params.source
|
|
4147
4763
|
: 'all';
|
|
4148
4764
|
const rawLimit = Number(params.limit);
|
|
@@ -4155,12 +4771,14 @@ async function listAllSessions(params = {}) {
|
|
|
4155
4771
|
const queryTokens = expandSessionQueryTokens(normalizeQueryTokens(params.query));
|
|
4156
4772
|
const hasQuery = queryTokens.length > 0;
|
|
4157
4773
|
const browseLightweight = params.browseLightweight === true && !hasQuery && !hasPathFilter;
|
|
4158
|
-
const
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4774
|
+
const queryKeyRaw = typeof params.query === 'string' ? params.query.trim() : '';
|
|
4775
|
+
const queryKey = queryKeyRaw.length > 240 ? queryKeyRaw.slice(0, 240) : queryKeyRaw;
|
|
4776
|
+
const cacheKey = hasQuery
|
|
4777
|
+
? `query:${source}:${limit}:${normalizedPathFilter}:${params.queryMode || ''}:${params.queryScope || ''}:${params.roleFilter || ''}:${Number(params.contentScanLimit) || ''}:${Number(params.contentScanBytes) || ''}:${queryKey}`
|
|
4778
|
+
: `${browseLightweight ? 'browse' : 'default'}:${source}:${limit}:${normalizedPathFilter}`;
|
|
4779
|
+
const cached = getSessionListCache(cacheKey, forceRefresh);
|
|
4780
|
+
if (cached) {
|
|
4781
|
+
return cached;
|
|
4164
4782
|
}
|
|
4165
4783
|
|
|
4166
4784
|
const scanOptions = hasPathFilter
|
|
@@ -4184,6 +4802,12 @@ async function listAllSessions(params = {}) {
|
|
|
4184
4802
|
if (source === 'all' || source === 'claude') {
|
|
4185
4803
|
sessions = sessions.concat(listSessionInventoryBySource('claude', limit, scanOptions, { forceRefresh }));
|
|
4186
4804
|
}
|
|
4805
|
+
if (source === 'all' || source === 'gemini') {
|
|
4806
|
+
sessions = sessions.concat(listSessionInventoryBySource('gemini', limit, scanOptions, { forceRefresh }));
|
|
4807
|
+
}
|
|
4808
|
+
if (source === 'all' || source === 'codebuddy') {
|
|
4809
|
+
sessions = sessions.concat(listSessionInventoryBySource('codebuddy', limit, scanOptions, { forceRefresh }));
|
|
4810
|
+
}
|
|
4187
4811
|
|
|
4188
4812
|
if (hasPathFilter) {
|
|
4189
4813
|
sessions = sessions.filter(item => matchesSessionPathFilter(item, normalizedPathFilter));
|
|
@@ -4201,9 +4825,7 @@ async function listAllSessions(params = {}) {
|
|
|
4201
4825
|
});
|
|
4202
4826
|
}
|
|
4203
4827
|
result = mergeAndLimitSessions(result, limit);
|
|
4204
|
-
|
|
4205
|
-
setSessionListCache(cacheKey, result);
|
|
4206
|
-
}
|
|
4828
|
+
setSessionListCache(cacheKey, result);
|
|
4207
4829
|
return result;
|
|
4208
4830
|
}
|
|
4209
4831
|
|
|
@@ -4266,6 +4888,8 @@ async function listSessionUsage(params = {}) {
|
|
|
4266
4888
|
listSessionBrowse,
|
|
4267
4889
|
parseCodexSessionSummary,
|
|
4268
4890
|
parseClaudeSessionSummary,
|
|
4891
|
+
parseCodeBuddySessionSummary,
|
|
4892
|
+
parseGeminiSessionSummary,
|
|
4269
4893
|
MAX_SESSION_USAGE_LIST_SIZE,
|
|
4270
4894
|
SESSION_BROWSE_SUMMARY_READ_BYTES
|
|
4271
4895
|
});
|
|
@@ -4273,10 +4897,10 @@ async function listSessionUsage(params = {}) {
|
|
|
4273
4897
|
|
|
4274
4898
|
function listSessionPaths(params = {}) {
|
|
4275
4899
|
const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
|
|
4276
|
-
if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
|
|
4900
|
+
if (source && source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'all') {
|
|
4277
4901
|
return [];
|
|
4278
4902
|
}
|
|
4279
|
-
const validSource = source === 'codex' || source === 'claude' ? source : 'all';
|
|
4903
|
+
const validSource = source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy' ? source : 'all';
|
|
4280
4904
|
const rawLimit = Number(params.limit);
|
|
4281
4905
|
const limit = Number.isFinite(rawLimit)
|
|
4282
4906
|
? Math.max(1, Math.min(rawLimit, MAX_SESSION_PATH_LIST_SIZE))
|
|
@@ -4304,6 +4928,12 @@ function listSessionPaths(params = {}) {
|
|
|
4304
4928
|
if (validSource === 'all' || validSource === 'claude') {
|
|
4305
4929
|
sessions = sessions.concat(listSessionInventoryBySource('claude', gatherLimit, scanOptions, { forceRefresh }));
|
|
4306
4930
|
}
|
|
4931
|
+
if (validSource === 'all' || validSource === 'gemini') {
|
|
4932
|
+
sessions = sessions.concat(listSessionInventoryBySource('gemini', gatherLimit, scanOptions, { forceRefresh }));
|
|
4933
|
+
}
|
|
4934
|
+
if (validSource === 'all' || validSource === 'codebuddy') {
|
|
4935
|
+
sessions = sessions.concat(listSessionInventoryBySource('codebuddy', gatherLimit, scanOptions, { forceRefresh }));
|
|
4936
|
+
}
|
|
4307
4937
|
|
|
4308
4938
|
const dedupedPaths = [];
|
|
4309
4939
|
const seen = new Set();
|
|
@@ -4329,7 +4959,14 @@ function listSessionPaths(params = {}) {
|
|
|
4329
4959
|
}
|
|
4330
4960
|
|
|
4331
4961
|
function resolveSessionFilePath(source, filePath, sessionId) {
|
|
4332
|
-
const
|
|
4962
|
+
const normalizedSource = source === 'claude' || source === 'gemini' || source === 'codebuddy'
|
|
4963
|
+
? source
|
|
4964
|
+
: 'codex';
|
|
4965
|
+
const root = normalizedSource === 'claude'
|
|
4966
|
+
? getClaudeProjectsDir()
|
|
4967
|
+
: (normalizedSource === 'gemini'
|
|
4968
|
+
? getGeminiTmpDir()
|
|
4969
|
+
: (normalizedSource === 'codebuddy' ? getCodeBuddyProjectsDir() : getCodexSessionsDir()));
|
|
4333
4970
|
if (!root || !fs.existsSync(root)) {
|
|
4334
4971
|
return '';
|
|
4335
4972
|
}
|
|
@@ -4344,7 +4981,7 @@ function resolveSessionFilePath(source, filePath, sessionId) {
|
|
|
4344
4981
|
|
|
4345
4982
|
if (typeof sessionId === 'string' && sessionId.trim()) {
|
|
4346
4983
|
const targetId = sessionId.trim().toLowerCase();
|
|
4347
|
-
const lookupStore = g_sessionFileLookupCache[
|
|
4984
|
+
const lookupStore = g_sessionFileLookupCache[normalizedSource];
|
|
4348
4985
|
if (lookupStore instanceof Map && lookupStore.has(targetId)) {
|
|
4349
4986
|
const cachedPath = lookupStore.get(targetId);
|
|
4350
4987
|
if (cachedPath && fs.existsSync(cachedPath) && isPathInside(cachedPath, root)) {
|
|
@@ -4352,8 +4989,39 @@ function resolveSessionFilePath(source, filePath, sessionId) {
|
|
|
4352
4989
|
}
|
|
4353
4990
|
lookupStore.delete(targetId);
|
|
4354
4991
|
}
|
|
4355
|
-
|
|
4356
|
-
|
|
4992
|
+
let matchedFile = '';
|
|
4993
|
+
if (normalizedSource === 'gemini') {
|
|
4994
|
+
const filesMeta = [];
|
|
4995
|
+
let projectDirs = [];
|
|
4996
|
+
try {
|
|
4997
|
+
projectDirs = fs.readdirSync(root, { withFileTypes: true })
|
|
4998
|
+
.filter(entry => entry.isDirectory())
|
|
4999
|
+
.map(entry => path.join(root, entry.name));
|
|
5000
|
+
} catch (_) {
|
|
5001
|
+
projectDirs = [];
|
|
5002
|
+
}
|
|
5003
|
+
for (const projectDir of projectDirs) {
|
|
5004
|
+
const chatsDir = path.join(projectDir, 'chats');
|
|
5005
|
+
if (!fs.existsSync(chatsDir)) continue;
|
|
5006
|
+
let entries = [];
|
|
5007
|
+
try {
|
|
5008
|
+
entries = fs.readdirSync(chatsDir, { withFileTypes: true });
|
|
5009
|
+
} catch (_) {
|
|
5010
|
+
entries = [];
|
|
5011
|
+
}
|
|
5012
|
+
for (const entry of entries) {
|
|
5013
|
+
if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
|
|
5014
|
+
const fullPath = path.join(chatsDir, entry.name);
|
|
5015
|
+
filesMeta.push(fullPath);
|
|
5016
|
+
if (filesMeta.length >= 5000) break;
|
|
5017
|
+
}
|
|
5018
|
+
if (filesMeta.length >= 5000) break;
|
|
5019
|
+
}
|
|
5020
|
+
matchedFile = filesMeta.find(item => path.basename(item, '.json').toLowerCase() === targetId) || '';
|
|
5021
|
+
} else {
|
|
5022
|
+
const files = collectJsonlFiles(root, 5000);
|
|
5023
|
+
matchedFile = files.find(item => path.basename(item, '.jsonl').toLowerCase() === targetId) || '';
|
|
5024
|
+
}
|
|
4357
5025
|
if (matchedFile && fs.existsSync(matchedFile)) {
|
|
4358
5026
|
return matchedFile;
|
|
4359
5027
|
}
|
|
@@ -4530,11 +5198,15 @@ function moveFileSync(sourcePath, targetPath) {
|
|
|
4530
5198
|
|
|
4531
5199
|
function buildSessionSummaryFallback(source, filePath, sessionId = '') {
|
|
4532
5200
|
const resolvedSessionId = sessionId || path.basename(filePath, '.jsonl');
|
|
4533
|
-
const sourceLabel = source === 'claude'
|
|
5201
|
+
const sourceLabel = source === 'claude'
|
|
5202
|
+
? 'Claude Code'
|
|
5203
|
+
: (source === 'gemini' ? 'Gemini CLI' : (source === 'codebuddy' ? 'CodeBuddy Code' : 'Codex'));
|
|
4534
5204
|
return {
|
|
4535
5205
|
source,
|
|
4536
5206
|
sourceLabel,
|
|
4537
|
-
provider: source === 'claude'
|
|
5207
|
+
provider: source === 'claude'
|
|
5208
|
+
? 'claude'
|
|
5209
|
+
: (source === 'gemini' ? 'gemini' : (source === 'codebuddy' ? 'codebuddy' : 'codex')),
|
|
4538
5210
|
sessionId: resolvedSessionId,
|
|
4539
5211
|
title: resolvedSessionId,
|
|
4540
5212
|
cwd: '',
|
|
@@ -4545,7 +5217,7 @@ function buildSessionSummaryFallback(source, filePath, sessionId = '') {
|
|
|
4545
5217
|
contextWindow: 0,
|
|
4546
5218
|
filePath,
|
|
4547
5219
|
keywords: [],
|
|
4548
|
-
capabilities: source === 'claude' ? { code: true } : {}
|
|
5220
|
+
capabilities: source === 'claude' || source === 'gemini' || source === 'codebuddy' ? { code: true } : {}
|
|
4549
5221
|
};
|
|
4550
5222
|
}
|
|
4551
5223
|
|
|
@@ -4556,11 +5228,14 @@ function generateSessionTrashId() {
|
|
|
4556
5228
|
return `trash-${Date.now().toString(36)}-${crypto.randomBytes(8).toString('hex')}`;
|
|
4557
5229
|
}
|
|
4558
5230
|
|
|
4559
|
-
function allocateSessionTrashTarget() {
|
|
5231
|
+
function allocateSessionTrashTarget(extension = 'jsonl') {
|
|
4560
5232
|
ensureDir(SESSION_TRASH_FILES_DIR);
|
|
5233
|
+
const safeExt = typeof extension === 'string' && extension.trim()
|
|
5234
|
+
? extension.trim().replace(/^\./, '')
|
|
5235
|
+
: 'jsonl';
|
|
4561
5236
|
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
4562
5237
|
const trashId = generateSessionTrashId();
|
|
4563
|
-
const trashFileName = `${trashId}
|
|
5238
|
+
const trashFileName = `${trashId}.${safeExt}`;
|
|
4564
5239
|
const trashFilePath = path.join(SESSION_TRASH_FILES_DIR, trashFileName);
|
|
4565
5240
|
if (!fs.existsSync(trashFilePath)) {
|
|
4566
5241
|
return { trashId, trashFileName, trashFilePath };
|
|
@@ -4569,8 +5244,8 @@ function allocateSessionTrashTarget() {
|
|
|
4569
5244
|
const fallbackId = `trash-${Date.now().toString(36)}-${crypto.randomBytes(8).toString('hex')}`;
|
|
4570
5245
|
return {
|
|
4571
5246
|
trashId: fallbackId,
|
|
4572
|
-
trashFileName: `${fallbackId}
|
|
4573
|
-
trashFilePath: path.join(SESSION_TRASH_FILES_DIR, `${fallbackId}
|
|
5247
|
+
trashFileName: `${fallbackId}.${safeExt}`,
|
|
5248
|
+
trashFilePath: path.join(SESSION_TRASH_FILES_DIR, `${fallbackId}.${safeExt}`)
|
|
4574
5249
|
};
|
|
4575
5250
|
}
|
|
4576
5251
|
|
|
@@ -4578,7 +5253,13 @@ function normalizeSessionTrashEntry(entry) {
|
|
|
4578
5253
|
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
4579
5254
|
return null;
|
|
4580
5255
|
}
|
|
4581
|
-
const source = entry.source === 'claude'
|
|
5256
|
+
const source = entry.source === 'claude'
|
|
5257
|
+
? 'claude'
|
|
5258
|
+
: (entry.source === 'codex'
|
|
5259
|
+
? 'codex'
|
|
5260
|
+
: (entry.source === 'gemini'
|
|
5261
|
+
? 'gemini'
|
|
5262
|
+
: (entry.source === 'codebuddy' ? 'codebuddy' : '')));
|
|
4582
5263
|
const trashId = typeof entry.trashId === 'string' ? entry.trashId.trim() : '';
|
|
4583
5264
|
if (!source || !trashId || trashId.includes('/') || trashId.includes('\\') || trashId.includes('\0')) {
|
|
4584
5265
|
return null;
|
|
@@ -4593,7 +5274,9 @@ function normalizeSessionTrashEntry(entry) {
|
|
|
4593
5274
|
trashId,
|
|
4594
5275
|
trashFileName,
|
|
4595
5276
|
source,
|
|
4596
|
-
sourceLabel: source === 'claude'
|
|
5277
|
+
sourceLabel: source === 'claude'
|
|
5278
|
+
? 'Claude Code'
|
|
5279
|
+
: (source === 'gemini' ? 'Gemini CLI' : (source === 'codebuddy' ? 'CodeBuddy Code' : 'Codex')),
|
|
4597
5280
|
sessionId: sessionId || trashId,
|
|
4598
5281
|
title: typeof entry.title === 'string' && entry.title.trim() ? entry.title.trim() : (sessionId || trashId),
|
|
4599
5282
|
cwd: typeof entry.cwd === 'string' ? entry.cwd : '',
|
|
@@ -4609,7 +5292,7 @@ function normalizeSessionTrashEntry(entry) {
|
|
|
4609
5292
|
originalFilePath: typeof entry.originalFilePath === 'string' ? entry.originalFilePath : '',
|
|
4610
5293
|
provider: typeof entry.provider === 'string' && entry.provider.trim()
|
|
4611
5294
|
? entry.provider.trim()
|
|
4612
|
-
: (source === 'claude' ? 'claude' : 'codex'),
|
|
5295
|
+
: (source === 'claude' ? 'claude' : (source === 'gemini' ? 'gemini' : (source === 'codebuddy' ? 'codebuddy' : 'codex'))),
|
|
4613
5296
|
keywords: normalizeKeywords(entry.keywords),
|
|
4614
5297
|
capabilities: normalizeCapabilities(entry.capabilities),
|
|
4615
5298
|
claudeIndexPath: typeof entry.claudeIndexPath === 'string' ? entry.claudeIndexPath : '',
|
|
@@ -4727,7 +5410,11 @@ function resolveSessionRestoreTarget(entry) {
|
|
|
4727
5410
|
if (!normalized) {
|
|
4728
5411
|
return '';
|
|
4729
5412
|
}
|
|
4730
|
-
const root = normalized.source === 'claude'
|
|
5413
|
+
const root = normalized.source === 'claude'
|
|
5414
|
+
? getClaudeProjectsDir()
|
|
5415
|
+
: (normalized.source === 'gemini'
|
|
5416
|
+
? getGeminiTmpDir()
|
|
5417
|
+
: (normalized.source === 'codebuddy' ? getCodeBuddyProjectsDir() : getCodexSessionsDir()));
|
|
4731
5418
|
const originalFilePath = typeof normalized.originalFilePath === 'string' ? normalized.originalFilePath.trim() : '';
|
|
4732
5419
|
if (!root || !originalFilePath) {
|
|
4733
5420
|
return '';
|
|
@@ -4861,14 +5548,20 @@ function upsertClaudeSessionIndexEntry(indexPath, sessionFilePath, entry) {
|
|
|
4861
5548
|
}
|
|
4862
5549
|
|
|
4863
5550
|
async function listSessionTrashItems(params = {}) {
|
|
4864
|
-
const source = params.source === 'claude'
|
|
5551
|
+
const source = params.source === 'claude'
|
|
5552
|
+
? 'claude'
|
|
5553
|
+
: (params.source === 'codex'
|
|
5554
|
+
? 'codex'
|
|
5555
|
+
: (params.source === 'gemini'
|
|
5556
|
+
? 'gemini'
|
|
5557
|
+
: (params.source === 'codebuddy' ? 'codebuddy' : 'all')));
|
|
4865
5558
|
const countOnly = params.countOnly === true;
|
|
4866
5559
|
const rawLimit = Number(params.limit);
|
|
4867
5560
|
const limit = Number.isFinite(rawLimit)
|
|
4868
5561
|
? Math.max(1, Math.min(rawLimit, MAX_SESSION_TRASH_LIST_SIZE))
|
|
4869
5562
|
: 200;
|
|
4870
5563
|
const allEntries = readSessionTrashEntries();
|
|
4871
|
-
let items = source === 'codex' || source === 'claude'
|
|
5564
|
+
let items = source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy'
|
|
4872
5565
|
? allEntries.filter((entry) => entry.source === source)
|
|
4873
5566
|
: allEntries.slice();
|
|
4874
5567
|
items.sort((a, b) => {
|
|
@@ -5048,7 +5741,13 @@ async function purgeSessionTrashItems(params = {}) {
|
|
|
5048
5741
|
}
|
|
5049
5742
|
|
|
5050
5743
|
async function trashSessionData(params = {}) {
|
|
5051
|
-
const source = params.source === 'claude'
|
|
5744
|
+
const source = params.source === 'claude'
|
|
5745
|
+
? 'claude'
|
|
5746
|
+
: (params.source === 'codex'
|
|
5747
|
+
? 'codex'
|
|
5748
|
+
: (params.source === 'gemini'
|
|
5749
|
+
? 'gemini'
|
|
5750
|
+
: (params.source === 'codebuddy' ? 'codebuddy' : '')));
|
|
5052
5751
|
if (!source) {
|
|
5053
5752
|
return { error: 'Invalid source' };
|
|
5054
5753
|
}
|
|
@@ -5058,14 +5757,18 @@ async function trashSessionData(params = {}) {
|
|
|
5058
5757
|
return { error: 'Session file not found' };
|
|
5059
5758
|
}
|
|
5060
5759
|
|
|
5061
|
-
const summary = (source === 'claude'
|
|
5760
|
+
const summary = (source === 'claude'
|
|
5761
|
+
? parseClaudeSessionSummary(filePath)
|
|
5762
|
+
: (source === 'gemini'
|
|
5763
|
+
? parseGeminiSessionSummary(filePath)
|
|
5764
|
+
: (source === 'codebuddy' ? parseCodeBuddySessionSummary(filePath) : parseCodexSessionSummary(filePath))))
|
|
5062
5765
|
|| buildSessionSummaryFallback(source, filePath, params.sessionId);
|
|
5063
5766
|
const exactMessageCount = await countConversationMessagesInFile(filePath, source);
|
|
5064
5767
|
if (Number.isFinite(Number(exactMessageCount))) {
|
|
5065
5768
|
summary.messageCount = Math.max(0, Math.floor(Number(exactMessageCount)));
|
|
5066
5769
|
}
|
|
5067
|
-
const sessionId = summary.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
|
|
5068
|
-
const { trashId, trashFileName, trashFilePath } = allocateSessionTrashTarget();
|
|
5770
|
+
const sessionId = summary.sessionId || params.sessionId || path.basename(filePath, source === 'gemini' ? '.json' : '.jsonl');
|
|
5771
|
+
const { trashId, trashFileName, trashFilePath } = allocateSessionTrashTarget(source === 'gemini' ? 'json' : 'jsonl');
|
|
5069
5772
|
const deletedAt = new Date().toISOString();
|
|
5070
5773
|
const claudeIndexPath = source === 'claude' ? findClaudeSessionIndexPath(filePath) : '';
|
|
5071
5774
|
let removedClaudeIndexEntry = null;
|
|
@@ -5149,7 +5852,13 @@ async function trashSessionData(params = {}) {
|
|
|
5149
5852
|
}
|
|
5150
5853
|
|
|
5151
5854
|
async function deleteSessionData(params = {}) {
|
|
5152
|
-
const source = params.source === 'claude'
|
|
5855
|
+
const source = params.source === 'claude'
|
|
5856
|
+
? 'claude'
|
|
5857
|
+
: (params.source === 'codex'
|
|
5858
|
+
? 'codex'
|
|
5859
|
+
: (params.source === 'gemini'
|
|
5860
|
+
? 'gemini'
|
|
5861
|
+
: (params.source === 'codebuddy' ? 'codebuddy' : '')));
|
|
5153
5862
|
if (!source) {
|
|
5154
5863
|
return { error: 'Invalid source' };
|
|
5155
5864
|
}
|
|
@@ -5159,7 +5868,7 @@ async function deleteSessionData(params = {}) {
|
|
|
5159
5868
|
return { error: 'Session file not found' };
|
|
5160
5869
|
}
|
|
5161
5870
|
|
|
5162
|
-
const sessionId = params.sessionId || path.basename(filePath, '.jsonl');
|
|
5871
|
+
const sessionId = params.sessionId || path.basename(filePath, source === 'gemini' ? '.json' : '.jsonl');
|
|
5163
5872
|
let fileDeleted = false;
|
|
5164
5873
|
try {
|
|
5165
5874
|
fs.unlinkSync(filePath);
|
|
@@ -5484,6 +6193,38 @@ function extractClaudeMessageFromRecord(record, state, lineIndex = -1) {
|
|
|
5484
6193
|
}
|
|
5485
6194
|
}
|
|
5486
6195
|
|
|
6196
|
+
function extractCodeBuddyMessageFromRecord(record, state, lineIndex = -1) {
|
|
6197
|
+
if (record && record.timestamp) {
|
|
6198
|
+
state.updatedAt = toIsoTime(record.timestamp, state.updatedAt);
|
|
6199
|
+
}
|
|
6200
|
+
|
|
6201
|
+
if (record && typeof record.sessionId === 'string' && record.sessionId.trim()) {
|
|
6202
|
+
state.sessionId = record.sessionId.trim();
|
|
6203
|
+
}
|
|
6204
|
+
|
|
6205
|
+
if (!state.cwd && record && typeof record.cwd === 'string' && record.cwd.trim()) {
|
|
6206
|
+
state.cwd = record.cwd.trim();
|
|
6207
|
+
}
|
|
6208
|
+
|
|
6209
|
+
if (!record || record.type !== 'message') {
|
|
6210
|
+
return;
|
|
6211
|
+
}
|
|
6212
|
+
|
|
6213
|
+
const role = normalizeRole(record.role);
|
|
6214
|
+
if (role === 'user' || role === 'assistant' || role === 'system') {
|
|
6215
|
+
const content = record.message?.content ?? record.content ?? '';
|
|
6216
|
+
const text = extractMessageText(content);
|
|
6217
|
+
if (text && canAppendMessage(state)) {
|
|
6218
|
+
state.messages.push({
|
|
6219
|
+
role,
|
|
6220
|
+
text,
|
|
6221
|
+
timestamp: toIsoTime(record.timestamp, ''),
|
|
6222
|
+
recordLineIndex: Number.isInteger(lineIndex) ? lineIndex : -1
|
|
6223
|
+
});
|
|
6224
|
+
}
|
|
6225
|
+
}
|
|
6226
|
+
}
|
|
6227
|
+
|
|
5487
6228
|
function recordHasCodexMessage(record) {
|
|
5488
6229
|
if (!record || record.type !== 'response_item' || !record.payload) {
|
|
5489
6230
|
return false;
|
|
@@ -5512,10 +6253,23 @@ function recordHasClaudeMessage(record) {
|
|
|
5512
6253
|
return !!text;
|
|
5513
6254
|
}
|
|
5514
6255
|
|
|
6256
|
+
function recordHasCodeBuddyMessage(record) {
|
|
6257
|
+
if (!record || record.type !== 'message') {
|
|
6258
|
+
return false;
|
|
6259
|
+
}
|
|
6260
|
+
const role = normalizeRole(record.role);
|
|
6261
|
+
if (role !== 'user' && role !== 'assistant' && role !== 'system') {
|
|
6262
|
+
return false;
|
|
6263
|
+
}
|
|
6264
|
+
const content = record.message?.content ?? record.content ?? '';
|
|
6265
|
+
const text = extractMessageText(content);
|
|
6266
|
+
return !!text;
|
|
6267
|
+
}
|
|
6268
|
+
|
|
5515
6269
|
function recordHasMessage(record, source) {
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
6270
|
+
if (source === 'codex') return recordHasCodexMessage(record);
|
|
6271
|
+
if (source === 'codebuddy') return recordHasCodeBuddyMessage(record);
|
|
6272
|
+
return recordHasClaudeMessage(record);
|
|
5519
6273
|
}
|
|
5520
6274
|
|
|
5521
6275
|
function extractMessagesFromRecords(records, source, options = {}) {
|
|
@@ -5533,6 +6287,8 @@ function extractMessagesFromRecords(records, source, options = {}) {
|
|
|
5533
6287
|
const record = records[lineIndex];
|
|
5534
6288
|
if (source === 'codex') {
|
|
5535
6289
|
extractCodexMessageFromRecord(record, state, lineIndex);
|
|
6290
|
+
} else if (source === 'codebuddy') {
|
|
6291
|
+
extractCodeBuddyMessageFromRecord(record, state, lineIndex);
|
|
5536
6292
|
} else {
|
|
5537
6293
|
extractClaudeMessageFromRecord(record, state, lineIndex);
|
|
5538
6294
|
}
|
|
@@ -5594,6 +6350,8 @@ async function extractMessagesFromFile(filePath, source, options = {}) {
|
|
|
5594
6350
|
|
|
5595
6351
|
if (source === 'codex') {
|
|
5596
6352
|
extractCodexMessageFromRecord(record, state, currentLineIndex);
|
|
6353
|
+
} else if (source === 'codebuddy') {
|
|
6354
|
+
extractCodeBuddyMessageFromRecord(record, state, currentLineIndex);
|
|
5597
6355
|
} else {
|
|
5598
6356
|
extractClaudeMessageFromRecord(record, state, currentLineIndex);
|
|
5599
6357
|
}
|
|
@@ -5618,7 +6376,13 @@ async function extractMessagesFromFile(filePath, source, options = {}) {
|
|
|
5618
6376
|
}
|
|
5619
6377
|
|
|
5620
6378
|
async function readSessionDetail(params = {}) {
|
|
5621
|
-
const source = params.source === 'claude'
|
|
6379
|
+
const source = params.source === 'claude'
|
|
6380
|
+
? 'claude'
|
|
6381
|
+
: (params.source === 'codex'
|
|
6382
|
+
? 'codex'
|
|
6383
|
+
: (params.source === 'gemini'
|
|
6384
|
+
? 'gemini'
|
|
6385
|
+
: (params.source === 'codebuddy' ? 'codebuddy' : '')));
|
|
5622
6386
|
if (!source) {
|
|
5623
6387
|
return { error: 'Invalid source' };
|
|
5624
6388
|
}
|
|
@@ -5635,9 +6399,52 @@ async function readSessionDetail(params = {}) {
|
|
|
5635
6399
|
: DEFAULT_SESSION_DETAIL_MESSAGES;
|
|
5636
6400
|
const preview = params.preview === true || params.preview === 'true';
|
|
5637
6401
|
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
6402
|
+
let extracted;
|
|
6403
|
+
if (source === 'gemini') {
|
|
6404
|
+
let json;
|
|
6405
|
+
try {
|
|
6406
|
+
json = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
6407
|
+
} catch (_) {
|
|
6408
|
+
json = null;
|
|
6409
|
+
}
|
|
6410
|
+
if (!json || typeof json !== 'object') {
|
|
6411
|
+
return { error: 'Failed to parse session file' };
|
|
6412
|
+
}
|
|
6413
|
+
const rawMessages = Array.isArray(json.messages) ? json.messages : [];
|
|
6414
|
+
const messages = [];
|
|
6415
|
+
for (const entry of rawMessages) {
|
|
6416
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
6417
|
+
const role = normalizeGeminiMessageRole(entry.type);
|
|
6418
|
+
if (!role) continue;
|
|
6419
|
+
const text = extractMessageText(extractGeminiMessageText(entry.content ?? entry.message ?? entry.text));
|
|
6420
|
+
if (!text && role !== 'system') continue;
|
|
6421
|
+
messages.push({
|
|
6422
|
+
role,
|
|
6423
|
+
text,
|
|
6424
|
+
timestamp: toIsoTime(entry.timestamp ?? entry.time ?? entry.at, '')
|
|
6425
|
+
});
|
|
6426
|
+
}
|
|
6427
|
+
const filtered = removeLeadingSystemMessage(messages);
|
|
6428
|
+
const totalMessages = filtered.length;
|
|
6429
|
+
const clipped = totalMessages > messageLimit;
|
|
6430
|
+
const sliced = clipped ? filtered.slice(Math.max(0, totalMessages - messageLimit)) : filtered;
|
|
6431
|
+
extracted = {
|
|
6432
|
+
sessionId: typeof json.sessionId === 'string' && json.sessionId.trim() ? json.sessionId.trim() : path.basename(filePath, '.json'),
|
|
6433
|
+
cwd: typeof json.projectRoot === 'string' ? json.projectRoot : (typeof json.cwd === 'string' ? json.cwd : ''),
|
|
6434
|
+
updatedAt: toIsoTime(json.lastUpdated ?? json.updatedAt, ''),
|
|
6435
|
+
totalMessages,
|
|
6436
|
+
clipped,
|
|
6437
|
+
messages: sliced
|
|
6438
|
+
};
|
|
6439
|
+
} else {
|
|
6440
|
+
extracted = await extractSessionDetailPreviewFromFile(filePath, source, messageLimit, { preview });
|
|
6441
|
+
}
|
|
6442
|
+
const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, source === 'gemini' ? '.json' : '.jsonl');
|
|
6443
|
+
const sourceLabel = source === 'codex'
|
|
6444
|
+
? 'Codex'
|
|
6445
|
+
: (source === 'claude'
|
|
6446
|
+
? 'Claude Code'
|
|
6447
|
+
: (source === 'gemini' ? 'Gemini CLI' : 'CodeBuddy Code'));
|
|
5641
6448
|
const clippedMessages = Array.isArray(extracted.messages) ? extracted.messages : [];
|
|
5642
6449
|
const hasExactTotalMessages = Number.isFinite(extracted.totalMessages);
|
|
5643
6450
|
const startIndex = hasExactTotalMessages
|
|
@@ -5671,7 +6478,13 @@ async function readSessionDetail(params = {}) {
|
|
|
5671
6478
|
}
|
|
5672
6479
|
|
|
5673
6480
|
async function readSessionPlain(params = {}) {
|
|
5674
|
-
const source = params.source === 'claude'
|
|
6481
|
+
const source = params.source === 'claude'
|
|
6482
|
+
? 'claude'
|
|
6483
|
+
: (params.source === 'codex'
|
|
6484
|
+
? 'codex'
|
|
6485
|
+
: (params.source === 'gemini'
|
|
6486
|
+
? 'gemini'
|
|
6487
|
+
: (params.source === 'codebuddy' ? 'codebuddy' : '')));
|
|
5675
6488
|
if (!source) {
|
|
5676
6489
|
return { error: 'Invalid source' };
|
|
5677
6490
|
}
|
|
@@ -5682,26 +6495,57 @@ async function readSessionPlain(params = {}) {
|
|
|
5682
6495
|
}
|
|
5683
6496
|
|
|
5684
6497
|
let extracted;
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
6498
|
+
if (source === 'gemini') {
|
|
6499
|
+
let json;
|
|
6500
|
+
try {
|
|
6501
|
+
json = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
6502
|
+
} catch (_) {
|
|
6503
|
+
json = null;
|
|
6504
|
+
}
|
|
6505
|
+
if (!json || typeof json !== 'object') {
|
|
6506
|
+
return { error: 'Failed to parse session file' };
|
|
6507
|
+
}
|
|
6508
|
+
const rawMessages = Array.isArray(json.messages) ? json.messages : [];
|
|
6509
|
+
const messages = [];
|
|
6510
|
+
for (const entry of rawMessages) {
|
|
6511
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
6512
|
+
const role = normalizeGeminiMessageRole(entry.type);
|
|
6513
|
+
if (!role) continue;
|
|
6514
|
+
const text = extractMessageText(extractGeminiMessageText(entry.content ?? entry.message ?? entry.text));
|
|
6515
|
+
if (!text && role !== 'system') continue;
|
|
6516
|
+
messages.push({ role, text });
|
|
6517
|
+
}
|
|
6518
|
+
extracted = {
|
|
6519
|
+
sessionId: typeof json.sessionId === 'string' && json.sessionId.trim() ? json.sessionId.trim() : path.basename(filePath, '.json'),
|
|
6520
|
+
cwd: typeof json.projectRoot === 'string' ? json.projectRoot : '',
|
|
6521
|
+
messages
|
|
6522
|
+
};
|
|
6523
|
+
} else {
|
|
6524
|
+
try {
|
|
6525
|
+
extracted = await extractMessagesFromFile(filePath, source, { maxMessages: Infinity });
|
|
6526
|
+
} catch (e) {
|
|
6527
|
+
extracted = null;
|
|
6528
|
+
}
|
|
5690
6529
|
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
6530
|
+
if (!extracted) {
|
|
6531
|
+
return { error: 'Failed to parse session file' };
|
|
6532
|
+
}
|
|
5694
6533
|
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
6534
|
+
if ((!extracted.messages || extracted.messages.length === 0) && !extracted.sessionId && !extracted.cwd) {
|
|
6535
|
+
const fallbackRecords = readJsonlRecords(filePath);
|
|
6536
|
+
if (fallbackRecords.length === 0) {
|
|
6537
|
+
return { error: 'Session file is empty' };
|
|
6538
|
+
}
|
|
6539
|
+
extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages: Infinity });
|
|
5699
6540
|
}
|
|
5700
|
-
extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages: Infinity });
|
|
5701
6541
|
}
|
|
5702
6542
|
|
|
5703
|
-
const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
|
|
5704
|
-
const sourceLabel = source === 'codex'
|
|
6543
|
+
const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, source === 'gemini' ? '.json' : '.jsonl');
|
|
6544
|
+
const sourceLabel = source === 'codex'
|
|
6545
|
+
? 'Codex'
|
|
6546
|
+
: (source === 'claude'
|
|
6547
|
+
? 'Claude Code'
|
|
6548
|
+
: (source === 'gemini' ? 'Gemini CLI' : 'CodeBuddy Code'));
|
|
5705
6549
|
const messages = removeLeadingSystemMessage(Array.isArray(extracted.messages) ? extracted.messages : []);
|
|
5706
6550
|
const text = buildSessionPlainText(messages);
|
|
5707
6551
|
|
|
@@ -5716,7 +6560,13 @@ async function readSessionPlain(params = {}) {
|
|
|
5716
6560
|
}
|
|
5717
6561
|
|
|
5718
6562
|
async function exportSessionData(params = {}) {
|
|
5719
|
-
const source = params.source === 'claude'
|
|
6563
|
+
const source = params.source === 'claude'
|
|
6564
|
+
? 'claude'
|
|
6565
|
+
: (params.source === 'codex'
|
|
6566
|
+
? 'codex'
|
|
6567
|
+
: (params.source === 'gemini'
|
|
6568
|
+
? 'gemini'
|
|
6569
|
+
: (params.source === 'codebuddy' ? 'codebuddy' : '')));
|
|
5720
6570
|
if (!source) {
|
|
5721
6571
|
return { error: 'Invalid source' };
|
|
5722
6572
|
}
|
|
@@ -5728,22 +6578,51 @@ async function exportSessionData(params = {}) {
|
|
|
5728
6578
|
}
|
|
5729
6579
|
|
|
5730
6580
|
let extracted;
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
6581
|
+
if (source === 'gemini') {
|
|
6582
|
+
let json;
|
|
6583
|
+
try {
|
|
6584
|
+
json = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
6585
|
+
} catch (_) {
|
|
6586
|
+
json = null;
|
|
6587
|
+
}
|
|
6588
|
+
if (!json || typeof json !== 'object') {
|
|
6589
|
+
return { error: 'Failed to parse session file' };
|
|
6590
|
+
}
|
|
6591
|
+
const rawMessages = Array.isArray(json.messages) ? json.messages : [];
|
|
6592
|
+
const messages = [];
|
|
6593
|
+
for (const entry of rawMessages) {
|
|
6594
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
6595
|
+
const role = normalizeGeminiMessageRole(entry.type);
|
|
6596
|
+
if (!role) continue;
|
|
6597
|
+
const text = extractMessageText(extractGeminiMessageText(entry.content ?? entry.message ?? entry.text));
|
|
6598
|
+
if (!text && role !== 'system') continue;
|
|
6599
|
+
messages.push({ role, text, timestamp: toIsoTime(entry.timestamp ?? entry.time ?? entry.at, '') });
|
|
6600
|
+
}
|
|
6601
|
+
extracted = {
|
|
6602
|
+
sessionId: typeof json.sessionId === 'string' && json.sessionId.trim() ? json.sessionId.trim() : path.basename(filePath, '.json'),
|
|
6603
|
+
cwd: typeof json.projectRoot === 'string' ? json.projectRoot : '',
|
|
6604
|
+
updatedAt: toIsoTime(json.lastUpdated ?? json.updatedAt, ''),
|
|
6605
|
+
messages: maxMessages === Infinity ? messages : messages.slice(-maxMessages),
|
|
6606
|
+
truncated: maxMessages !== Infinity && messages.length > maxMessages
|
|
6607
|
+
};
|
|
6608
|
+
} else {
|
|
6609
|
+
try {
|
|
6610
|
+
extracted = await extractMessagesFromFile(filePath, source, { maxMessages });
|
|
6611
|
+
} catch (e) {
|
|
6612
|
+
extracted = null;
|
|
6613
|
+
}
|
|
5736
6614
|
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
6615
|
+
if (!extracted) {
|
|
6616
|
+
return { error: 'Failed to parse session file' };
|
|
6617
|
+
}
|
|
5740
6618
|
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
6619
|
+
if ((!extracted.messages || extracted.messages.length === 0) && !extracted.sessionId && !extracted.cwd) {
|
|
6620
|
+
const fallbackRecords = readJsonlRecords(filePath);
|
|
6621
|
+
if (fallbackRecords.length === 0) {
|
|
6622
|
+
return { error: 'Session file is empty' };
|
|
6623
|
+
}
|
|
6624
|
+
extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages });
|
|
5745
6625
|
}
|
|
5746
|
-
extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages });
|
|
5747
6626
|
}
|
|
5748
6627
|
|
|
5749
6628
|
extracted.messages = removeLeadingSystemMessage(Array.isArray(extracted.messages) ? extracted.messages : []);
|
|
@@ -5755,9 +6634,13 @@ async function exportSessionData(params = {}) {
|
|
|
5755
6634
|
}
|
|
5756
6635
|
}
|
|
5757
6636
|
|
|
5758
|
-
const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
|
|
6637
|
+
const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, source === 'gemini' ? '.json' : '.jsonl');
|
|
5759
6638
|
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
5760
|
-
const sourceLabel = source === 'codex'
|
|
6639
|
+
const sourceLabel = source === 'codex'
|
|
6640
|
+
? 'Codex'
|
|
6641
|
+
: (source === 'claude'
|
|
6642
|
+
? 'Claude Code'
|
|
6643
|
+
: (source === 'gemini' ? 'Gemini CLI' : 'CodeBuddy Code'));
|
|
5761
6644
|
const truncated = !!extracted.truncated;
|
|
5762
6645
|
const maxMessagesLabel = maxMessages === Infinity ? 'all' : maxMessages;
|
|
5763
6646
|
const markdown = buildSessionMarkdown({
|
|
@@ -6003,6 +6886,60 @@ function importConfigData(payload, options = {}) {
|
|
|
6003
6886
|
function resolveSpeedTestTarget(params) {
|
|
6004
6887
|
if (!params) return { error: 'Missing params' };
|
|
6005
6888
|
|
|
6889
|
+
if (typeof params.kind === 'string' && params.kind.trim() === 'claude') {
|
|
6890
|
+
const baseUrl = typeof params.url === 'string' ? params.url.trim() : '';
|
|
6891
|
+
const apiKey = typeof params.apiKey === 'string' ? params.apiKey.trim() : '';
|
|
6892
|
+
const model = typeof params.model === 'string' ? params.model.trim() : '';
|
|
6893
|
+
if (!baseUrl) {
|
|
6894
|
+
return { error: 'Missing url' };
|
|
6895
|
+
}
|
|
6896
|
+
if (!apiKey) {
|
|
6897
|
+
return { error: 'Missing apiKey' };
|
|
6898
|
+
}
|
|
6899
|
+
if (!model) {
|
|
6900
|
+
return { error: 'Missing model' };
|
|
6901
|
+
}
|
|
6902
|
+
const normalizedBase = baseUrl.replace(/\/+$/, '');
|
|
6903
|
+
let parsed = null;
|
|
6904
|
+
try {
|
|
6905
|
+
parsed = new URL(normalizedBase);
|
|
6906
|
+
} catch (_) {
|
|
6907
|
+
return { error: 'Invalid URL' };
|
|
6908
|
+
}
|
|
6909
|
+
const pathname = typeof parsed.pathname === 'string' ? parsed.pathname : '';
|
|
6910
|
+
const trimmedPath = pathname.replace(/\/+$/, '');
|
|
6911
|
+
const isRootPath = !trimmedPath || trimmedPath === '/';
|
|
6912
|
+
const endsWithV1 = trimmedPath.endsWith('/v1');
|
|
6913
|
+
const makeCandidate = (url) => ({
|
|
6914
|
+
method: 'POST',
|
|
6915
|
+
url,
|
|
6916
|
+
body: {
|
|
6917
|
+
model,
|
|
6918
|
+
max_tokens: 16,
|
|
6919
|
+
messages: [{ role: 'user', content: 'ping' }]
|
|
6920
|
+
}
|
|
6921
|
+
});
|
|
6922
|
+
const candidates = [];
|
|
6923
|
+
if (endsWithV1) {
|
|
6924
|
+
candidates.push(makeCandidate(`${normalizedBase}/messages`));
|
|
6925
|
+
} else if (isRootPath) {
|
|
6926
|
+
candidates.push(makeCandidate(`${normalizedBase}/v1/messages`));
|
|
6927
|
+
candidates.push(makeCandidate(`${normalizedBase}/messages`));
|
|
6928
|
+
} else {
|
|
6929
|
+
candidates.push(makeCandidate(`${normalizedBase}/messages`));
|
|
6930
|
+
candidates.push(makeCandidate(`${normalizedBase}/v1/messages`));
|
|
6931
|
+
}
|
|
6932
|
+
return {
|
|
6933
|
+
kind: 'claude',
|
|
6934
|
+
candidates,
|
|
6935
|
+
apiKey,
|
|
6936
|
+
apiKeyHeader: 'x-api-key',
|
|
6937
|
+
headers: {
|
|
6938
|
+
'anthropic-version': '2023-06-01'
|
|
6939
|
+
}
|
|
6940
|
+
};
|
|
6941
|
+
}
|
|
6942
|
+
|
|
6006
6943
|
if (params.name) {
|
|
6007
6944
|
const { config } = readConfigOrVirtualDefault();
|
|
6008
6945
|
const providers = config.model_providers || {};
|
|
@@ -6013,20 +6950,32 @@ function resolveSpeedTestTarget(params) {
|
|
|
6013
6950
|
if (!provider.base_url) {
|
|
6014
6951
|
return { error: 'Provider missing URL' };
|
|
6015
6952
|
}
|
|
6016
|
-
const
|
|
6017
|
-
const
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6953
|
+
const providerName = String(params.name).trim();
|
|
6954
|
+
const currentModels = readCurrentModels();
|
|
6955
|
+
const selectedModel = typeof currentModels[providerName] === 'string' && currentModels[providerName].trim()
|
|
6956
|
+
? currentModels[providerName].trim()
|
|
6957
|
+
: (typeof config.model === 'string' ? config.model.trim() : '');
|
|
6958
|
+
|
|
6959
|
+
const apiKey = typeof provider.preferred_auth_method === 'string'
|
|
6960
|
+
? provider.preferred_auth_method.trim()
|
|
6961
|
+
: '';
|
|
6962
|
+
|
|
6963
|
+
const candidates = [];
|
|
6964
|
+
for (const spec of buildModelProbeSpecs(provider, selectedModel, provider.base_url)) {
|
|
6965
|
+
if (!spec || !spec.url) continue;
|
|
6966
|
+
candidates.push({ method: 'POST', url: spec.url, body: spec.body });
|
|
6967
|
+
}
|
|
6968
|
+
for (const url of buildApiProbeUrlCandidates(provider.base_url, 'models')) {
|
|
6969
|
+
candidates.push({ method: 'GET', url });
|
|
6970
|
+
}
|
|
6971
|
+
if (candidates.length === 0) {
|
|
6972
|
+
candidates.push({ method: 'GET', url: provider.base_url });
|
|
6025
6973
|
}
|
|
6974
|
+
|
|
6026
6975
|
return {
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
apiKey
|
|
6976
|
+
kind: 'provider',
|
|
6977
|
+
candidates,
|
|
6978
|
+
apiKey
|
|
6030
6979
|
};
|
|
6031
6980
|
}
|
|
6032
6981
|
|
|
@@ -6041,155 +6990,6 @@ function resolveSpeedTestTarget(params) {
|
|
|
6041
6990
|
return { error: 'Missing name or url' };
|
|
6042
6991
|
}
|
|
6043
6992
|
|
|
6044
|
-
function extractApiPayloadErrorMessage(payload) {
|
|
6045
|
-
if (!payload || typeof payload !== 'object') {
|
|
6046
|
-
return '';
|
|
6047
|
-
}
|
|
6048
|
-
if (typeof payload.error === 'string' && payload.error.trim()) {
|
|
6049
|
-
return payload.error.trim();
|
|
6050
|
-
}
|
|
6051
|
-
if (!payload.error || typeof payload.error !== 'object') {
|
|
6052
|
-
return '';
|
|
6053
|
-
}
|
|
6054
|
-
if (typeof payload.error.message === 'string' && payload.error.message.trim()) {
|
|
6055
|
-
return payload.error.message.trim();
|
|
6056
|
-
}
|
|
6057
|
-
if (typeof payload.error.code === 'string' && payload.error.code.trim()) {
|
|
6058
|
-
return payload.error.code.trim();
|
|
6059
|
-
}
|
|
6060
|
-
return '';
|
|
6061
|
-
}
|
|
6062
|
-
|
|
6063
|
-
function resolveProviderChatTarget(params) {
|
|
6064
|
-
const providerName = typeof (params && params.name) === 'string' ? params.name.trim() : '';
|
|
6065
|
-
const prompt = typeof (params && params.prompt) === 'string' ? params.prompt.trim() : '';
|
|
6066
|
-
if (!providerName) {
|
|
6067
|
-
return { error: 'Provider name is required' };
|
|
6068
|
-
}
|
|
6069
|
-
if (!prompt) {
|
|
6070
|
-
return { error: 'Prompt is required' };
|
|
6071
|
-
}
|
|
6072
|
-
|
|
6073
|
-
const { config } = readConfigOrVirtualDefault();
|
|
6074
|
-
const providers = config.model_providers || {};
|
|
6075
|
-
const provider = providers[providerName];
|
|
6076
|
-
if (!provider || typeof provider !== 'object') {
|
|
6077
|
-
return { error: `Provider not found: ${providerName}` };
|
|
6078
|
-
}
|
|
6079
|
-
|
|
6080
|
-
const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
|
|
6081
|
-
if (!baseUrl) {
|
|
6082
|
-
return { error: `Provider ${providerName} missing URL` };
|
|
6083
|
-
}
|
|
6084
|
-
|
|
6085
|
-
const currentModels = readCurrentModels();
|
|
6086
|
-
const savedModel = currentModels && typeof currentModels[providerName] === 'string'
|
|
6087
|
-
? currentModels[providerName].trim()
|
|
6088
|
-
: '';
|
|
6089
|
-
const activeProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
|
|
6090
|
-
const activeModel = typeof config.model === 'string' ? config.model.trim() : '';
|
|
6091
|
-
const model = savedModel || (activeProvider === providerName ? activeModel : '');
|
|
6092
|
-
if (!model) {
|
|
6093
|
-
return { error: `Provider ${providerName} missing current model` };
|
|
6094
|
-
}
|
|
6095
|
-
|
|
6096
|
-
const specs = buildModelConversationSpecs(provider, model, baseUrl, prompt, {
|
|
6097
|
-
maxOutputTokens: 256
|
|
6098
|
-
});
|
|
6099
|
-
if (!specs.length) {
|
|
6100
|
-
return { error: `Provider ${providerName} missing available conversation endpoint` };
|
|
6101
|
-
}
|
|
6102
|
-
|
|
6103
|
-
return {
|
|
6104
|
-
providerName,
|
|
6105
|
-
provider,
|
|
6106
|
-
model,
|
|
6107
|
-
prompt,
|
|
6108
|
-
specs,
|
|
6109
|
-
apiKey: typeof provider.preferred_auth_method === 'string'
|
|
6110
|
-
? provider.preferred_auth_method.trim()
|
|
6111
|
-
: ''
|
|
6112
|
-
};
|
|
6113
|
-
}
|
|
6114
|
-
|
|
6115
|
-
async function runProviderChatCheck(params = {}) {
|
|
6116
|
-
const target = resolveProviderChatTarget(params);
|
|
6117
|
-
if (target.error) {
|
|
6118
|
-
return { ok: false, error: target.error };
|
|
6119
|
-
}
|
|
6120
|
-
|
|
6121
|
-
const timeoutMs = Number.isFinite(params.timeoutMs)
|
|
6122
|
-
? Math.max(1000, Number(params.timeoutMs))
|
|
6123
|
-
: 30000;
|
|
6124
|
-
let finalSpec = target.specs[0];
|
|
6125
|
-
let result = null;
|
|
6126
|
-
|
|
6127
|
-
for (let index = 0; index < target.specs.length; index += 1) {
|
|
6128
|
-
const candidate = target.specs[index];
|
|
6129
|
-
const probeResult = await probeJsonPost(candidate.url, candidate.body, {
|
|
6130
|
-
apiKey: target.apiKey,
|
|
6131
|
-
timeoutMs,
|
|
6132
|
-
maxBytes: 512 * 1024
|
|
6133
|
-
});
|
|
6134
|
-
finalSpec = candidate;
|
|
6135
|
-
result = probeResult;
|
|
6136
|
-
const shouldTryNextCandidate = index < target.specs.length - 1
|
|
6137
|
-
&& (!probeResult.ok || probeResult.status === 404);
|
|
6138
|
-
if (!shouldTryNextCandidate) {
|
|
6139
|
-
break;
|
|
6140
|
-
}
|
|
6141
|
-
}
|
|
6142
|
-
|
|
6143
|
-
if (!result || !result.ok) {
|
|
6144
|
-
return {
|
|
6145
|
-
ok: false,
|
|
6146
|
-
provider: target.providerName,
|
|
6147
|
-
model: target.model,
|
|
6148
|
-
url: finalSpec.url,
|
|
6149
|
-
status: Number.isFinite(result && result.status) ? result.status : 0,
|
|
6150
|
-
durationMs: Number.isFinite(result && result.durationMs) ? result.durationMs : 0,
|
|
6151
|
-
reply: '',
|
|
6152
|
-
rawPreview: '',
|
|
6153
|
-
error: result && result.error ? result.error : 'request failed'
|
|
6154
|
-
};
|
|
6155
|
-
}
|
|
6156
|
-
|
|
6157
|
-
let payload = null;
|
|
6158
|
-
try {
|
|
6159
|
-
payload = result.body ? JSON.parse(result.body) : null;
|
|
6160
|
-
} catch (e) {
|
|
6161
|
-
payload = null;
|
|
6162
|
-
}
|
|
6163
|
-
|
|
6164
|
-
const payloadError = extractApiPayloadErrorMessage(payload);
|
|
6165
|
-
if (result.status >= 400 || payloadError) {
|
|
6166
|
-
return {
|
|
6167
|
-
ok: false,
|
|
6168
|
-
provider: target.providerName,
|
|
6169
|
-
model: target.model,
|
|
6170
|
-
url: finalSpec.url,
|
|
6171
|
-
status: Number.isFinite(result.status) ? result.status : 0,
|
|
6172
|
-
durationMs: Number.isFinite(result.durationMs) ? result.durationMs : 0,
|
|
6173
|
-
reply: '',
|
|
6174
|
-
rawPreview: result.body ? truncateText(result.body, 600) : '',
|
|
6175
|
-
error: payloadError || `HTTP ${result.status}`
|
|
6176
|
-
};
|
|
6177
|
-
}
|
|
6178
|
-
|
|
6179
|
-
const reply = extractModelResponseText(payload);
|
|
6180
|
-
return {
|
|
6181
|
-
ok: true,
|
|
6182
|
-
provider: target.providerName,
|
|
6183
|
-
model: target.model,
|
|
6184
|
-
url: finalSpec.url,
|
|
6185
|
-
status: Number.isFinite(result.status) ? result.status : 0,
|
|
6186
|
-
durationMs: Number.isFinite(result.durationMs) ? result.durationMs : 0,
|
|
6187
|
-
reply,
|
|
6188
|
-
rawPreview: reply ? '' : (result.body ? truncateText(result.body, 600) : ''),
|
|
6189
|
-
error: ''
|
|
6190
|
-
};
|
|
6191
|
-
}
|
|
6192
|
-
|
|
6193
6993
|
function runSpeedTest(targetUrl, apiKey, options = {}) {
|
|
6194
6994
|
const timeoutMs = Number.isFinite(options.timeoutMs)
|
|
6195
6995
|
? Math.max(1000, Number(options.timeoutMs))
|
|
@@ -6198,6 +6998,8 @@ function runSpeedTest(targetUrl, apiKey, options = {}) {
|
|
|
6198
6998
|
if (method === 'POST') {
|
|
6199
6999
|
return probeJsonPost(targetUrl, options.body || {}, {
|
|
6200
7000
|
apiKey,
|
|
7001
|
+
apiKeyHeader: typeof options.apiKeyHeader === 'string' ? options.apiKeyHeader : '',
|
|
7002
|
+
headers: options.headers && typeof options.headers === 'object' ? options.headers : null,
|
|
6201
7003
|
timeoutMs,
|
|
6202
7004
|
maxBytes: 256 * 1024
|
|
6203
7005
|
}).then((result) => ({
|
|
@@ -6209,6 +7011,8 @@ function runSpeedTest(targetUrl, apiKey, options = {}) {
|
|
|
6209
7011
|
}
|
|
6210
7012
|
return probeUrl(targetUrl, {
|
|
6211
7013
|
apiKey,
|
|
7014
|
+
apiKeyHeader: typeof options.apiKeyHeader === 'string' ? options.apiKeyHeader : '',
|
|
7015
|
+
headers: options.headers && typeof options.headers === 'object' ? options.headers : null,
|
|
6212
7016
|
timeoutMs,
|
|
6213
7017
|
maxBytes: 256 * 1024
|
|
6214
7018
|
}).then((result) => ({
|
|
@@ -6471,25 +7275,142 @@ async function cmdSetup() {
|
|
|
6471
7275
|
}
|
|
6472
7276
|
}
|
|
6473
7277
|
|
|
6474
|
-
// 显示当前状态
|
|
6475
|
-
function cmdStatus() {
|
|
6476
|
-
const configResult = readConfigOrVirtualDefault();
|
|
6477
|
-
if (hasConfigLoadError(configResult)) {
|
|
6478
|
-
printConfigLoadErrorAndMarkExit(configResult);
|
|
6479
|
-
return;
|
|
7278
|
+
// 显示当前状态
|
|
7279
|
+
function cmdStatus() {
|
|
7280
|
+
const configResult = readConfigOrVirtualDefault();
|
|
7281
|
+
if (hasConfigLoadError(configResult)) {
|
|
7282
|
+
printConfigLoadErrorAndMarkExit(configResult);
|
|
7283
|
+
return;
|
|
7284
|
+
}
|
|
7285
|
+
const { config, isVirtual } = configResult;
|
|
7286
|
+
const current = config.model_provider || '未设置';
|
|
7287
|
+
const currentModel = config.model || '未设置';
|
|
7288
|
+
|
|
7289
|
+
console.log('\n当前状态:');
|
|
7290
|
+
console.log(' 提供商:', current);
|
|
7291
|
+
console.log(' 模型:', currentModel);
|
|
7292
|
+
console.log(' 模型列表: 接口提供');
|
|
7293
|
+
if (isVirtual) {
|
|
7294
|
+
console.log(' 说明: 当前为虚拟默认配置(config.toml 尚未创建)');
|
|
7295
|
+
}
|
|
7296
|
+
console.log();
|
|
7297
|
+
}
|
|
7298
|
+
|
|
7299
|
+
function parseDoctorCommandArgs(argv = []) {
|
|
7300
|
+
const options = {
|
|
7301
|
+
format: 'json',
|
|
7302
|
+
lang: '',
|
|
7303
|
+
range: '7d',
|
|
7304
|
+
targetApp: 'codex',
|
|
7305
|
+
remote: true,
|
|
7306
|
+
includeInstall: true,
|
|
7307
|
+
includeUsage: true,
|
|
7308
|
+
includeTasks: true,
|
|
7309
|
+
includeSkills: true,
|
|
7310
|
+
output: ''
|
|
7311
|
+
};
|
|
7312
|
+
let cursor = 0;
|
|
7313
|
+
while (cursor < argv.length) {
|
|
7314
|
+
const token = String(argv[cursor] || '');
|
|
7315
|
+
if (token === '--json') {
|
|
7316
|
+
options.format = 'json';
|
|
7317
|
+
cursor += 1;
|
|
7318
|
+
continue;
|
|
7319
|
+
}
|
|
7320
|
+
if (token === '--format') {
|
|
7321
|
+
const value = String(argv[cursor + 1] || '').trim().toLowerCase();
|
|
7322
|
+
if (!value || value.startsWith('--')) {
|
|
7323
|
+
throw new Error('错误: --format 需要一个值(json/md)');
|
|
7324
|
+
}
|
|
7325
|
+
options.format = value === 'md' || value === 'markdown' ? 'md' : 'json';
|
|
7326
|
+
cursor += 2;
|
|
7327
|
+
continue;
|
|
7328
|
+
}
|
|
7329
|
+
if (token === '--output') {
|
|
7330
|
+
const value = String(argv[cursor + 1] || '').trim();
|
|
7331
|
+
if (!value || value.startsWith('--')) {
|
|
7332
|
+
throw new Error('错误: --output 需要一个值(文件路径)');
|
|
7333
|
+
}
|
|
7334
|
+
options.output = value;
|
|
7335
|
+
cursor += 2;
|
|
7336
|
+
continue;
|
|
7337
|
+
}
|
|
7338
|
+
if (token === '--lang') {
|
|
7339
|
+
const value = String(argv[cursor + 1] || '').trim().toLowerCase();
|
|
7340
|
+
if (!value || value.startsWith('--')) {
|
|
7341
|
+
throw new Error('错误: --lang 需要一个值(zh/en)');
|
|
7342
|
+
}
|
|
7343
|
+
options.lang = value === 'en' ? 'en' : 'zh';
|
|
7344
|
+
cursor += 2;
|
|
7345
|
+
continue;
|
|
7346
|
+
}
|
|
7347
|
+
if (token === '--range') {
|
|
7348
|
+
const value = String(argv[cursor + 1] || '').trim().toLowerCase();
|
|
7349
|
+
if (!value || value.startsWith('--')) {
|
|
7350
|
+
throw new Error('错误: --range 需要一个值(7d/30d/all)');
|
|
7351
|
+
}
|
|
7352
|
+
options.range = value === 'all' ? 'all' : (value === '30d' ? '30d' : '7d');
|
|
7353
|
+
cursor += 2;
|
|
7354
|
+
continue;
|
|
7355
|
+
}
|
|
7356
|
+
if (token === '--target-app') {
|
|
7357
|
+
const value = String(argv[cursor + 1] || '').trim().toLowerCase();
|
|
7358
|
+
if (!value || value.startsWith('--')) {
|
|
7359
|
+
throw new Error('错误: --target-app 需要一个值(codex/claude)');
|
|
7360
|
+
}
|
|
7361
|
+
options.targetApp = value === 'claude' ? 'claude' : 'codex';
|
|
7362
|
+
cursor += 2;
|
|
7363
|
+
continue;
|
|
7364
|
+
}
|
|
7365
|
+
if (token === '--no-remote') {
|
|
7366
|
+
options.remote = false;
|
|
7367
|
+
cursor += 1;
|
|
7368
|
+
continue;
|
|
7369
|
+
}
|
|
7370
|
+
if (token === '--no-install') {
|
|
7371
|
+
options.includeInstall = false;
|
|
7372
|
+
cursor += 1;
|
|
7373
|
+
continue;
|
|
7374
|
+
}
|
|
7375
|
+
cursor += 1;
|
|
7376
|
+
}
|
|
7377
|
+
return options;
|
|
7378
|
+
}
|
|
7379
|
+
|
|
7380
|
+
async function cmdDoctor(argv = []) {
|
|
7381
|
+
try {
|
|
7382
|
+
const options = parseDoctorCommandArgs(argv);
|
|
7383
|
+
const report = await buildDoctorReport(options, {
|
|
7384
|
+
getStatusPayload: buildMcpStatusPayload,
|
|
7385
|
+
buildInstallStatusReport,
|
|
7386
|
+
buildConfigHealthReport,
|
|
7387
|
+
listSessionUsage,
|
|
7388
|
+
buildTaskOverviewPayload,
|
|
7389
|
+
listSkills
|
|
7390
|
+
});
|
|
7391
|
+
const format = options.format === 'md' ? 'md' : 'json';
|
|
7392
|
+
const text = format === 'md'
|
|
7393
|
+
? renderDoctorMarkdown(report)
|
|
7394
|
+
: JSON.stringify(report, null, 2);
|
|
7395
|
+
if (options.output) {
|
|
7396
|
+
ensureDir(path.dirname(options.output));
|
|
7397
|
+
fs.writeFileSync(options.output, text);
|
|
7398
|
+
} else {
|
|
7399
|
+
process.stdout.write(text + '\n');
|
|
7400
|
+
}
|
|
7401
|
+
} catch (e) {
|
|
7402
|
+
console.error('错误:', e && e.message ? e.message : e);
|
|
7403
|
+
process.exitCode = 1;
|
|
6480
7404
|
}
|
|
6481
|
-
|
|
6482
|
-
const current = config.model_provider || '未设置';
|
|
6483
|
-
const currentModel = config.model || '未设置';
|
|
7405
|
+
}
|
|
6484
7406
|
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
7407
|
+
async function cmdImportSkills(argv = []) {
|
|
7408
|
+
try {
|
|
7409
|
+
await cmdImportSkillsFromUrl(argv);
|
|
7410
|
+
} catch (e) {
|
|
7411
|
+
console.error('错误:', e && e.message ? e.message : e);
|
|
7412
|
+
process.exitCode = 1;
|
|
6491
7413
|
}
|
|
6492
|
-
console.log();
|
|
6493
7414
|
}
|
|
6494
7415
|
|
|
6495
7416
|
// 列出所有提供商
|
|
@@ -7262,12 +8183,57 @@ function cmdClaude(baseUrl, apiKey, model, silent = false) {
|
|
|
7262
8183
|
}
|
|
7263
8184
|
|
|
7264
8185
|
function commandExists(command, args = '') {
|
|
8186
|
+
const cmd = typeof command === 'string' ? command.trim() : '';
|
|
8187
|
+
const argText = typeof args === 'string' ? args.trim() : '';
|
|
8188
|
+
if (!cmd || cmd.includes('\0') || /[\r\n]/.test(cmd)) {
|
|
8189
|
+
return false;
|
|
8190
|
+
}
|
|
8191
|
+
const argv = argText ? argText.split(/\s+/g).filter(Boolean) : [];
|
|
8192
|
+
const hasSeparators = cmd.includes('/') || cmd.includes('\\');
|
|
8193
|
+
const useShell = process.platform === 'win32' && !hasSeparators;
|
|
8194
|
+
if (useShell) {
|
|
8195
|
+
if (!/^[A-Za-z0-9._-]+$/.test(cmd)) return false;
|
|
8196
|
+
if (argText && /[\r\n;&|<>`$]/.test(argText)) return false;
|
|
8197
|
+
}
|
|
7265
8198
|
try {
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
8199
|
+
const probe = spawnSync(cmd, argv, {
|
|
8200
|
+
stdio: 'ignore',
|
|
8201
|
+
windowsHide: true,
|
|
8202
|
+
timeout: 5000,
|
|
8203
|
+
shell: useShell
|
|
8204
|
+
});
|
|
8205
|
+
return probe.status === 0;
|
|
8206
|
+
} catch (_) {
|
|
8207
|
+
return false;
|
|
8208
|
+
}
|
|
8209
|
+
}
|
|
8210
|
+
|
|
8211
|
+
function isPrivateNetworkHost(hostname) {
|
|
8212
|
+
const host = typeof hostname === 'string' ? hostname.trim().toLowerCase() : '';
|
|
8213
|
+
if (!host) return true;
|
|
8214
|
+
if (host === 'localhost') return true;
|
|
8215
|
+
const ipVer = net.isIP(host);
|
|
8216
|
+
if (!ipVer) {
|
|
8217
|
+
return false;
|
|
8218
|
+
}
|
|
8219
|
+
if (ipVer === 4) {
|
|
8220
|
+
const parts = host.split('.').map((x) => parseInt(x, 10));
|
|
8221
|
+
if (parts.length !== 4 || parts.some((x) => !Number.isFinite(x))) return true;
|
|
8222
|
+
const [a, b] = parts;
|
|
8223
|
+
if (a === 10) return true;
|
|
8224
|
+
if (a === 127) return true;
|
|
8225
|
+
if (a === 169 && b === 254) return true;
|
|
8226
|
+
if (a === 192 && b === 168) return true;
|
|
8227
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
7269
8228
|
return false;
|
|
7270
8229
|
}
|
|
8230
|
+
if (ipVer === 6) {
|
|
8231
|
+
if (host === '::1') return true;
|
|
8232
|
+
if (host.startsWith('fe80:')) return true;
|
|
8233
|
+
if (host.startsWith('fc') || host.startsWith('fd')) return true;
|
|
8234
|
+
return false;
|
|
8235
|
+
}
|
|
8236
|
+
return false;
|
|
7271
8237
|
}
|
|
7272
8238
|
|
|
7273
8239
|
function detectPreferredPackageManager() {
|
|
@@ -7486,10 +8452,11 @@ function resolveExportOutputPath(outputPath, defaultFileName) {
|
|
|
7486
8452
|
}
|
|
7487
8453
|
|
|
7488
8454
|
function printExportSessionUsage() {
|
|
7489
|
-
console.log('\n用法: codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
|
|
8455
|
+
console.log('\n用法: codexmate export-session --source <codex|claude|gemini|codebuddy> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
|
|
7490
8456
|
console.log('\n示例:');
|
|
7491
8457
|
console.log(' codexmate export-session --source codex --session-id 123456');
|
|
7492
8458
|
console.log(' codexmate export-session --source claude --file "~/.claude/projects/demo/session.jsonl"');
|
|
8459
|
+
console.log(' codexmate export-session --source codebuddy --file "~/.codebuddy/projects/demo/session.jsonl"');
|
|
7493
8460
|
console.log(' codexmate export-session --source codex --session-id 123456 --max-messages=all');
|
|
7494
8461
|
}
|
|
7495
8462
|
|
|
@@ -7902,6 +8869,234 @@ function writeJsonResponse(res, statusCode, payload) {
|
|
|
7902
8869
|
res.end(body, 'utf-8');
|
|
7903
8870
|
}
|
|
7904
8871
|
|
|
8872
|
+
function readJsonRequestBody(req, res, options = {}) {
|
|
8873
|
+
const maxBytes = Number.isFinite(options.maxBytes) ? Math.max(1024, Math.floor(options.maxBytes)) : MAX_API_BODY_SIZE;
|
|
8874
|
+
return new Promise((resolve) => {
|
|
8875
|
+
const chunks = [];
|
|
8876
|
+
let bodySize = 0;
|
|
8877
|
+
let bodyTooLarge = false;
|
|
8878
|
+
req.on('data', (chunk) => {
|
|
8879
|
+
if (bodyTooLarge) return;
|
|
8880
|
+
bodySize += chunk.length;
|
|
8881
|
+
if (bodySize > maxBytes) {
|
|
8882
|
+
bodyTooLarge = true;
|
|
8883
|
+
writeJsonResponse(res, 413, {
|
|
8884
|
+
error: `请求体过大(>${Math.floor(maxBytes / 1024 / 1024)}MB)`
|
|
8885
|
+
});
|
|
8886
|
+
req.destroy();
|
|
8887
|
+
resolve({ ok: false, error: 'payload-too-large' });
|
|
8888
|
+
return;
|
|
8889
|
+
}
|
|
8890
|
+
chunks.push(chunk);
|
|
8891
|
+
});
|
|
8892
|
+
req.on('end', () => {
|
|
8893
|
+
if (bodyTooLarge) return;
|
|
8894
|
+
const rawBuffer = chunks.length ? Buffer.concat(chunks) : Buffer.alloc(0);
|
|
8895
|
+
const rawText = rawBuffer.length ? rawBuffer.toString('utf-8') : '';
|
|
8896
|
+
try {
|
|
8897
|
+
resolve({ ok: true, body: JSON.parse(rawText || '{}'), rawText, rawBuffer });
|
|
8898
|
+
} catch (error) {
|
|
8899
|
+
resolve({ ok: false, error: error && error.message ? error.message : 'invalid json' });
|
|
8900
|
+
}
|
|
8901
|
+
});
|
|
8902
|
+
});
|
|
8903
|
+
}
|
|
8904
|
+
|
|
8905
|
+
function isLoopbackRemoteAddress(value) {
|
|
8906
|
+
const addr = typeof value === 'string' ? value.trim() : '';
|
|
8907
|
+
if (!addr) return false;
|
|
8908
|
+
if (addr === '127.0.0.1' || addr === '::1') return true;
|
|
8909
|
+
if (addr === '::ffff:127.0.0.1') return true;
|
|
8910
|
+
return false;
|
|
8911
|
+
}
|
|
8912
|
+
|
|
8913
|
+
function extractRequestToken(req) {
|
|
8914
|
+
const headers = req && req.headers && typeof req.headers === 'object' ? req.headers : {};
|
|
8915
|
+
const rawAuth = typeof headers.authorization === 'string' ? headers.authorization.trim() : '';
|
|
8916
|
+
if (rawAuth) {
|
|
8917
|
+
const match = rawAuth.match(/^bearer\s+(.+)$/i);
|
|
8918
|
+
if (match && match[1]) return match[1].trim();
|
|
8919
|
+
return rawAuth;
|
|
8920
|
+
}
|
|
8921
|
+
const raw = typeof headers['x-codexmate-token'] === 'string' ? headers['x-codexmate-token'].trim() : '';
|
|
8922
|
+
return raw;
|
|
8923
|
+
}
|
|
8924
|
+
|
|
8925
|
+
function readServerToken() {
|
|
8926
|
+
const raw = typeof process.env.CODEXMATE_HTTP_TOKEN === 'string' ? process.env.CODEXMATE_HTTP_TOKEN.trim() : '';
|
|
8927
|
+
return raw;
|
|
8928
|
+
}
|
|
8929
|
+
|
|
8930
|
+
function assertRequestAuthorized(req, res) {
|
|
8931
|
+
const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
|
|
8932
|
+
if (isLoopbackRemoteAddress(remoteAddr)) {
|
|
8933
|
+
return { ok: true, mode: 'loopback' };
|
|
8934
|
+
}
|
|
8935
|
+
const expected = readServerToken();
|
|
8936
|
+
if (!expected) {
|
|
8937
|
+
writeJsonResponse(res, 403, {
|
|
8938
|
+
error: 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN or use --host 127.0.0.1)'
|
|
8939
|
+
});
|
|
8940
|
+
return { ok: false, mode: 'missing-token' };
|
|
8941
|
+
}
|
|
8942
|
+
const actual = extractRequestToken(req);
|
|
8943
|
+
if (!actual || actual !== expected) {
|
|
8944
|
+
writeJsonResponse(res, 401, { error: 'Unauthorized' });
|
|
8945
|
+
return { ok: false, mode: 'unauthorized' };
|
|
8946
|
+
}
|
|
8947
|
+
return { ok: true, mode: 'token' };
|
|
8948
|
+
}
|
|
8949
|
+
|
|
8950
|
+
const g_webhookDeliveryCache = new Map();
|
|
8951
|
+
|
|
8952
|
+
function pruneWebhookDeliveryCache() {
|
|
8953
|
+
const now = Date.now();
|
|
8954
|
+
for (const [key, expiresAt] of g_webhookDeliveryCache.entries()) {
|
|
8955
|
+
if (now >= expiresAt) {
|
|
8956
|
+
g_webhookDeliveryCache.delete(key);
|
|
8957
|
+
}
|
|
8958
|
+
}
|
|
8959
|
+
}
|
|
8960
|
+
|
|
8961
|
+
function rememberWebhookDeliveryId(value, ttlMs = 10 * 60 * 1000) {
|
|
8962
|
+
const id = typeof value === 'string' ? value.trim() : '';
|
|
8963
|
+
if (!id) return { ok: true, seen: false };
|
|
8964
|
+
pruneWebhookDeliveryCache();
|
|
8965
|
+
if (g_webhookDeliveryCache.has(id)) {
|
|
8966
|
+
return { ok: true, seen: true };
|
|
8967
|
+
}
|
|
8968
|
+
g_webhookDeliveryCache.set(id, Date.now() + ttlMs);
|
|
8969
|
+
while (g_webhookDeliveryCache.size > 2000) {
|
|
8970
|
+
const firstKey = g_webhookDeliveryCache.keys().next().value;
|
|
8971
|
+
if (!firstKey) break;
|
|
8972
|
+
g_webhookDeliveryCache.delete(firstKey);
|
|
8973
|
+
}
|
|
8974
|
+
return { ok: true, seen: false };
|
|
8975
|
+
}
|
|
8976
|
+
|
|
8977
|
+
function safeTimingEqual(a, b) {
|
|
8978
|
+
try {
|
|
8979
|
+
const ba = Buffer.isBuffer(a) ? a : Buffer.from(String(a || ''), 'utf-8');
|
|
8980
|
+
const bb = Buffer.isBuffer(b) ? b : Buffer.from(String(b || ''), 'utf-8');
|
|
8981
|
+
if (ba.length !== bb.length) return false;
|
|
8982
|
+
return crypto.timingSafeEqual(ba, bb);
|
|
8983
|
+
} catch (_) {
|
|
8984
|
+
return false;
|
|
8985
|
+
}
|
|
8986
|
+
}
|
|
8987
|
+
|
|
8988
|
+
function verifyGithubWebhookSignature(secret, signatureHeader, rawBuffer) {
|
|
8989
|
+
const key = typeof secret === 'string' ? secret : '';
|
|
8990
|
+
const signature = typeof signatureHeader === 'string' ? signatureHeader.trim() : '';
|
|
8991
|
+
if (!key || !signature || !signature.startsWith('sha256=')) return false;
|
|
8992
|
+
const expected = 'sha256=' + crypto.createHmac('sha256', key).update(rawBuffer || Buffer.alloc(0)).digest('hex');
|
|
8993
|
+
return safeTimingEqual(signature, expected);
|
|
8994
|
+
}
|
|
8995
|
+
|
|
8996
|
+
async function handleAutomationHook(req, res, source) {
|
|
8997
|
+
const method = (req.method || 'GET').toUpperCase();
|
|
8998
|
+
if (method !== 'POST') {
|
|
8999
|
+
writeJsonResponse(res, 405, { error: 'Method Not Allowed' });
|
|
9000
|
+
return;
|
|
9001
|
+
}
|
|
9002
|
+
const deliveryId = typeof (req.headers || {})['x-github-delivery'] === 'string'
|
|
9003
|
+
? String(req.headers['x-github-delivery'] || '')
|
|
9004
|
+
: (typeof (req.headers || {})['x-gitlab-event-uuid'] === 'string' ? String(req.headers['x-gitlab-event-uuid'] || '') : '');
|
|
9005
|
+
const remember = rememberWebhookDeliveryId(deliveryId);
|
|
9006
|
+
if (remember.seen) {
|
|
9007
|
+
writeJsonResponse(res, 200, { ok: true, deduped: true });
|
|
9008
|
+
return;
|
|
9009
|
+
}
|
|
9010
|
+
const parsedBody = await readJsonRequestBody(req, res);
|
|
9011
|
+
if (!parsedBody.ok) {
|
|
9012
|
+
if (parsedBody.error !== 'payload-too-large') {
|
|
9013
|
+
writeJsonResponse(res, 400, { error: parsedBody.error || 'invalid request body' });
|
|
9014
|
+
}
|
|
9015
|
+
return;
|
|
9016
|
+
}
|
|
9017
|
+
const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
|
|
9018
|
+
const isLoopback = !remoteAddr || isLoopbackRemoteAddress(remoteAddr);
|
|
9019
|
+
const normalizedSource = typeof source === 'string' ? source.trim().toLowerCase() : '';
|
|
9020
|
+
if (normalizedSource === 'github') {
|
|
9021
|
+
const secret = typeof process.env.CODEXMATE_GITHUB_WEBHOOK_SECRET === 'string'
|
|
9022
|
+
? process.env.CODEXMATE_GITHUB_WEBHOOK_SECRET
|
|
9023
|
+
: '';
|
|
9024
|
+
if (!secret && !isLoopback) {
|
|
9025
|
+
writeJsonResponse(res, 403, { error: 'Remote GitHub webhook is disabled (set CODEXMATE_GITHUB_WEBHOOK_SECRET)' });
|
|
9026
|
+
return;
|
|
9027
|
+
}
|
|
9028
|
+
if (secret) {
|
|
9029
|
+
const signature = (req.headers || {})['x-hub-signature-256'];
|
|
9030
|
+
if (!verifyGithubWebhookSignature(secret, signature, parsedBody.rawBuffer)) {
|
|
9031
|
+
writeJsonResponse(res, 401, { error: 'Invalid webhook signature' });
|
|
9032
|
+
return;
|
|
9033
|
+
}
|
|
9034
|
+
}
|
|
9035
|
+
} else if (normalizedSource === 'gitlab') {
|
|
9036
|
+
const secret = typeof process.env.CODEXMATE_GITLAB_WEBHOOK_SECRET === 'string'
|
|
9037
|
+
? process.env.CODEXMATE_GITLAB_WEBHOOK_SECRET.trim()
|
|
9038
|
+
: '';
|
|
9039
|
+
if (!secret && !isLoopback) {
|
|
9040
|
+
writeJsonResponse(res, 403, { error: 'Remote GitLab webhook is disabled (set CODEXMATE_GITLAB_WEBHOOK_SECRET)' });
|
|
9041
|
+
return;
|
|
9042
|
+
}
|
|
9043
|
+
if (secret) {
|
|
9044
|
+
const tokenHeader = typeof (req.headers || {})['x-gitlab-token'] === 'string'
|
|
9045
|
+
? String(req.headers['x-gitlab-token']).trim()
|
|
9046
|
+
: '';
|
|
9047
|
+
if (!tokenHeader || tokenHeader !== secret) {
|
|
9048
|
+
writeJsonResponse(res, 401, { error: 'Invalid webhook token' });
|
|
9049
|
+
return;
|
|
9050
|
+
}
|
|
9051
|
+
}
|
|
9052
|
+
}
|
|
9053
|
+
const payload = parsedBody.body && typeof parsedBody.body === 'object' ? parsedBody.body : {};
|
|
9054
|
+
const eventKey = buildAutomationEventKey(source, req.headers || {}, payload);
|
|
9055
|
+
if (!eventKey) {
|
|
9056
|
+
writeJsonResponse(res, 400, { error: 'unknown event' });
|
|
9057
|
+
return;
|
|
9058
|
+
}
|
|
9059
|
+
const cfg = readAutomationConfig(AUTOMATION_CONFIG_FILE, { env: process.env });
|
|
9060
|
+
if (!cfg.ok) {
|
|
9061
|
+
writeJsonResponse(res, 500, { error: cfg.error || 'failed to load automation config' });
|
|
9062
|
+
return;
|
|
9063
|
+
}
|
|
9064
|
+
const rule = matchAutomationRule(cfg.config, { source, event: eventKey });
|
|
9065
|
+
if (!rule) {
|
|
9066
|
+
writeJsonResponse(res, 404, { error: 'no matching rule', source, event: eventKey });
|
|
9067
|
+
return;
|
|
9068
|
+
}
|
|
9069
|
+
const action = rule.action && typeof rule.action === 'object' ? rule.action : {};
|
|
9070
|
+
const actionType = typeof action.type === 'string' ? action.type.trim().toLowerCase() : '';
|
|
9071
|
+
if (actionType !== 'task.queue.add') {
|
|
9072
|
+
writeJsonResponse(res, 400, { error: 'unsupported rule action', action: actionType || '' });
|
|
9073
|
+
return;
|
|
9074
|
+
}
|
|
9075
|
+
const taskPayload = action.task && typeof action.task === 'object' ? action.task : {};
|
|
9076
|
+
const enqueue = addTaskToQueue(taskPayload);
|
|
9077
|
+
if (enqueue.error) {
|
|
9078
|
+
writeJsonResponse(res, 400, { error: enqueue.error, issues: enqueue.issues || [], warnings: enqueue.warnings || [] });
|
|
9079
|
+
return;
|
|
9080
|
+
}
|
|
9081
|
+
const taskId = enqueue.task && enqueue.task.taskId ? enqueue.task.taskId : '';
|
|
9082
|
+
const shouldStart = action.startQueue === true;
|
|
9083
|
+
const queueResult = shouldStart
|
|
9084
|
+
? await startTaskQueueProcessing({ taskId: '', detach: true })
|
|
9085
|
+
: { ok: true, started: false };
|
|
9086
|
+
writeJsonResponse(res, 200, {
|
|
9087
|
+
ok: true,
|
|
9088
|
+
ruleId: rule.id,
|
|
9089
|
+
source,
|
|
9090
|
+
event: eventKey,
|
|
9091
|
+
taskId,
|
|
9092
|
+
queue: {
|
|
9093
|
+
started: !!queueResult.started,
|
|
9094
|
+
alreadyRunning: !!queueResult.alreadyRunning,
|
|
9095
|
+
detached: !!queueResult.detached
|
|
9096
|
+
}
|
|
9097
|
+
});
|
|
9098
|
+
}
|
|
9099
|
+
|
|
7905
9100
|
function streamZipDownloadResponse(res, filePath, options = {}) {
|
|
7906
9101
|
if (!filePath || !fs.existsSync(filePath)) {
|
|
7907
9102
|
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
@@ -8204,9 +9399,50 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8204
9399
|
|
|
8205
9400
|
const server = http.createServer((req, res) => {
|
|
8206
9401
|
const requestPath = (req.url || '/').split('?')[0];
|
|
9402
|
+
const sendJson = (statusCode, payload) => {
|
|
9403
|
+
const body = JSON.stringify(payload || {}, null, 2);
|
|
9404
|
+
res.writeHead(statusCode, {
|
|
9405
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
9406
|
+
'Content-Length': Buffer.byteLength(body, 'utf-8')
|
|
9407
|
+
});
|
|
9408
|
+
res.end(body, 'utf-8');
|
|
9409
|
+
};
|
|
8207
9410
|
if (typeof openaiBridgeHandler === 'function' && openaiBridgeHandler(req, res)) {
|
|
8208
9411
|
return;
|
|
8209
9412
|
}
|
|
9413
|
+
if (
|
|
9414
|
+
requestPath === '/api'
|
|
9415
|
+
|| requestPath.startsWith('/api/import-')
|
|
9416
|
+
|| requestPath.startsWith('/hooks/')
|
|
9417
|
+
|| requestPath.startsWith('/download/')
|
|
9418
|
+
) {
|
|
9419
|
+
const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
|
|
9420
|
+
const isLoopback = !remoteAddr
|
|
9421
|
+
|| remoteAddr === '127.0.0.1'
|
|
9422
|
+
|| remoteAddr === '::1'
|
|
9423
|
+
|| remoteAddr === '::ffff:127.0.0.1';
|
|
9424
|
+
if (!isLoopback) {
|
|
9425
|
+
const expected = typeof process.env.CODEXMATE_HTTP_TOKEN === 'string'
|
|
9426
|
+
? process.env.CODEXMATE_HTTP_TOKEN.trim()
|
|
9427
|
+
: '';
|
|
9428
|
+
if (!expected) {
|
|
9429
|
+
sendJson(403, {
|
|
9430
|
+
error: 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN or use --host 127.0.0.1)'
|
|
9431
|
+
});
|
|
9432
|
+
return;
|
|
9433
|
+
}
|
|
9434
|
+
const headers = req && req.headers && typeof req.headers === 'object' ? req.headers : {};
|
|
9435
|
+
const rawAuth = typeof headers.authorization === 'string' ? headers.authorization.trim() : '';
|
|
9436
|
+
const match = rawAuth ? rawAuth.match(/^bearer\s+(.+)$/i) : null;
|
|
9437
|
+
const actual = match && match[1]
|
|
9438
|
+
? match[1].trim()
|
|
9439
|
+
: (rawAuth ? rawAuth : (typeof headers['x-codexmate-token'] === 'string' ? String(headers['x-codexmate-token']).trim() : ''));
|
|
9440
|
+
if (!actual || actual !== expected) {
|
|
9441
|
+
sendJson(401, { error: 'Unauthorized' });
|
|
9442
|
+
return;
|
|
9443
|
+
}
|
|
9444
|
+
}
|
|
9445
|
+
}
|
|
8210
9446
|
if (requestPath === '/api/import-skills-zip') {
|
|
8211
9447
|
void handleImportSkillsZipUpload(req, res);
|
|
8212
9448
|
return;
|
|
@@ -8215,7 +9451,18 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8215
9451
|
void handleImportSkillsZipUpload(req, res, { targetApp: 'codex' });
|
|
8216
9452
|
return;
|
|
8217
9453
|
}
|
|
9454
|
+
if (requestPath.startsWith('/hooks/')) {
|
|
9455
|
+
const segments = requestPath.split('/').filter(Boolean);
|
|
9456
|
+
const source = segments[1] ? String(segments[1]) : '';
|
|
9457
|
+
void handleAutomationHook(req, res, source);
|
|
9458
|
+
return;
|
|
9459
|
+
}
|
|
8218
9460
|
if (requestPath === '/api') {
|
|
9461
|
+
const method = (req.method ? String(req.method) : 'POST').toUpperCase();
|
|
9462
|
+
if (method !== 'POST') {
|
|
9463
|
+
sendJson(405, { error: 'Method Not Allowed' });
|
|
9464
|
+
return;
|
|
9465
|
+
}
|
|
8219
9466
|
let body = '';
|
|
8220
9467
|
let bodySize = 0;
|
|
8221
9468
|
let bodyTooLarge = false;
|
|
@@ -8224,7 +9471,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8224
9471
|
bodySize += chunk.length;
|
|
8225
9472
|
if (bodySize > MAX_API_BODY_SIZE) {
|
|
8226
9473
|
bodyTooLarge = true;
|
|
8227
|
-
|
|
9474
|
+
sendJson(413, {
|
|
8228
9475
|
error: `请求体过大(>${Math.floor(MAX_API_BODY_SIZE / 1024 / 1024)}MB)`
|
|
8229
9476
|
});
|
|
8230
9477
|
req.destroy();
|
|
@@ -8235,7 +9482,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8235
9482
|
req.on('end', async () => {
|
|
8236
9483
|
if (bodyTooLarge) return;
|
|
8237
9484
|
try {
|
|
8238
|
-
const { action, params } = JSON.parse(body);
|
|
9485
|
+
const { action, params } = JSON.parse(body || '{}');
|
|
8239
9486
|
let result;
|
|
8240
9487
|
|
|
8241
9488
|
switch (action) {
|
|
@@ -8304,6 +9551,20 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8304
9551
|
if (!baseUrl) {
|
|
8305
9552
|
result = { error: 'Base URL is required' };
|
|
8306
9553
|
} else {
|
|
9554
|
+
const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
|
|
9555
|
+
const requesterIsLoopback = !remoteAddr
|
|
9556
|
+
|| remoteAddr === '127.0.0.1'
|
|
9557
|
+
|| remoteAddr === '::1'
|
|
9558
|
+
|| remoteAddr === '::ffff:127.0.0.1';
|
|
9559
|
+
if (!requesterIsLoopback) {
|
|
9560
|
+
try {
|
|
9561
|
+
const parsedUrl = new URL(baseUrl);
|
|
9562
|
+
if (isPrivateNetworkHost(parsedUrl.hostname || '')) {
|
|
9563
|
+
result = { error: 'Refusing to access private network baseUrl from non-loopback request' };
|
|
9564
|
+
break;
|
|
9565
|
+
}
|
|
9566
|
+
} catch (_) {}
|
|
9567
|
+
}
|
|
8307
9568
|
const res = await fetchModelsFromBaseUrl(baseUrl, apiKey);
|
|
8308
9569
|
if (res.error) {
|
|
8309
9570
|
result = { error: res.error, models: [], source: 'remote' };
|
|
@@ -8339,6 +9600,21 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8339
9600
|
case 'config-health-check':
|
|
8340
9601
|
result = await buildConfigHealthReport(params || {});
|
|
8341
9602
|
break;
|
|
9603
|
+
case 'doctor':
|
|
9604
|
+
{
|
|
9605
|
+
const doctorParams = isPlainObject(params) ? params : {};
|
|
9606
|
+
const report = await buildDoctorReport(doctorParams, {
|
|
9607
|
+
getStatusPayload: buildMcpStatusPayload,
|
|
9608
|
+
buildInstallStatusReport,
|
|
9609
|
+
buildConfigHealthReport,
|
|
9610
|
+
listSessionUsage,
|
|
9611
|
+
buildTaskOverviewPayload,
|
|
9612
|
+
listSkills
|
|
9613
|
+
});
|
|
9614
|
+
result = buildDoctorLegacyPayload(report);
|
|
9615
|
+
result.markdown = renderDoctorMarkdown(report);
|
|
9616
|
+
}
|
|
9617
|
+
break;
|
|
8342
9618
|
case 'get-agents-file':
|
|
8343
9619
|
result = readAgentsFile(params || {});
|
|
8344
9620
|
break;
|
|
@@ -8446,11 +9722,41 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8446
9722
|
result = { error: target.error };
|
|
8447
9723
|
break;
|
|
8448
9724
|
}
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
9725
|
+
const timeoutMs = Number.isFinite(params && params.timeoutMs)
|
|
9726
|
+
? Math.max(1000, Number(params.timeoutMs))
|
|
9727
|
+
: 0;
|
|
9728
|
+
if (Array.isArray(target.candidates) && target.candidates.length > 0) {
|
|
9729
|
+
let finalCandidate = target.candidates[0];
|
|
9730
|
+
let finalResult = null;
|
|
9731
|
+
for (let index = 0; index < target.candidates.length; index += 1) {
|
|
9732
|
+
const candidate = target.candidates[index];
|
|
9733
|
+
const probeResult = await runSpeedTest(candidate.url, target.apiKey, {
|
|
9734
|
+
...candidate,
|
|
9735
|
+
apiKeyHeader: target.apiKeyHeader,
|
|
9736
|
+
headers: target.headers,
|
|
9737
|
+
timeoutMs: timeoutMs || undefined
|
|
9738
|
+
});
|
|
9739
|
+
finalCandidate = candidate;
|
|
9740
|
+
finalResult = probeResult;
|
|
9741
|
+
const status = Number.isFinite(probeResult && probeResult.status) ? probeResult.status : 0;
|
|
9742
|
+
const shouldTryNext = index < target.candidates.length - 1 && status === 404;
|
|
9743
|
+
if (!shouldTryNext) {
|
|
9744
|
+
break;
|
|
9745
|
+
}
|
|
9746
|
+
}
|
|
9747
|
+
result = {
|
|
9748
|
+
ok: !!(finalResult && finalResult.ok),
|
|
9749
|
+
status: Number.isFinite(finalResult && finalResult.status) ? finalResult.status : 0,
|
|
9750
|
+
durationMs: Number.isFinite(finalResult && finalResult.durationMs) ? finalResult.durationMs : 0,
|
|
9751
|
+
error: finalResult && finalResult.ok ? '' : (finalResult && finalResult.error ? finalResult.error : ''),
|
|
9752
|
+
url: finalCandidate && finalCandidate.url ? finalCandidate.url : ''
|
|
9753
|
+
};
|
|
9754
|
+
break;
|
|
9755
|
+
}
|
|
9756
|
+
result = await runSpeedTest(target.url, target.apiKey, {
|
|
9757
|
+
...target,
|
|
9758
|
+
timeoutMs: timeoutMs || undefined
|
|
9759
|
+
});
|
|
8454
9760
|
break;
|
|
8455
9761
|
}
|
|
8456
9762
|
case 'openai-bridge-get-provider': {
|
|
@@ -8471,8 +9777,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8471
9777
|
case 'list-sessions':
|
|
8472
9778
|
{
|
|
8473
9779
|
const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
|
|
8474
|
-
if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
|
|
8475
|
-
result = { error: 'Invalid source. Must be codex, claude, or all' };
|
|
9780
|
+
if (source && source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'all') {
|
|
9781
|
+
result = { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, or all' };
|
|
8476
9782
|
} else {
|
|
8477
9783
|
result = {
|
|
8478
9784
|
sessions: await listSessionBrowse(params),
|
|
@@ -8485,8 +9791,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8485
9791
|
{
|
|
8486
9792
|
const usageParams = isPlainObject(params) ? params : {};
|
|
8487
9793
|
const source = typeof usageParams.source === 'string' ? usageParams.source.trim().toLowerCase() : '';
|
|
8488
|
-
if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
|
|
8489
|
-
result = { error: 'Invalid source. Must be codex, claude, or all' };
|
|
9794
|
+
if (source && source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'all') {
|
|
9795
|
+
result = { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, or all' };
|
|
8490
9796
|
} else {
|
|
8491
9797
|
result = {
|
|
8492
9798
|
sessions: await listSessionUsage({
|
|
@@ -8501,8 +9807,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8501
9807
|
case 'list-session-paths':
|
|
8502
9808
|
{
|
|
8503
9809
|
const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
|
|
8504
|
-
if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
|
|
8505
|
-
result = { error: 'Invalid source. Must be codex, claude, or all' };
|
|
9810
|
+
if (source && source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'all') {
|
|
9811
|
+
result = { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, or all' };
|
|
8506
9812
|
} else {
|
|
8507
9813
|
result = {
|
|
8508
9814
|
paths: listSessionPaths(params)
|
|
@@ -8846,7 +10152,14 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8846
10152
|
});
|
|
8847
10153
|
return;
|
|
8848
10154
|
}
|
|
8849
|
-
|
|
10155
|
+
const allowLegacy = process.env.CODEXMATE_ALLOW_LEGACY_DOWNLOAD === '1';
|
|
10156
|
+
const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
|
|
10157
|
+
const isLoopback = !remoteAddr || remoteAddr === '127.0.0.1' || remoteAddr === '::1' || remoteAddr === '::ffff:127.0.0.1';
|
|
10158
|
+
if (!allowLegacy || !isLoopback) {
|
|
10159
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
10160
|
+
res.end('Not Found');
|
|
10161
|
+
return;
|
|
10162
|
+
}
|
|
8850
10163
|
const tempDir = os.tmpdir();
|
|
8851
10164
|
const legacyFilePath = path.join(tempDir, decodedFileName);
|
|
8852
10165
|
if (!isPathInside(legacyFilePath, tempDir)) {
|
|
@@ -8859,8 +10172,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8859
10172
|
deleteAfterDownload: false
|
|
8860
10173
|
});
|
|
8861
10174
|
} else if (requestPath.startsWith('/res/')) {
|
|
8862
|
-
const normalized = path.normalize(requestPath).replace(/^([\\.\\/])+/, '');
|
|
8863
|
-
const filePath = path.join(
|
|
10175
|
+
const normalized = path.normalize(requestPath.slice('/res/'.length)).replace(/^([\\.\\/])+/, '');
|
|
10176
|
+
const filePath = path.join(assetsDir, normalized);
|
|
8864
10177
|
if (!isPathInside(filePath, assetsDir)) {
|
|
8865
10178
|
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
8866
10179
|
res.end('Forbidden');
|
|
@@ -8913,19 +10226,23 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8913
10226
|
const openUrl = `http://${formatHostForUrl(openHost)}:${port}`;
|
|
8914
10227
|
server.listen(port, host, () => {
|
|
8915
10228
|
console.log('\n✓ Web UI 已启动');
|
|
8916
|
-
|
|
10229
|
+
const willOpenBrowser = !!openBrowser && !process.env.CODEXMATE_NO_BROWSER;
|
|
10230
|
+
console.log(` ${willOpenBrowser ? '已打开' : '待访问'}: ${openUrl}`);
|
|
8917
10231
|
if (host && host !== openHost) {
|
|
8918
10232
|
console.log(' 监听地址:', host);
|
|
8919
10233
|
}
|
|
8920
10234
|
console.log(' 退出: Ctrl+C\n');
|
|
8921
10235
|
if (isAnyAddressHost(host)) {
|
|
8922
|
-
|
|
8923
|
-
console.warn('
|
|
10236
|
+
const tokenEnabled = typeof process.env.CODEXMATE_HTTP_TOKEN === 'string' && process.env.CODEXMATE_HTTP_TOKEN.trim().length > 0;
|
|
10237
|
+
console.warn(`! 安全提示: 当前监听所有网卡(${tokenEnabled ? '已启用鉴权' : '无鉴权'})。`);
|
|
10238
|
+
if (!tokenEnabled) {
|
|
10239
|
+
console.warn(' 建议仅在可信网络使用,或改用 --host 127.0.0.1。');
|
|
10240
|
+
console.warn(' 如需远程访问,请设置 CODEXMATE_HTTP_TOKEN。');
|
|
10241
|
+
}
|
|
8924
10242
|
}
|
|
8925
10243
|
|
|
8926
|
-
if (
|
|
8927
|
-
|
|
8928
|
-
openBrowserAfterReady(url);
|
|
10244
|
+
if (willOpenBrowser) {
|
|
10245
|
+
openBrowserAfterReady(openUrl);
|
|
8929
10246
|
}
|
|
8930
10247
|
});
|
|
8931
10248
|
|
|
@@ -9022,7 +10339,7 @@ function cmdStart(options = {}) {
|
|
|
9022
10339
|
const newHtmlPath = path.join(webDir, 'index.html');
|
|
9023
10340
|
const legacyHtmlPath = path.join(__dirname, 'web-ui.html');
|
|
9024
10341
|
const htmlPath = fs.existsSync(newHtmlPath) ? newHtmlPath : legacyHtmlPath;
|
|
9025
|
-
const assetsDir = path.join(
|
|
10342
|
+
const assetsDir = path.join(webDir, 'res');
|
|
9026
10343
|
if (!fs.existsSync(htmlPath)) {
|
|
9027
10344
|
console.error('错误: Web UI 页面不存在(尝试路径: web-ui/index.html, web-ui.html)');
|
|
9028
10345
|
process.exit(1);
|
|
@@ -9036,22 +10353,26 @@ function cmdStart(options = {}) {
|
|
|
9036
10353
|
|| process.env.CODEXMATE_DEV === '1'
|
|
9037
10354
|
|| process.env.CODEXMATE_DEV === 'true';
|
|
9038
10355
|
|
|
9039
|
-
|
|
10356
|
+
const shouldOpenBrowser = !options.noBrowser && !process.env.CODEXMATE_NO_BROWSER;
|
|
10357
|
+
|
|
9040
10358
|
let serverHandle = createWebServer({
|
|
9041
10359
|
htmlPath,
|
|
9042
10360
|
assetsDir,
|
|
9043
10361
|
webDir,
|
|
9044
10362
|
host,
|
|
9045
10363
|
port,
|
|
9046
|
-
openBrowser:
|
|
10364
|
+
openBrowser: shouldOpenBrowser
|
|
9047
10365
|
});
|
|
9048
10366
|
|
|
10367
|
+
const stopAutomationScheduler = startAutomationScheduler();
|
|
10368
|
+
|
|
9049
10369
|
// 禁止前端变更侦测与自动重启:避免终端输出噪音与访问时短暂 Connection Refused。
|
|
9050
10370
|
// 如需热重启,请由开发者自行使用外部 watcher / nodemon 等工具。
|
|
9051
10371
|
const stopWatch = () => {};
|
|
9052
10372
|
|
|
9053
10373
|
const handleExit = () => {
|
|
9054
10374
|
stopWatch();
|
|
10375
|
+
stopAutomationScheduler();
|
|
9055
10376
|
Promise.allSettled([
|
|
9056
10377
|
serverHandle.stop(),
|
|
9057
10378
|
stopBuiltinProxyRuntime(),
|
|
@@ -10591,7 +11912,7 @@ function buildMcpClaudeSettingsPayload() {
|
|
|
10591
11912
|
function normalizeMcpSource(value) {
|
|
10592
11913
|
const source = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
10593
11914
|
if (!source) return '';
|
|
10594
|
-
if (source === 'codex' || source === 'claude' || source === 'all') {
|
|
11915
|
+
if (source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'all') {
|
|
10595
11916
|
return source;
|
|
10596
11917
|
}
|
|
10597
11918
|
return null;
|
|
@@ -10904,7 +12225,7 @@ function createWorkflowToolCatalog() {
|
|
|
10904
12225
|
handler: async (args = {}) => {
|
|
10905
12226
|
const source = normalizeMcpSource(args.source);
|
|
10906
12227
|
if (source === null) {
|
|
10907
|
-
return { error: 'Invalid source. Must be codex, claude, or all' };
|
|
12228
|
+
return { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, or all' };
|
|
10908
12229
|
}
|
|
10909
12230
|
return {
|
|
10910
12231
|
source: source || 'all',
|
|
@@ -11276,14 +12597,21 @@ function normalizeTaskQueueItem(raw = {}) {
|
|
|
11276
12597
|
|
|
11277
12598
|
function readTaskQueueState() {
|
|
11278
12599
|
const parsed = readJsonObjectFromFile(TASK_QUEUE_FILE, {});
|
|
12600
|
+
if (!parsed.ok && parsed.exists) {
|
|
12601
|
+
return {
|
|
12602
|
+
tasks: [],
|
|
12603
|
+
error: parsed.error || 'failed to read task queue'
|
|
12604
|
+
};
|
|
12605
|
+
}
|
|
11279
12606
|
if (!parsed.ok || !parsed.exists) {
|
|
11280
12607
|
return {
|
|
11281
|
-
tasks: []
|
|
12608
|
+
tasks: [],
|
|
12609
|
+
error: ''
|
|
11282
12610
|
};
|
|
11283
12611
|
}
|
|
11284
12612
|
const source = parsed.data && typeof parsed.data === 'object' ? parsed.data : {};
|
|
11285
12613
|
const tasks = Array.isArray(source.tasks) ? source.tasks.map((item) => normalizeTaskQueueItem(item)) : [];
|
|
11286
|
-
return { tasks };
|
|
12614
|
+
return { tasks, error: '' };
|
|
11287
12615
|
}
|
|
11288
12616
|
|
|
11289
12617
|
function writeTaskQueueState(state = {}) {
|
|
@@ -11293,17 +12621,58 @@ function writeTaskQueueState(state = {}) {
|
|
|
11293
12621
|
});
|
|
11294
12622
|
}
|
|
11295
12623
|
|
|
11296
|
-
function
|
|
11297
|
-
const
|
|
11298
|
-
|
|
11299
|
-
|
|
11300
|
-
|
|
11301
|
-
|
|
11302
|
-
}
|
|
11303
|
-
|
|
12624
|
+
function withTaskQueueLock(fn) {
|
|
12625
|
+
const lockPath = `${TASK_QUEUE_FILE}.lock`;
|
|
12626
|
+
ensureDir(path.dirname(lockPath));
|
|
12627
|
+
let lockFd = null;
|
|
12628
|
+
try {
|
|
12629
|
+
lockFd = fs.openSync(lockPath, 'wx', 0o600);
|
|
12630
|
+
} catch (error) {
|
|
12631
|
+
const code = error && error.code ? error.code : '';
|
|
12632
|
+
if (code === 'EEXIST') {
|
|
12633
|
+
try {
|
|
12634
|
+
const stat = fs.statSync(lockPath);
|
|
12635
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
12636
|
+
if (ageMs > 5000) {
|
|
12637
|
+
try {
|
|
12638
|
+
fs.unlinkSync(lockPath);
|
|
12639
|
+
} catch (_) {}
|
|
12640
|
+
lockFd = fs.openSync(lockPath, 'wx', 0o600);
|
|
12641
|
+
}
|
|
12642
|
+
} catch (_) {}
|
|
12643
|
+
}
|
|
12644
|
+
}
|
|
12645
|
+
if (!lockFd) {
|
|
12646
|
+
return { error: 'task queue is busy' };
|
|
12647
|
+
}
|
|
12648
|
+
try {
|
|
12649
|
+
return fn();
|
|
12650
|
+
} finally {
|
|
12651
|
+
try {
|
|
12652
|
+
fs.closeSync(lockFd);
|
|
12653
|
+
} catch (_) {}
|
|
12654
|
+
try {
|
|
12655
|
+
fs.unlinkSync(lockPath);
|
|
12656
|
+
} catch (_) {}
|
|
11304
12657
|
}
|
|
11305
|
-
|
|
11306
|
-
|
|
12658
|
+
}
|
|
12659
|
+
|
|
12660
|
+
function upsertTaskQueueItem(item) {
|
|
12661
|
+
return withTaskQueueLock(() => {
|
|
12662
|
+
const state = readTaskQueueState();
|
|
12663
|
+
if (state.error) {
|
|
12664
|
+
return { error: state.error };
|
|
12665
|
+
}
|
|
12666
|
+
const next = normalizeTaskQueueItem(item || {});
|
|
12667
|
+
const index = state.tasks.findIndex((entry) => entry.taskId === next.taskId);
|
|
12668
|
+
if (index >= 0) {
|
|
12669
|
+
state.tasks[index] = next;
|
|
12670
|
+
} else {
|
|
12671
|
+
state.tasks.push(next);
|
|
12672
|
+
}
|
|
12673
|
+
writeTaskQueueState(state);
|
|
12674
|
+
return next;
|
|
12675
|
+
});
|
|
11307
12676
|
}
|
|
11308
12677
|
|
|
11309
12678
|
function getTaskQueueItem(taskId) {
|
|
@@ -11338,15 +12707,19 @@ function appendTaskRunRecord(record) {
|
|
|
11338
12707
|
fs.appendFileSync(TASK_RUNS_FILE, `${JSON.stringify(record)}\n`, { encoding: 'utf-8', mode: 0o600 });
|
|
11339
12708
|
}
|
|
11340
12709
|
|
|
12710
|
+
let g_taskRunRecordsLastParseErrors = 0;
|
|
12711
|
+
|
|
11341
12712
|
function listTaskRunRecords(limit = 20) {
|
|
11342
12713
|
const max = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 20;
|
|
11343
12714
|
if (!fs.existsSync(TASK_RUNS_FILE)) {
|
|
12715
|
+
g_taskRunRecordsLastParseErrors = 0;
|
|
11344
12716
|
return [];
|
|
11345
12717
|
}
|
|
11346
12718
|
let content = '';
|
|
11347
12719
|
try {
|
|
11348
12720
|
content = fs.readFileSync(TASK_RUNS_FILE, 'utf-8');
|
|
11349
12721
|
} catch (_) {
|
|
12722
|
+
g_taskRunRecordsLastParseErrors = 0;
|
|
11350
12723
|
return [];
|
|
11351
12724
|
}
|
|
11352
12725
|
const rows = content
|
|
@@ -11354,14 +12727,18 @@ function listTaskRunRecords(limit = 20) {
|
|
|
11354
12727
|
.map((line) => line.trim())
|
|
11355
12728
|
.filter(Boolean);
|
|
11356
12729
|
const parsed = [];
|
|
12730
|
+
let parseErrors = 0;
|
|
11357
12731
|
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
11358
12732
|
try {
|
|
11359
12733
|
parsed.push(JSON.parse(rows[i]));
|
|
11360
12734
|
if (parsed.length >= max) {
|
|
11361
12735
|
break;
|
|
11362
12736
|
}
|
|
11363
|
-
} catch (_) {
|
|
12737
|
+
} catch (_) {
|
|
12738
|
+
parseErrors += 1;
|
|
12739
|
+
}
|
|
11364
12740
|
}
|
|
12741
|
+
g_taskRunRecordsLastParseErrors = parseErrors;
|
|
11365
12742
|
return parsed;
|
|
11366
12743
|
}
|
|
11367
12744
|
|
|
@@ -11423,18 +12800,117 @@ function collectTaskRunSummary(detail = {}) {
|
|
|
11423
12800
|
};
|
|
11424
12801
|
}
|
|
11425
12802
|
|
|
12803
|
+
function writeTaskRunArtifacts(detail = {}) {
|
|
12804
|
+
const validation = validateTaskRunId(detail && typeof detail.runId === 'string' ? detail.runId : '');
|
|
12805
|
+
if (!validation.ok) {
|
|
12806
|
+
return;
|
|
12807
|
+
}
|
|
12808
|
+
const run = detail.run && typeof detail.run === 'object' ? detail.run : {};
|
|
12809
|
+
const nodes = Array.isArray(run.nodes) ? run.nodes : [];
|
|
12810
|
+
const dir = path.join(TASK_ARTIFACTS_DIR, validation.runId);
|
|
12811
|
+
ensureDir(dir);
|
|
12812
|
+
writeJsonAtomic(path.join(dir, 'summary.json'), collectTaskRunSummary(detail));
|
|
12813
|
+
const runLogText = summarizeTaskLogs(run.logs || [], 200);
|
|
12814
|
+
const nodeLogText = nodes
|
|
12815
|
+
.map((node) => {
|
|
12816
|
+
const header = node && node.id ? `\n# ${node.id}\n` : '\n# node\n';
|
|
12817
|
+
const text = summarizeTaskLogs(node && node.logs ? node.logs : [], 200);
|
|
12818
|
+
return `${header}${text}`.trimEnd();
|
|
12819
|
+
})
|
|
12820
|
+
.filter(Boolean)
|
|
12821
|
+
.join('\n');
|
|
12822
|
+
const combined = `${runLogText}${nodeLogText ? `\n\n${nodeLogText}` : ''}`.trim();
|
|
12823
|
+
try {
|
|
12824
|
+
fs.writeFileSync(path.join(dir, 'logs.txt'), combined, { encoding: 'utf-8', mode: 0o600 });
|
|
12825
|
+
} catch (_) {}
|
|
12826
|
+
}
|
|
12827
|
+
|
|
12828
|
+
async function notifyAutomationOnTaskRun(detail = {}) {
|
|
12829
|
+
const cfg = readAutomationConfig(AUTOMATION_CONFIG_FILE, { env: process.env });
|
|
12830
|
+
if (!cfg.ok || !cfg.config) {
|
|
12831
|
+
return [];
|
|
12832
|
+
}
|
|
12833
|
+
const payload = formatTaskRunNotificationPayload(detail);
|
|
12834
|
+
const status = String(payload.status || '').toLowerCase();
|
|
12835
|
+
const eventType = status === 'success'
|
|
12836
|
+
? 'task.completed'
|
|
12837
|
+
: (status === 'failed' ? 'task.failed' : 'task.finished');
|
|
12838
|
+
return await dispatchAutomationNotifiers(cfg.config, eventType, payload);
|
|
12839
|
+
}
|
|
12840
|
+
|
|
12841
|
+
function startAutomationScheduler() {
|
|
12842
|
+
const lastTicks = new Map();
|
|
12843
|
+
let tickInFlight = false;
|
|
12844
|
+
let timer = setInterval(async () => {
|
|
12845
|
+
if (tickInFlight) {
|
|
12846
|
+
return;
|
|
12847
|
+
}
|
|
12848
|
+
tickInFlight = true;
|
|
12849
|
+
try {
|
|
12850
|
+
const cfg = readAutomationConfig(AUTOMATION_CONFIG_FILE, { env: process.env });
|
|
12851
|
+
if (!cfg.ok || !cfg.config) {
|
|
12852
|
+
return;
|
|
12853
|
+
}
|
|
12854
|
+
const schedules = Array.isArray(cfg.config.schedules) ? cfg.config.schedules : [];
|
|
12855
|
+
if (schedules.length === 0) {
|
|
12856
|
+
return;
|
|
12857
|
+
}
|
|
12858
|
+
const now = new Date();
|
|
12859
|
+
const tickKey = now.toISOString().slice(0, 16);
|
|
12860
|
+
for (const schedule of schedules) {
|
|
12861
|
+
if (!schedule || schedule.enabled === false) continue;
|
|
12862
|
+
if (!schedule.id || !schedule.cron) continue;
|
|
12863
|
+
if (!isCronMatch(schedule.cron, now)) continue;
|
|
12864
|
+
if (lastTicks.get(schedule.id) === tickKey) continue;
|
|
12865
|
+
lastTicks.set(schedule.id, tickKey);
|
|
12866
|
+
const action = schedule.action && typeof schedule.action === 'object' ? schedule.action : {};
|
|
12867
|
+
const actionType = typeof action.type === 'string' ? action.type.trim().toLowerCase() : '';
|
|
12868
|
+
if (actionType !== 'task.queue.add') continue;
|
|
12869
|
+
const taskPayload = action.task && typeof action.task === 'object' ? action.task : {};
|
|
12870
|
+
try {
|
|
12871
|
+
const enqueue = addTaskToQueue(taskPayload);
|
|
12872
|
+
if (enqueue && enqueue.error) continue;
|
|
12873
|
+
if (action.startQueue === true) {
|
|
12874
|
+
await startTaskQueueProcessing({ taskId: '', detach: true });
|
|
12875
|
+
}
|
|
12876
|
+
} catch (_) {}
|
|
12877
|
+
}
|
|
12878
|
+
} finally {
|
|
12879
|
+
tickInFlight = false;
|
|
12880
|
+
}
|
|
12881
|
+
}, 30000);
|
|
12882
|
+
if (timer && typeof timer.unref === 'function') {
|
|
12883
|
+
timer.unref();
|
|
12884
|
+
}
|
|
12885
|
+
return () => {
|
|
12886
|
+
if (!timer) return;
|
|
12887
|
+
clearInterval(timer);
|
|
12888
|
+
timer = null;
|
|
12889
|
+
};
|
|
12890
|
+
}
|
|
12891
|
+
|
|
11426
12892
|
function buildTaskOverviewPayload(options = {}) {
|
|
11427
12893
|
const queueLimit = Number.isFinite(options.queueLimit) ? Math.max(1, Math.floor(options.queueLimit)) : 20;
|
|
11428
12894
|
const runLimit = Number.isFinite(options.runLimit) ? Math.max(1, Math.floor(options.runLimit)) : 20;
|
|
11429
12895
|
const workflowCatalog = buildTaskWorkflowCatalog();
|
|
11430
|
-
const
|
|
12896
|
+
const queueState = readTaskQueueState();
|
|
12897
|
+
const queue = queueState.error ? [] : listTaskQueueItems({ limit: queueLimit });
|
|
11431
12898
|
const runs = listTaskRunRecords(runLimit);
|
|
12899
|
+
const warnings = Array.isArray(workflowCatalog.warnings) ? [...workflowCatalog.warnings] : [];
|
|
12900
|
+
if (queueState.error) {
|
|
12901
|
+
warnings.push(`task queue read error: ${queueState.error}`);
|
|
12902
|
+
}
|
|
12903
|
+
if (g_taskRunRecordsLastParseErrors > 0) {
|
|
12904
|
+
warnings.push(`task run history parse errors: ${g_taskRunRecordsLastParseErrors}`);
|
|
12905
|
+
}
|
|
11432
12906
|
return {
|
|
11433
12907
|
workflows: workflowCatalog.workflows,
|
|
11434
|
-
warnings
|
|
12908
|
+
warnings,
|
|
11435
12909
|
queue,
|
|
11436
12910
|
runs,
|
|
11437
|
-
activeRunIds: Array.from(g_taskRunControllers.keys())
|
|
12911
|
+
activeRunIds: Array.from(g_taskRunControllers.keys()),
|
|
12912
|
+
queueError: queueState.error || '',
|
|
12913
|
+
runParseErrors: g_taskRunRecordsLastParseErrors
|
|
11438
12914
|
};
|
|
11439
12915
|
}
|
|
11440
12916
|
|
|
@@ -11732,7 +13208,7 @@ async function runTaskPlanInternal(plan, options = {}) {
|
|
|
11732
13208
|
}
|
|
11733
13209
|
});
|
|
11734
13210
|
if (options.queueItem) {
|
|
11735
|
-
upsertTaskQueueItem({
|
|
13211
|
+
const queued = upsertTaskQueueItem({
|
|
11736
13212
|
...options.queueItem,
|
|
11737
13213
|
taskId,
|
|
11738
13214
|
status: 'running',
|
|
@@ -11742,6 +13218,7 @@ async function runTaskPlanInternal(plan, options = {}) {
|
|
|
11742
13218
|
updatedAt: toIsoTime(Date.now()),
|
|
11743
13219
|
plan
|
|
11744
13220
|
});
|
|
13221
|
+
if (queued && queued.error) {}
|
|
11745
13222
|
}
|
|
11746
13223
|
try {
|
|
11747
13224
|
const run = await executeTaskPlan(plan, {
|
|
@@ -11765,7 +13242,7 @@ async function runTaskPlanInternal(plan, options = {}) {
|
|
|
11765
13242
|
};
|
|
11766
13243
|
writeTaskRunDetail(nextDetail);
|
|
11767
13244
|
if (options.queueItem) {
|
|
11768
|
-
upsertTaskQueueItem({
|
|
13245
|
+
const queued = upsertTaskQueueItem({
|
|
11769
13246
|
...options.queueItem,
|
|
11770
13247
|
taskId,
|
|
11771
13248
|
status: snapshot.status === 'success'
|
|
@@ -11777,6 +13254,7 @@ async function runTaskPlanInternal(plan, options = {}) {
|
|
|
11777
13254
|
updatedAt: toIsoTime(Date.now()),
|
|
11778
13255
|
plan
|
|
11779
13256
|
});
|
|
13257
|
+
if (queued && queued.error) {}
|
|
11780
13258
|
}
|
|
11781
13259
|
}
|
|
11782
13260
|
});
|
|
@@ -11788,8 +13266,12 @@ async function runTaskPlanInternal(plan, options = {}) {
|
|
|
11788
13266
|
};
|
|
11789
13267
|
writeTaskRunDetail(detail);
|
|
11790
13268
|
appendTaskRunRecord(collectTaskRunSummary(detail));
|
|
13269
|
+
writeTaskRunArtifacts(detail);
|
|
13270
|
+
try {
|
|
13271
|
+
await notifyAutomationOnTaskRun(detail);
|
|
13272
|
+
} catch (_) {}
|
|
11791
13273
|
if (options.queueItem) {
|
|
11792
|
-
upsertTaskQueueItem({
|
|
13274
|
+
const queued = upsertTaskQueueItem({
|
|
11793
13275
|
...options.queueItem,
|
|
11794
13276
|
taskId,
|
|
11795
13277
|
status: run.status === 'success'
|
|
@@ -11801,6 +13283,7 @@ async function runTaskPlanInternal(plan, options = {}) {
|
|
|
11801
13283
|
updatedAt: toIsoTime(Date.now()),
|
|
11802
13284
|
plan
|
|
11803
13285
|
});
|
|
13286
|
+
if (queued && queued.error) {}
|
|
11804
13287
|
}
|
|
11805
13288
|
return detail;
|
|
11806
13289
|
} finally {
|
|
@@ -11836,6 +13319,9 @@ function addTaskToQueue(params = {}) {
|
|
|
11836
13319
|
runStatus: '',
|
|
11837
13320
|
plan
|
|
11838
13321
|
});
|
|
13322
|
+
if (item && item.error) {
|
|
13323
|
+
return { error: item.error };
|
|
13324
|
+
}
|
|
11839
13325
|
return {
|
|
11840
13326
|
ok: true,
|
|
11841
13327
|
task: item,
|
|
@@ -12006,6 +13492,9 @@ function cancelTaskRunOrQueue(params = {}) {
|
|
|
12006
13492
|
updatedAt: toIsoTime(Date.now()),
|
|
12007
13493
|
lastSummary: queueItem.lastSummary || '已取消'
|
|
12008
13494
|
});
|
|
13495
|
+
if (next && next.error) {
|
|
13496
|
+
return { error: next.error };
|
|
13497
|
+
}
|
|
12009
13498
|
return {
|
|
12010
13499
|
ok: true,
|
|
12011
13500
|
cancelled: true,
|
|
@@ -12144,13 +13633,37 @@ function isLiveProcessId(value) {
|
|
|
12144
13633
|
}
|
|
12145
13634
|
}
|
|
12146
13635
|
|
|
13636
|
+
function isTaskWorkerProcessId(value) {
|
|
13637
|
+
const pid = Number.isFinite(Number(value)) ? Math.floor(Number(value)) : 0;
|
|
13638
|
+
if (!isLiveProcessId(pid)) return false;
|
|
13639
|
+
if (process.platform === 'linux') {
|
|
13640
|
+
try {
|
|
13641
|
+
const raw = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf-8');
|
|
13642
|
+
return raw.includes('__task-worker');
|
|
13643
|
+
} catch (_) {
|
|
13644
|
+
return true;
|
|
13645
|
+
}
|
|
13646
|
+
}
|
|
13647
|
+
if (process.platform === 'darwin') {
|
|
13648
|
+
try {
|
|
13649
|
+
const probe = spawnSync('ps', ['-p', String(pid), '-o', 'command='], { encoding: 'utf-8', timeout: 1500 });
|
|
13650
|
+
if (probe.error || probe.status !== 0) return true;
|
|
13651
|
+
const cmd = String(probe.stdout || '');
|
|
13652
|
+
return cmd.includes('__task-worker');
|
|
13653
|
+
} catch (_) {
|
|
13654
|
+
return true;
|
|
13655
|
+
}
|
|
13656
|
+
}
|
|
13657
|
+
return true;
|
|
13658
|
+
}
|
|
13659
|
+
|
|
12147
13660
|
function readTaskQueueWorkerState() {
|
|
12148
13661
|
const parsed = readJsonObjectFromFile(TASK_QUEUE_WORKER_FILE, {});
|
|
12149
13662
|
if (!parsed.ok || !parsed.exists || !parsed.data || typeof parsed.data !== 'object') {
|
|
12150
13663
|
return null;
|
|
12151
13664
|
}
|
|
12152
13665
|
const state = parsed.data;
|
|
12153
|
-
if (!
|
|
13666
|
+
if (!isTaskWorkerProcessId(state.pid)) {
|
|
12154
13667
|
try {
|
|
12155
13668
|
fs.unlinkSync(TASK_QUEUE_WORKER_FILE);
|
|
12156
13669
|
} catch (_) {}
|
|
@@ -12405,7 +13918,7 @@ function createMcpTools(options = {}) {
|
|
|
12405
13918
|
const input = args && typeof args === 'object' ? args : {};
|
|
12406
13919
|
const source = normalizeMcpSource(input.source);
|
|
12407
13920
|
if (source === null) {
|
|
12408
|
-
return { error: 'Invalid source. Must be codex, claude, or all' };
|
|
13921
|
+
return { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, or all' };
|
|
12409
13922
|
}
|
|
12410
13923
|
const normalizedInput = {
|
|
12411
13924
|
...input,
|
|
@@ -12854,7 +14367,7 @@ function createMcpResources() {
|
|
|
12854
14367
|
contents: [{
|
|
12855
14368
|
uri,
|
|
12856
14369
|
mimeType: 'application/json',
|
|
12857
|
-
text: JSON.stringify({ error: 'Invalid source. Must be codex, claude, or all' }, null, 2)
|
|
14370
|
+
text: JSON.stringify({ error: 'Invalid source. Must be codex, claude, gemini, codebuddy, or all' }, null, 2)
|
|
12858
14371
|
}]
|
|
12859
14372
|
};
|
|
12860
14373
|
}
|
|
@@ -13076,6 +14589,37 @@ async function cmdMcp(args = []) {
|
|
|
13076
14589
|
});
|
|
13077
14590
|
}
|
|
13078
14591
|
|
|
14592
|
+
function printMainHelp() {
|
|
14593
|
+
console.log('\nCodex Mate - Codex 提供商管理工具');
|
|
14594
|
+
console.log('\n用法:');
|
|
14595
|
+
console.log(' codexmate status 显示当前状态');
|
|
14596
|
+
console.log(' codexmate doctor [--format json|md] [--lang zh|en] [--output <PATH>] 输出诊断报告');
|
|
14597
|
+
console.log(' codexmate import-skills <URL> [--target-app codex|claude] [--name <NAME>] [--timeout-ms <MS>] 从 URL 导入 skills');
|
|
14598
|
+
console.log(' codexmate setup 交互式配置向导');
|
|
14599
|
+
console.log(' codexmate list 列出所有提供商');
|
|
14600
|
+
console.log(' codexmate models 列出所有模型');
|
|
14601
|
+
console.log(' codexmate switch <名称> 切换提供商');
|
|
14602
|
+
console.log(' codexmate use <模型> 切换模型');
|
|
14603
|
+
console.log(' codexmate add <名称> <URL> [密钥] [--bridge <openai>]');
|
|
14604
|
+
console.log(' codexmate delete <名称> 删除提供商');
|
|
14605
|
+
console.log(' codexmate claude <BaseURL> <API密钥> [模型] 写入 Claude Code 配置');
|
|
14606
|
+
console.log(' codexmate auth <list|import|switch|delete|status> 认证管理');
|
|
14607
|
+
console.log(' codexmate add-model <模型> 添加模型');
|
|
14608
|
+
console.log(' codexmate delete-model <模型> 删除模型');
|
|
14609
|
+
console.log(' codexmate workflow <list|get|validate|run|runs> MCP 工作流中心');
|
|
14610
|
+
console.log(' codexmate task <plan|run|runs|queue|retry|cancel|logs> 本地任务编排');
|
|
14611
|
+
console.log(' codexmate run [--host <HOST>] [--no-browser] 启动 Web 界面');
|
|
14612
|
+
console.log(' codexmate codex [参数...] [--follow-up <文本>|--queued-follow-up <文本> 可重复] 等同于 codex --yolo');
|
|
14613
|
+
console.log(' 注: follow-up 自动排队仅支持 linux/android/netbsd/openbsd/darwin/freebsd 且 stdin 必须是 TTY,其他平台会报错');
|
|
14614
|
+
console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
|
|
14615
|
+
console.log(' codexmate mcp [serve] [--transport stdio] [--allow-write|--read-only]');
|
|
14616
|
+
console.log(' codexmate export-session --source <codex|claude|gemini|codebuddy> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
|
|
14617
|
+
console.log(' codexmate zip <路径> [--max:级别] 压缩(系统 zip 优先,其次 zip-lib)');
|
|
14618
|
+
console.log(' codexmate unzip <zip文件> [输出目录] 解压(zip-lib)');
|
|
14619
|
+
console.log(' codexmate unzip-ext <zip目录> [输出目录] [--ext:后缀[,后缀...]] [--no-recursive] 批量提取 ZIP 指定后缀文件(默认递归)');
|
|
14620
|
+
console.log('');
|
|
14621
|
+
}
|
|
14622
|
+
|
|
13079
14623
|
// ============================================================================
|
|
13080
14624
|
// 主程序
|
|
13081
14625
|
// ============================================================================
|
|
@@ -13091,32 +14635,8 @@ async function main() {
|
|
|
13091
14635
|
}
|
|
13092
14636
|
}
|
|
13093
14637
|
|
|
13094
|
-
if (args.length === 0) {
|
|
13095
|
-
|
|
13096
|
-
console.log('\n用法:');
|
|
13097
|
-
console.log(' codexmate status 显示当前状态');
|
|
13098
|
-
console.log(' codexmate setup 交互式配置向导');
|
|
13099
|
-
console.log(' codexmate list 列出所有提供商');
|
|
13100
|
-
console.log(' codexmate models 列出所有模型');
|
|
13101
|
-
console.log(' codexmate switch <名称> 切换提供商');
|
|
13102
|
-
console.log(' codexmate use <模型> 切换模型');
|
|
13103
|
-
console.log(' codexmate add <名称> <URL> [密钥] [--bridge <openai>]');
|
|
13104
|
-
console.log(' codexmate delete <名称> 删除提供商');
|
|
13105
|
-
console.log(' codexmate claude <BaseURL> <API密钥> [模型] 写入 Claude Code 配置');
|
|
13106
|
-
console.log(' codexmate add-model <模型> 添加模型');
|
|
13107
|
-
console.log(' codexmate delete-model <模型> 删除模型');
|
|
13108
|
-
console.log(' codexmate workflow <list|get|validate|run|runs> MCP 工作流中心');
|
|
13109
|
-
console.log(' codexmate task <plan|run|runs|queue|retry|cancel|logs> 本地任务编排');
|
|
13110
|
-
console.log(' codexmate run [--host <HOST>] [--no-browser] 启动 Web 界面');
|
|
13111
|
-
console.log(' codexmate codex [参数...] [--follow-up <文本>|--queued-follow-up <文本> 可重复] 等同于 codex --yolo');
|
|
13112
|
-
console.log(' 注: follow-up 自动排队仅支持 linux/android/netbsd/openbsd/darwin/freebsd 且 stdin 必须是 TTY,其他平台会报错');
|
|
13113
|
-
console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
|
|
13114
|
-
console.log(' codexmate mcp [serve] [--transport stdio] [--allow-write|--read-only]');
|
|
13115
|
-
console.log(' codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
|
|
13116
|
-
console.log(' codexmate zip <路径> [--max:级别] 压缩(系统 zip 优先,其次 zip-lib)');
|
|
13117
|
-
console.log(' codexmate unzip <zip文件> [输出目录] 解压(zip-lib)');
|
|
13118
|
-
console.log(' codexmate unzip-ext <zip目录> [输出目录] [--ext:后缀[,后缀...]] [--no-recursive] 批量提取 ZIP 指定后缀文件(默认递归)');
|
|
13119
|
-
console.log('');
|
|
14638
|
+
if (args.length === 0 || command === '--help' || command === '-h' || command === 'help') {
|
|
14639
|
+
printMainHelp();
|
|
13120
14640
|
process.exit(0);
|
|
13121
14641
|
}
|
|
13122
14642
|
|
|
@@ -13155,6 +14675,8 @@ async function main() {
|
|
|
13155
14675
|
switch (command) {
|
|
13156
14676
|
case '__task-worker': await cmdTaskWorker(args.slice(1)); break;
|
|
13157
14677
|
case 'status': cmdStatus(); break;
|
|
14678
|
+
case 'doctor': await cmdDoctor(args.slice(1)); break;
|
|
14679
|
+
case 'import-skills': await cmdImportSkills(args.slice(1)); break;
|
|
13158
14680
|
case 'setup': await cmdSetup(); break;
|
|
13159
14681
|
case 'list': cmdList(); break;
|
|
13160
14682
|
case 'models': await cmdModels(); break;
|