@love-moon/ai-sdk 0.11.0 → 0.11.2
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/CHANGELOG.md +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/manager/manager.d.ts +1 -1
- package/dist/manager/paths.d.ts +13 -0
- package/dist/manager/paths.js +28 -2
- package/dist/manager/quota/claude.d.ts +10 -1
- package/dist/manager/quota/claude.js +52 -25
- package/dist/resume/claude.d.ts +15 -1
- package/dist/resume/claude.js +139 -20
- package/dist/resume/codex.d.ts +14 -0
- package/dist/resume/codex.js +100 -1
- package/dist/resume/index.d.ts +11 -0
- package/dist/resume/index.js +11 -0
- package/dist/resume/kimi.d.ts +14 -0
- package/dist/resume/kimi.js +145 -1
- package/dist/resume/shared.d.ts +8 -0
- package/dist/resume/shared.js +45 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -7,4 +7,4 @@ export { assertMediaCapabilities, fileMediaToBase64, fileMediaToDataUri, normali
|
|
|
7
7
|
export { GOAL_STATUSES, TERMINAL_GOAL_STATUSES, isGoalStatus, isTerminalGoalStatus } from "./shared.js";
|
|
8
8
|
export { AiManager, accountNameFromPath, checkInstall, checkInstallAll, checkNetwork, checkNetworkAll, getClaudeQuota, getCodexQuota, getCopilotQuota, getCurrentCodexAccount, getDshQuota, readCachedDshQuota, resolveDeepSeekCredential, getExternalQuota, getExternalQuotaList, getKimiQuota, listCodexAccounts, loadAiManagerConfig, normalizeExternalQuota, normalizeExternalQuotaList, parseAuthFile, parseAuthFileContents, parseCopilotQuotaSnapshots, readCachedCodexQuota, resolveClaudeCredential, switchCodexAccount } from "./manager/index.js";
|
|
9
9
|
export { getExternalProviderDescriptor, getExternalProviderRegistry, resetExternalProviderRegistryForTests, resolveExternalBackend } from "./external-provider-registry.js";
|
|
10
|
-
export { resolveResumeContext, buildResumeArgsForBackend, resumeProviderForBackend, findSessionPath, resolveSessionRunDirectory, inspectResumeTarget } from "./resume/index.js";
|
|
10
|
+
export { resolveResumeContext, buildResumeArgsForBackend, resumeProviderForBackend, findSessionPath, resolveSessionRunDirectory, inspectResumeTarget, listSessionsForBackend } from "./resume/index.js";
|
package/dist/index.js
CHANGED
|
@@ -7,4 +7,4 @@ export { assertMediaCapabilities, fileMediaToBase64, fileMediaToDataUri, normali
|
|
|
7
7
|
export { GOAL_STATUSES, TERMINAL_GOAL_STATUSES, isGoalStatus, isTerminalGoalStatus, } from "./shared.js";
|
|
8
8
|
export { AiManager, accountNameFromPath, checkInstall, checkInstallAll, checkNetwork, checkNetworkAll, getClaudeQuota, getCodexQuota, getCopilotQuota, getCurrentCodexAccount, getDshQuota, readCachedDshQuota, resolveDeepSeekCredential, getExternalQuota, getExternalQuotaList, getKimiQuota, listCodexAccounts, loadAiManagerConfig, normalizeExternalQuota, normalizeExternalQuotaList, parseAuthFile, parseAuthFileContents, parseCopilotQuotaSnapshots, readCachedCodexQuota, resolveClaudeCredential, switchCodexAccount, } from "./manager/index.js";
|
|
9
9
|
export { getExternalProviderDescriptor, getExternalProviderRegistry, resetExternalProviderRegistryForTests, resolveExternalBackend, } from "./external-provider-registry.js";
|
|
10
|
-
export { resolveResumeContext, buildResumeArgsForBackend, resumeProviderForBackend, findSessionPath, resolveSessionRunDirectory, inspectResumeTarget, } from "./resume/index.js";
|
|
10
|
+
export { resolveResumeContext, buildResumeArgsForBackend, resumeProviderForBackend, findSessionPath, resolveSessionRunDirectory, inspectResumeTarget, listSessionsForBackend, } from "./resume/index.js";
|
|
@@ -8,7 +8,7 @@ import type { AiManagerConfig, CodexAccount, CodexQuota, ClaudeQuota, DshQuota,
|
|
|
8
8
|
export interface AiManagerOptions {
|
|
9
9
|
/** Path to conductor config.yaml. Default: $CONDUCTOR_HOME/config.yaml. */
|
|
10
10
|
configPath?: string;
|
|
11
|
-
/** Path to the active codex auth.json. Default: ~/.codex/auth.json */
|
|
11
|
+
/** Path to the active codex auth.json. Default: $CODEX_HOME/auth.json, else ~/.codex/auth.json */
|
|
12
12
|
codexAuthPath?: string;
|
|
13
13
|
/** Pre-loaded config; skips loading from disk if provided. */
|
|
14
14
|
config?: AiManagerConfig;
|
package/dist/manager/paths.d.ts
CHANGED
|
@@ -2,6 +2,19 @@ type PathEnv = Record<string, string | undefined>;
|
|
|
2
2
|
export declare function expandHome(p: string): string;
|
|
3
3
|
export declare function resolveConductorHome(env?: PathEnv): string;
|
|
4
4
|
export declare function resolveDefaultConductorConfig(env?: PathEnv): string;
|
|
5
|
+
/**
|
|
6
|
+
* Claude Code keeps its config (credentials, projects/, tasks/) under
|
|
7
|
+
* $CLAUDE_CONFIG_DIR when that is set - shared hosts point it at network
|
|
8
|
+
* storage - and under ~/.claude otherwise. Returns both, in priority order, so
|
|
9
|
+
* callers can search the override first and still find pre-existing state that
|
|
10
|
+
* was written before the variable was introduced.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveClaudeConfigDirs(env?: PathEnv, homeDir?: string): string[];
|
|
13
|
+
/**
|
|
14
|
+
* Codex keeps auth.json/config.toml under $CODEX_HOME when that is set
|
|
15
|
+
* (identity isolation, multi-instance daemons) and under ~/.codex otherwise.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveCodexHome(env?: PathEnv): string;
|
|
5
18
|
export declare function resolveDefaultQuotaCacheDir(env?: PathEnv): string;
|
|
6
19
|
export declare const DEFAULT_CONDUCTOR_CONFIG: string;
|
|
7
20
|
export declare const DEFAULT_CODEX_AUTH: string;
|
package/dist/manager/paths.js
CHANGED
|
@@ -34,12 +34,38 @@ export function resolveDefaultConductorConfig(env = process.env) {
|
|
|
34
34
|
}
|
|
35
35
|
return join(resolveConductorHome(env), "config.yaml");
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Claude Code keeps its config (credentials, projects/, tasks/) under
|
|
39
|
+
* $CLAUDE_CONFIG_DIR when that is set - shared hosts point it at network
|
|
40
|
+
* storage - and under ~/.claude otherwise. Returns both, in priority order, so
|
|
41
|
+
* callers can search the override first and still find pre-existing state that
|
|
42
|
+
* was written before the variable was introduced.
|
|
43
|
+
*/
|
|
44
|
+
export function resolveClaudeConfigDirs(env = process.env, homeDir) {
|
|
45
|
+
const fallback = join(homeDir ? resolve(homeDir) : resolve(userHome(env)), ".claude");
|
|
46
|
+
// An explicit homeDir is a caller/test override that pins the lookup.
|
|
47
|
+
const configured = homeDir ? "" : optionalString(env.CLAUDE_CONFIG_DIR);
|
|
48
|
+
if (!configured)
|
|
49
|
+
return [fallback];
|
|
50
|
+
const expanded = resolve(expandHomeWithEnv(configured, env));
|
|
51
|
+
return expanded === fallback ? [fallback] : [expanded, fallback];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Codex keeps auth.json/config.toml under $CODEX_HOME when that is set
|
|
55
|
+
* (identity isolation, multi-instance daemons) and under ~/.codex otherwise.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveCodexHome(env = process.env) {
|
|
58
|
+
const configured = optionalString(env.CODEX_HOME);
|
|
59
|
+
if (configured)
|
|
60
|
+
return resolve(expandHomeWithEnv(configured, env));
|
|
61
|
+
return join(resolve(userHome(env)), ".codex");
|
|
62
|
+
}
|
|
37
63
|
export function resolveDefaultQuotaCacheDir(env = process.env) {
|
|
38
64
|
return join(resolveConductorHome(env), "cache", "ai-manager");
|
|
39
65
|
}
|
|
40
66
|
export const DEFAULT_CONDUCTOR_CONFIG = resolveDefaultConductorConfig();
|
|
41
|
-
export const DEFAULT_CODEX_AUTH = join(
|
|
42
|
-
export const DEFAULT_CODEX_CONFIG = join(
|
|
67
|
+
export const DEFAULT_CODEX_AUTH = join(resolveCodexHome(), "auth.json");
|
|
68
|
+
export const DEFAULT_CODEX_CONFIG = join(resolveCodexHome(), "config.toml");
|
|
43
69
|
export const DEFAULT_KIMI_CODE_HOME = process.env.KIMI_CODE_HOME?.trim() || join(homedir(), ".kimi-code");
|
|
44
70
|
export const DEFAULT_KIMI_CREDENTIAL = join(DEFAULT_KIMI_CODE_HOME, "credentials", "kimi-code.json");
|
|
45
71
|
export const LEGACY_KIMI_CREDENTIAL = join(homedir(), ".kimi", "credentials", "kimi-code.json");
|
|
@@ -4,6 +4,15 @@ export interface ClaudeCredential {
|
|
|
4
4
|
token: string;
|
|
5
5
|
/** JSON source string (for fingerprinting). */
|
|
6
6
|
fingerprintInput: string;
|
|
7
|
+
/** OAuth expiry in epoch ms, when the source reports one. */
|
|
8
|
+
expiresAt?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ResolveClaudeCredentialOptions {
|
|
11
|
+
env?: Record<string, string | undefined>;
|
|
12
|
+
/** Pins the lookup to one home, ignoring CLAUDE_CONFIG_DIR (tests). */
|
|
13
|
+
homeDir?: string;
|
|
14
|
+
platform?: string;
|
|
15
|
+
readKeychain?: () => Promise<string | null>;
|
|
7
16
|
}
|
|
8
17
|
export interface GetClaudeQuotaOptions {
|
|
9
18
|
/** Cache TTL in seconds. Default 60. */
|
|
@@ -15,5 +24,5 @@ export interface GetClaudeQuotaOptions {
|
|
|
15
24
|
/** Override credential resolution (mainly for tests). */
|
|
16
25
|
credential?: ClaudeCredential;
|
|
17
26
|
}
|
|
18
|
-
export declare function resolveClaudeCredential(): Promise<ClaudeCredential | null>;
|
|
27
|
+
export declare function resolveClaudeCredential(opts?: ResolveClaudeCredentialOptions): Promise<ClaudeCredential | null>;
|
|
19
28
|
export declare function getClaudeQuota(opts?: GetClaudeQuotaOptions): Promise<ClaudeQuota>;
|
|
@@ -1,40 +1,58 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
3
|
import { join } from "node:path";
|
|
5
4
|
import { headersToMap, num, str } from "./headers.js";
|
|
6
5
|
import { cacheFile, fingerprintKey, isFresh, readCache, writeCache } from "./cache.js";
|
|
6
|
+
import { resolveClaudeConfigDirs } from "../paths.js";
|
|
7
7
|
const MESSAGES_URL = "https://api.anthropic.com/v1/messages";
|
|
8
8
|
const DEFAULT_TTL = 60;
|
|
9
9
|
const DEFAULT_TIMEOUT_MS = 20000;
|
|
10
10
|
const CLAUDE_CODE_SYSTEM_PROMPT = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
11
|
-
export async function resolveClaudeCredential() {
|
|
12
|
-
const
|
|
11
|
+
export async function resolveClaudeCredential(opts = {}) {
|
|
12
|
+
const env = opts.env ?? process.env;
|
|
13
|
+
const envKey = env.ANTHROPIC_API_KEY;
|
|
13
14
|
if (envKey && envKey.length > 10) {
|
|
14
15
|
return { kind: "api-key", token: envKey, fingerprintInput: envKey };
|
|
15
16
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
17
|
+
// Candidates in priority order:
|
|
18
|
+
// $CLAUDE_CONFIG_DIR - explicitly chosen, so it outranks the machine-wide
|
|
19
|
+
// Keychain, which belongs to a different account.
|
|
20
|
+
// Keychain (macOS) - where Claude Code refreshes tokens to.
|
|
21
|
+
// ~/.claude - last, because on macOS it is the stale-file trap:
|
|
22
|
+
// refreshes go to the Keychain, so a leftover file
|
|
23
|
+
// here holds a dead token.
|
|
24
|
+
const dirs = resolveClaudeConfigDirs(env, opts.homeDir);
|
|
25
|
+
const overrideDir = dirs.length > 1 ? dirs[0] : undefined;
|
|
26
|
+
const homeDir = dirs[dirs.length - 1];
|
|
27
|
+
const readFileCred = (dir) => () => oauthFromFile(join(dir, ".credentials.json"));
|
|
28
|
+
const sources = [];
|
|
29
|
+
if (overrideDir)
|
|
30
|
+
sources.push(readFileCred(overrideDir));
|
|
31
|
+
if ((opts.platform ?? process.platform) === "darwin") {
|
|
32
|
+
const readKeychain = opts.readKeychain ?? readMacKeychain;
|
|
33
|
+
sources.push(async () => {
|
|
34
|
+
const raw = await readKeychain();
|
|
35
|
+
return raw ? oauthFromJson(raw) : null;
|
|
36
|
+
});
|
|
34
37
|
}
|
|
35
|
-
|
|
38
|
+
sources.push(readFileCred(homeDir));
|
|
39
|
+
// Prefer the first live credential. An expired one is remembered but not
|
|
40
|
+
// returned yet, so a stale file cannot shadow a working Keychain entry.
|
|
41
|
+
let expired = null;
|
|
42
|
+
for (const read of sources) {
|
|
43
|
+
const cred = await read();
|
|
44
|
+
if (!cred)
|
|
45
|
+
continue;
|
|
46
|
+
if (!cred.expiresAt || cred.expiresAt > Date.now())
|
|
47
|
+
return cred;
|
|
48
|
+
expired ??= cred;
|
|
49
|
+
}
|
|
50
|
+
// Everything on offer is expired: return it anyway so the caller surfaces an
|
|
51
|
+
// auth error rather than a misleading "no credential found".
|
|
52
|
+
return expired;
|
|
53
|
+
}
|
|
54
|
+
function oauthFromJson(raw) {
|
|
36
55
|
try {
|
|
37
|
-
const raw = await readFile(join(homedir(), ".claude", ".credentials.json"), "utf8");
|
|
38
56
|
const parsed = JSON.parse(raw);
|
|
39
57
|
const oauth = parsed?.claudeAiOauth ?? parsed;
|
|
40
58
|
if (oauth?.accessToken) {
|
|
@@ -42,14 +60,23 @@ export async function resolveClaudeCredential() {
|
|
|
42
60
|
kind: "oauth",
|
|
43
61
|
token: oauth.accessToken,
|
|
44
62
|
fingerprintInput: oauth.accessToken,
|
|
63
|
+
expiresAt: typeof oauth.expiresAt === "number" ? oauth.expiresAt : undefined,
|
|
45
64
|
};
|
|
46
65
|
}
|
|
47
66
|
}
|
|
48
67
|
catch {
|
|
49
|
-
//
|
|
68
|
+
// not JSON, or no token in it
|
|
50
69
|
}
|
|
51
70
|
return null;
|
|
52
71
|
}
|
|
72
|
+
async function oauthFromFile(path) {
|
|
73
|
+
try {
|
|
74
|
+
return oauthFromJson(await readFile(path, "utf8"));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
53
80
|
function readMacKeychain() {
|
|
54
81
|
return new Promise((resolve) => {
|
|
55
82
|
const user = process.env.USER ?? "";
|
|
@@ -99,7 +126,7 @@ export async function getClaudeQuota(opts = {}) {
|
|
|
99
126
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
100
127
|
const credential = opts.credential ?? (await resolveClaudeCredential());
|
|
101
128
|
if (!credential) {
|
|
102
|
-
return emptyQuota("unknown", "no Claude credential found (ANTHROPIC_API_KEY env, macOS Keychain, or ~/.claude/.credentials.json)");
|
|
129
|
+
return emptyQuota("unknown", "no Claude credential found (ANTHROPIC_API_KEY env, $CLAUDE_CONFIG_DIR/.credentials.json, macOS Keychain, or ~/.claude/.credentials.json)");
|
|
103
130
|
}
|
|
104
131
|
const fp = fingerprintKey(["claude", credential.kind, credential.fingerprintInput.slice(0, 32)]);
|
|
105
132
|
const file = cacheFile("claude", fp, opts.cacheDir);
|
package/dist/resume/claude.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export function buildCliArgs(sessionId: any): string[];
|
|
2
|
-
export function findSessionPath(sessionId: any, options?: {}): Promise<
|
|
2
|
+
export function findSessionPath(sessionId: any, options?: {}): Promise<string | null>;
|
|
3
3
|
export function extractResumeCwd(sessionPath: any, sessionId: any): Promise<string | null>;
|
|
4
4
|
export function resolveResumeContext(sessionId: any, options?: {}): Promise<{
|
|
5
5
|
provider: any;
|
|
@@ -11,4 +11,18 @@ export function resolveResumeContext(sessionId: any, options?: {}): Promise<{
|
|
|
11
11
|
sessionPath: null;
|
|
12
12
|
};
|
|
13
13
|
}>;
|
|
14
|
+
/**
|
|
15
|
+
* Lists local Claude Code sessions, newest first.
|
|
16
|
+
*
|
|
17
|
+
* Candidates are collected with readdir + stat only; session files can be tens
|
|
18
|
+
* of megabytes, so file contents (cwd/title) are read - head only - for the
|
|
19
|
+
* `limit` newest files after sorting by mtime.
|
|
20
|
+
*/
|
|
21
|
+
export function listSessions(options?: {}): Promise<{
|
|
22
|
+
sessionId: any;
|
|
23
|
+
sessionFilePath: any;
|
|
24
|
+
cwd: any;
|
|
25
|
+
title: string | null;
|
|
26
|
+
updatedAt: any;
|
|
27
|
+
}[]>;
|
|
14
28
|
export const BACKEND: "claude";
|
package/dist/resume/claude.js
CHANGED
|
@@ -2,7 +2,8 @@ import fs from "node:fs";
|
|
|
2
2
|
import { promises as fsp } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import readline from "node:readline";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveClaudeConfigDirs } from "../manager/paths.js";
|
|
6
|
+
import { buildResumeContext, isExistingDirectory, normalizeSessionId, normalizeSessionTitle, pathExists, readJsonlHeadEntries, resolveSessionRunDirectory, } from "./shared.js";
|
|
6
7
|
export const BACKEND = "claude";
|
|
7
8
|
export function buildCliArgs(sessionId) {
|
|
8
9
|
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
@@ -16,16 +17,20 @@ export async function findSessionPath(sessionId, options = {}) {
|
|
|
16
17
|
if (!normalizedSessionId) {
|
|
17
18
|
return null;
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
// Search $CLAUDE_CONFIG_DIR first, then ~/.claude, so sessions recorded
|
|
21
|
+
// before that variable was introduced stay resumable.
|
|
22
|
+
const configDirs = resolveClaudeConfigDirs(options.env ?? process.env, options.homeDir);
|
|
23
|
+
for (const configDir of configDirs) {
|
|
24
|
+
const projectsDir = options.claudeProjectsDir || path.join(configDir, "projects");
|
|
25
|
+
const sessionPath = await findClaudeSessionPath(projectsDir, normalizedSessionId);
|
|
26
|
+
if (sessionPath) {
|
|
27
|
+
return sessionPath;
|
|
28
|
+
}
|
|
29
|
+
const tasksDir = options.claudeTasksDir || path.join(configDir, "tasks");
|
|
30
|
+
const directTaskDir = path.join(tasksDir, normalizedSessionId);
|
|
31
|
+
if (await pathExists(directTaskDir, "directory")) {
|
|
32
|
+
return directTaskDir;
|
|
33
|
+
}
|
|
29
34
|
}
|
|
30
35
|
return null;
|
|
31
36
|
}
|
|
@@ -83,14 +88,127 @@ export async function resolveResumeContext(sessionId, options = {}) {
|
|
|
83
88
|
cwdSource: cwdFromSession ? "session" : "session_path",
|
|
84
89
|
});
|
|
85
90
|
}
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Lists local Claude Code sessions, newest first.
|
|
93
|
+
*
|
|
94
|
+
* Candidates are collected with readdir + stat only; session files can be tens
|
|
95
|
+
* of megabytes, so file contents (cwd/title) are read - head only - for the
|
|
96
|
+
* `limit` newest files after sorting by mtime.
|
|
97
|
+
*/
|
|
98
|
+
export async function listSessions(options = {}) {
|
|
99
|
+
const { limit = 20, env = process.env } = options;
|
|
100
|
+
const configDirs = resolveClaudeConfigDirs(env, options.homeDir);
|
|
101
|
+
const scannedProjectsDirs = new Set();
|
|
102
|
+
const candidatesById = new Map();
|
|
103
|
+
for (const configDir of configDirs) {
|
|
104
|
+
const projectsDir = options.claudeProjectsDir || path.join(configDir, "projects");
|
|
105
|
+
if (scannedProjectsDirs.has(projectsDir)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
scannedProjectsDirs.add(projectsDir);
|
|
109
|
+
let projectDirs = [];
|
|
110
|
+
try {
|
|
111
|
+
projectDirs = await fsp.readdir(projectsDir, { withFileTypes: true });
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
for (const projectDir of projectDirs) {
|
|
117
|
+
if (!projectDir.isDirectory()) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const projectPath = path.join(projectsDir, projectDir.name);
|
|
121
|
+
let files = [];
|
|
122
|
+
try {
|
|
123
|
+
files = await fsp.readdir(projectPath, { withFileTypes: true });
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
for (const file of files) {
|
|
129
|
+
if (!file.isFile() || !file.name.endsWith(".jsonl") || file.name.startsWith("agent-")) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const sessionId = file.name.slice(0, -".jsonl".length);
|
|
133
|
+
// Config dirs are searched in priority order; the first hit wins.
|
|
134
|
+
if (!sessionId || candidatesById.has(sessionId)) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const filePath = path.join(projectPath, file.name);
|
|
138
|
+
let stats;
|
|
139
|
+
try {
|
|
140
|
+
stats = await fsp.stat(filePath);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
candidatesById.set(sessionId, {
|
|
146
|
+
sessionId,
|
|
147
|
+
sessionFilePath: filePath,
|
|
148
|
+
updatedAt: Math.round(stats.mtimeMs),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const selected = [...candidatesById.values()]
|
|
154
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
155
|
+
.slice(0, Math.max(0, limit));
|
|
156
|
+
const sessions = [];
|
|
157
|
+
for (const candidate of selected) {
|
|
158
|
+
const entries = await readJsonlHeadEntries(candidate.sessionFilePath);
|
|
159
|
+
sessions.push({
|
|
160
|
+
sessionId: candidate.sessionId,
|
|
161
|
+
sessionFilePath: candidate.sessionFilePath,
|
|
162
|
+
cwd: extractHeadCwd(entries),
|
|
163
|
+
title: extractClaudeTitle(entries),
|
|
164
|
+
updatedAt: candidate.updatedAt,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return sessions;
|
|
168
|
+
}
|
|
169
|
+
function extractHeadCwd(entries) {
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
if (typeof entry?.cwd === "string" && entry.cwd.trim()) {
|
|
172
|
+
return entry.cwd.trim();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
function extractClaudeTitle(entries) {
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
if (entry?.type !== "user" || entry?.isMeta === true) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const content = entry?.message?.content;
|
|
183
|
+
let text = "";
|
|
184
|
+
if (typeof content === "string") {
|
|
185
|
+
text = content;
|
|
186
|
+
}
|
|
187
|
+
else if (Array.isArray(content)) {
|
|
188
|
+
text = content
|
|
189
|
+
.filter((item) => item?.type === "text" && typeof item.text === "string")
|
|
190
|
+
.map((item) => item.text)
|
|
191
|
+
.join(" ");
|
|
192
|
+
}
|
|
193
|
+
const title = normalizeSessionTitle(text);
|
|
194
|
+
if (title) {
|
|
195
|
+
return title;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Returns the first session file mentioning `sessionId`. Session ids are
|
|
202
|
+
* unique, so this stops at the first hit instead of walking the whole history
|
|
203
|
+
* tree - which now may live on network storage.
|
|
204
|
+
*/
|
|
205
|
+
async function findClaudeSessionPath(projectsDir, sessionId) {
|
|
88
206
|
let projectDirs = [];
|
|
89
207
|
try {
|
|
90
208
|
projectDirs = await fsp.readdir(projectsDir, { withFileTypes: true });
|
|
91
209
|
}
|
|
92
210
|
catch {
|
|
93
|
-
return
|
|
211
|
+
return null;
|
|
94
212
|
}
|
|
95
213
|
for (const projectDir of projectDirs) {
|
|
96
214
|
if (!projectDir.isDirectory()) {
|
|
@@ -112,10 +230,8 @@ async function findClaudeSessionEntries(projectsDir, sessionId) {
|
|
|
112
230
|
continue;
|
|
113
231
|
}
|
|
114
232
|
const filePath = path.join(projectPath, file.name);
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
crlfDelay: Infinity,
|
|
118
|
-
});
|
|
233
|
+
const input = fs.createReadStream(filePath);
|
|
234
|
+
const rl = readline.createInterface({ input, crlfDelay: Infinity });
|
|
119
235
|
for await (const line of rl) {
|
|
120
236
|
const trimmed = line.trim();
|
|
121
237
|
if (!trimmed) {
|
|
@@ -129,10 +245,13 @@ async function findClaudeSessionEntries(projectsDir, sessionId) {
|
|
|
129
245
|
continue;
|
|
130
246
|
}
|
|
131
247
|
if (entry.sessionId === sessionId) {
|
|
132
|
-
|
|
248
|
+
// Close both: rl.close() alone leaves the fd open.
|
|
249
|
+
rl.close();
|
|
250
|
+
input.destroy();
|
|
251
|
+
return filePath;
|
|
133
252
|
}
|
|
134
253
|
}
|
|
135
254
|
}
|
|
136
255
|
}
|
|
137
|
-
return
|
|
256
|
+
return null;
|
|
138
257
|
}
|
package/dist/resume/codex.d.ts
CHANGED
|
@@ -11,4 +11,18 @@ export function resolveResumeContext(sessionId: any, options?: {}): Promise<{
|
|
|
11
11
|
sessionPath: null;
|
|
12
12
|
};
|
|
13
13
|
}>;
|
|
14
|
+
/**
|
|
15
|
+
* Lists local Codex sessions, newest first.
|
|
16
|
+
*
|
|
17
|
+
* Candidates are collected with readdir + stat only; session files can be tens
|
|
18
|
+
* of megabytes, so file contents (session_meta/title) are read - head only -
|
|
19
|
+
* for the `limit` newest files after sorting by mtime.
|
|
20
|
+
*/
|
|
21
|
+
export function listSessions(options?: {}): Promise<{
|
|
22
|
+
sessionId: string;
|
|
23
|
+
sessionFilePath: string;
|
|
24
|
+
cwd: string | null;
|
|
25
|
+
title: string | null;
|
|
26
|
+
updatedAt: number;
|
|
27
|
+
}[]>;
|
|
14
28
|
export const BACKEND: "codex";
|
package/dist/resume/codex.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { promises as fsp } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { buildResumeContext, isExistingDirectory, iterateJsonlEntries, normalizeSessionId, resolveHomeDir, resolveSessionRunDirectory, } from "./shared.js";
|
|
3
|
+
import { buildResumeContext, isExistingDirectory, iterateJsonlEntries, normalizeSessionId, normalizeSessionTitle, readJsonlHeadEntries, resolveHomeDir, resolveSessionRunDirectory, } from "./shared.js";
|
|
4
4
|
export const BACKEND = "codex";
|
|
5
5
|
export function buildCliArgs(sessionId) {
|
|
6
6
|
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
@@ -56,6 +56,105 @@ export async function resolveResumeContext(sessionId, options = {}) {
|
|
|
56
56
|
cwdSource: cwdFromSession ? "session" : "session_path",
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Lists local Codex sessions, newest first.
|
|
61
|
+
*
|
|
62
|
+
* Candidates are collected with readdir + stat only; session files can be tens
|
|
63
|
+
* of megabytes, so file contents (session_meta/title) are read - head only -
|
|
64
|
+
* for the `limit` newest files after sorting by mtime.
|
|
65
|
+
*/
|
|
66
|
+
export async function listSessions(options = {}) {
|
|
67
|
+
const { limit = 20 } = options;
|
|
68
|
+
const homeDir = resolveHomeDir(options);
|
|
69
|
+
const sessionsDir = options.codexSessionsDir || path.join(homeDir, ".codex", "sessions");
|
|
70
|
+
const candidates = [];
|
|
71
|
+
const queue = [sessionsDir];
|
|
72
|
+
while (queue.length) {
|
|
73
|
+
const current = queue.pop();
|
|
74
|
+
let entries = [];
|
|
75
|
+
try {
|
|
76
|
+
entries = await fsp.readdir(current, { withFileTypes: true });
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
const fullPath = path.join(current, entry.name);
|
|
83
|
+
if (entry.isDirectory()) {
|
|
84
|
+
queue.push(fullPath);
|
|
85
|
+
}
|
|
86
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
87
|
+
let stats;
|
|
88
|
+
try {
|
|
89
|
+
stats = await fsp.stat(fullPath);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
candidates.push({ filePath: fullPath, updatedAt: Math.round(stats.mtimeMs) });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
candidates.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
99
|
+
const maxSessions = Math.max(0, limit);
|
|
100
|
+
const sessions = [];
|
|
101
|
+
for (const candidate of candidates) {
|
|
102
|
+
if (sessions.length >= maxSessions) {
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
const headEntries = await readJsonlHeadEntries(candidate.filePath);
|
|
106
|
+
const meta = headEntries.find((entry) => entry?.type === "session_meta") || null;
|
|
107
|
+
const sessionId = normalizeSessionId(meta?.payload?.id)
|
|
108
|
+
|| normalizeSessionId(meta?.payload?.session_id)
|
|
109
|
+
|| extractSessionIdFromFileName(candidate.filePath);
|
|
110
|
+
if (!sessionId) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const metaCwd = meta?.payload?.cwd;
|
|
114
|
+
sessions.push({
|
|
115
|
+
sessionId,
|
|
116
|
+
sessionFilePath: candidate.filePath,
|
|
117
|
+
cwd: typeof metaCwd === "string" && metaCwd.trim() ? metaCwd.trim() : null,
|
|
118
|
+
title: extractCodexTitle(headEntries),
|
|
119
|
+
updatedAt: candidate.updatedAt,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return sessions;
|
|
123
|
+
}
|
|
124
|
+
const SESSION_ID_IN_FILE_NAME = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
125
|
+
function extractSessionIdFromFileName(filePath) {
|
|
126
|
+
const match = path.basename(filePath).match(SESSION_ID_IN_FILE_NAME);
|
|
127
|
+
return match ? match[0] : "";
|
|
128
|
+
}
|
|
129
|
+
function extractCodexTitle(entries) {
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
if (entry?.type === "event_msg" && entry?.payload?.type === "user_message") {
|
|
132
|
+
const title = normalizeSessionTitle(entry.payload.message);
|
|
133
|
+
if (title) {
|
|
134
|
+
return title;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
const payload = entry?.type === "response_item" ? entry.payload : null;
|
|
140
|
+
if (payload?.type !== "message" || payload?.role !== "user" || !Array.isArray(payload.content)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const text = payload.content
|
|
144
|
+
.filter((item) => item?.type === "input_text" && typeof item.text === "string")
|
|
145
|
+
.map((item) => item.text)
|
|
146
|
+
.join(" ");
|
|
147
|
+
if (text.trim().startsWith("<")) {
|
|
148
|
+
// Skip injected wrappers such as <environment_context> / <user_instructions>.
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const title = normalizeSessionTitle(text);
|
|
152
|
+
if (title) {
|
|
153
|
+
return title;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
59
158
|
async function findCodexSessionFile(rootDir, sessionId) {
|
|
60
159
|
const queue = [rootDir];
|
|
61
160
|
while (queue.length) {
|
package/dist/resume/index.d.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
export function resumeProviderForBackend(backend: any): "codex" | null;
|
|
2
2
|
export function buildResumeArgsForBackend(backend: any, sessionId: any): string[];
|
|
3
3
|
export function findSessionPath(provider: any, sessionId: any, options?: {}): Promise<string | null>;
|
|
4
|
+
/**
|
|
5
|
+
* Lists local sessions for a backend (aliases resolve like every other resume
|
|
6
|
+
* entry point). Backends without a `listSessions` implementation yield [].
|
|
7
|
+
*/
|
|
8
|
+
export function listSessionsForBackend(backendType: any, opts?: {}): Promise<{
|
|
9
|
+
sessionId: string;
|
|
10
|
+
sessionFilePath: string;
|
|
11
|
+
cwd: string | null;
|
|
12
|
+
title: string | null;
|
|
13
|
+
updatedAt: number;
|
|
14
|
+
}[]>;
|
|
4
15
|
export function resolveResumeContext(backend: any, sessionId: any, options?: {}): Promise<{
|
|
5
16
|
provider: any;
|
|
6
17
|
sessionId: any;
|
package/dist/resume/index.js
CHANGED
|
@@ -76,6 +76,17 @@ export async function findSessionPath(provider, sessionId, options = {}) {
|
|
|
76
76
|
return descriptor.findSessionPath(sessionId, options);
|
|
77
77
|
}
|
|
78
78
|
export { resolveSessionRunDirectory };
|
|
79
|
+
/**
|
|
80
|
+
* Lists local sessions for a backend (aliases resolve like every other resume
|
|
81
|
+
* entry point). Backends without a `listSessions` implementation yield [].
|
|
82
|
+
*/
|
|
83
|
+
export async function listSessionsForBackend(backendType, opts = {}) {
|
|
84
|
+
const provider = lookupBuiltInProvider(backendType);
|
|
85
|
+
if (!provider || typeof provider.listSessions !== "function") {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
return provider.listSessions(opts);
|
|
89
|
+
}
|
|
79
90
|
export async function resolveResumeContext(backend, sessionId, options = {}) {
|
|
80
91
|
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
81
92
|
if (!normalizedSessionId) {
|
package/dist/resume/kimi.d.ts
CHANGED
|
@@ -11,4 +11,18 @@ export function resolveResumeContext(sessionId: any, options?: {}): Promise<{
|
|
|
11
11
|
};
|
|
12
12
|
}>;
|
|
13
13
|
export function md5Hex(value: any): string;
|
|
14
|
+
/**
|
|
15
|
+
* Lists local Kimi Code sessions, newest first.
|
|
16
|
+
*
|
|
17
|
+
* Prefers the session_index.jsonl fast path and falls back to scanning
|
|
18
|
+
* `<KIMI_CODE_HOME>/sessions/<hash>/<sessionId>/`. Candidates are ranked by
|
|
19
|
+
* directory mtime; state.json is only read for the `limit` newest sessions.
|
|
20
|
+
*/
|
|
21
|
+
export function listSessions(options?: {}): Promise<{
|
|
22
|
+
sessionId: any;
|
|
23
|
+
sessionFilePath: string | null;
|
|
24
|
+
cwd: any;
|
|
25
|
+
title: string | null;
|
|
26
|
+
updatedAt: any;
|
|
27
|
+
}[]>;
|
|
14
28
|
export const BACKEND: "kimi";
|
package/dist/resume/kimi.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { promises as fsp } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { buildResumeContext, isExistingDirectory, listCandidateWorkingDirectories, normalizeSessionId, pathExists, resolveHomeDir, } from "./shared.js";
|
|
4
|
+
import { buildResumeContext, isExistingDirectory, listCandidateWorkingDirectories, normalizeSessionId, normalizeSessionTitle, pathExists, resolveHomeDir, } from "./shared.js";
|
|
5
5
|
export const BACKEND = "kimi";
|
|
6
6
|
export function buildCliArgs(sessionId) {
|
|
7
7
|
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
@@ -68,6 +68,150 @@ async function resolveLegacyKimiResumeCwd(sessionPath, _sessionId, options = {})
|
|
|
68
68
|
}
|
|
69
69
|
return null;
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Lists local Kimi Code sessions, newest first.
|
|
73
|
+
*
|
|
74
|
+
* Prefers the session_index.jsonl fast path and falls back to scanning
|
|
75
|
+
* `<KIMI_CODE_HOME>/sessions/<hash>/<sessionId>/`. Candidates are ranked by
|
|
76
|
+
* directory mtime; state.json is only read for the `limit` newest sessions.
|
|
77
|
+
*/
|
|
78
|
+
export async function listSessions(options = {}) {
|
|
79
|
+
const { limit = 20 } = options;
|
|
80
|
+
const homeDir = resolveHomeDir(options);
|
|
81
|
+
const kimiCodeHome = resolveKimiCodeHome(homeDir, options);
|
|
82
|
+
const sessionsDir = path.join(kimiCodeHome, "sessions");
|
|
83
|
+
let stated = await statKimiSessionCandidates(await listIndexedKimiCodeSessions(kimiCodeHome, sessionsDir));
|
|
84
|
+
if (!stated.length) {
|
|
85
|
+
// The index can go stale wholesale (sessions dir cleaned, index copied from
|
|
86
|
+
// another machine), so fall back to scanning even when it had records.
|
|
87
|
+
stated = await statKimiSessionCandidates(await scanKimiCodeSessionDirs(sessionsDir));
|
|
88
|
+
}
|
|
89
|
+
stated.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
90
|
+
const sessions = [];
|
|
91
|
+
for (const candidate of stated.slice(0, Math.max(0, limit))) {
|
|
92
|
+
const statePath = path.join(candidate.sessionPath, "state.json");
|
|
93
|
+
let state = null;
|
|
94
|
+
try {
|
|
95
|
+
state = JSON.parse(await fsp.readFile(statePath, "utf8"));
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Fall back to the session index record, which also carries workDir.
|
|
99
|
+
}
|
|
100
|
+
const stateWorkDir = typeof state?.workDir === "string" ? state.workDir.trim() : "";
|
|
101
|
+
// The Kimi Code message log format is not stable; use the title recorded in
|
|
102
|
+
// state.json / the session index when present instead of parsing messages.
|
|
103
|
+
const title = normalizeSessionTitle(state?.title) || normalizeSessionTitle(candidate.title) || null;
|
|
104
|
+
sessions.push({
|
|
105
|
+
sessionId: candidate.sessionId,
|
|
106
|
+
sessionFilePath: (await pathExists(statePath, "file")) ? statePath : null,
|
|
107
|
+
cwd: stateWorkDir || candidate.workDir || null,
|
|
108
|
+
title,
|
|
109
|
+
updatedAt: candidate.updatedAt,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return sessions;
|
|
113
|
+
}
|
|
114
|
+
async function statKimiSessionCandidates(candidates) {
|
|
115
|
+
const stated = [];
|
|
116
|
+
for (const candidate of candidates) {
|
|
117
|
+
let stats;
|
|
118
|
+
try {
|
|
119
|
+
stats = await fsp.stat(candidate.sessionPath);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!stats.isDirectory()) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
stated.push({ ...candidate, updatedAt: Math.round(stats.mtimeMs) });
|
|
128
|
+
}
|
|
129
|
+
return stated;
|
|
130
|
+
}
|
|
131
|
+
async function listIndexedKimiCodeSessions(kimiCodeHome, sessionsDir) {
|
|
132
|
+
let raw = "";
|
|
133
|
+
try {
|
|
134
|
+
raw = await fsp.readFile(path.join(kimiCodeHome, "session_index.jsonl"), "utf8");
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
const recordsBySessionId = new Map();
|
|
140
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
141
|
+
const trimmed = line.trim();
|
|
142
|
+
if (!trimmed) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
let record = null;
|
|
146
|
+
try {
|
|
147
|
+
record = JSON.parse(trimmed);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const sessionId = typeof record?.sessionId === "string" ? record.sessionId.trim() : "";
|
|
153
|
+
if (!sessionId) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (record?.deleted === true) {
|
|
157
|
+
recordsBySessionId.delete(sessionId);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const sessionDir = typeof record?.sessionDir === "string" ? record.sessionDir.trim() : "";
|
|
161
|
+
if (!sessionDir || !path.isAbsolute(sessionDir)) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const relativePath = path.relative(path.resolve(sessionsDir), path.resolve(sessionDir));
|
|
165
|
+
if (!relativePath ||
|
|
166
|
+
relativePath === ".." ||
|
|
167
|
+
relativePath.startsWith(`..${path.sep}`) ||
|
|
168
|
+
path.isAbsolute(relativePath) ||
|
|
169
|
+
path.basename(sessionDir) !== sessionId) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
recordsBySessionId.set(sessionId, {
|
|
173
|
+
sessionId,
|
|
174
|
+
sessionPath: sessionDir,
|
|
175
|
+
workDir: typeof record?.workDir === "string" && record.workDir.trim() ? record.workDir.trim() : null,
|
|
176
|
+
title: typeof record?.title === "string" ? record.title : null,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return [...recordsBySessionId.values()];
|
|
180
|
+
}
|
|
181
|
+
async function scanKimiCodeSessionDirs(sessionsDir) {
|
|
182
|
+
let hashDirs = [];
|
|
183
|
+
try {
|
|
184
|
+
hashDirs = await fsp.readdir(sessionsDir, { withFileTypes: true });
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
const candidates = [];
|
|
190
|
+
for (const hashDir of hashDirs) {
|
|
191
|
+
if (!hashDir.isDirectory()) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
let sessionDirs = [];
|
|
195
|
+
try {
|
|
196
|
+
sessionDirs = await fsp.readdir(path.join(sessionsDir, hashDir.name), { withFileTypes: true });
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
for (const sessionDir of sessionDirs) {
|
|
202
|
+
if (!sessionDir.isDirectory()) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
candidates.push({
|
|
206
|
+
sessionId: sessionDir.name,
|
|
207
|
+
sessionPath: path.join(sessionsDir, hashDir.name, sessionDir.name),
|
|
208
|
+
workDir: null,
|
|
209
|
+
title: null,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return candidates;
|
|
214
|
+
}
|
|
71
215
|
async function findKimiSessionTarget(sessionId, options = {}) {
|
|
72
216
|
const homeDir = resolveHomeDir(options);
|
|
73
217
|
const kimiCodeHome = resolveKimiCodeHome(homeDir, options);
|
package/dist/resume/shared.d.ts
CHANGED
|
@@ -3,6 +3,14 @@ export function normalizeSessionId(sessionId: any): string;
|
|
|
3
3
|
export function resolveHomeDir(options: any): any;
|
|
4
4
|
export function normalizeProjectPathCandidate(value: any): string;
|
|
5
5
|
export function pathExists(targetPath: any, expectedType: any): Promise<boolean>;
|
|
6
|
+
/** Collapses whitespace and truncates a session title for list display. */
|
|
7
|
+
export function normalizeSessionTitle(text: any, maxLength?: number): string | null;
|
|
8
|
+
/**
|
|
9
|
+
* Parses the head of a jsonl file (bounded by lines and bytes so multi-MB
|
|
10
|
+
* session files stay cheap to inspect). Unreadable files and unparsable lines
|
|
11
|
+
* are skipped.
|
|
12
|
+
*/
|
|
13
|
+
export function readJsonlHeadEntries(filePath: any, maxLines?: number, maxBytes?: number): Promise<any[]>;
|
|
6
14
|
export function isExistingDirectory(targetPath: any): Promise<boolean>;
|
|
7
15
|
export function resolveSessionRunDirectory(sessionPath: any): Promise<string>;
|
|
8
16
|
export function listCandidateWorkingDirectories(options?: {}): any[];
|
package/dist/resume/shared.js
CHANGED
|
@@ -33,6 +33,51 @@ export async function pathExists(targetPath, expectedType) {
|
|
|
33
33
|
return false;
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
+
/** Collapses whitespace and truncates a session title for list display. */
|
|
37
|
+
export function normalizeSessionTitle(text, maxLength = 80) {
|
|
38
|
+
const collapsed = String(text || "").replace(/\s+/g, " ").trim();
|
|
39
|
+
if (!collapsed) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return collapsed.length > maxLength ? collapsed.slice(0, maxLength) : collapsed;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Parses the head of a jsonl file (bounded by lines and bytes so multi-MB
|
|
46
|
+
* session files stay cheap to inspect). Unreadable files and unparsable lines
|
|
47
|
+
* are skipped.
|
|
48
|
+
*/
|
|
49
|
+
export async function readJsonlHeadEntries(filePath, maxLines = 50, maxBytes = 128 * 1024) {
|
|
50
|
+
const entries = [];
|
|
51
|
+
const input = fs.createReadStream(filePath, { end: maxBytes - 1 });
|
|
52
|
+
const rl = readline.createInterface({ input, crlfDelay: Infinity });
|
|
53
|
+
try {
|
|
54
|
+
let lineCount = 0;
|
|
55
|
+
for await (const line of rl) {
|
|
56
|
+
if (++lineCount > maxLines) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
const trimmed = line.trim();
|
|
60
|
+
if (!trimmed) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
entries.push(JSON.parse(trimmed));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Ignore read errors; callers degrade to null cwd/title.
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
// Close both: rl.close() alone leaves the fd open.
|
|
76
|
+
rl.close();
|
|
77
|
+
input.destroy();
|
|
78
|
+
}
|
|
79
|
+
return entries;
|
|
80
|
+
}
|
|
36
81
|
export async function isExistingDirectory(targetPath) {
|
|
37
82
|
const normalizedPath = typeof targetPath === "string" ? targetPath.trim() : "";
|
|
38
83
|
if (!normalizedPath) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/ai-sdk",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/lovemoon-ai/conductor.git"
|
|
@@ -53,12 +53,12 @@
|
|
|
53
53
|
"zod": "^4.1.5"
|
|
54
54
|
},
|
|
55
55
|
"optionalDependencies": {
|
|
56
|
-
"@love-moon/chat-web": "^0.11.
|
|
56
|
+
"@love-moon/chat-web": "^0.11.2"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/node": "^22.10.2",
|
|
60
60
|
"tsx": "^4.20.6",
|
|
61
61
|
"typescript": "^5.6.3"
|
|
62
62
|
},
|
|
63
|
-
"gitCommitId": "
|
|
63
|
+
"gitCommitId": "067735d"
|
|
64
64
|
}
|