telecodex 0.1.1 → 0.1.3

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.
@@ -0,0 +1,215 @@
1
+ import { createReadStream, existsSync, opendirSync, realpathSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ import { createInterface } from "node:readline";
5
+ export class CodexSessionCatalog {
6
+ sessionsRoot;
7
+ constructor(input) {
8
+ this.sessionsRoot = input?.sessionsRoot ?? defaultSessionsRoot();
9
+ this.logger = input?.logger;
10
+ }
11
+ logger;
12
+ async listProjectThreads(input) {
13
+ const projectRoot = canonicalizePath(input.projectRoot);
14
+ const limit = Math.max(1, input.limit ?? 8);
15
+ const files = listSessionFiles(this.sessionsRoot)
16
+ .filter((entry) => entry.mtimeMs > 0)
17
+ .sort((left, right) => right.mtimeMs - left.mtimeMs);
18
+ const matches = [];
19
+ for (const file of files) {
20
+ const summary = await readSessionSummary(file.path, file.updatedAt);
21
+ if (!summary)
22
+ continue;
23
+ if (!isPathWithinRoot(summary.cwd, projectRoot))
24
+ continue;
25
+ matches.push(summary);
26
+ if (matches.length >= limit)
27
+ break;
28
+ }
29
+ return matches;
30
+ }
31
+ async findProjectThreadById(input) {
32
+ const projectRoot = canonicalizePath(input.projectRoot);
33
+ const threadId = input.threadId.trim();
34
+ if (!threadId)
35
+ return null;
36
+ const files = listSessionFiles(this.sessionsRoot).sort((left, right) => right.mtimeMs - left.mtimeMs);
37
+ for (const file of files) {
38
+ const summary = await readSessionSummary(file.path, file.updatedAt);
39
+ if (!summary)
40
+ continue;
41
+ if (summary.id !== threadId)
42
+ continue;
43
+ if (!isPathWithinRoot(summary.cwd, projectRoot))
44
+ return null;
45
+ return summary;
46
+ }
47
+ this.logger?.debug("codex thread not found in saved sessions", {
48
+ projectRoot,
49
+ threadId,
50
+ sessionsRoot: this.sessionsRoot,
51
+ });
52
+ return null;
53
+ }
54
+ }
55
+ function defaultSessionsRoot() {
56
+ const codexHome = process.env.CODEX_HOME?.trim();
57
+ const root = codexHome ? path.resolve(codexHome) : path.join(homedir(), ".codex");
58
+ return path.join(root, "sessions");
59
+ }
60
+ function listSessionFiles(root) {
61
+ if (!existsSync(root))
62
+ return [];
63
+ const files = [];
64
+ const stack = [root];
65
+ while (stack.length > 0) {
66
+ const current = stack.pop();
67
+ if (!current)
68
+ continue;
69
+ let directory;
70
+ try {
71
+ directory = opendirSync(current);
72
+ }
73
+ catch {
74
+ continue;
75
+ }
76
+ for (let entry = directory.readSync(); entry != null; entry = directory.readSync()) {
77
+ const resolved = path.join(current, entry.name);
78
+ if (entry.isDirectory()) {
79
+ stack.push(resolved);
80
+ continue;
81
+ }
82
+ if (!entry.isFile() || !resolved.endsWith(".jsonl"))
83
+ continue;
84
+ try {
85
+ const stat = statSync(resolved);
86
+ files.push({
87
+ path: resolved,
88
+ updatedAt: stat.mtime.toISOString(),
89
+ mtimeMs: stat.mtimeMs,
90
+ });
91
+ }
92
+ catch {
93
+ continue;
94
+ }
95
+ }
96
+ directory.closeSync();
97
+ }
98
+ return files;
99
+ }
100
+ async function readSessionSummary(filePath, updatedAt) {
101
+ const input = createReadStream(filePath, { encoding: "utf8" });
102
+ const reader = createInterface({
103
+ input,
104
+ crlfDelay: Infinity,
105
+ });
106
+ let meta = null;
107
+ let preview = "";
108
+ try {
109
+ for await (const line of reader) {
110
+ const parsed = parseJsonLine(line);
111
+ if (!parsed)
112
+ continue;
113
+ if (!meta) {
114
+ meta = parseSessionMeta(parsed);
115
+ }
116
+ if (!preview) {
117
+ preview = parseUserPreview(parsed);
118
+ }
119
+ if (meta && preview)
120
+ break;
121
+ }
122
+ }
123
+ finally {
124
+ reader.close();
125
+ input.destroy();
126
+ }
127
+ if (!meta?.id || !meta.cwd)
128
+ return null;
129
+ return {
130
+ id: meta.id,
131
+ cwd: canonicalizePath(meta.cwd),
132
+ createdAt: typeof meta.timestamp === "string" ? meta.timestamp : null,
133
+ updatedAt,
134
+ preview: preview || "(no user message preview)",
135
+ source: typeof meta.source === "string" ? meta.source : null,
136
+ modelProvider: typeof meta.model_provider === "string" ? meta.model_provider : null,
137
+ sessionPath: filePath,
138
+ };
139
+ }
140
+ function parseJsonLine(line) {
141
+ try {
142
+ const parsed = JSON.parse(line);
143
+ return isRecord(parsed) ? parsed : null;
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ }
149
+ function parseSessionMeta(parsed) {
150
+ if (parsed.type !== "session_meta")
151
+ return null;
152
+ const payload = isRecord(parsed.payload) ? parsed.payload : null;
153
+ if (!payload)
154
+ return null;
155
+ if (typeof payload.id !== "string" || typeof payload.cwd !== "string")
156
+ return null;
157
+ return {
158
+ id: payload.id,
159
+ cwd: payload.cwd,
160
+ ...(typeof payload.timestamp === "string" ? { timestamp: payload.timestamp } : {}),
161
+ ...(typeof payload.source === "string" ? { source: payload.source } : {}),
162
+ ...(typeof payload.model_provider === "string" ? { model_provider: payload.model_provider } : {}),
163
+ };
164
+ }
165
+ function parseUserPreview(parsed) {
166
+ if (parsed.type === "event_msg") {
167
+ const payload = isRecord(parsed.payload) ? parsed.payload : null;
168
+ if (payload?.type === "user_message" && typeof payload.message === "string") {
169
+ return normalizePreview(payload.message);
170
+ }
171
+ }
172
+ if (parsed.type !== "response_item")
173
+ return "";
174
+ const payload = isRecord(parsed.payload) ? parsed.payload : null;
175
+ if (!payload || payload.type !== "message" || payload.role !== "user")
176
+ return "";
177
+ const content = Array.isArray(payload.content) ? payload.content : [];
178
+ for (const item of content) {
179
+ if (!isRecord(item) || item.type !== "input_text" || typeof item.text !== "string")
180
+ continue;
181
+ const preview = normalizePreview(item.text);
182
+ if (preview)
183
+ return preview;
184
+ }
185
+ return "";
186
+ }
187
+ function normalizePreview(value) {
188
+ const normalized = value.replace(/\s+/g, " ").trim();
189
+ if (!normalized)
190
+ return "";
191
+ if (normalized.startsWith("# AGENTS.md instructions") ||
192
+ normalized.startsWith("<environment_context>") ||
193
+ normalized.startsWith("<app-context>") ||
194
+ normalized.startsWith("<permissions instructions>")) {
195
+ return "";
196
+ }
197
+ if (normalized.length <= 96)
198
+ return normalized;
199
+ return `${normalized.slice(0, 93)}...`;
200
+ }
201
+ function isPathWithinRoot(candidate, root) {
202
+ return candidate === root || candidate.startsWith(`${root}${path.sep}`);
203
+ }
204
+ function canonicalizePath(value) {
205
+ const resolved = path.resolve(value);
206
+ try {
207
+ return realpathSync.native(resolved);
208
+ }
209
+ catch {
210
+ return resolved;
211
+ }
212
+ }
213
+ function isRecord(value) {
214
+ return typeof value === "object" && value !== null && !Array.isArray(value);
215
+ }
package/dist/config.js CHANGED
@@ -14,7 +14,6 @@ export function buildConfig(input) {
14
14
  telegramBotToken: input.telegramBotToken,
15
15
  defaultCwd,
16
16
  defaultModel: input.defaultModel?.trim() || "gpt-5.4",
17
- dbPath: path.resolve(input.dbPath),
18
17
  codexBin: input.codexBin,
19
18
  updateIntervalMs: input.updateIntervalMs ?? 700,
20
19
  };
@@ -3,7 +3,10 @@ import path from "node:path";
3
3
  export function getAppHome() {
4
4
  return path.join(homedir(), ".telecodex");
5
5
  }
6
- export function getStateDbPath() {
6
+ export function getStateDir() {
7
+ return path.join(getAppHome(), "state");
8
+ }
9
+ export function getLegacyStateDbPath() {
7
10
  return path.join(getAppHome(), "state.sqlite");
8
11
  }
9
12
  export function getLogsDir() {
@@ -5,19 +5,24 @@ import { existsSync } from "node:fs";
5
5
  import path from "node:path";
6
6
  import { Bot, GrammyError, HttpError } from "grammy";
7
7
  import { buildConfig } from "../config.js";
8
- import { openDatabase } from "../store/db.js";
8
+ import { FileStateStorage } from "../store/fileState.js";
9
+ import { migrateLegacySqliteState } from "../store/legacyMigration.js";
9
10
  import { ProjectStore } from "../store/projects.js";
10
11
  import { BINDING_CODE_MAX_ATTEMPTS, SessionStore } from "../store/sessions.js";
11
- import { getStateDbPath } from "./appPaths.js";
12
+ import { getLegacyStateDbPath, getStateDir } from "./appPaths.js";
12
13
  import { generateBindingCode } from "./bindingCodes.js";
13
14
  import { PLAINTEXT_TOKEN_FALLBACK_ENV, SecretStore, } from "./secrets.js";
14
15
  const MAC_CODEX_BIN = "/Applications/Codex.app/Contents/Resources/codex";
15
16
  export async function bootstrapRuntime() {
16
17
  intro("telecodex");
17
- const dbPath = getStateDbPath();
18
- const db = openDatabase(dbPath);
19
- const store = new SessionStore(db);
20
- const projects = new ProjectStore(db);
18
+ const stateDir = getStateDir();
19
+ const storage = new FileStateStorage(stateDir);
20
+ migrateLegacySqliteState({
21
+ storage,
22
+ legacyDbPath: getLegacyStateDbPath(),
23
+ });
24
+ const store = new SessionStore(storage);
25
+ const projects = new ProjectStore(storage);
21
26
  const secrets = new SecretStore(store, {
22
27
  allowPlaintextFallback: process.env[PLAINTEXT_TOKEN_FALLBACK_ENV] === "1",
23
28
  });
@@ -30,7 +35,6 @@ export async function bootstrapRuntime() {
30
35
  const config = buildConfig({
31
36
  telegramBotToken: token,
32
37
  defaultCwd: process.cwd(),
33
- dbPath,
34
38
  codexBin,
35
39
  });
36
40
  let bootstrapCode = null;
@@ -1,5 +1,6 @@
1
1
  import { run } from "@grammyjs/runner";
2
2
  import { createBot } from "../bot/createBot.js";
3
+ import { CodexSessionCatalog } from "../codex/sessionCatalog.js";
3
4
  import { CodexSdkRuntime } from "../codex/sdkRuntime.js";
4
5
  import { bootstrapRuntime } from "./bootstrap.js";
5
6
  import { acquireInstanceLock } from "./instanceLock.js";
@@ -29,11 +30,15 @@ export async function startTelecodex() {
29
30
  logger: logger.child("codex-sdk"),
30
31
  ...(configOverrides ? { configOverrides } : {}),
31
32
  });
33
+ const threadCatalog = new CodexSessionCatalog({
34
+ logger: logger.child("codex-sessions"),
35
+ });
32
36
  const bot = createBot({
33
37
  config,
34
38
  store,
35
39
  projects,
36
40
  codex,
41
+ threadCatalog,
37
42
  bootstrapCode,
38
43
  logger: logger.child("bot"),
39
44
  onAdminBound: () => {
@@ -0,0 +1,370 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { DEFAULT_SESSION_PROFILE, isSessionApprovalPolicy, isSessionReasoningEffort, isSessionSandboxMode, isSessionWebSearchMode, } from "../config.js";
4
+ const FILE_STATE_VERSION = 1;
5
+ export class FileStateStorage {
6
+ rootDir;
7
+ appPath;
8
+ projectsPath;
9
+ sessionsPath;
10
+ appState = new Map();
11
+ projects = new Map();
12
+ sessions = new Map();
13
+ constructor(rootDir) {
14
+ this.rootDir = rootDir;
15
+ mkdirSync(rootDir, { recursive: true });
16
+ this.appPath = path.join(rootDir, "app.json");
17
+ this.projectsPath = path.join(rootDir, "projects.json");
18
+ this.sessionsPath = path.join(rootDir, "sessions.json");
19
+ for (const [key, value] of Object.entries(loadAppStateFile(this.appPath).values)) {
20
+ this.appState.set(key, value);
21
+ }
22
+ for (const project of loadProjectsFile(this.projectsPath).projects) {
23
+ this.projects.set(project.chatId, normalizeStoredProjectBinding(project));
24
+ }
25
+ for (const session of loadSessionsFile(this.sessionsPath).sessions) {
26
+ this.sessions.set(session.sessionKey, normalizeStoredSessionRecord(session));
27
+ }
28
+ }
29
+ getAppState(key) {
30
+ return this.appState.get(key) ?? null;
31
+ }
32
+ setAppState(key, value) {
33
+ this.appState.set(key, value);
34
+ this.flushAppState();
35
+ }
36
+ deleteAppState(key) {
37
+ if (!this.appState.delete(key))
38
+ return;
39
+ this.flushAppState();
40
+ }
41
+ mergeImportedAppState(values) {
42
+ let changed = false;
43
+ for (const [key, value] of Object.entries(values)) {
44
+ if (this.appState.has(key))
45
+ continue;
46
+ this.appState.set(key, value);
47
+ changed = true;
48
+ }
49
+ if (changed)
50
+ this.flushAppState();
51
+ }
52
+ getProject(chatId) {
53
+ return cloneProjectBinding(this.projects.get(chatId) ?? null);
54
+ }
55
+ upsertProject(input) {
56
+ const existing = this.projects.get(input.chatId);
57
+ const now = input.now ?? new Date().toISOString();
58
+ const project = {
59
+ chatId: input.chatId,
60
+ cwd: path.resolve(input.cwd),
61
+ name: input.name.trim(),
62
+ createdAt: existing?.createdAt ?? now,
63
+ updatedAt: now,
64
+ };
65
+ this.projects.set(project.chatId, project);
66
+ this.flushProjects();
67
+ return cloneProjectBinding(project);
68
+ }
69
+ removeProject(chatId) {
70
+ if (!this.projects.delete(chatId))
71
+ return;
72
+ this.flushProjects();
73
+ }
74
+ listProjects() {
75
+ return [...this.projects.values()]
76
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
77
+ .map((project) => cloneProjectBinding(project))
78
+ .filter((project) => project != null);
79
+ }
80
+ mergeImportedProjects(projects) {
81
+ let changed = false;
82
+ for (const project of projects) {
83
+ if (this.projects.has(project.chatId))
84
+ continue;
85
+ this.projects.set(project.chatId, normalizeStoredProjectBinding(project));
86
+ changed = true;
87
+ }
88
+ if (changed)
89
+ this.flushProjects();
90
+ }
91
+ getSession(sessionKey) {
92
+ return cloneSessionRecord(this.sessions.get(sessionKey) ?? null);
93
+ }
94
+ listSessions() {
95
+ return [...this.sessions.values()]
96
+ .sort((left, right) => left.sessionKey.localeCompare(right.sessionKey))
97
+ .map((session) => cloneSessionRecord(session))
98
+ .filter((session) => session != null);
99
+ }
100
+ getSessionByThreadId(threadId) {
101
+ for (const session of this.sessions.values()) {
102
+ if (session.codexThreadId === threadId) {
103
+ return cloneSessionRecord(session);
104
+ }
105
+ }
106
+ return null;
107
+ }
108
+ putSession(session) {
109
+ const normalized = normalizeStoredSessionRecord(session);
110
+ this.sessions.set(normalized.sessionKey, normalized);
111
+ this.flushSessions();
112
+ return cloneSessionRecord(normalized);
113
+ }
114
+ patchSession(sessionKey, patch) {
115
+ const existing = this.sessions.get(sessionKey);
116
+ if (!existing)
117
+ return null;
118
+ const next = normalizeStoredSessionRecord({
119
+ ...existing,
120
+ ...patch,
121
+ sessionKey,
122
+ createdAt: existing.createdAt,
123
+ updatedAt: new Date().toISOString(),
124
+ });
125
+ this.sessions.set(sessionKey, next);
126
+ this.flushSessions();
127
+ return cloneSessionRecord(next);
128
+ }
129
+ removeSession(sessionKey) {
130
+ if (!this.sessions.delete(sessionKey))
131
+ return;
132
+ this.flushSessions();
133
+ }
134
+ mergeImportedSessions(sessions) {
135
+ let changed = false;
136
+ for (const session of sessions) {
137
+ if (this.sessions.has(session.sessionKey))
138
+ continue;
139
+ this.sessions.set(session.sessionKey, normalizeStoredSessionRecord(session));
140
+ changed = true;
141
+ }
142
+ if (changed)
143
+ this.flushSessions();
144
+ }
145
+ flushAppState() {
146
+ writeJsonFile(this.appPath, {
147
+ version: FILE_STATE_VERSION,
148
+ values: Object.fromEntries([...this.appState.entries()].sort(([left], [right]) => left.localeCompare(right))),
149
+ });
150
+ }
151
+ flushProjects() {
152
+ writeJsonFile(this.projectsPath, {
153
+ version: FILE_STATE_VERSION,
154
+ projects: [...this.projects.values()].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)),
155
+ });
156
+ }
157
+ flushSessions() {
158
+ writeJsonFile(this.sessionsPath, {
159
+ version: FILE_STATE_VERSION,
160
+ sessions: [...this.sessions.values()].sort((left, right) => left.sessionKey.localeCompare(right.sessionKey)),
161
+ });
162
+ }
163
+ }
164
+ function loadAppStateFile(filePath) {
165
+ try {
166
+ const parsed = readJsonFile(filePath);
167
+ if (!parsed) {
168
+ return {
169
+ version: FILE_STATE_VERSION,
170
+ values: {},
171
+ };
172
+ }
173
+ if (!isRecord(parsed) || !isRecord(parsed.values)) {
174
+ throw new Error(`Invalid app state file: ${filePath}`);
175
+ }
176
+ const values = {};
177
+ for (const [key, value] of Object.entries(parsed.values)) {
178
+ if (typeof value === "string")
179
+ values[key] = value;
180
+ }
181
+ return {
182
+ version: FILE_STATE_VERSION,
183
+ values,
184
+ };
185
+ }
186
+ catch (error) {
187
+ recoverCorruptStateFile(filePath, error);
188
+ return {
189
+ version: FILE_STATE_VERSION,
190
+ values: {},
191
+ };
192
+ }
193
+ }
194
+ function loadProjectsFile(filePath) {
195
+ try {
196
+ const parsed = readJsonFile(filePath);
197
+ if (!parsed) {
198
+ return {
199
+ version: FILE_STATE_VERSION,
200
+ projects: [],
201
+ };
202
+ }
203
+ if (!isRecord(parsed) || !Array.isArray(parsed.projects)) {
204
+ throw new Error(`Invalid projects state file: ${filePath}`);
205
+ }
206
+ return {
207
+ version: FILE_STATE_VERSION,
208
+ projects: parsed.projects.map((project) => normalizeStoredProjectBinding(project)),
209
+ };
210
+ }
211
+ catch (error) {
212
+ recoverCorruptStateFile(filePath, error);
213
+ return {
214
+ version: FILE_STATE_VERSION,
215
+ projects: [],
216
+ };
217
+ }
218
+ }
219
+ function loadSessionsFile(filePath) {
220
+ try {
221
+ const parsed = readJsonFile(filePath);
222
+ if (!parsed) {
223
+ return {
224
+ version: FILE_STATE_VERSION,
225
+ sessions: [],
226
+ };
227
+ }
228
+ if (!isRecord(parsed) || !Array.isArray(parsed.sessions)) {
229
+ throw new Error(`Invalid sessions state file: ${filePath}`);
230
+ }
231
+ return {
232
+ version: FILE_STATE_VERSION,
233
+ sessions: parsed.sessions.map((session) => normalizeStoredSessionRecord(session)),
234
+ };
235
+ }
236
+ catch (error) {
237
+ recoverCorruptStateFile(filePath, error);
238
+ return {
239
+ version: FILE_STATE_VERSION,
240
+ sessions: [],
241
+ };
242
+ }
243
+ }
244
+ function normalizeStoredProjectBinding(value) {
245
+ if (!isRecord(value)) {
246
+ throw new Error("Invalid stored project binding");
247
+ }
248
+ if (typeof value.chatId !== "string" || typeof value.cwd !== "string") {
249
+ throw new Error("Stored project binding is missing required fields");
250
+ }
251
+ const cwd = path.resolve(value.cwd);
252
+ const now = new Date().toISOString();
253
+ return {
254
+ chatId: value.chatId,
255
+ cwd,
256
+ name: typeof value.name === "string" && value.name.trim() ? value.name : path.basename(cwd) || cwd,
257
+ createdAt: typeof value.createdAt === "string" ? value.createdAt : now,
258
+ updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : now,
259
+ };
260
+ }
261
+ function normalizeStoredSessionRecord(value) {
262
+ if (!isRecord(value)) {
263
+ throw new Error("Invalid stored session");
264
+ }
265
+ if (typeof value.sessionKey !== "string" ||
266
+ typeof value.chatId !== "string" ||
267
+ typeof value.cwd !== "string" ||
268
+ typeof value.model !== "string") {
269
+ throw new Error("Stored session is missing required fields");
270
+ }
271
+ const now = new Date().toISOString();
272
+ return {
273
+ sessionKey: value.sessionKey,
274
+ chatId: value.chatId,
275
+ messageThreadId: typeof value.messageThreadId === "string" ? value.messageThreadId : null,
276
+ telegramTopicName: typeof value.telegramTopicName === "string" ? value.telegramTopicName : null,
277
+ codexThreadId: typeof value.codexThreadId === "string" ? value.codexThreadId : null,
278
+ cwd: path.resolve(value.cwd),
279
+ model: value.model.trim() || "gpt-5.4",
280
+ sandboxMode: normalizeSandboxMode(value.sandboxMode),
281
+ approvalPolicy: normalizeApprovalPolicy(value.approvalPolicy),
282
+ reasoningEffort: normalizeReasoningEffort(value.reasoningEffort),
283
+ webSearchMode: normalizeWebSearchMode(value.webSearchMode),
284
+ networkAccessEnabled: normalizeBoolean(value.networkAccessEnabled, true),
285
+ skipGitRepoCheck: normalizeBoolean(value.skipGitRepoCheck, true),
286
+ additionalDirectories: normalizeStringArray(value.additionalDirectories),
287
+ outputSchema: normalizeOutputSchema(value.outputSchema),
288
+ createdAt: typeof value.createdAt === "string" ? value.createdAt : now,
289
+ updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : now,
290
+ };
291
+ }
292
+ function writeJsonFile(filePath, value) {
293
+ mkdirSync(path.dirname(filePath), { recursive: true });
294
+ const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
295
+ writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
296
+ renameSync(tempPath, filePath);
297
+ }
298
+ function readJsonFile(filePath) {
299
+ try {
300
+ return JSON.parse(readFileSync(filePath, "utf8"));
301
+ }
302
+ catch (error) {
303
+ if (isMissingFileError(error))
304
+ return null;
305
+ throw error;
306
+ }
307
+ }
308
+ function isMissingFileError(error) {
309
+ return isRecord(error) && error.code === "ENOENT";
310
+ }
311
+ function recoverCorruptStateFile(filePath, error) {
312
+ if (isMissingFileError(error) || !existsSync(filePath))
313
+ return;
314
+ const recoveredPath = `${filePath}.corrupt-${Date.now()}`;
315
+ try {
316
+ renameSync(filePath, recoveredPath);
317
+ }
318
+ catch {
319
+ // Keep startup resilient even if the corrupt file cannot be moved.
320
+ }
321
+ }
322
+ function cloneProjectBinding(project) {
323
+ return project ? { ...project } : null;
324
+ }
325
+ function cloneSessionRecord(session) {
326
+ return session
327
+ ? {
328
+ ...session,
329
+ additionalDirectories: [...session.additionalDirectories],
330
+ }
331
+ : null;
332
+ }
333
+ function normalizeSandboxMode(value) {
334
+ return typeof value === "string" && isSessionSandboxMode(value) ? value : DEFAULT_SESSION_PROFILE.sandboxMode;
335
+ }
336
+ function normalizeApprovalPolicy(value) {
337
+ return typeof value === "string" && isSessionApprovalPolicy(value) ? value : DEFAULT_SESSION_PROFILE.approvalPolicy;
338
+ }
339
+ function normalizeReasoningEffort(value) {
340
+ return typeof value === "string" && isSessionReasoningEffort(value) ? value : null;
341
+ }
342
+ function normalizeWebSearchMode(value) {
343
+ return typeof value === "string" && isSessionWebSearchMode(value) ? value : null;
344
+ }
345
+ function normalizeBoolean(value, fallback) {
346
+ if (typeof value === "boolean")
347
+ return value;
348
+ if (typeof value === "number" || typeof value === "bigint")
349
+ return Number(value) !== 0;
350
+ return fallback;
351
+ }
352
+ function normalizeStringArray(value) {
353
+ if (!Array.isArray(value))
354
+ return [];
355
+ return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
356
+ }
357
+ function normalizeOutputSchema(value) {
358
+ if (typeof value !== "string" || !value.trim())
359
+ return null;
360
+ try {
361
+ const parsed = JSON.parse(value);
362
+ return isRecord(parsed) ? JSON.stringify(parsed) : null;
363
+ }
364
+ catch {
365
+ return null;
366
+ }
367
+ }
368
+ function isRecord(value) {
369
+ return typeof value === "object" && value !== null && !Array.isArray(value);
370
+ }