@zhongqian97-code/ecode 0.4.0 → 0.5.1
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 +21 -1
- package/dist/index.js +239 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -211,7 +211,27 @@ ecode
|
|
|
211
211
|
# "logDir": "~/.ecode/logs"
|
|
212
212
|
```
|
|
213
213
|
|
|
214
|
-
Each session writes a timestamped `.jsonl` file
|
|
214
|
+
Each session writes a timestamped `.jsonl` log file and a companion `-session.json` metadata file (id, title, model, token count, turn count).
|
|
215
|
+
|
|
216
|
+
### `ecode sessions` subcommands
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
# list all sessions, sorted by most recent activity
|
|
220
|
+
ecode sessions list
|
|
221
|
+
|
|
222
|
+
# show full metadata for a session (use 8-char prefix or full UUID)
|
|
223
|
+
ecode sessions inspect a1b2c3d4
|
|
224
|
+
|
|
225
|
+
# get a shell command to replay a session
|
|
226
|
+
ecode sessions replay a1b2c3d4
|
|
227
|
+
# → ecode --replay ~/.ecode/logs/2026-05-13T08-47-00.jsonl
|
|
228
|
+
|
|
229
|
+
# get a shell command to fork a session at a specific turn (default: last turn)
|
|
230
|
+
ecode sessions fork a1b2c3d4 5
|
|
231
|
+
# → ecode --fork ~/.ecode/logs/2026-05-13T08-47-00.jsonl:5
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The `fork` command is the basis for `/goal` rollback: when the evaluator detects context drift, it surfaces a `sessions fork` command pointing to the last known-good turn.
|
|
215
235
|
|
|
216
236
|
## Requirements
|
|
217
237
|
|
package/dist/index.js
CHANGED
|
@@ -3,11 +3,11 @@ const _ew=process.emitWarning.bind(process);process.emitWarning=function(w,...a)
|
|
|
3
3
|
|
|
4
4
|
// src/index.ts
|
|
5
5
|
import { createRequire } from "module";
|
|
6
|
-
import { resolve as resolve4, dirname as
|
|
6
|
+
import { resolve as resolve4, dirname as dirname7 } from "path";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
import React4 from "react";
|
|
9
9
|
import { render } from "ink";
|
|
10
|
-
import { readFileSync as
|
|
10
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
11
11
|
|
|
12
12
|
// src/config.ts
|
|
13
13
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -537,13 +537,13 @@ var READ_TOOL = {
|
|
|
537
537
|
}
|
|
538
538
|
};
|
|
539
539
|
async function readFile2(params) {
|
|
540
|
-
const { path:
|
|
540
|
+
const { path: path9, offset = 0, limit } = params;
|
|
541
541
|
let raw;
|
|
542
542
|
try {
|
|
543
|
-
raw = await fs.readFile(
|
|
543
|
+
raw = await fs.readFile(path9, "utf8");
|
|
544
544
|
} catch (err) {
|
|
545
545
|
const msg = err instanceof Error ? err.message : String(err);
|
|
546
|
-
return `Error reading ${
|
|
546
|
+
return `Error reading ${path9}: ${msg}`;
|
|
547
547
|
}
|
|
548
548
|
const lines = raw.split("\n");
|
|
549
549
|
const sliced = limit !== void 0 ? lines.slice(offset, offset + limit) : lines.slice(offset);
|
|
@@ -599,28 +599,28 @@ var EDIT_TOOL = {
|
|
|
599
599
|
}
|
|
600
600
|
};
|
|
601
601
|
async function editFile(params) {
|
|
602
|
-
const { path:
|
|
602
|
+
const { path: path9, old_string, new_string } = params;
|
|
603
603
|
let content;
|
|
604
604
|
try {
|
|
605
|
-
content = await fs3.readFile(
|
|
605
|
+
content = await fs3.readFile(path9, "utf8");
|
|
606
606
|
} catch (err) {
|
|
607
607
|
const msg = err instanceof Error ? err.message : String(err);
|
|
608
|
-
return `Error reading ${
|
|
608
|
+
return `Error reading ${path9}: ${msg}`;
|
|
609
609
|
}
|
|
610
610
|
const count = countOccurrences(content, old_string);
|
|
611
611
|
if (count === 0) {
|
|
612
|
-
return `Error: old_string not found in ${
|
|
612
|
+
return `Error: old_string not found in ${path9}`;
|
|
613
613
|
}
|
|
614
614
|
if (count > 1) {
|
|
615
|
-
return `Error: old_string appears ${count} times in ${
|
|
615
|
+
return `Error: old_string appears ${count} times in ${path9} (ambiguous \u2014 add more context)`;
|
|
616
616
|
}
|
|
617
617
|
const updated = content.replace(old_string, new_string);
|
|
618
618
|
try {
|
|
619
|
-
await fs3.writeFile(
|
|
620
|
-
return `Edited ${
|
|
619
|
+
await fs3.writeFile(path9, updated, "utf8");
|
|
620
|
+
return `Edited ${path9}`;
|
|
621
621
|
} catch (err) {
|
|
622
622
|
const msg = err instanceof Error ? err.message : String(err);
|
|
623
|
-
return `Error writing ${
|
|
623
|
+
return `Error writing ${path9}: ${msg}`;
|
|
624
624
|
}
|
|
625
625
|
}
|
|
626
626
|
function countOccurrences(haystack, needle) {
|
|
@@ -2128,7 +2128,7 @@ function parseMouseScroll(data) {
|
|
|
2128
2128
|
|
|
2129
2129
|
// src/ui/App.tsx
|
|
2130
2130
|
import { homedir as homedir2 } from "os";
|
|
2131
|
-
import { join as
|
|
2131
|
+
import { join as join10 } from "path";
|
|
2132
2132
|
|
|
2133
2133
|
// src/automation/store.ts
|
|
2134
2134
|
import { readFile as readFile8, writeFile as writeFile5, mkdir as mkdir2 } from "fs/promises";
|
|
@@ -2512,7 +2512,10 @@ async function cmdGoal(args, deps) {
|
|
|
2512
2512
|
condition,
|
|
2513
2513
|
activeTurnCount: 0,
|
|
2514
2514
|
runCount: 0,
|
|
2515
|
-
failureCount: 0
|
|
2515
|
+
failureCount: 0,
|
|
2516
|
+
// 快照字段:仅在调用方提供时写入,旧格式兼容(undefined 即不存在)
|
|
2517
|
+
snapshotLogFile: deps.snapshotLogFile,
|
|
2518
|
+
snapshotTurn: deps.snapshotTurn
|
|
2516
2519
|
};
|
|
2517
2520
|
await upsertJob(deps.dataDir, job);
|
|
2518
2521
|
deps.onSchedule(job);
|
|
@@ -2531,6 +2534,17 @@ async function cmdUngoal(idOrPrefix, deps) {
|
|
|
2531
2534
|
return `\u5DF2\u53D6\u6D88 goal job [${target.id.slice(0, 8)}]\uFF1A${target.promptTemplate}`;
|
|
2532
2535
|
}
|
|
2533
2536
|
|
|
2537
|
+
// src/automation/goal/rollback.ts
|
|
2538
|
+
function buildRollbackMessage(job) {
|
|
2539
|
+
if (job.snapshotLogFile === void 0 || job.snapshotTurn === void 0) {
|
|
2540
|
+
return "";
|
|
2541
|
+
}
|
|
2542
|
+
return `
|
|
2543
|
+
|
|
2544
|
+
\u4E0A\u4E0B\u6587\u53EF\u80FD\u5DF2\u504F\u79BB\u76EE\u6807\uFF0C\u56DE\u6EDA\u5230\u76EE\u6807\u521B\u5EFA\u524D\u7684\u4F4D\u7F6E\uFF1A
|
|
2545
|
+
ecode --fork ${job.snapshotLogFile}:${job.snapshotTurn}`;
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2534
2548
|
// src/automation/index.ts
|
|
2535
2549
|
var AutomationManager = class {
|
|
2536
2550
|
constructor(config2) {
|
|
@@ -2597,9 +2611,15 @@ var AutomationManager = class {
|
|
|
2597
2611
|
onUnschedule: (id) => this.scheduler.remove(id)
|
|
2598
2612
|
});
|
|
2599
2613
|
}
|
|
2600
|
-
|
|
2614
|
+
/**
|
|
2615
|
+
* 处理 /goal 命令,可选接收当前会话上下文以支持 blocked 时的自动回滚提示。
|
|
2616
|
+
* sessionContext 由 App.tsx 在用户提交 /goal 时注入(logFile + turnCount)。
|
|
2617
|
+
*/
|
|
2618
|
+
async cmdGoal(args, sessionContext) {
|
|
2601
2619
|
return cmdGoal(args, {
|
|
2602
2620
|
dataDir: this.config.dataDir,
|
|
2621
|
+
snapshotLogFile: sessionContext?.logFile,
|
|
2622
|
+
snapshotTurn: sessionContext?.turnCount,
|
|
2603
2623
|
onSchedule: (job) => this.startGoalRunner(job),
|
|
2604
2624
|
onUnschedule: (id) => this.stopGoalRunner(id)
|
|
2605
2625
|
});
|
|
@@ -2709,9 +2729,10 @@ var AutomationManager = class {
|
|
|
2709
2729
|
if (evaluation.verdict === "blocked") {
|
|
2710
2730
|
const errorJob = { ...updatedJob, state: "error" };
|
|
2711
2731
|
await upsertJob(this.config.dataDir, errorJob);
|
|
2732
|
+
const rollback = buildRollbackMessage(errorJob);
|
|
2712
2733
|
this.config.onJobResult?.(
|
|
2713
2734
|
job.id,
|
|
2714
|
-
`[goal ${job.id.slice(0, 8)}] \u6267\u884C\u53D7\u963B\uFF1A${evaluation.reason}`
|
|
2735
|
+
`[goal ${job.id.slice(0, 8)}] \u6267\u884C\u53D7\u963B\uFF1A${evaluation.reason}${rollback}`
|
|
2715
2736
|
);
|
|
2716
2737
|
break;
|
|
2717
2738
|
}
|
|
@@ -2733,9 +2754,87 @@ var AutomationManager = class {
|
|
|
2733
2754
|
}
|
|
2734
2755
|
};
|
|
2735
2756
|
|
|
2757
|
+
// src/sessions/metadata.ts
|
|
2758
|
+
import * as crypto from "crypto";
|
|
2759
|
+
import * as fs9 from "fs";
|
|
2760
|
+
import * as path7 from "path";
|
|
2761
|
+
function metadataPathFromLogFile(logFilePath2) {
|
|
2762
|
+
const base = path7.basename(logFilePath2, ".jsonl");
|
|
2763
|
+
const dir = path7.dirname(logFilePath2);
|
|
2764
|
+
return path7.join(dir, `${base}-session.json`);
|
|
2765
|
+
}
|
|
2766
|
+
function createSessionMetadata(logFilePath2, model) {
|
|
2767
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2768
|
+
return {
|
|
2769
|
+
id: crypto.randomUUID(),
|
|
2770
|
+
startTime: now,
|
|
2771
|
+
lastActivity: now,
|
|
2772
|
+
cwd: process.cwd(),
|
|
2773
|
+
model,
|
|
2774
|
+
title: "",
|
|
2775
|
+
turnCount: 0,
|
|
2776
|
+
totalTokens: 0,
|
|
2777
|
+
logFile: path7.basename(logFilePath2)
|
|
2778
|
+
};
|
|
2779
|
+
}
|
|
2780
|
+
function writeSessionMetadata(logFilePath2, metadata) {
|
|
2781
|
+
const metaPath = metadataPathFromLogFile(logFilePath2);
|
|
2782
|
+
try {
|
|
2783
|
+
fs9.writeFileSync(metaPath, JSON.stringify(metadata, null, 2) + "\n");
|
|
2784
|
+
} catch (err) {
|
|
2785
|
+
process.stderr.write(`[sessions] Failed to write metadata: ${err}
|
|
2786
|
+
`);
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
function readSessionMetadata(metaFilePath) {
|
|
2790
|
+
try {
|
|
2791
|
+
const raw = fs9.readFileSync(metaFilePath, "utf-8");
|
|
2792
|
+
return JSON.parse(raw);
|
|
2793
|
+
} catch {
|
|
2794
|
+
return null;
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
function updateSessionMetadata(logFilePath2, partial) {
|
|
2798
|
+
const metaPath = metadataPathFromLogFile(logFilePath2);
|
|
2799
|
+
let existing = null;
|
|
2800
|
+
try {
|
|
2801
|
+
const raw = fs9.readFileSync(metaPath, "utf-8");
|
|
2802
|
+
existing = JSON.parse(raw);
|
|
2803
|
+
} catch {
|
|
2804
|
+
return;
|
|
2805
|
+
}
|
|
2806
|
+
writeSessionMetadata(logFilePath2, { ...existing, ...partial });
|
|
2807
|
+
}
|
|
2808
|
+
function listSessions(logDir) {
|
|
2809
|
+
try {
|
|
2810
|
+
const files = fs9.readdirSync(logDir);
|
|
2811
|
+
const metaFiles = files.filter((f) => f.endsWith("-session.json"));
|
|
2812
|
+
const sessions = [];
|
|
2813
|
+
for (const file of metaFiles) {
|
|
2814
|
+
const meta = readSessionMetadata(path7.join(logDir, file));
|
|
2815
|
+
if (meta) sessions.push(meta);
|
|
2816
|
+
}
|
|
2817
|
+
return sessions.sort(
|
|
2818
|
+
(a, b) => b.lastActivity.localeCompare(a.lastActivity)
|
|
2819
|
+
);
|
|
2820
|
+
} catch {
|
|
2821
|
+
return [];
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
function findSession(logDir, idOrPrefix) {
|
|
2825
|
+
const sessions = listSessions(logDir);
|
|
2826
|
+
return sessions.find(
|
|
2827
|
+
(s) => s.id === idOrPrefix || s.id.startsWith(idOrPrefix)
|
|
2828
|
+
) ?? null;
|
|
2829
|
+
}
|
|
2830
|
+
function generateTitle(firstUserMessage) {
|
|
2831
|
+
const oneLine = firstUserMessage.replace(/\n+/g, " ").trim();
|
|
2832
|
+
return oneLine.length > 50 ? oneLine.slice(0, 47) + "..." : oneLine;
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2736
2835
|
// src/ui/App.tsx
|
|
2737
2836
|
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2738
|
-
async function handleAutomationCommand(input, manager) {
|
|
2837
|
+
async function handleAutomationCommand(input, manager, sessionContext) {
|
|
2739
2838
|
if (!input.startsWith("/")) return null;
|
|
2740
2839
|
const spaceIdx = input.indexOf(" ");
|
|
2741
2840
|
const command = spaceIdx === -1 ? input : input.slice(0, spaceIdx);
|
|
@@ -2743,8 +2842,9 @@ async function handleAutomationCommand(input, manager) {
|
|
|
2743
2842
|
switch (command) {
|
|
2744
2843
|
case "/loop":
|
|
2745
2844
|
return manager.cmdLoop(args);
|
|
2845
|
+
// 传入会话快照:goal blocked 时可生成 ecode --fork 回滚命令
|
|
2746
2846
|
case "/goal":
|
|
2747
|
-
return manager.cmdGoal(args);
|
|
2847
|
+
return manager.cmdGoal(args, sessionContext);
|
|
2748
2848
|
case "/jobs":
|
|
2749
2849
|
return manager.cmdJobs();
|
|
2750
2850
|
case "/unloop":
|
|
@@ -2788,7 +2888,7 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
2788
2888
|
const pendingConfirmRef = useRef2(null);
|
|
2789
2889
|
const abortControllerRef = useRef2(null);
|
|
2790
2890
|
const llmRef = useRef2(llmClient ?? createProvider(resolveActiveProfile(config2)));
|
|
2791
|
-
const automationDataDir = config2.logDir ?
|
|
2891
|
+
const automationDataDir = config2.logDir ? join10(config2.logDir, "automation") : join10(homedir2(), ".config", "ecode", "automation");
|
|
2792
2892
|
const automationManagerRef = useRef2(
|
|
2793
2893
|
new AutomationManager({
|
|
2794
2894
|
dataDir: automationDataDir,
|
|
@@ -2808,9 +2908,15 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
2808
2908
|
const [fileSuggestions, setFileSuggestions] = useState3([]);
|
|
2809
2909
|
const loggerRef = useRef2(null);
|
|
2810
2910
|
const loggedCountRef = useRef2(0);
|
|
2911
|
+
const sessionMetaRef = useRef2(null);
|
|
2912
|
+
const finalTokensRef = useRef2(0);
|
|
2811
2913
|
useEffect3(() => {
|
|
2812
2914
|
if (config2.logDir) {
|
|
2813
|
-
|
|
2915
|
+
const now = /* @__PURE__ */ new Date();
|
|
2916
|
+
loggerRef.current = createLogger(config2.logDir, now);
|
|
2917
|
+
const meta = createSessionMetadata(loggerRef.current.filePath, config2.model);
|
|
2918
|
+
writeSessionMetadata(loggerRef.current.filePath, meta);
|
|
2919
|
+
sessionMetaRef.current = meta;
|
|
2814
2920
|
}
|
|
2815
2921
|
}, []);
|
|
2816
2922
|
useEffect3(() => {
|
|
@@ -3033,6 +3139,7 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
3033
3139
|
estimated: false,
|
|
3034
3140
|
limit: getContextLimit(config2.model, config2.contextLimit)
|
|
3035
3141
|
});
|
|
3142
|
+
finalTokensRef.current = chunk.usage.totalTokens;
|
|
3036
3143
|
}
|
|
3037
3144
|
} else {
|
|
3038
3145
|
const estimatedUsed = Math.floor(
|
|
@@ -3160,6 +3267,16 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
3160
3267
|
}
|
|
3161
3268
|
}
|
|
3162
3269
|
}
|
|
3270
|
+
if (loggerRef.current && sessionMetaRef.current) {
|
|
3271
|
+
const prev = sessionMetaRef.current;
|
|
3272
|
+
const updated = {
|
|
3273
|
+
lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3274
|
+
totalTokens: finalTokensRef.current,
|
|
3275
|
+
turnCount: prev.turnCount + 1
|
|
3276
|
+
};
|
|
3277
|
+
updateSessionMetadata(loggerRef.current.filePath, updated);
|
|
3278
|
+
sessionMetaRef.current = { ...prev, ...updated };
|
|
3279
|
+
}
|
|
3163
3280
|
setStatus("idle");
|
|
3164
3281
|
setToolName(void 0);
|
|
3165
3282
|
abortControllerRef.current = null;
|
|
@@ -3186,7 +3303,12 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
3186
3303
|
{
|
|
3187
3304
|
const automationResult = await handleAutomationCommand(
|
|
3188
3305
|
trimmed,
|
|
3189
|
-
automationManagerRef.current
|
|
3306
|
+
automationManagerRef.current,
|
|
3307
|
+
// 注入会话快照:/goal blocked 时用于生成 ecode --fork 回滚命令
|
|
3308
|
+
{
|
|
3309
|
+
logFile: loggerRef.current?.filePath,
|
|
3310
|
+
turnCount: sessionMetaRef.current?.turnCount
|
|
3311
|
+
}
|
|
3190
3312
|
);
|
|
3191
3313
|
if (automationResult !== null) {
|
|
3192
3314
|
setMessages((prev) => [
|
|
@@ -3240,6 +3362,11 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
3240
3362
|
const userMsg = { role: "user", content };
|
|
3241
3363
|
const nextMessages = [...messages, userMsg];
|
|
3242
3364
|
setMessages(nextMessages);
|
|
3365
|
+
if (loggerRef.current && sessionMetaRef.current && !sessionMetaRef.current.title) {
|
|
3366
|
+
const title = generateTitle(content);
|
|
3367
|
+
updateSessionMetadata(loggerRef.current.filePath, { title });
|
|
3368
|
+
sessionMetaRef.current = { ...sessionMetaRef.current, title };
|
|
3369
|
+
}
|
|
3243
3370
|
runLlmLoop(nextMessages).catch((err) => {
|
|
3244
3371
|
setStatus("idle");
|
|
3245
3372
|
setToolName(void 0);
|
|
@@ -3424,6 +3551,53 @@ function readStdin() {
|
|
|
3424
3551
|
});
|
|
3425
3552
|
}
|
|
3426
3553
|
|
|
3554
|
+
// src/sessions/command.ts
|
|
3555
|
+
import * as path8 from "path";
|
|
3556
|
+
function cmdSessionsList(logDir) {
|
|
3557
|
+
const sessions = listSessions(logDir);
|
|
3558
|
+
if (sessions.length === 0) return "(no sessions found)";
|
|
3559
|
+
const lines = sessions.map((s) => {
|
|
3560
|
+
const id = s.id.slice(0, 8);
|
|
3561
|
+
const date = new Date(s.lastActivity).toLocaleString();
|
|
3562
|
+
const title = s.title || "(no title)";
|
|
3563
|
+
const tokens = s.totalTokens.toLocaleString();
|
|
3564
|
+
return `${id} ${date} ${tokens}t ${title}`;
|
|
3565
|
+
});
|
|
3566
|
+
return `Sessions (${sessions.length}):
|
|
3567
|
+
` + lines.join("\n");
|
|
3568
|
+
}
|
|
3569
|
+
function cmdSessionsInspect(logDir, idOrPrefix) {
|
|
3570
|
+
const session = findSession(logDir, idOrPrefix);
|
|
3571
|
+
if (!session) return `Session not found: ${idOrPrefix}`;
|
|
3572
|
+
return formatInspect(session, logDir);
|
|
3573
|
+
}
|
|
3574
|
+
function cmdSessionsReplay(logDir, idOrPrefix) {
|
|
3575
|
+
const session = findSession(logDir, idOrPrefix);
|
|
3576
|
+
if (!session) return `Session not found: ${idOrPrefix}`;
|
|
3577
|
+
const logPath = path8.join(logDir, session.logFile);
|
|
3578
|
+
return `ecode --replay ${logPath}`;
|
|
3579
|
+
}
|
|
3580
|
+
function cmdSessionsFork(logDir, idOrPrefix, turn) {
|
|
3581
|
+
const session = findSession(logDir, idOrPrefix);
|
|
3582
|
+
if (!session) return `Session not found: ${idOrPrefix}`;
|
|
3583
|
+
const logPath = path8.join(logDir, session.logFile);
|
|
3584
|
+
const turnStr = turn !== void 0 ? turn : session.turnCount;
|
|
3585
|
+
return `ecode --fork ${logPath}:${turnStr}`;
|
|
3586
|
+
}
|
|
3587
|
+
function formatInspect(session, logDir) {
|
|
3588
|
+
return [
|
|
3589
|
+
`id: ${session.id}`,
|
|
3590
|
+
`title: ${session.title || "(no title)"}`,
|
|
3591
|
+
`model: ${session.model}`,
|
|
3592
|
+
`cwd: ${session.cwd}`,
|
|
3593
|
+
`started: ${new Date(session.startTime).toLocaleString()}`,
|
|
3594
|
+
`last active: ${new Date(session.lastActivity).toLocaleString()}`,
|
|
3595
|
+
`turns: ${session.turnCount}`,
|
|
3596
|
+
`tokens: ${session.totalTokens.toLocaleString()}`,
|
|
3597
|
+
`log file: ${path8.join(logDir, session.logFile)}`
|
|
3598
|
+
].join("\n");
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3427
3601
|
// src/index.ts
|
|
3428
3602
|
var require2 = createRequire(import.meta.url);
|
|
3429
3603
|
var { version } = require2("../package.json");
|
|
@@ -3477,6 +3651,46 @@ for (let i = 0; i < rawArgs.length; i++) {
|
|
|
3477
3651
|
}
|
|
3478
3652
|
var config = loadConfig();
|
|
3479
3653
|
var finalConfig = cliLogDir ? { ...config, logDir: cliLogDir } : config;
|
|
3654
|
+
if (rawArgs[0] === "sessions") {
|
|
3655
|
+
const sub = rawArgs[1];
|
|
3656
|
+
const logDir = finalConfig.logDir;
|
|
3657
|
+
if (!logDir) {
|
|
3658
|
+
console.error(
|
|
3659
|
+
"Error: no log directory configured.\nSet ECODE_LOG_DIR or logDir in ~/.ecode/config.json"
|
|
3660
|
+
);
|
|
3661
|
+
process.exit(1);
|
|
3662
|
+
}
|
|
3663
|
+
let output;
|
|
3664
|
+
if (sub === "list") {
|
|
3665
|
+
output = cmdSessionsList(logDir);
|
|
3666
|
+
} else if (sub === "inspect") {
|
|
3667
|
+
const id = rawArgs[2];
|
|
3668
|
+
if (!id) {
|
|
3669
|
+
console.error("Usage: ecode sessions inspect <id>");
|
|
3670
|
+
process.exit(1);
|
|
3671
|
+
}
|
|
3672
|
+
output = cmdSessionsInspect(logDir, id);
|
|
3673
|
+
} else if (sub === "replay") {
|
|
3674
|
+
const id = rawArgs[2];
|
|
3675
|
+
if (!id) {
|
|
3676
|
+
console.error("Usage: ecode sessions replay <id>");
|
|
3677
|
+
process.exit(1);
|
|
3678
|
+
}
|
|
3679
|
+
output = cmdSessionsReplay(logDir, id);
|
|
3680
|
+
} else if (sub === "fork") {
|
|
3681
|
+
const id = rawArgs[2];
|
|
3682
|
+
if (!id) {
|
|
3683
|
+
console.error("Usage: ecode sessions fork <id> [turn]");
|
|
3684
|
+
process.exit(1);
|
|
3685
|
+
}
|
|
3686
|
+
const turnArg = rawArgs[3] ? parseInt(rawArgs[3], 10) : void 0;
|
|
3687
|
+
output = cmdSessionsFork(logDir, id, turnArg);
|
|
3688
|
+
} else {
|
|
3689
|
+
output = "Usage: ecode sessions <list|inspect|replay|fork> [args...]\n\n list \u5217\u51FA\u6240\u6709\u5386\u53F2\u4F1A\u8BDD\n inspect <id> \u663E\u793A\u4F1A\u8BDD\u8BE6\u60C5\n replay <id> \u8F93\u51FA\u7528\u4E8E\u56DE\u653E\u7684\u547D\u4EE4\n fork <id> [turn] \u8F93\u51FA\u7528\u4E8E\u5206\u53C9\u5230\u6307\u5B9A\u8F6E\u6B21\u7684\u547D\u4EE4";
|
|
3690
|
+
}
|
|
3691
|
+
console.log(output);
|
|
3692
|
+
process.exit(0);
|
|
3693
|
+
}
|
|
3480
3694
|
if (!finalConfig.apiKey) {
|
|
3481
3695
|
console.error(
|
|
3482
3696
|
"Error: no API key configured.\nSet ECODE_API_KEY or add apiKey to ~/.ecode/config.json"
|
|
@@ -3486,7 +3700,7 @@ if (!finalConfig.apiKey) {
|
|
|
3486
3700
|
var initialMessages = [];
|
|
3487
3701
|
if (replayFile) {
|
|
3488
3702
|
try {
|
|
3489
|
-
const raw =
|
|
3703
|
+
const raw = readFileSync3(replayFile, "utf-8");
|
|
3490
3704
|
initialMessages = parseReplayLog(raw);
|
|
3491
3705
|
} catch (err) {
|
|
3492
3706
|
console.error(`Error reading replay file: ${err}`);
|
|
@@ -3494,14 +3708,14 @@ if (replayFile) {
|
|
|
3494
3708
|
}
|
|
3495
3709
|
} else if (forkSpec) {
|
|
3496
3710
|
try {
|
|
3497
|
-
const raw =
|
|
3711
|
+
const raw = readFileSync3(forkSpec.file, "utf-8");
|
|
3498
3712
|
initialMessages = truncateToTurn(parseReplayLog(raw), forkSpec.turn);
|
|
3499
3713
|
} catch (err) {
|
|
3500
3714
|
console.error(`Error reading fork file: ${err}`);
|
|
3501
3715
|
process.exit(1);
|
|
3502
3716
|
}
|
|
3503
3717
|
}
|
|
3504
|
-
var __dirname =
|
|
3718
|
+
var __dirname = dirname7(fileURLToPath(import.meta.url));
|
|
3505
3719
|
var builtinSkillsDir = resolve4(__dirname, "../skills");
|
|
3506
3720
|
var userSkillsDir = resolve4(process.env.HOME ?? "~", ".ecode/skills");
|
|
3507
3721
|
var projectSkillsDir = resolve4(process.cwd(), ".ecode/skills");
|