codexmate 0.0.24 → 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 +28 -6
- package/README.zh.md +29 -7
- package/cli/builtin-proxy.js +35 -0
- package/cli/claude-proxy.js +24 -0
- package/cli/import-skills-url.js +23 -1
- package/cli/openai-bridge.js +51 -4
- package/cli/session-usage.js +8 -2
- package/cli.js +1543 -117
- package/lib/automation.js +404 -0
- 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 +2 -5
- package/web-ui/app.js +6 -3
- package/web-ui/logic.sessions.mjs +2 -2
- package/web-ui/modules/app.computed.dashboard.mjs +2 -0
- package/web-ui/modules/app.computed.session.mjs +17 -0
- package/web-ui/modules/app.methods.install.mjs +28 -0
- package/web-ui/modules/app.methods.session-actions.mjs +8 -1
- package/web-ui/modules/app.methods.session-browser.mjs +28 -4
- package/web-ui/modules/app.methods.session-trash.mjs +4 -2
- package/web-ui/modules/i18n.dict.mjs +24 -8
- package/web-ui/partials/index/layout-header.html +12 -2
- package/web-ui/partials/index/panel-sessions.html +4 -2
- package/web-ui/partials/index/panel-usage.html +7 -0
- package/web-ui/styles/controls-forms.css +49 -2
- package/web-ui/styles/layout-shell.css +1 -0
- package/web-ui/styles/responsive.css +0 -2
- package/web-ui/styles/sessions-list.css +2 -4
- package/web-ui/styles/sessions-toolbar-trash.css +4 -4
- package/web-ui/styles/sessions-usage.css +24 -0
- /package/{res → web-ui/res}/json5.min.js +0 -0
- /package/{res → web-ui/res}/logo-pack.webp +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,
|
|
@@ -74,6 +75,14 @@ const {
|
|
|
74
75
|
validateTaskPlan,
|
|
75
76
|
executeTaskPlan
|
|
76
77
|
} = require('./lib/task-orchestrator');
|
|
78
|
+
const {
|
|
79
|
+
readAutomationConfig,
|
|
80
|
+
matchAutomationRule,
|
|
81
|
+
buildAutomationEventKey,
|
|
82
|
+
isCronMatch,
|
|
83
|
+
dispatchAutomationNotifiers,
|
|
84
|
+
formatTaskRunNotificationPayload
|
|
85
|
+
} = require('./lib/automation');
|
|
77
86
|
const { buildConfigHealthReport: buildConfigHealthReportCore } = require('./cli/config-health');
|
|
78
87
|
const { buildDoctorReport, buildDoctorLegacyPayload, renderDoctorMarkdown } = require('./cli/doctor-core');
|
|
79
88
|
const {
|
|
@@ -185,6 +194,10 @@ const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
|
185
194
|
const CLAUDE_SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
|
|
186
195
|
const CLAUDE_MD_FILE_NAME = 'CLAUDE.md';
|
|
187
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');
|
|
188
201
|
const RECENT_CONFIGS_FILE = path.join(CONFIG_DIR, 'recent-configs.json');
|
|
189
202
|
const WORKFLOW_DEFINITIONS_FILE = path.join(CONFIG_DIR, 'codexmate-workflows.json');
|
|
190
203
|
const WORKFLOW_RUNS_FILE = path.join(CONFIG_DIR, 'codexmate-workflow-runs.jsonl');
|
|
@@ -192,6 +205,8 @@ const TASK_QUEUE_FILE = path.join(CONFIG_DIR, 'codexmate-task-queue.json');
|
|
|
192
205
|
const TASK_RUNS_FILE = path.join(CONFIG_DIR, 'codexmate-task-runs.jsonl');
|
|
193
206
|
const TASK_RUN_DETAILS_DIR = path.join(CONFIG_DIR, 'codexmate-task-runs');
|
|
194
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');
|
|
195
210
|
const DEFAULT_CLAUDE_MODEL = 'glm-4.7';
|
|
196
211
|
const DEFAULT_MODEL_CONTEXT_WINDOW = 190000;
|
|
197
212
|
const DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT = 185000;
|
|
@@ -262,6 +277,18 @@ const CLI_INSTALL_TARGETS = Object.freeze([
|
|
|
262
277
|
packageName: '@anthropic-ai/claude-code',
|
|
263
278
|
bins: ['claude']
|
|
264
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
|
+
},
|
|
265
292
|
{
|
|
266
293
|
id: 'codex',
|
|
267
294
|
name: 'Codex CLI',
|
|
@@ -275,7 +302,7 @@ const HTTPS_KEEP_ALIVE_AGENT = new https.Agent({ keepAlive: true });
|
|
|
275
302
|
|
|
276
303
|
const openaiBridgeHandler = createOpenaiBridgeHttpHandler({
|
|
277
304
|
settingsFile: OPENAI_BRIDGE_SETTINGS_FILE,
|
|
278
|
-
expectedToken: '
|
|
305
|
+
expectedToken: typeof process.env.CODEXMATE_HTTP_TOKEN === 'string' ? process.env.CODEXMATE_HTTP_TOKEN.trim() : '',
|
|
279
306
|
maxBodySize: MAX_API_BODY_SIZE,
|
|
280
307
|
httpAgent: HTTP_KEEP_ALIVE_AGENT,
|
|
281
308
|
httpsAgent: HTTPS_KEEP_ALIVE_AGENT
|
|
@@ -559,7 +586,9 @@ let g_sessionListCache = new Map();
|
|
|
559
586
|
let g_sessionInventoryCache = new Map();
|
|
560
587
|
let g_sessionFileLookupCache = {
|
|
561
588
|
codex: new Map(),
|
|
562
|
-
claude: new Map()
|
|
589
|
+
claude: new Map(),
|
|
590
|
+
gemini: new Map(),
|
|
591
|
+
codebuddy: new Map()
|
|
563
592
|
};
|
|
564
593
|
let g_exactMessageCountCache = new Map();
|
|
565
594
|
let g_modelsCache = new Map();
|
|
@@ -1254,6 +1283,31 @@ function getClaudeProjectsDir() {
|
|
|
1254
1283
|
return resolveExistingDir(candidates, CLAUDE_PROJECTS_DIR);
|
|
1255
1284
|
}
|
|
1256
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
|
+
|
|
1257
1311
|
function readModelsCacheEntry(cacheKey) {
|
|
1258
1312
|
if (!cacheKey) return null;
|
|
1259
1313
|
const entry = g_modelsCache.get(cacheKey);
|
|
@@ -2501,6 +2555,19 @@ function countConversationMessagesInRecords(records, source) {
|
|
|
2501
2555
|
}
|
|
2502
2556
|
continue;
|
|
2503
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
|
+
}
|
|
2504
2571
|
|
|
2505
2572
|
const role = normalizeRole(record.type);
|
|
2506
2573
|
if (role === 'assistant' || role === 'user' || role === 'system') {
|
|
@@ -2522,6 +2589,28 @@ async function countConversationMessagesInFile(filePath, source) {
|
|
|
2522
2589
|
return cached;
|
|
2523
2590
|
}
|
|
2524
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
|
+
|
|
2525
2614
|
let stream;
|
|
2526
2615
|
let rl;
|
|
2527
2616
|
let messageCount = 0;
|
|
@@ -2549,6 +2638,15 @@ async function countConversationMessagesInFile(filePath, source) {
|
|
|
2549
2638
|
role = normalizeRole(record.payload.role);
|
|
2550
2639
|
text = extractMessageText(record.payload.content);
|
|
2551
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
|
+
}
|
|
2552
2650
|
} else {
|
|
2553
2651
|
role = normalizeRole(record.type);
|
|
2554
2652
|
if (role === 'assistant' || role === 'user' || role === 'system') {
|
|
@@ -2711,7 +2809,13 @@ async function resolveSessionTrashEntryExactMessageCount(entry) {
|
|
|
2711
2809
|
}
|
|
2712
2810
|
|
|
2713
2811
|
async function hydrateSessionTrashEntries(entries, options = {}) {
|
|
2714
|
-
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')));
|
|
2715
2819
|
const hydratedEntries = await mapWithConcurrency(Array.isArray(entries) ? entries : [], 8, async (entry) => {
|
|
2716
2820
|
const normalizedEntry = normalizeSessionTrashEntry(entry);
|
|
2717
2821
|
if (!normalizedEntry) {
|
|
@@ -2720,7 +2824,7 @@ async function hydrateSessionTrashEntries(entries, options = {}) {
|
|
|
2720
2824
|
return await resolveSessionTrashEntryExactMessageCount(normalizedEntry);
|
|
2721
2825
|
});
|
|
2722
2826
|
|
|
2723
|
-
if (source === 'codex' || source === 'claude') {
|
|
2827
|
+
if (source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy') {
|
|
2724
2828
|
return hydratedEntries.filter((entry) => entry.source === source);
|
|
2725
2829
|
}
|
|
2726
2830
|
return hydratedEntries;
|
|
@@ -2734,7 +2838,11 @@ async function hydrateSessionItemsExactMessageCount(items) {
|
|
|
2734
2838
|
if (item.__messageCountExact === true) {
|
|
2735
2839
|
return item;
|
|
2736
2840
|
}
|
|
2737
|
-
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' : '')));
|
|
2738
2846
|
const filePath = typeof item.filePath === 'string' ? item.filePath : '';
|
|
2739
2847
|
if (!source || !filePath || !fs.existsSync(filePath)) {
|
|
2740
2848
|
return item;
|
|
@@ -2961,6 +3069,54 @@ async function scanSessionContentForQuery(session, tokens, options = {}) {
|
|
|
2961
3069
|
? Math.max(1024, rawMaxBytes)
|
|
2962
3070
|
: 0;
|
|
2963
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
|
+
}
|
|
2964
3120
|
let stream;
|
|
2965
3121
|
let rl;
|
|
2966
3122
|
try {
|
|
@@ -3243,7 +3399,9 @@ function getSessionInventoryCache(cacheKey, forceRefresh = false) {
|
|
|
3243
3399
|
}
|
|
3244
3400
|
|
|
3245
3401
|
function registerSessionFileLookupEntries(source, sessions = []) {
|
|
3246
|
-
const normalizedSource = source === 'claude'
|
|
3402
|
+
const normalizedSource = source === 'claude' || source === 'gemini' || source === 'codebuddy'
|
|
3403
|
+
? source
|
|
3404
|
+
: 'codex';
|
|
3247
3405
|
const store = g_sessionFileLookupCache[normalizedSource];
|
|
3248
3406
|
if (!(store instanceof Map) || !Array.isArray(sessions)) {
|
|
3249
3407
|
return;
|
|
@@ -3282,7 +3440,9 @@ function setSessionInventoryCache(cacheKey, source, value) {
|
|
|
3282
3440
|
}
|
|
3283
3441
|
|
|
3284
3442
|
function listSessionInventoryBySource(source, limit, scanOptions = {}, options = {}) {
|
|
3285
|
-
const normalizedSource = source === 'claude'
|
|
3443
|
+
const normalizedSource = source === 'claude' || source === 'gemini' || source === 'codebuddy'
|
|
3444
|
+
? source
|
|
3445
|
+
: 'codex';
|
|
3286
3446
|
const forceRefresh = !!options.forceRefresh;
|
|
3287
3447
|
const cacheKey = buildSessionInventoryCacheKey(normalizedSource, limit, scanOptions);
|
|
3288
3448
|
const cached = getSessionInventoryCache(cacheKey, forceRefresh);
|
|
@@ -3292,7 +3452,11 @@ function listSessionInventoryBySource(source, limit, scanOptions = {}, options =
|
|
|
3292
3452
|
|
|
3293
3453
|
const sessions = normalizedSource === 'claude'
|
|
3294
3454
|
? listClaudeSessions(limit, scanOptions)
|
|
3295
|
-
:
|
|
3455
|
+
: (normalizedSource === 'gemini'
|
|
3456
|
+
? listGeminiSessions(limit, scanOptions)
|
|
3457
|
+
: (normalizedSource === 'codebuddy'
|
|
3458
|
+
? listCodeBuddySessions(limit, scanOptions)
|
|
3459
|
+
: listCodexSessions(limit, scanOptions)));
|
|
3296
3460
|
setSessionInventoryCache(cacheKey, normalizedSource, sessions);
|
|
3297
3461
|
return sessions;
|
|
3298
3462
|
}
|
|
@@ -3302,7 +3466,9 @@ function invalidateSessionListCache() {
|
|
|
3302
3466
|
g_sessionInventoryCache.clear();
|
|
3303
3467
|
g_sessionFileLookupCache = {
|
|
3304
3468
|
codex: new Map(),
|
|
3305
|
-
claude: new Map()
|
|
3469
|
+
claude: new Map(),
|
|
3470
|
+
gemini: new Map(),
|
|
3471
|
+
codebuddy: new Map()
|
|
3306
3472
|
};
|
|
3307
3473
|
}
|
|
3308
3474
|
|
|
@@ -3894,6 +4060,317 @@ function parseClaudeSessionSummary(filePath, options = {}) {
|
|
|
3894
4060
|
};
|
|
3895
4061
|
}
|
|
3896
4062
|
|
|
4063
|
+
function parseCodeBuddySessionSummary(filePath, options = {}) {
|
|
4064
|
+
const summaryReadBytes = Number.isFinite(Number(options.summaryReadBytes))
|
|
4065
|
+
? Math.max(1024, Math.floor(Number(options.summaryReadBytes)))
|
|
4066
|
+
: SESSION_SUMMARY_READ_BYTES;
|
|
4067
|
+
const titleReadBytes = Number.isFinite(Number(options.titleReadBytes))
|
|
4068
|
+
? Math.max(1024, Math.floor(Number(options.titleReadBytes)))
|
|
4069
|
+
: SESSION_TITLE_READ_BYTES;
|
|
4070
|
+
const records = parseJsonlHeadRecords(filePath, summaryReadBytes);
|
|
4071
|
+
if (records.length === 0) {
|
|
4072
|
+
return null;
|
|
4073
|
+
}
|
|
4074
|
+
|
|
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);
|
|
4106
|
+
}
|
|
4107
|
+
|
|
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
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
}
|
|
4143
|
+
|
|
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
|
+
};
|
|
4218
|
+
}
|
|
4219
|
+
|
|
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
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
return '';
|
|
4257
|
+
}
|
|
4258
|
+
|
|
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 = {}) {
|
|
4268
|
+
const summaryReadBytes = Number.isFinite(Number(options.summaryReadBytes))
|
|
4269
|
+
? Math.max(1024, Math.floor(Number(options.summaryReadBytes)))
|
|
4270
|
+
: SESSION_SUMMARY_READ_BYTES;
|
|
4271
|
+
const titleReadBytes = Number.isFinite(Number(options.titleReadBytes))
|
|
4272
|
+
? Math.max(1024, Math.floor(Number(options.titleReadBytes)))
|
|
4273
|
+
: SESSION_TITLE_READ_BYTES;
|
|
4274
|
+
let stat;
|
|
4275
|
+
try {
|
|
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
|
+
|
|
3897
4374
|
function listCodexSessions(limit, options = {}) {
|
|
3898
4375
|
const codexSessionsDir = getCodexSessionsDir();
|
|
3899
4376
|
const scanFactor = Number.isFinite(Number(options.scanFactor))
|
|
@@ -4144,8 +4621,144 @@ function listClaudeSessions(limit, options = {}) {
|
|
|
4144
4621
|
return mergeAndLimitSessions(sessions, limit);
|
|
4145
4622
|
}
|
|
4146
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
|
+
|
|
4147
4760
|
async function listAllSessions(params = {}) {
|
|
4148
|
-
const source = params.source === 'codex' || params.source === 'claude'
|
|
4761
|
+
const source = params.source === 'codex' || params.source === 'claude' || params.source === 'gemini' || params.source === 'codebuddy'
|
|
4149
4762
|
? params.source
|
|
4150
4763
|
: 'all';
|
|
4151
4764
|
const rawLimit = Number(params.limit);
|
|
@@ -4189,6 +4802,12 @@ async function listAllSessions(params = {}) {
|
|
|
4189
4802
|
if (source === 'all' || source === 'claude') {
|
|
4190
4803
|
sessions = sessions.concat(listSessionInventoryBySource('claude', limit, scanOptions, { forceRefresh }));
|
|
4191
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
|
+
}
|
|
4192
4811
|
|
|
4193
4812
|
if (hasPathFilter) {
|
|
4194
4813
|
sessions = sessions.filter(item => matchesSessionPathFilter(item, normalizedPathFilter));
|
|
@@ -4269,6 +4888,8 @@ async function listSessionUsage(params = {}) {
|
|
|
4269
4888
|
listSessionBrowse,
|
|
4270
4889
|
parseCodexSessionSummary,
|
|
4271
4890
|
parseClaudeSessionSummary,
|
|
4891
|
+
parseCodeBuddySessionSummary,
|
|
4892
|
+
parseGeminiSessionSummary,
|
|
4272
4893
|
MAX_SESSION_USAGE_LIST_SIZE,
|
|
4273
4894
|
SESSION_BROWSE_SUMMARY_READ_BYTES
|
|
4274
4895
|
});
|
|
@@ -4276,10 +4897,10 @@ async function listSessionUsage(params = {}) {
|
|
|
4276
4897
|
|
|
4277
4898
|
function listSessionPaths(params = {}) {
|
|
4278
4899
|
const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
|
|
4279
|
-
if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
|
|
4900
|
+
if (source && source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'all') {
|
|
4280
4901
|
return [];
|
|
4281
4902
|
}
|
|
4282
|
-
const validSource = source === 'codex' || source === 'claude' ? source : 'all';
|
|
4903
|
+
const validSource = source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy' ? source : 'all';
|
|
4283
4904
|
const rawLimit = Number(params.limit);
|
|
4284
4905
|
const limit = Number.isFinite(rawLimit)
|
|
4285
4906
|
? Math.max(1, Math.min(rawLimit, MAX_SESSION_PATH_LIST_SIZE))
|
|
@@ -4307,6 +4928,12 @@ function listSessionPaths(params = {}) {
|
|
|
4307
4928
|
if (validSource === 'all' || validSource === 'claude') {
|
|
4308
4929
|
sessions = sessions.concat(listSessionInventoryBySource('claude', gatherLimit, scanOptions, { forceRefresh }));
|
|
4309
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
|
+
}
|
|
4310
4937
|
|
|
4311
4938
|
const dedupedPaths = [];
|
|
4312
4939
|
const seen = new Set();
|
|
@@ -4332,7 +4959,14 @@ function listSessionPaths(params = {}) {
|
|
|
4332
4959
|
}
|
|
4333
4960
|
|
|
4334
4961
|
function resolveSessionFilePath(source, filePath, sessionId) {
|
|
4335
|
-
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()));
|
|
4336
4970
|
if (!root || !fs.existsSync(root)) {
|
|
4337
4971
|
return '';
|
|
4338
4972
|
}
|
|
@@ -4347,7 +4981,7 @@ function resolveSessionFilePath(source, filePath, sessionId) {
|
|
|
4347
4981
|
|
|
4348
4982
|
if (typeof sessionId === 'string' && sessionId.trim()) {
|
|
4349
4983
|
const targetId = sessionId.trim().toLowerCase();
|
|
4350
|
-
const lookupStore = g_sessionFileLookupCache[
|
|
4984
|
+
const lookupStore = g_sessionFileLookupCache[normalizedSource];
|
|
4351
4985
|
if (lookupStore instanceof Map && lookupStore.has(targetId)) {
|
|
4352
4986
|
const cachedPath = lookupStore.get(targetId);
|
|
4353
4987
|
if (cachedPath && fs.existsSync(cachedPath) && isPathInside(cachedPath, root)) {
|
|
@@ -4355,8 +4989,39 @@ function resolveSessionFilePath(source, filePath, sessionId) {
|
|
|
4355
4989
|
}
|
|
4356
4990
|
lookupStore.delete(targetId);
|
|
4357
4991
|
}
|
|
4358
|
-
|
|
4359
|
-
|
|
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
|
+
}
|
|
4360
5025
|
if (matchedFile && fs.existsSync(matchedFile)) {
|
|
4361
5026
|
return matchedFile;
|
|
4362
5027
|
}
|
|
@@ -4533,11 +5198,15 @@ function moveFileSync(sourcePath, targetPath) {
|
|
|
4533
5198
|
|
|
4534
5199
|
function buildSessionSummaryFallback(source, filePath, sessionId = '') {
|
|
4535
5200
|
const resolvedSessionId = sessionId || path.basename(filePath, '.jsonl');
|
|
4536
|
-
const sourceLabel = source === 'claude'
|
|
5201
|
+
const sourceLabel = source === 'claude'
|
|
5202
|
+
? 'Claude Code'
|
|
5203
|
+
: (source === 'gemini' ? 'Gemini CLI' : (source === 'codebuddy' ? 'CodeBuddy Code' : 'Codex'));
|
|
4537
5204
|
return {
|
|
4538
5205
|
source,
|
|
4539
5206
|
sourceLabel,
|
|
4540
|
-
provider: source === 'claude'
|
|
5207
|
+
provider: source === 'claude'
|
|
5208
|
+
? 'claude'
|
|
5209
|
+
: (source === 'gemini' ? 'gemini' : (source === 'codebuddy' ? 'codebuddy' : 'codex')),
|
|
4541
5210
|
sessionId: resolvedSessionId,
|
|
4542
5211
|
title: resolvedSessionId,
|
|
4543
5212
|
cwd: '',
|
|
@@ -4548,7 +5217,7 @@ function buildSessionSummaryFallback(source, filePath, sessionId = '') {
|
|
|
4548
5217
|
contextWindow: 0,
|
|
4549
5218
|
filePath,
|
|
4550
5219
|
keywords: [],
|
|
4551
|
-
capabilities: source === 'claude' ? { code: true } : {}
|
|
5220
|
+
capabilities: source === 'claude' || source === 'gemini' || source === 'codebuddy' ? { code: true } : {}
|
|
4552
5221
|
};
|
|
4553
5222
|
}
|
|
4554
5223
|
|
|
@@ -4559,11 +5228,14 @@ function generateSessionTrashId() {
|
|
|
4559
5228
|
return `trash-${Date.now().toString(36)}-${crypto.randomBytes(8).toString('hex')}`;
|
|
4560
5229
|
}
|
|
4561
5230
|
|
|
4562
|
-
function allocateSessionTrashTarget() {
|
|
5231
|
+
function allocateSessionTrashTarget(extension = 'jsonl') {
|
|
4563
5232
|
ensureDir(SESSION_TRASH_FILES_DIR);
|
|
5233
|
+
const safeExt = typeof extension === 'string' && extension.trim()
|
|
5234
|
+
? extension.trim().replace(/^\./, '')
|
|
5235
|
+
: 'jsonl';
|
|
4564
5236
|
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
4565
5237
|
const trashId = generateSessionTrashId();
|
|
4566
|
-
const trashFileName = `${trashId}
|
|
5238
|
+
const trashFileName = `${trashId}.${safeExt}`;
|
|
4567
5239
|
const trashFilePath = path.join(SESSION_TRASH_FILES_DIR, trashFileName);
|
|
4568
5240
|
if (!fs.existsSync(trashFilePath)) {
|
|
4569
5241
|
return { trashId, trashFileName, trashFilePath };
|
|
@@ -4572,8 +5244,8 @@ function allocateSessionTrashTarget() {
|
|
|
4572
5244
|
const fallbackId = `trash-${Date.now().toString(36)}-${crypto.randomBytes(8).toString('hex')}`;
|
|
4573
5245
|
return {
|
|
4574
5246
|
trashId: fallbackId,
|
|
4575
|
-
trashFileName: `${fallbackId}
|
|
4576
|
-
trashFilePath: path.join(SESSION_TRASH_FILES_DIR, `${fallbackId}
|
|
5247
|
+
trashFileName: `${fallbackId}.${safeExt}`,
|
|
5248
|
+
trashFilePath: path.join(SESSION_TRASH_FILES_DIR, `${fallbackId}.${safeExt}`)
|
|
4577
5249
|
};
|
|
4578
5250
|
}
|
|
4579
5251
|
|
|
@@ -4581,7 +5253,13 @@ function normalizeSessionTrashEntry(entry) {
|
|
|
4581
5253
|
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
4582
5254
|
return null;
|
|
4583
5255
|
}
|
|
4584
|
-
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' : '')));
|
|
4585
5263
|
const trashId = typeof entry.trashId === 'string' ? entry.trashId.trim() : '';
|
|
4586
5264
|
if (!source || !trashId || trashId.includes('/') || trashId.includes('\\') || trashId.includes('\0')) {
|
|
4587
5265
|
return null;
|
|
@@ -4596,7 +5274,9 @@ function normalizeSessionTrashEntry(entry) {
|
|
|
4596
5274
|
trashId,
|
|
4597
5275
|
trashFileName,
|
|
4598
5276
|
source,
|
|
4599
|
-
sourceLabel: source === 'claude'
|
|
5277
|
+
sourceLabel: source === 'claude'
|
|
5278
|
+
? 'Claude Code'
|
|
5279
|
+
: (source === 'gemini' ? 'Gemini CLI' : (source === 'codebuddy' ? 'CodeBuddy Code' : 'Codex')),
|
|
4600
5280
|
sessionId: sessionId || trashId,
|
|
4601
5281
|
title: typeof entry.title === 'string' && entry.title.trim() ? entry.title.trim() : (sessionId || trashId),
|
|
4602
5282
|
cwd: typeof entry.cwd === 'string' ? entry.cwd : '',
|
|
@@ -4612,7 +5292,7 @@ function normalizeSessionTrashEntry(entry) {
|
|
|
4612
5292
|
originalFilePath: typeof entry.originalFilePath === 'string' ? entry.originalFilePath : '',
|
|
4613
5293
|
provider: typeof entry.provider === 'string' && entry.provider.trim()
|
|
4614
5294
|
? entry.provider.trim()
|
|
4615
|
-
: (source === 'claude' ? 'claude' : 'codex'),
|
|
5295
|
+
: (source === 'claude' ? 'claude' : (source === 'gemini' ? 'gemini' : (source === 'codebuddy' ? 'codebuddy' : 'codex'))),
|
|
4616
5296
|
keywords: normalizeKeywords(entry.keywords),
|
|
4617
5297
|
capabilities: normalizeCapabilities(entry.capabilities),
|
|
4618
5298
|
claudeIndexPath: typeof entry.claudeIndexPath === 'string' ? entry.claudeIndexPath : '',
|
|
@@ -4730,7 +5410,11 @@ function resolveSessionRestoreTarget(entry) {
|
|
|
4730
5410
|
if (!normalized) {
|
|
4731
5411
|
return '';
|
|
4732
5412
|
}
|
|
4733
|
-
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()));
|
|
4734
5418
|
const originalFilePath = typeof normalized.originalFilePath === 'string' ? normalized.originalFilePath.trim() : '';
|
|
4735
5419
|
if (!root || !originalFilePath) {
|
|
4736
5420
|
return '';
|
|
@@ -4864,14 +5548,20 @@ function upsertClaudeSessionIndexEntry(indexPath, sessionFilePath, entry) {
|
|
|
4864
5548
|
}
|
|
4865
5549
|
|
|
4866
5550
|
async function listSessionTrashItems(params = {}) {
|
|
4867
|
-
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')));
|
|
4868
5558
|
const countOnly = params.countOnly === true;
|
|
4869
5559
|
const rawLimit = Number(params.limit);
|
|
4870
5560
|
const limit = Number.isFinite(rawLimit)
|
|
4871
5561
|
? Math.max(1, Math.min(rawLimit, MAX_SESSION_TRASH_LIST_SIZE))
|
|
4872
5562
|
: 200;
|
|
4873
5563
|
const allEntries = readSessionTrashEntries();
|
|
4874
|
-
let items = source === 'codex' || source === 'claude'
|
|
5564
|
+
let items = source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy'
|
|
4875
5565
|
? allEntries.filter((entry) => entry.source === source)
|
|
4876
5566
|
: allEntries.slice();
|
|
4877
5567
|
items.sort((a, b) => {
|
|
@@ -5051,7 +5741,13 @@ async function purgeSessionTrashItems(params = {}) {
|
|
|
5051
5741
|
}
|
|
5052
5742
|
|
|
5053
5743
|
async function trashSessionData(params = {}) {
|
|
5054
|
-
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' : '')));
|
|
5055
5751
|
if (!source) {
|
|
5056
5752
|
return { error: 'Invalid source' };
|
|
5057
5753
|
}
|
|
@@ -5061,14 +5757,18 @@ async function trashSessionData(params = {}) {
|
|
|
5061
5757
|
return { error: 'Session file not found' };
|
|
5062
5758
|
}
|
|
5063
5759
|
|
|
5064
|
-
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))))
|
|
5065
5765
|
|| buildSessionSummaryFallback(source, filePath, params.sessionId);
|
|
5066
5766
|
const exactMessageCount = await countConversationMessagesInFile(filePath, source);
|
|
5067
5767
|
if (Number.isFinite(Number(exactMessageCount))) {
|
|
5068
5768
|
summary.messageCount = Math.max(0, Math.floor(Number(exactMessageCount)));
|
|
5069
5769
|
}
|
|
5070
|
-
const sessionId = summary.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
|
|
5071
|
-
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');
|
|
5072
5772
|
const deletedAt = new Date().toISOString();
|
|
5073
5773
|
const claudeIndexPath = source === 'claude' ? findClaudeSessionIndexPath(filePath) : '';
|
|
5074
5774
|
let removedClaudeIndexEntry = null;
|
|
@@ -5152,7 +5852,13 @@ async function trashSessionData(params = {}) {
|
|
|
5152
5852
|
}
|
|
5153
5853
|
|
|
5154
5854
|
async function deleteSessionData(params = {}) {
|
|
5155
|
-
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' : '')));
|
|
5156
5862
|
if (!source) {
|
|
5157
5863
|
return { error: 'Invalid source' };
|
|
5158
5864
|
}
|
|
@@ -5162,7 +5868,7 @@ async function deleteSessionData(params = {}) {
|
|
|
5162
5868
|
return { error: 'Session file not found' };
|
|
5163
5869
|
}
|
|
5164
5870
|
|
|
5165
|
-
const sessionId = params.sessionId || path.basename(filePath, '.jsonl');
|
|
5871
|
+
const sessionId = params.sessionId || path.basename(filePath, source === 'gemini' ? '.json' : '.jsonl');
|
|
5166
5872
|
let fileDeleted = false;
|
|
5167
5873
|
try {
|
|
5168
5874
|
fs.unlinkSync(filePath);
|
|
@@ -5487,6 +6193,38 @@ function extractClaudeMessageFromRecord(record, state, lineIndex = -1) {
|
|
|
5487
6193
|
}
|
|
5488
6194
|
}
|
|
5489
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
|
+
|
|
5490
6228
|
function recordHasCodexMessage(record) {
|
|
5491
6229
|
if (!record || record.type !== 'response_item' || !record.payload) {
|
|
5492
6230
|
return false;
|
|
@@ -5515,10 +6253,23 @@ function recordHasClaudeMessage(record) {
|
|
|
5515
6253
|
return !!text;
|
|
5516
6254
|
}
|
|
5517
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
|
+
|
|
5518
6269
|
function recordHasMessage(record, source) {
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
6270
|
+
if (source === 'codex') return recordHasCodexMessage(record);
|
|
6271
|
+
if (source === 'codebuddy') return recordHasCodeBuddyMessage(record);
|
|
6272
|
+
return recordHasClaudeMessage(record);
|
|
5522
6273
|
}
|
|
5523
6274
|
|
|
5524
6275
|
function extractMessagesFromRecords(records, source, options = {}) {
|
|
@@ -5536,6 +6287,8 @@ function extractMessagesFromRecords(records, source, options = {}) {
|
|
|
5536
6287
|
const record = records[lineIndex];
|
|
5537
6288
|
if (source === 'codex') {
|
|
5538
6289
|
extractCodexMessageFromRecord(record, state, lineIndex);
|
|
6290
|
+
} else if (source === 'codebuddy') {
|
|
6291
|
+
extractCodeBuddyMessageFromRecord(record, state, lineIndex);
|
|
5539
6292
|
} else {
|
|
5540
6293
|
extractClaudeMessageFromRecord(record, state, lineIndex);
|
|
5541
6294
|
}
|
|
@@ -5597,6 +6350,8 @@ async function extractMessagesFromFile(filePath, source, options = {}) {
|
|
|
5597
6350
|
|
|
5598
6351
|
if (source === 'codex') {
|
|
5599
6352
|
extractCodexMessageFromRecord(record, state, currentLineIndex);
|
|
6353
|
+
} else if (source === 'codebuddy') {
|
|
6354
|
+
extractCodeBuddyMessageFromRecord(record, state, currentLineIndex);
|
|
5600
6355
|
} else {
|
|
5601
6356
|
extractClaudeMessageFromRecord(record, state, currentLineIndex);
|
|
5602
6357
|
}
|
|
@@ -5621,7 +6376,13 @@ async function extractMessagesFromFile(filePath, source, options = {}) {
|
|
|
5621
6376
|
}
|
|
5622
6377
|
|
|
5623
6378
|
async function readSessionDetail(params = {}) {
|
|
5624
|
-
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' : '')));
|
|
5625
6386
|
if (!source) {
|
|
5626
6387
|
return { error: 'Invalid source' };
|
|
5627
6388
|
}
|
|
@@ -5638,9 +6399,52 @@ async function readSessionDetail(params = {}) {
|
|
|
5638
6399
|
: DEFAULT_SESSION_DETAIL_MESSAGES;
|
|
5639
6400
|
const preview = params.preview === true || params.preview === 'true';
|
|
5640
6401
|
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
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'));
|
|
5644
6448
|
const clippedMessages = Array.isArray(extracted.messages) ? extracted.messages : [];
|
|
5645
6449
|
const hasExactTotalMessages = Number.isFinite(extracted.totalMessages);
|
|
5646
6450
|
const startIndex = hasExactTotalMessages
|
|
@@ -5674,7 +6478,13 @@ async function readSessionDetail(params = {}) {
|
|
|
5674
6478
|
}
|
|
5675
6479
|
|
|
5676
6480
|
async function readSessionPlain(params = {}) {
|
|
5677
|
-
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' : '')));
|
|
5678
6488
|
if (!source) {
|
|
5679
6489
|
return { error: 'Invalid source' };
|
|
5680
6490
|
}
|
|
@@ -5685,26 +6495,57 @@ async function readSessionPlain(params = {}) {
|
|
|
5685
6495
|
}
|
|
5686
6496
|
|
|
5687
6497
|
let extracted;
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
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
|
+
}
|
|
5693
6529
|
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
6530
|
+
if (!extracted) {
|
|
6531
|
+
return { error: 'Failed to parse session file' };
|
|
6532
|
+
}
|
|
5697
6533
|
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
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 });
|
|
5702
6540
|
}
|
|
5703
|
-
extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages: Infinity });
|
|
5704
6541
|
}
|
|
5705
6542
|
|
|
5706
|
-
const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
|
|
5707
|
-
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'));
|
|
5708
6549
|
const messages = removeLeadingSystemMessage(Array.isArray(extracted.messages) ? extracted.messages : []);
|
|
5709
6550
|
const text = buildSessionPlainText(messages);
|
|
5710
6551
|
|
|
@@ -5719,7 +6560,13 @@ async function readSessionPlain(params = {}) {
|
|
|
5719
6560
|
}
|
|
5720
6561
|
|
|
5721
6562
|
async function exportSessionData(params = {}) {
|
|
5722
|
-
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' : '')));
|
|
5723
6570
|
if (!source) {
|
|
5724
6571
|
return { error: 'Invalid source' };
|
|
5725
6572
|
}
|
|
@@ -5731,22 +6578,51 @@ async function exportSessionData(params = {}) {
|
|
|
5731
6578
|
}
|
|
5732
6579
|
|
|
5733
6580
|
let extracted;
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
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
|
+
}
|
|
5739
6614
|
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
6615
|
+
if (!extracted) {
|
|
6616
|
+
return { error: 'Failed to parse session file' };
|
|
6617
|
+
}
|
|
5743
6618
|
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
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 });
|
|
5748
6625
|
}
|
|
5749
|
-
extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages });
|
|
5750
6626
|
}
|
|
5751
6627
|
|
|
5752
6628
|
extracted.messages = removeLeadingSystemMessage(Array.isArray(extracted.messages) ? extracted.messages : []);
|
|
@@ -5758,9 +6634,13 @@ async function exportSessionData(params = {}) {
|
|
|
5758
6634
|
}
|
|
5759
6635
|
}
|
|
5760
6636
|
|
|
5761
|
-
const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
|
|
6637
|
+
const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, source === 'gemini' ? '.json' : '.jsonl');
|
|
5762
6638
|
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
5763
|
-
const sourceLabel = source === 'codex'
|
|
6639
|
+
const sourceLabel = source === 'codex'
|
|
6640
|
+
? 'Codex'
|
|
6641
|
+
: (source === 'claude'
|
|
6642
|
+
? 'Claude Code'
|
|
6643
|
+
: (source === 'gemini' ? 'Gemini CLI' : 'CodeBuddy Code'));
|
|
5764
6644
|
const truncated = !!extracted.truncated;
|
|
5765
6645
|
const maxMessagesLabel = maxMessages === Infinity ? 'all' : maxMessages;
|
|
5766
6646
|
const markdown = buildSessionMarkdown({
|
|
@@ -7303,12 +8183,57 @@ function cmdClaude(baseUrl, apiKey, model, silent = false) {
|
|
|
7303
8183
|
}
|
|
7304
8184
|
|
|
7305
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
|
+
}
|
|
7306
8198
|
try {
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
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;
|
|
7310
8228
|
return false;
|
|
7311
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;
|
|
7312
8237
|
}
|
|
7313
8238
|
|
|
7314
8239
|
function detectPreferredPackageManager() {
|
|
@@ -7527,10 +8452,11 @@ function resolveExportOutputPath(outputPath, defaultFileName) {
|
|
|
7527
8452
|
}
|
|
7528
8453
|
|
|
7529
8454
|
function printExportSessionUsage() {
|
|
7530
|
-
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>]');
|
|
7531
8456
|
console.log('\n示例:');
|
|
7532
8457
|
console.log(' codexmate export-session --source codex --session-id 123456');
|
|
7533
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"');
|
|
7534
8460
|
console.log(' codexmate export-session --source codex --session-id 123456 --max-messages=all');
|
|
7535
8461
|
}
|
|
7536
8462
|
|
|
@@ -7943,6 +8869,234 @@ function writeJsonResponse(res, statusCode, payload) {
|
|
|
7943
8869
|
res.end(body, 'utf-8');
|
|
7944
8870
|
}
|
|
7945
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
|
+
|
|
7946
9100
|
function streamZipDownloadResponse(res, filePath, options = {}) {
|
|
7947
9101
|
if (!filePath || !fs.existsSync(filePath)) {
|
|
7948
9102
|
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
@@ -8245,9 +9399,50 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8245
9399
|
|
|
8246
9400
|
const server = http.createServer((req, res) => {
|
|
8247
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
|
+
};
|
|
8248
9410
|
if (typeof openaiBridgeHandler === 'function' && openaiBridgeHandler(req, res)) {
|
|
8249
9411
|
return;
|
|
8250
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
|
+
}
|
|
8251
9446
|
if (requestPath === '/api/import-skills-zip') {
|
|
8252
9447
|
void handleImportSkillsZipUpload(req, res);
|
|
8253
9448
|
return;
|
|
@@ -8256,7 +9451,18 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8256
9451
|
void handleImportSkillsZipUpload(req, res, { targetApp: 'codex' });
|
|
8257
9452
|
return;
|
|
8258
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
|
+
}
|
|
8259
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
|
+
}
|
|
8260
9466
|
let body = '';
|
|
8261
9467
|
let bodySize = 0;
|
|
8262
9468
|
let bodyTooLarge = false;
|
|
@@ -8265,7 +9471,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8265
9471
|
bodySize += chunk.length;
|
|
8266
9472
|
if (bodySize > MAX_API_BODY_SIZE) {
|
|
8267
9473
|
bodyTooLarge = true;
|
|
8268
|
-
|
|
9474
|
+
sendJson(413, {
|
|
8269
9475
|
error: `请求体过大(>${Math.floor(MAX_API_BODY_SIZE / 1024 / 1024)}MB)`
|
|
8270
9476
|
});
|
|
8271
9477
|
req.destroy();
|
|
@@ -8276,7 +9482,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8276
9482
|
req.on('end', async () => {
|
|
8277
9483
|
if (bodyTooLarge) return;
|
|
8278
9484
|
try {
|
|
8279
|
-
const { action, params } = JSON.parse(body);
|
|
9485
|
+
const { action, params } = JSON.parse(body || '{}');
|
|
8280
9486
|
let result;
|
|
8281
9487
|
|
|
8282
9488
|
switch (action) {
|
|
@@ -8345,6 +9551,20 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8345
9551
|
if (!baseUrl) {
|
|
8346
9552
|
result = { error: 'Base URL is required' };
|
|
8347
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
|
+
}
|
|
8348
9568
|
const res = await fetchModelsFromBaseUrl(baseUrl, apiKey);
|
|
8349
9569
|
if (res.error) {
|
|
8350
9570
|
result = { error: res.error, models: [], source: 'remote' };
|
|
@@ -8557,8 +9777,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8557
9777
|
case 'list-sessions':
|
|
8558
9778
|
{
|
|
8559
9779
|
const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
|
|
8560
|
-
if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
|
|
8561
|
-
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' };
|
|
8562
9782
|
} else {
|
|
8563
9783
|
result = {
|
|
8564
9784
|
sessions: await listSessionBrowse(params),
|
|
@@ -8571,8 +9791,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8571
9791
|
{
|
|
8572
9792
|
const usageParams = isPlainObject(params) ? params : {};
|
|
8573
9793
|
const source = typeof usageParams.source === 'string' ? usageParams.source.trim().toLowerCase() : '';
|
|
8574
|
-
if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
|
|
8575
|
-
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' };
|
|
8576
9796
|
} else {
|
|
8577
9797
|
result = {
|
|
8578
9798
|
sessions: await listSessionUsage({
|
|
@@ -8587,8 +9807,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8587
9807
|
case 'list-session-paths':
|
|
8588
9808
|
{
|
|
8589
9809
|
const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
|
|
8590
|
-
if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
|
|
8591
|
-
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' };
|
|
8592
9812
|
} else {
|
|
8593
9813
|
result = {
|
|
8594
9814
|
paths: listSessionPaths(params)
|
|
@@ -8932,7 +10152,14 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8932
10152
|
});
|
|
8933
10153
|
return;
|
|
8934
10154
|
}
|
|
8935
|
-
|
|
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
|
+
}
|
|
8936
10163
|
const tempDir = os.tmpdir();
|
|
8937
10164
|
const legacyFilePath = path.join(tempDir, decodedFileName);
|
|
8938
10165
|
if (!isPathInside(legacyFilePath, tempDir)) {
|
|
@@ -8945,8 +10172,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
8945
10172
|
deleteAfterDownload: false
|
|
8946
10173
|
});
|
|
8947
10174
|
} else if (requestPath.startsWith('/res/')) {
|
|
8948
|
-
const normalized = path.normalize(requestPath).replace(/^([\\.\\/])+/, '');
|
|
8949
|
-
const filePath = path.join(
|
|
10175
|
+
const normalized = path.normalize(requestPath.slice('/res/'.length)).replace(/^([\\.\\/])+/, '');
|
|
10176
|
+
const filePath = path.join(assetsDir, normalized);
|
|
8950
10177
|
if (!isPathInside(filePath, assetsDir)) {
|
|
8951
10178
|
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
8952
10179
|
res.end('Forbidden');
|
|
@@ -9006,8 +10233,12 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
9006
10233
|
}
|
|
9007
10234
|
console.log(' 退出: Ctrl+C\n');
|
|
9008
10235
|
if (isAnyAddressHost(host)) {
|
|
9009
|
-
|
|
9010
|
-
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
|
+
}
|
|
9011
10242
|
}
|
|
9012
10243
|
|
|
9013
10244
|
if (willOpenBrowser) {
|
|
@@ -9108,7 +10339,7 @@ function cmdStart(options = {}) {
|
|
|
9108
10339
|
const newHtmlPath = path.join(webDir, 'index.html');
|
|
9109
10340
|
const legacyHtmlPath = path.join(__dirname, 'web-ui.html');
|
|
9110
10341
|
const htmlPath = fs.existsSync(newHtmlPath) ? newHtmlPath : legacyHtmlPath;
|
|
9111
|
-
const assetsDir = path.join(
|
|
10342
|
+
const assetsDir = path.join(webDir, 'res');
|
|
9112
10343
|
if (!fs.existsSync(htmlPath)) {
|
|
9113
10344
|
console.error('错误: Web UI 页面不存在(尝试路径: web-ui/index.html, web-ui.html)');
|
|
9114
10345
|
process.exit(1);
|
|
@@ -9133,12 +10364,15 @@ function cmdStart(options = {}) {
|
|
|
9133
10364
|
openBrowser: shouldOpenBrowser
|
|
9134
10365
|
});
|
|
9135
10366
|
|
|
10367
|
+
const stopAutomationScheduler = startAutomationScheduler();
|
|
10368
|
+
|
|
9136
10369
|
// 禁止前端变更侦测与自动重启:避免终端输出噪音与访问时短暂 Connection Refused。
|
|
9137
10370
|
// 如需热重启,请由开发者自行使用外部 watcher / nodemon 等工具。
|
|
9138
10371
|
const stopWatch = () => {};
|
|
9139
10372
|
|
|
9140
10373
|
const handleExit = () => {
|
|
9141
10374
|
stopWatch();
|
|
10375
|
+
stopAutomationScheduler();
|
|
9142
10376
|
Promise.allSettled([
|
|
9143
10377
|
serverHandle.stop(),
|
|
9144
10378
|
stopBuiltinProxyRuntime(),
|
|
@@ -10678,7 +11912,7 @@ function buildMcpClaudeSettingsPayload() {
|
|
|
10678
11912
|
function normalizeMcpSource(value) {
|
|
10679
11913
|
const source = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
10680
11914
|
if (!source) return '';
|
|
10681
|
-
if (source === 'codex' || source === 'claude' || source === 'all') {
|
|
11915
|
+
if (source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'all') {
|
|
10682
11916
|
return source;
|
|
10683
11917
|
}
|
|
10684
11918
|
return null;
|
|
@@ -10991,7 +12225,7 @@ function createWorkflowToolCatalog() {
|
|
|
10991
12225
|
handler: async (args = {}) => {
|
|
10992
12226
|
const source = normalizeMcpSource(args.source);
|
|
10993
12227
|
if (source === null) {
|
|
10994
|
-
return { error: 'Invalid source. Must be codex, claude, or all' };
|
|
12228
|
+
return { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, or all' };
|
|
10995
12229
|
}
|
|
10996
12230
|
return {
|
|
10997
12231
|
source: source || 'all',
|
|
@@ -11363,14 +12597,21 @@ function normalizeTaskQueueItem(raw = {}) {
|
|
|
11363
12597
|
|
|
11364
12598
|
function readTaskQueueState() {
|
|
11365
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
|
+
}
|
|
11366
12606
|
if (!parsed.ok || !parsed.exists) {
|
|
11367
12607
|
return {
|
|
11368
|
-
tasks: []
|
|
12608
|
+
tasks: [],
|
|
12609
|
+
error: ''
|
|
11369
12610
|
};
|
|
11370
12611
|
}
|
|
11371
12612
|
const source = parsed.data && typeof parsed.data === 'object' ? parsed.data : {};
|
|
11372
12613
|
const tasks = Array.isArray(source.tasks) ? source.tasks.map((item) => normalizeTaskQueueItem(item)) : [];
|
|
11373
|
-
return { tasks };
|
|
12614
|
+
return { tasks, error: '' };
|
|
11374
12615
|
}
|
|
11375
12616
|
|
|
11376
12617
|
function writeTaskQueueState(state = {}) {
|
|
@@ -11380,17 +12621,58 @@ function writeTaskQueueState(state = {}) {
|
|
|
11380
12621
|
});
|
|
11381
12622
|
}
|
|
11382
12623
|
|
|
11383
|
-
function
|
|
11384
|
-
const
|
|
11385
|
-
|
|
11386
|
-
|
|
11387
|
-
|
|
11388
|
-
|
|
11389
|
-
}
|
|
11390
|
-
|
|
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' };
|
|
11391
12647
|
}
|
|
11392
|
-
|
|
11393
|
-
|
|
12648
|
+
try {
|
|
12649
|
+
return fn();
|
|
12650
|
+
} finally {
|
|
12651
|
+
try {
|
|
12652
|
+
fs.closeSync(lockFd);
|
|
12653
|
+
} catch (_) {}
|
|
12654
|
+
try {
|
|
12655
|
+
fs.unlinkSync(lockPath);
|
|
12656
|
+
} catch (_) {}
|
|
12657
|
+
}
|
|
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
|
+
});
|
|
11394
12676
|
}
|
|
11395
12677
|
|
|
11396
12678
|
function getTaskQueueItem(taskId) {
|
|
@@ -11425,15 +12707,19 @@ function appendTaskRunRecord(record) {
|
|
|
11425
12707
|
fs.appendFileSync(TASK_RUNS_FILE, `${JSON.stringify(record)}\n`, { encoding: 'utf-8', mode: 0o600 });
|
|
11426
12708
|
}
|
|
11427
12709
|
|
|
12710
|
+
let g_taskRunRecordsLastParseErrors = 0;
|
|
12711
|
+
|
|
11428
12712
|
function listTaskRunRecords(limit = 20) {
|
|
11429
12713
|
const max = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 20;
|
|
11430
12714
|
if (!fs.existsSync(TASK_RUNS_FILE)) {
|
|
12715
|
+
g_taskRunRecordsLastParseErrors = 0;
|
|
11431
12716
|
return [];
|
|
11432
12717
|
}
|
|
11433
12718
|
let content = '';
|
|
11434
12719
|
try {
|
|
11435
12720
|
content = fs.readFileSync(TASK_RUNS_FILE, 'utf-8');
|
|
11436
12721
|
} catch (_) {
|
|
12722
|
+
g_taskRunRecordsLastParseErrors = 0;
|
|
11437
12723
|
return [];
|
|
11438
12724
|
}
|
|
11439
12725
|
const rows = content
|
|
@@ -11441,14 +12727,18 @@ function listTaskRunRecords(limit = 20) {
|
|
|
11441
12727
|
.map((line) => line.trim())
|
|
11442
12728
|
.filter(Boolean);
|
|
11443
12729
|
const parsed = [];
|
|
12730
|
+
let parseErrors = 0;
|
|
11444
12731
|
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
11445
12732
|
try {
|
|
11446
12733
|
parsed.push(JSON.parse(rows[i]));
|
|
11447
12734
|
if (parsed.length >= max) {
|
|
11448
12735
|
break;
|
|
11449
12736
|
}
|
|
11450
|
-
} catch (_) {
|
|
12737
|
+
} catch (_) {
|
|
12738
|
+
parseErrors += 1;
|
|
12739
|
+
}
|
|
11451
12740
|
}
|
|
12741
|
+
g_taskRunRecordsLastParseErrors = parseErrors;
|
|
11452
12742
|
return parsed;
|
|
11453
12743
|
}
|
|
11454
12744
|
|
|
@@ -11510,18 +12800,117 @@ function collectTaskRunSummary(detail = {}) {
|
|
|
11510
12800
|
};
|
|
11511
12801
|
}
|
|
11512
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
|
+
|
|
11513
12892
|
function buildTaskOverviewPayload(options = {}) {
|
|
11514
12893
|
const queueLimit = Number.isFinite(options.queueLimit) ? Math.max(1, Math.floor(options.queueLimit)) : 20;
|
|
11515
12894
|
const runLimit = Number.isFinite(options.runLimit) ? Math.max(1, Math.floor(options.runLimit)) : 20;
|
|
11516
12895
|
const workflowCatalog = buildTaskWorkflowCatalog();
|
|
11517
|
-
const
|
|
12896
|
+
const queueState = readTaskQueueState();
|
|
12897
|
+
const queue = queueState.error ? [] : listTaskQueueItems({ limit: queueLimit });
|
|
11518
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
|
+
}
|
|
11519
12906
|
return {
|
|
11520
12907
|
workflows: workflowCatalog.workflows,
|
|
11521
|
-
warnings
|
|
12908
|
+
warnings,
|
|
11522
12909
|
queue,
|
|
11523
12910
|
runs,
|
|
11524
|
-
activeRunIds: Array.from(g_taskRunControllers.keys())
|
|
12911
|
+
activeRunIds: Array.from(g_taskRunControllers.keys()),
|
|
12912
|
+
queueError: queueState.error || '',
|
|
12913
|
+
runParseErrors: g_taskRunRecordsLastParseErrors
|
|
11525
12914
|
};
|
|
11526
12915
|
}
|
|
11527
12916
|
|
|
@@ -11819,7 +13208,7 @@ async function runTaskPlanInternal(plan, options = {}) {
|
|
|
11819
13208
|
}
|
|
11820
13209
|
});
|
|
11821
13210
|
if (options.queueItem) {
|
|
11822
|
-
upsertTaskQueueItem({
|
|
13211
|
+
const queued = upsertTaskQueueItem({
|
|
11823
13212
|
...options.queueItem,
|
|
11824
13213
|
taskId,
|
|
11825
13214
|
status: 'running',
|
|
@@ -11829,6 +13218,7 @@ async function runTaskPlanInternal(plan, options = {}) {
|
|
|
11829
13218
|
updatedAt: toIsoTime(Date.now()),
|
|
11830
13219
|
plan
|
|
11831
13220
|
});
|
|
13221
|
+
if (queued && queued.error) {}
|
|
11832
13222
|
}
|
|
11833
13223
|
try {
|
|
11834
13224
|
const run = await executeTaskPlan(plan, {
|
|
@@ -11852,7 +13242,7 @@ async function runTaskPlanInternal(plan, options = {}) {
|
|
|
11852
13242
|
};
|
|
11853
13243
|
writeTaskRunDetail(nextDetail);
|
|
11854
13244
|
if (options.queueItem) {
|
|
11855
|
-
upsertTaskQueueItem({
|
|
13245
|
+
const queued = upsertTaskQueueItem({
|
|
11856
13246
|
...options.queueItem,
|
|
11857
13247
|
taskId,
|
|
11858
13248
|
status: snapshot.status === 'success'
|
|
@@ -11864,6 +13254,7 @@ async function runTaskPlanInternal(plan, options = {}) {
|
|
|
11864
13254
|
updatedAt: toIsoTime(Date.now()),
|
|
11865
13255
|
plan
|
|
11866
13256
|
});
|
|
13257
|
+
if (queued && queued.error) {}
|
|
11867
13258
|
}
|
|
11868
13259
|
}
|
|
11869
13260
|
});
|
|
@@ -11875,8 +13266,12 @@ async function runTaskPlanInternal(plan, options = {}) {
|
|
|
11875
13266
|
};
|
|
11876
13267
|
writeTaskRunDetail(detail);
|
|
11877
13268
|
appendTaskRunRecord(collectTaskRunSummary(detail));
|
|
13269
|
+
writeTaskRunArtifacts(detail);
|
|
13270
|
+
try {
|
|
13271
|
+
await notifyAutomationOnTaskRun(detail);
|
|
13272
|
+
} catch (_) {}
|
|
11878
13273
|
if (options.queueItem) {
|
|
11879
|
-
upsertTaskQueueItem({
|
|
13274
|
+
const queued = upsertTaskQueueItem({
|
|
11880
13275
|
...options.queueItem,
|
|
11881
13276
|
taskId,
|
|
11882
13277
|
status: run.status === 'success'
|
|
@@ -11888,6 +13283,7 @@ async function runTaskPlanInternal(plan, options = {}) {
|
|
|
11888
13283
|
updatedAt: toIsoTime(Date.now()),
|
|
11889
13284
|
plan
|
|
11890
13285
|
});
|
|
13286
|
+
if (queued && queued.error) {}
|
|
11891
13287
|
}
|
|
11892
13288
|
return detail;
|
|
11893
13289
|
} finally {
|
|
@@ -11923,6 +13319,9 @@ function addTaskToQueue(params = {}) {
|
|
|
11923
13319
|
runStatus: '',
|
|
11924
13320
|
plan
|
|
11925
13321
|
});
|
|
13322
|
+
if (item && item.error) {
|
|
13323
|
+
return { error: item.error };
|
|
13324
|
+
}
|
|
11926
13325
|
return {
|
|
11927
13326
|
ok: true,
|
|
11928
13327
|
task: item,
|
|
@@ -12093,6 +13492,9 @@ function cancelTaskRunOrQueue(params = {}) {
|
|
|
12093
13492
|
updatedAt: toIsoTime(Date.now()),
|
|
12094
13493
|
lastSummary: queueItem.lastSummary || '已取消'
|
|
12095
13494
|
});
|
|
13495
|
+
if (next && next.error) {
|
|
13496
|
+
return { error: next.error };
|
|
13497
|
+
}
|
|
12096
13498
|
return {
|
|
12097
13499
|
ok: true,
|
|
12098
13500
|
cancelled: true,
|
|
@@ -12231,13 +13633,37 @@ function isLiveProcessId(value) {
|
|
|
12231
13633
|
}
|
|
12232
13634
|
}
|
|
12233
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
|
+
|
|
12234
13660
|
function readTaskQueueWorkerState() {
|
|
12235
13661
|
const parsed = readJsonObjectFromFile(TASK_QUEUE_WORKER_FILE, {});
|
|
12236
13662
|
if (!parsed.ok || !parsed.exists || !parsed.data || typeof parsed.data !== 'object') {
|
|
12237
13663
|
return null;
|
|
12238
13664
|
}
|
|
12239
13665
|
const state = parsed.data;
|
|
12240
|
-
if (!
|
|
13666
|
+
if (!isTaskWorkerProcessId(state.pid)) {
|
|
12241
13667
|
try {
|
|
12242
13668
|
fs.unlinkSync(TASK_QUEUE_WORKER_FILE);
|
|
12243
13669
|
} catch (_) {}
|
|
@@ -12492,7 +13918,7 @@ function createMcpTools(options = {}) {
|
|
|
12492
13918
|
const input = args && typeof args === 'object' ? args : {};
|
|
12493
13919
|
const source = normalizeMcpSource(input.source);
|
|
12494
13920
|
if (source === null) {
|
|
12495
|
-
return { error: 'Invalid source. Must be codex, claude, or all' };
|
|
13921
|
+
return { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, or all' };
|
|
12496
13922
|
}
|
|
12497
13923
|
const normalizedInput = {
|
|
12498
13924
|
...input,
|
|
@@ -12941,7 +14367,7 @@ function createMcpResources() {
|
|
|
12941
14367
|
contents: [{
|
|
12942
14368
|
uri,
|
|
12943
14369
|
mimeType: 'application/json',
|
|
12944
|
-
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)
|
|
12945
14371
|
}]
|
|
12946
14372
|
};
|
|
12947
14373
|
}
|
|
@@ -13187,7 +14613,7 @@ function printMainHelp() {
|
|
|
13187
14613
|
console.log(' 注: follow-up 自动排队仅支持 linux/android/netbsd/openbsd/darwin/freebsd 且 stdin 必须是 TTY,其他平台会报错');
|
|
13188
14614
|
console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
|
|
13189
14615
|
console.log(' codexmate mcp [serve] [--transport stdio] [--allow-write|--read-only]');
|
|
13190
|
-
console.log(' codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
|
|
14616
|
+
console.log(' codexmate export-session --source <codex|claude|gemini|codebuddy> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
|
|
13191
14617
|
console.log(' codexmate zip <路径> [--max:级别] 压缩(系统 zip 优先,其次 zip-lib)');
|
|
13192
14618
|
console.log(' codexmate unzip <zip文件> [输出目录] 解压(zip-lib)');
|
|
13193
14619
|
console.log(' codexmate unzip-ext <zip目录> [输出目录] [--ext:后缀[,后缀...]] [--no-recursive] 批量提取 ZIP 指定后缀文件(默认递归)');
|