claude-code-rust 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/agent-sdk/dist/bridge/agents.js +75 -0
- package/agent-sdk/dist/bridge/commands.js +45 -14
- package/agent-sdk/dist/bridge/error_classification.js +55 -0
- package/agent-sdk/dist/bridge/events.js +83 -0
- package/agent-sdk/dist/bridge/history.js +49 -258
- package/agent-sdk/dist/bridge/message_handlers.js +428 -0
- package/agent-sdk/dist/bridge/permissions.js +15 -3
- package/agent-sdk/dist/bridge/session_lifecycle.js +368 -0
- package/agent-sdk/dist/bridge/shared.js +49 -0
- package/agent-sdk/dist/bridge/state_parsing.js +66 -0
- package/agent-sdk/dist/bridge/tool_calls.js +168 -0
- package/agent-sdk/dist/bridge/tooling.js +27 -5
- package/agent-sdk/dist/bridge/user_interaction.js +175 -0
- package/agent-sdk/dist/bridge.js +37 -1106
- package/agent-sdk/dist/bridge.test.js +304 -150
- package/bin/claude-rs.js +1 -1
- package/package.json +6 -7
- package/scripts/postinstall.js +2 -2
- package/agent-sdk/README.md +0 -13
- package/agent-sdk/dist/bridge/usage.js +0 -95
package/README.md
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
A native Rust terminal interface for Claude Code. Drop-in replacement for Anthropic's stock Node.js/React Ink TUI, built for performance and a better user experience.
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/claude-code-rust)
|
|
6
|
+
[](https://www.npmjs.com/package/claude-code-rust)
|
|
5
7
|
[](https://github.com/srothgan/claude-code-rust/actions/workflows/ci.yml)
|
|
6
8
|
[](https://www.gnu.org/licenses/agpl-3.0)
|
|
7
|
-
[](https://nodejs.org/)
|
|
8
10
|
|
|
9
11
|
## About
|
|
10
12
|
|
|
11
|
-
Claude Code Rust replaces the stock Claude Code terminal interface with a native Rust binary built on [Ratatui](https://ratatui.rs/). It connects to the same Claude API through a local Agent SDK bridge
|
|
13
|
+
Claude Code Rust replaces the stock Claude Code terminal interface with a native Rust binary built on [Ratatui](https://ratatui.rs/). It connects to the same Claude API through a local Agent SDK bridge. Core Claude Code functionality - tool calls, file editing, terminal commands, and permissions - works unchanged.
|
|
12
14
|
|
|
13
15
|
## Requisites
|
|
14
16
|
|
|
@@ -17,13 +19,13 @@ Claude Code Rust replaces the stock Claude Code terminal interface with a native
|
|
|
17
19
|
|
|
18
20
|
## Install
|
|
19
21
|
|
|
20
|
-
###
|
|
22
|
+
### pnpm (global, recommended)
|
|
21
23
|
|
|
22
24
|
```bash
|
|
23
|
-
|
|
25
|
+
pnpm add -g claude-code-rust
|
|
24
26
|
```
|
|
25
27
|
|
|
26
|
-
The
|
|
28
|
+
The published package installs a `claude-rs` command and downloads the matching
|
|
27
29
|
prebuilt release binary for your platform during `postinstall`.
|
|
28
30
|
|
|
29
31
|
## Usage
|
|
@@ -56,9 +58,7 @@ Three-layer design:
|
|
|
56
58
|
|
|
57
59
|
## Known Limitations
|
|
58
60
|
|
|
59
|
-
-
|
|
60
|
-
- Session resume via `--resume` depends on runtime support and account/session availability.
|
|
61
|
-
- `/login` and `/logout` are intentionally not offered in command discovery for this release.
|
|
61
|
+
- The config view includes the Settings tab but the Status, Usage, and MCP tabs are not yet implemented.
|
|
62
62
|
|
|
63
63
|
## Status
|
|
64
64
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { emitSessionUpdate } from "./events.js";
|
|
2
|
+
function availableAgentsSignature(agents) {
|
|
3
|
+
return JSON.stringify(agents);
|
|
4
|
+
}
|
|
5
|
+
function normalizeAvailableAgentName(value) {
|
|
6
|
+
if (typeof value !== "string") {
|
|
7
|
+
return "";
|
|
8
|
+
}
|
|
9
|
+
return value.trim();
|
|
10
|
+
}
|
|
11
|
+
export function mapAvailableAgents(value) {
|
|
12
|
+
if (!Array.isArray(value)) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const byName = new Map();
|
|
16
|
+
for (const entry of value) {
|
|
17
|
+
if (!entry || typeof entry !== "object") {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const record = entry;
|
|
21
|
+
const name = normalizeAvailableAgentName(record.name);
|
|
22
|
+
if (!name) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const description = typeof record.description === "string" ? record.description : "";
|
|
26
|
+
const model = typeof record.model === "string" && record.model.trim().length > 0 ? record.model : undefined;
|
|
27
|
+
const existing = byName.get(name);
|
|
28
|
+
if (!existing) {
|
|
29
|
+
byName.set(name, { name, description, model });
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (existing.description.trim().length === 0 && description.trim().length > 0) {
|
|
33
|
+
existing.description = description;
|
|
34
|
+
}
|
|
35
|
+
if (!existing.model && model) {
|
|
36
|
+
existing.model = model;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
40
|
+
}
|
|
41
|
+
export function mapAvailableAgentsFromNames(value) {
|
|
42
|
+
if (!Array.isArray(value)) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
const byName = new Map();
|
|
46
|
+
for (const entry of value) {
|
|
47
|
+
const name = normalizeAvailableAgentName(entry);
|
|
48
|
+
if (!name || byName.has(name)) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
byName.set(name, { name, description: "" });
|
|
52
|
+
}
|
|
53
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
54
|
+
}
|
|
55
|
+
export function emitAvailableAgentsIfChanged(session, agents) {
|
|
56
|
+
const signature = availableAgentsSignature(agents);
|
|
57
|
+
if (session.lastAvailableAgentsSignature === signature) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
session.lastAvailableAgentsSignature = signature;
|
|
61
|
+
emitSessionUpdate(session.sessionId, { type: "available_agents_update", agents });
|
|
62
|
+
}
|
|
63
|
+
export function refreshAvailableAgents(session) {
|
|
64
|
+
if (typeof session.query.supportedAgents !== "function") {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
void session.query
|
|
68
|
+
.supportedAgents()
|
|
69
|
+
.then((agents) => {
|
|
70
|
+
emitAvailableAgentsIfChanged(session, mapAvailableAgents(agents));
|
|
71
|
+
})
|
|
72
|
+
.catch(() => {
|
|
73
|
+
// Best-effort only.
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -25,13 +25,6 @@ function expectString(record, key, context) {
|
|
|
25
25
|
}
|
|
26
26
|
return value;
|
|
27
27
|
}
|
|
28
|
-
function expectBoolean(record, key, context) {
|
|
29
|
-
const value = record[key];
|
|
30
|
-
if (typeof value !== "boolean") {
|
|
31
|
-
throw new Error(`${context}.${key} must be a boolean`);
|
|
32
|
-
}
|
|
33
|
-
return value;
|
|
34
|
-
}
|
|
35
28
|
function optionalString(record, key, context) {
|
|
36
29
|
const value = record[key];
|
|
37
30
|
if (value === undefined || value === null) {
|
|
@@ -49,6 +42,45 @@ function optionalMetadata(record, key) {
|
|
|
49
42
|
}
|
|
50
43
|
return asRecord(value, `${key} metadata`);
|
|
51
44
|
}
|
|
45
|
+
function optionalLaunchSettings(record, key, context) {
|
|
46
|
+
const value = record[key];
|
|
47
|
+
if (value === undefined || value === null) {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
const parsed = asRecord(value, `${context}.${key}`);
|
|
51
|
+
const model = optionalString(parsed, "model", `${context}.${key}`);
|
|
52
|
+
const language = optionalString(parsed, "language", `${context}.${key}`);
|
|
53
|
+
const permissionMode = optionalString(parsed, "permission_mode", `${context}.${key}`);
|
|
54
|
+
const thinkingMode = optionalThinkingMode(parsed, "thinking_mode", `${context}.${key}`);
|
|
55
|
+
const effortLevel = optionalEffortLevel(parsed, "effort_level", `${context}.${key}`);
|
|
56
|
+
return {
|
|
57
|
+
...(model ? { model } : {}),
|
|
58
|
+
...(language ? { language } : {}),
|
|
59
|
+
...(permissionMode ? { permission_mode: permissionMode } : {}),
|
|
60
|
+
...(thinkingMode ? { thinking_mode: thinkingMode } : {}),
|
|
61
|
+
...(effortLevel ? { effort_level: effortLevel } : {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function optionalThinkingMode(record, key, context) {
|
|
65
|
+
const value = optionalString(record, key, context);
|
|
66
|
+
if (value === undefined) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
if (value === "adaptive" || value === "disabled") {
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`${context}.${key} must be "adaptive" or "disabled" when provided`);
|
|
73
|
+
}
|
|
74
|
+
function optionalEffortLevel(record, key, context) {
|
|
75
|
+
const value = optionalString(record, key, context);
|
|
76
|
+
if (value === undefined) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
if (value === "low" || value === "medium" || value === "high") {
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
throw new Error(`${context}.${key} must be "low", "medium", or "high" when provided`);
|
|
83
|
+
}
|
|
52
84
|
function parsePromptChunks(record, context) {
|
|
53
85
|
const rawChunks = record.chunks;
|
|
54
86
|
if (!Array.isArray(rawChunks)) {
|
|
@@ -76,23 +108,22 @@ export function parseCommandEnvelope(line) {
|
|
|
76
108
|
return {
|
|
77
109
|
command: "create_session",
|
|
78
110
|
cwd: expectString(raw, "cwd", "create_session"),
|
|
79
|
-
yolo: expectBoolean(raw, "yolo", "create_session"),
|
|
80
|
-
model: optionalString(raw, "model", "create_session"),
|
|
81
111
|
resume: optionalString(raw, "resume", "create_session"),
|
|
112
|
+
launch_settings: optionalLaunchSettings(raw, "launch_settings", "create_session"),
|
|
82
113
|
metadata: optionalMetadata(raw, "metadata"),
|
|
83
114
|
};
|
|
84
|
-
case "
|
|
115
|
+
case "resume_session":
|
|
85
116
|
return {
|
|
86
|
-
command: "
|
|
87
|
-
session_id: expectString(raw, "session_id", "
|
|
117
|
+
command: "resume_session",
|
|
118
|
+
session_id: expectString(raw, "session_id", "resume_session"),
|
|
119
|
+
launch_settings: optionalLaunchSettings(raw, "launch_settings", "resume_session"),
|
|
88
120
|
metadata: optionalMetadata(raw, "metadata"),
|
|
89
121
|
};
|
|
90
122
|
case "new_session":
|
|
91
123
|
return {
|
|
92
124
|
command: "new_session",
|
|
93
125
|
cwd: expectString(raw, "cwd", "new_session"),
|
|
94
|
-
|
|
95
|
-
model: optionalString(raw, "model", "new_session"),
|
|
126
|
+
launch_settings: optionalLaunchSettings(raw, "launch_settings", "new_session"),
|
|
96
127
|
};
|
|
97
128
|
case "prompt":
|
|
98
129
|
return {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { looksLikeAuthRequired } from "./auth.js";
|
|
2
|
+
import { writeEvent } from "./events.js";
|
|
3
|
+
import { emitSessionUpdate } from "./events.js";
|
|
4
|
+
import { parseFastModeState } from "./state_parsing.js";
|
|
5
|
+
export function emitAuthRequired(session, detail) {
|
|
6
|
+
if (session.authHintSent) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
session.authHintSent = true;
|
|
10
|
+
writeEvent({
|
|
11
|
+
event: "auth_required",
|
|
12
|
+
method_name: "Claude Login",
|
|
13
|
+
method_description: detail && detail.trim().length > 0
|
|
14
|
+
? detail
|
|
15
|
+
: "Type /login to authenticate.",
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export function looksLikePlanLimitError(input) {
|
|
19
|
+
const normalized = input.toLowerCase();
|
|
20
|
+
return (normalized.includes("rate limit") ||
|
|
21
|
+
normalized.includes("rate-limit") ||
|
|
22
|
+
normalized.includes("max turns") ||
|
|
23
|
+
normalized.includes("max budget") ||
|
|
24
|
+
normalized.includes("quota") ||
|
|
25
|
+
normalized.includes("plan limit") ||
|
|
26
|
+
normalized.includes("too many requests") ||
|
|
27
|
+
normalized.includes("insufficient quota") ||
|
|
28
|
+
normalized.includes("429"));
|
|
29
|
+
}
|
|
30
|
+
export function classifyTurnErrorKind(subtype, errors, assistantError) {
|
|
31
|
+
const combined = errors.join("\n");
|
|
32
|
+
if (subtype === "error_max_turns" ||
|
|
33
|
+
subtype === "error_max_budget_usd" ||
|
|
34
|
+
assistantError === "billing_error" ||
|
|
35
|
+
assistantError === "rate_limit" ||
|
|
36
|
+
(combined.length > 0 && looksLikePlanLimitError(combined))) {
|
|
37
|
+
return "plan_limit";
|
|
38
|
+
}
|
|
39
|
+
if (assistantError === "authentication_failed" ||
|
|
40
|
+
errors.some((entry) => looksLikeAuthRequired(entry))) {
|
|
41
|
+
return "auth_required";
|
|
42
|
+
}
|
|
43
|
+
if (assistantError === "server_error") {
|
|
44
|
+
return "internal";
|
|
45
|
+
}
|
|
46
|
+
return "other";
|
|
47
|
+
}
|
|
48
|
+
export function emitFastModeUpdateIfChanged(session, value) {
|
|
49
|
+
const next = parseFastModeState(value);
|
|
50
|
+
if (!next || next === session.fastModeState) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
session.fastModeState = next;
|
|
54
|
+
emitSessionUpdate(session.sessionId, { type: "fast_mode_update", fast_mode_state: next });
|
|
55
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { listSessions } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { buildModeState } from "./commands.js";
|
|
3
|
+
import { mapSdkSessions } from "./history.js";
|
|
4
|
+
const SESSION_LIST_LIMIT = 50;
|
|
5
|
+
export function writeEvent(event, requestId) {
|
|
6
|
+
const envelope = {
|
|
7
|
+
...(requestId ? { request_id: requestId } : {}),
|
|
8
|
+
...event,
|
|
9
|
+
};
|
|
10
|
+
process.stdout.write(`${JSON.stringify(envelope)}\n`);
|
|
11
|
+
}
|
|
12
|
+
export function failConnection(message, requestId) {
|
|
13
|
+
writeEvent({ event: "connection_failed", message }, requestId);
|
|
14
|
+
}
|
|
15
|
+
export function slashError(sessionId, message, requestId) {
|
|
16
|
+
writeEvent({ event: "slash_error", session_id: sessionId, message }, requestId);
|
|
17
|
+
}
|
|
18
|
+
export function emitSessionUpdate(sessionId, update) {
|
|
19
|
+
writeEvent({ event: "session_update", session_id: sessionId, update });
|
|
20
|
+
}
|
|
21
|
+
export function emitConnectEvent(session) {
|
|
22
|
+
const historyUpdates = session.resumeUpdates;
|
|
23
|
+
const connectEvent = session.connectEvent === "session_replaced"
|
|
24
|
+
? {
|
|
25
|
+
event: "session_replaced",
|
|
26
|
+
session_id: session.sessionId,
|
|
27
|
+
cwd: session.cwd,
|
|
28
|
+
model_name: session.model,
|
|
29
|
+
available_models: session.availableModels,
|
|
30
|
+
mode: session.mode ? buildModeState(session.mode) : null,
|
|
31
|
+
...(historyUpdates && historyUpdates.length > 0 ? { history_updates: historyUpdates } : {}),
|
|
32
|
+
}
|
|
33
|
+
: {
|
|
34
|
+
event: "connected",
|
|
35
|
+
session_id: session.sessionId,
|
|
36
|
+
cwd: session.cwd,
|
|
37
|
+
model_name: session.model,
|
|
38
|
+
available_models: session.availableModels,
|
|
39
|
+
mode: session.mode ? buildModeState(session.mode) : null,
|
|
40
|
+
...(historyUpdates && historyUpdates.length > 0 ? { history_updates: historyUpdates } : {}),
|
|
41
|
+
};
|
|
42
|
+
writeEvent(connectEvent, session.connectRequestId);
|
|
43
|
+
session.connectRequestId = undefined;
|
|
44
|
+
session.connected = true;
|
|
45
|
+
session.authHintSent = false;
|
|
46
|
+
session.resumeUpdates = undefined;
|
|
47
|
+
const staleSessions = session.sessionsToCloseAfterConnect;
|
|
48
|
+
session.sessionsToCloseAfterConnect = undefined;
|
|
49
|
+
if (!staleSessions || staleSessions.length === 0) {
|
|
50
|
+
refreshSessionsList();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
void (async () => {
|
|
54
|
+
// Lazy import to break circular dependency at module-evaluation time.
|
|
55
|
+
const { sessions, closeSession } = await import("./session_lifecycle.js");
|
|
56
|
+
for (const stale of staleSessions) {
|
|
57
|
+
if (stale === session) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (sessions.get(stale.sessionId) === stale) {
|
|
61
|
+
sessions.delete(stale.sessionId);
|
|
62
|
+
}
|
|
63
|
+
await closeSession(stale);
|
|
64
|
+
}
|
|
65
|
+
refreshSessionsList();
|
|
66
|
+
})();
|
|
67
|
+
}
|
|
68
|
+
export async function emitSessionsList(requestId) {
|
|
69
|
+
try {
|
|
70
|
+
const sdkSessions = await listSessions({ limit: SESSION_LIST_LIMIT });
|
|
71
|
+
writeEvent({ event: "sessions_listed", sessions: mapSdkSessions(sdkSessions, SESSION_LIST_LIMIT) }, requestId);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
75
|
+
console.error(`[sdk warn] listSessions failed: ${message}`);
|
|
76
|
+
writeEvent({ event: "sessions_listed", sessions: [] }, requestId);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function refreshSessionsList() {
|
|
80
|
+
void emitSessionsList().catch(() => {
|
|
81
|
+
// Defensive no-op.
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -1,224 +1,21 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { TOOL_RESULT_TYPES, buildToolResultFields, createToolCall, isToolUseBlockType } from "./tooling.js";
|
|
5
1
|
import { asRecordOrNull } from "./shared.js";
|
|
6
|
-
import {
|
|
7
|
-
function
|
|
8
|
-
|
|
9
|
-
text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
10
|
-
text = text.replace(/\s+/g, " ").trim();
|
|
11
|
-
return text;
|
|
12
|
-
}
|
|
13
|
-
function truncateTextByChars(text, maxChars) {
|
|
14
|
-
const chars = Array.from(text);
|
|
15
|
-
if (chars.length <= maxChars) {
|
|
16
|
-
return text;
|
|
17
|
-
}
|
|
18
|
-
return chars.slice(0, maxChars).join("");
|
|
19
|
-
}
|
|
20
|
-
function firstUserMessageTitleFromRecord(record) {
|
|
21
|
-
if (record.type !== "user") {
|
|
22
|
-
return undefined;
|
|
23
|
-
}
|
|
24
|
-
const message = asRecordOrNull(record.message);
|
|
25
|
-
if (!message || message.role !== "user" || !Array.isArray(message.content)) {
|
|
26
|
-
return undefined;
|
|
27
|
-
}
|
|
28
|
-
const parts = [];
|
|
29
|
-
for (const item of message.content) {
|
|
30
|
-
const block = asRecordOrNull(item);
|
|
31
|
-
if (!block || block.type !== "text" || typeof block.text !== "string") {
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
const cleaned = normalizeUserPromptText(block.text);
|
|
35
|
-
if (!cleaned) {
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
parts.push(cleaned);
|
|
39
|
-
const combined = parts.join(" ");
|
|
40
|
-
if (Array.from(combined).length >= 180) {
|
|
41
|
-
return truncateTextByChars(combined, 180);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
if (parts.length === 0) {
|
|
2
|
+
import { TOOL_RESULT_TYPES, buildToolResultFields, createToolCall, isToolUseBlockType } from "./tooling.js";
|
|
3
|
+
function nonEmptyTrimmed(value) {
|
|
4
|
+
if (typeof value !== "string") {
|
|
45
5
|
return undefined;
|
|
46
6
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
function extractSessionPreviewFromJsonl(filePath) {
|
|
50
|
-
let text;
|
|
51
|
-
try {
|
|
52
|
-
text = fs.readFileSync(filePath, "utf8");
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
return {};
|
|
56
|
-
}
|
|
57
|
-
let cwd;
|
|
58
|
-
let title;
|
|
59
|
-
const lines = text.split(/\r?\n/);
|
|
60
|
-
for (const rawLine of lines) {
|
|
61
|
-
const line = rawLine.trim();
|
|
62
|
-
if (line.length === 0) {
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
let parsed;
|
|
66
|
-
try {
|
|
67
|
-
parsed = JSON.parse(line);
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
const record = asRecordOrNull(parsed);
|
|
73
|
-
if (!record) {
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
if (!cwd && typeof record.cwd === "string" && record.cwd.trim().length > 0) {
|
|
77
|
-
cwd = record.cwd;
|
|
78
|
-
}
|
|
79
|
-
if (!title) {
|
|
80
|
-
title = firstUserMessageTitleFromRecord(record);
|
|
81
|
-
}
|
|
82
|
-
if (cwd && title) {
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return {
|
|
87
|
-
...(cwd ? { cwd } : {}),
|
|
88
|
-
...(title ? { title } : {}),
|
|
89
|
-
};
|
|
7
|
+
const trimmed = value.trim();
|
|
8
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
90
9
|
}
|
|
91
|
-
|
|
92
|
-
const root = path.join(os.homedir(), ".claude", "projects");
|
|
93
|
-
if (!fs.existsSync(root)) {
|
|
94
|
-
return [];
|
|
95
|
-
}
|
|
10
|
+
function messageCandidates(raw) {
|
|
96
11
|
const candidates = [];
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
projectDirs = fs.readdirSync(root, { withFileTypes: true }).filter((dirent) => dirent.isDirectory());
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
102
|
-
return [];
|
|
103
|
-
}
|
|
104
|
-
for (const dirent of projectDirs) {
|
|
105
|
-
const projectDir = path.join(root, dirent.name);
|
|
106
|
-
let sessionFiles;
|
|
107
|
-
try {
|
|
108
|
-
sessionFiles = fs
|
|
109
|
-
.readdirSync(projectDir, { withFileTypes: true })
|
|
110
|
-
.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"));
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
sessionFiles = [];
|
|
114
|
-
}
|
|
115
|
-
for (const sessionFile of sessionFiles) {
|
|
116
|
-
const sessionId = sessionFile.name.slice(0, -".jsonl".length);
|
|
117
|
-
if (!sessionId) {
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
let mtimeMs = 0;
|
|
121
|
-
try {
|
|
122
|
-
mtimeMs = fs.statSync(path.join(projectDir, sessionFile.name)).mtimeMs;
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
if (!Number.isFinite(mtimeMs) || mtimeMs <= 0) {
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
candidates.push({
|
|
131
|
-
session_id: sessionId,
|
|
132
|
-
cwd: "",
|
|
133
|
-
file_path: path.join(projectDir, sessionFile.name),
|
|
134
|
-
updated_at: new Date(mtimeMs).toISOString(),
|
|
135
|
-
sort_ms: mtimeMs,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
candidates.sort((a, b) => b.sort_ms - a.sort_ms);
|
|
140
|
-
const deduped = [];
|
|
141
|
-
const seen = new Set();
|
|
142
|
-
for (const candidate of candidates) {
|
|
143
|
-
if (seen.has(candidate.session_id)) {
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
seen.add(candidate.session_id);
|
|
147
|
-
const preview = extractSessionPreviewFromJsonl(candidate.file_path);
|
|
148
|
-
const cwd = preview.cwd?.trim();
|
|
149
|
-
if (!cwd) {
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
deduped.push({
|
|
153
|
-
session_id: candidate.session_id,
|
|
154
|
-
cwd,
|
|
155
|
-
file_path: candidate.file_path,
|
|
156
|
-
...(preview.title ? { title: preview.title } : {}),
|
|
157
|
-
...(candidate.updated_at ? { updated_at: candidate.updated_at } : {}),
|
|
158
|
-
sort_ms: candidate.sort_ms,
|
|
159
|
-
});
|
|
160
|
-
if (deduped.length >= limit) {
|
|
161
|
-
break;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return deduped;
|
|
165
|
-
}
|
|
166
|
-
export function resolvePersistedSessionEntry(sessionId) {
|
|
167
|
-
if (sessionId.trim().length === 0 ||
|
|
168
|
-
sessionId.includes("/") ||
|
|
169
|
-
sessionId.includes("\\") ||
|
|
170
|
-
sessionId.includes("..")) {
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
const root = path.join(os.homedir(), ".claude", "projects");
|
|
174
|
-
if (!fs.existsSync(root)) {
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
let projectDirs;
|
|
178
|
-
try {
|
|
179
|
-
projectDirs = fs.readdirSync(root, { withFileTypes: true }).filter((dirent) => dirent.isDirectory());
|
|
180
|
-
}
|
|
181
|
-
catch {
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
let best = null;
|
|
185
|
-
for (const dirent of projectDirs) {
|
|
186
|
-
const filePath = path.join(root, dirent.name, `${sessionId}.jsonl`);
|
|
187
|
-
if (!fs.existsSync(filePath)) {
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
const preview = extractSessionPreviewFromJsonl(filePath);
|
|
191
|
-
const cwd = preview.cwd?.trim();
|
|
192
|
-
if (!cwd) {
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
let mtimeMs = 0;
|
|
196
|
-
try {
|
|
197
|
-
mtimeMs = fs.statSync(filePath).mtimeMs;
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
mtimeMs = 0;
|
|
201
|
-
}
|
|
202
|
-
if (!best || mtimeMs >= best.sort_ms) {
|
|
203
|
-
best = {
|
|
204
|
-
session_id: sessionId,
|
|
205
|
-
cwd,
|
|
206
|
-
file_path: filePath,
|
|
207
|
-
sort_ms: mtimeMs,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
return best;
|
|
212
|
-
}
|
|
213
|
-
function persistedMessageCandidates(record) {
|
|
214
|
-
const candidates = [];
|
|
215
|
-
const topLevel = asRecordOrNull(record.message);
|
|
12
|
+
const topLevel = asRecordOrNull(raw);
|
|
216
13
|
if (topLevel) {
|
|
217
14
|
candidates.push(topLevel);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
15
|
+
const nested = asRecordOrNull(topLevel.message);
|
|
16
|
+
if (nested) {
|
|
17
|
+
candidates.push(nested);
|
|
18
|
+
}
|
|
222
19
|
}
|
|
223
20
|
return candidates;
|
|
224
21
|
}
|
|
@@ -264,53 +61,48 @@ function pushResumeToolResult(updates, toolCalls, block) {
|
|
|
264
61
|
base.content = fields.content;
|
|
265
62
|
}
|
|
266
63
|
}
|
|
267
|
-
function
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const usageUpdate = buildUsageUpdateFromResult(message);
|
|
273
|
-
if (!usageUpdate) {
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
updates.push(usageUpdate);
|
|
277
|
-
if (messageId) {
|
|
278
|
-
emittedUsageMessageIds.add(messageId);
|
|
279
|
-
}
|
|
64
|
+
function summaryFromSession(info) {
|
|
65
|
+
return (nonEmptyTrimmed(info.summary) ??
|
|
66
|
+
nonEmptyTrimmed(info.customTitle) ??
|
|
67
|
+
nonEmptyTrimmed(info.firstPrompt) ??
|
|
68
|
+
info.sessionId);
|
|
280
69
|
}
|
|
281
|
-
export function
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
try {
|
|
300
|
-
parsed = JSON.parse(line);
|
|
301
|
-
}
|
|
302
|
-
catch {
|
|
70
|
+
export function mapSdkSessionInfo(info) {
|
|
71
|
+
return {
|
|
72
|
+
session_id: info.sessionId,
|
|
73
|
+
summary: summaryFromSession(info),
|
|
74
|
+
last_modified_ms: info.lastModified,
|
|
75
|
+
file_size_bytes: info.fileSize,
|
|
76
|
+
...(nonEmptyTrimmed(info.cwd) ? { cwd: info.cwd?.trim() } : {}),
|
|
77
|
+
...(nonEmptyTrimmed(info.gitBranch) ? { git_branch: info.gitBranch?.trim() } : {}),
|
|
78
|
+
...(nonEmptyTrimmed(info.customTitle) ? { custom_title: info.customTitle?.trim() } : {}),
|
|
79
|
+
...(nonEmptyTrimmed(info.firstPrompt) ? { first_prompt: info.firstPrompt?.trim() } : {}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export function mapSdkSessions(infos, limit = 50) {
|
|
83
|
+
const sorted = [...infos].sort((a, b) => b.lastModified - a.lastModified);
|
|
84
|
+
const entries = [];
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
for (const info of sorted) {
|
|
87
|
+
if (!info.sessionId || seen.has(info.sessionId)) {
|
|
303
88
|
continue;
|
|
304
89
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
90
|
+
seen.add(info.sessionId);
|
|
91
|
+
entries.push(mapSdkSessionInfo(info));
|
|
92
|
+
if (entries.length >= limit) {
|
|
93
|
+
break;
|
|
308
94
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
95
|
+
}
|
|
96
|
+
return entries;
|
|
97
|
+
}
|
|
98
|
+
export function mapSessionMessagesToUpdates(messages) {
|
|
99
|
+
const updates = [];
|
|
100
|
+
const toolCalls = new Map();
|
|
101
|
+
for (const entry of messages) {
|
|
102
|
+
const fallbackRole = entry.type === "assistant" ? "assistant" : "user";
|
|
103
|
+
for (const message of messageCandidates(entry.message)) {
|
|
104
|
+
const roleCandidate = message.role;
|
|
105
|
+
const role = roleCandidate === "assistant" || roleCandidate === "user" ? roleCandidate : fallbackRole;
|
|
314
106
|
const content = Array.isArray(message.content) ? message.content : [];
|
|
315
107
|
for (const item of content) {
|
|
316
108
|
const block = asRecordOrNull(item);
|
|
@@ -337,7 +129,6 @@ export function extractSessionHistoryUpdatesFromJsonl(filePath) {
|
|
|
337
129
|
pushResumeTextChunk(updates, role, "[image]");
|
|
338
130
|
}
|
|
339
131
|
}
|
|
340
|
-
pushResumeUsageUpdate(updates, message, emittedUsageMessageIds);
|
|
341
132
|
}
|
|
342
133
|
}
|
|
343
134
|
return updates;
|