@indykish/oracle 0.9.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/LICENSE +21 -0
- package/README.md +215 -0
- package/assets-oracle-icon.png +0 -0
- package/dist/bin/oracle-cli.js +1252 -0
- package/dist/bin/oracle-mcp.js +6 -0
- package/dist/scripts/agent-send.js +147 -0
- package/dist/scripts/browser-tools.js +536 -0
- package/dist/scripts/check.js +21 -0
- package/dist/scripts/debug/extract-chatgpt-response.js +53 -0
- package/dist/scripts/docs-list.js +110 -0
- package/dist/scripts/git-policy.js +125 -0
- package/dist/scripts/run-cli.js +14 -0
- package/dist/scripts/runner.js +1378 -0
- package/dist/scripts/test-browser.js +103 -0
- package/dist/scripts/test-remote-chrome.js +68 -0
- package/dist/src/bridge/connection.js +103 -0
- package/dist/src/bridge/userConfigFile.js +28 -0
- package/dist/src/browser/actions/assistantResponse.js +1067 -0
- package/dist/src/browser/actions/attachmentDataTransfer.js +138 -0
- package/dist/src/browser/actions/attachments.js +1910 -0
- package/dist/src/browser/actions/domEvents.js +19 -0
- package/dist/src/browser/actions/modelSelection.js +485 -0
- package/dist/src/browser/actions/navigation.js +445 -0
- package/dist/src/browser/actions/promptComposer.js +485 -0
- package/dist/src/browser/actions/remoteFileTransfer.js +37 -0
- package/dist/src/browser/actions/thinkingTime.js +206 -0
- package/dist/src/browser/chromeLifecycle.js +344 -0
- package/dist/src/browser/config.js +103 -0
- package/dist/src/browser/constants.js +71 -0
- package/dist/src/browser/cookies.js +191 -0
- package/dist/src/browser/detect.js +164 -0
- package/dist/src/browser/domDebug.js +36 -0
- package/dist/src/browser/index.js +1741 -0
- package/dist/src/browser/modelStrategy.js +13 -0
- package/dist/src/browser/pageActions.js +5 -0
- package/dist/src/browser/policies.js +43 -0
- package/dist/src/browser/profileState.js +280 -0
- package/dist/src/browser/prompt.js +152 -0
- package/dist/src/browser/promptSummary.js +20 -0
- package/dist/src/browser/reattach.js +186 -0
- package/dist/src/browser/reattachHelpers.js +382 -0
- package/dist/src/browser/sessionRunner.js +119 -0
- package/dist/src/browser/types.js +1 -0
- package/dist/src/browser/utils.js +122 -0
- package/dist/src/browserMode.js +1 -0
- package/dist/src/cli/bridge/claudeConfig.js +54 -0
- package/dist/src/cli/bridge/client.js +73 -0
- package/dist/src/cli/bridge/codexConfig.js +43 -0
- package/dist/src/cli/bridge/doctor.js +107 -0
- package/dist/src/cli/bridge/host.js +259 -0
- package/dist/src/cli/browserConfig.js +278 -0
- package/dist/src/cli/browserDefaults.js +81 -0
- package/dist/src/cli/bundleWarnings.js +9 -0
- package/dist/src/cli/clipboard.js +10 -0
- package/dist/src/cli/detach.js +11 -0
- package/dist/src/cli/dryRun.js +105 -0
- package/dist/src/cli/duplicatePromptGuard.js +14 -0
- package/dist/src/cli/engine.js +41 -0
- package/dist/src/cli/errorUtils.js +9 -0
- package/dist/src/cli/format.js +13 -0
- package/dist/src/cli/help.js +77 -0
- package/dist/src/cli/hiddenAliases.js +22 -0
- package/dist/src/cli/markdownBundle.js +17 -0
- package/dist/src/cli/markdownRenderer.js +97 -0
- package/dist/src/cli/notifier.js +306 -0
- package/dist/src/cli/options.js +281 -0
- package/dist/src/cli/oscUtils.js +2 -0
- package/dist/src/cli/promptRequirement.js +17 -0
- package/dist/src/cli/renderFlags.js +9 -0
- package/dist/src/cli/renderOutput.js +26 -0
- package/dist/src/cli/rootAlias.js +30 -0
- package/dist/src/cli/runOptions.js +78 -0
- package/dist/src/cli/sessionCommand.js +111 -0
- package/dist/src/cli/sessionDisplay.js +567 -0
- package/dist/src/cli/sessionRunner.js +602 -0
- package/dist/src/cli/sessionTable.js +92 -0
- package/dist/src/cli/tagline.js +258 -0
- package/dist/src/cli/tui/index.js +486 -0
- package/dist/src/cli/writeOutputPath.js +21 -0
- package/dist/src/config.js +26 -0
- package/dist/src/gemini-web/client.js +328 -0
- package/dist/src/gemini-web/executor.js +285 -0
- package/dist/src/gemini-web/index.js +1 -0
- package/dist/src/gemini-web/types.js +1 -0
- package/dist/src/heartbeat.js +43 -0
- package/dist/src/mcp/server.js +40 -0
- package/dist/src/mcp/tools/consult.js +290 -0
- package/dist/src/mcp/tools/sessionResources.js +75 -0
- package/dist/src/mcp/tools/sessions.js +105 -0
- package/dist/src/mcp/types.js +22 -0
- package/dist/src/mcp/utils.js +37 -0
- package/dist/src/oracle/background.js +141 -0
- package/dist/src/oracle/claude.js +101 -0
- package/dist/src/oracle/client.js +197 -0
- package/dist/src/oracle/config.js +227 -0
- package/dist/src/oracle/errors.js +132 -0
- package/dist/src/oracle/files.js +378 -0
- package/dist/src/oracle/finishLine.js +32 -0
- package/dist/src/oracle/format.js +30 -0
- package/dist/src/oracle/fsAdapter.js +10 -0
- package/dist/src/oracle/gemini.js +195 -0
- package/dist/src/oracle/logging.js +36 -0
- package/dist/src/oracle/markdown.js +46 -0
- package/dist/src/oracle/modelResolver.js +183 -0
- package/dist/src/oracle/multiModelRunner.js +153 -0
- package/dist/src/oracle/oscProgress.js +24 -0
- package/dist/src/oracle/promptAssembly.js +13 -0
- package/dist/src/oracle/request.js +50 -0
- package/dist/src/oracle/run.js +596 -0
- package/dist/src/oracle/runUtils.js +31 -0
- package/dist/src/oracle/tokenEstimate.js +37 -0
- package/dist/src/oracle/tokenStats.js +39 -0
- package/dist/src/oracle/tokenStringifier.js +24 -0
- package/dist/src/oracle/types.js +1 -0
- package/dist/src/oracle.js +12 -0
- package/dist/src/oracleHome.js +13 -0
- package/dist/src/remote/client.js +129 -0
- package/dist/src/remote/health.js +113 -0
- package/dist/src/remote/remoteServiceConfig.js +31 -0
- package/dist/src/remote/server.js +533 -0
- package/dist/src/remote/types.js +1 -0
- package/dist/src/sessionManager.js +637 -0
- package/dist/src/sessionStore.js +56 -0
- package/dist/src/version.js +39 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
- package/dist/vendor/oracle-notifier/README.md +24 -0
- package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
- package/package.json +115 -0
- package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
- package/vendor/oracle-notifier/README.md +24 -0
- package/vendor/oracle-notifier/build-notifier.sh +93 -0
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import { createWriteStream } from 'node:fs';
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
import { DEFAULT_MODEL, formatElapsed } from './oracle.js';
|
|
6
|
+
import { safeModelSlug } from './oracle/modelResolver.js';
|
|
7
|
+
import { getOracleHomeDir } from './oracleHome.js';
|
|
8
|
+
export function getSessionsDir() {
|
|
9
|
+
return path.join(getOracleHomeDir(), 'sessions');
|
|
10
|
+
}
|
|
11
|
+
const METADATA_FILENAME = 'meta.json';
|
|
12
|
+
const LEGACY_SESSION_FILENAME = 'session.json';
|
|
13
|
+
const LEGACY_REQUEST_FILENAME = 'request.json';
|
|
14
|
+
const MODELS_DIRNAME = 'models';
|
|
15
|
+
const MODEL_JSON_EXTENSION = '.json';
|
|
16
|
+
const MODEL_LOG_EXTENSION = '.log';
|
|
17
|
+
const MAX_STATUS_LIMIT = 1000;
|
|
18
|
+
const ZOMBIE_MAX_AGE_MS = 60 * 60 * 1000; // 60 minutes
|
|
19
|
+
const CHROME_RUNTIME_TIMEOUT_MS = 250;
|
|
20
|
+
const DEFAULT_SLUG = 'session';
|
|
21
|
+
const MAX_SLUG_WORDS = 5;
|
|
22
|
+
const MIN_CUSTOM_SLUG_WORDS = 3;
|
|
23
|
+
const MAX_SLUG_WORD_LENGTH = 10;
|
|
24
|
+
async function ensureDir(dirPath) {
|
|
25
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
export async function ensureSessionStorage() {
|
|
28
|
+
await ensureDir(getSessionsDir());
|
|
29
|
+
}
|
|
30
|
+
function slugify(text, maxWords = MAX_SLUG_WORDS) {
|
|
31
|
+
const normalized = text?.toLowerCase() ?? '';
|
|
32
|
+
const words = normalized.match(/[a-z0-9]+/g) ?? [];
|
|
33
|
+
const trimmed = words
|
|
34
|
+
.slice(0, maxWords)
|
|
35
|
+
.map((word) => word.slice(0, MAX_SLUG_WORD_LENGTH));
|
|
36
|
+
return trimmed.length > 0 ? trimmed.join('-') : DEFAULT_SLUG;
|
|
37
|
+
}
|
|
38
|
+
function countSlugWords(slug) {
|
|
39
|
+
return slug.split('-').filter(Boolean).length;
|
|
40
|
+
}
|
|
41
|
+
function normalizeCustomSlug(candidate) {
|
|
42
|
+
const slug = slugify(candidate, MAX_SLUG_WORDS);
|
|
43
|
+
const wordCount = countSlugWords(slug);
|
|
44
|
+
if (wordCount < MIN_CUSTOM_SLUG_WORDS || wordCount > MAX_SLUG_WORDS) {
|
|
45
|
+
throw new Error(`Custom slug must include between ${MIN_CUSTOM_SLUG_WORDS} and ${MAX_SLUG_WORDS} words.`);
|
|
46
|
+
}
|
|
47
|
+
return slug;
|
|
48
|
+
}
|
|
49
|
+
export function createSessionId(prompt, customSlug) {
|
|
50
|
+
if (customSlug) {
|
|
51
|
+
return normalizeCustomSlug(customSlug);
|
|
52
|
+
}
|
|
53
|
+
return slugify(prompt);
|
|
54
|
+
}
|
|
55
|
+
function sessionDir(id) {
|
|
56
|
+
return path.join(getSessionsDir(), id);
|
|
57
|
+
}
|
|
58
|
+
function metaPath(id) {
|
|
59
|
+
return path.join(sessionDir(id), METADATA_FILENAME);
|
|
60
|
+
}
|
|
61
|
+
function requestPath(id) {
|
|
62
|
+
return path.join(sessionDir(id), LEGACY_REQUEST_FILENAME);
|
|
63
|
+
}
|
|
64
|
+
function legacySessionPath(id) {
|
|
65
|
+
return path.join(sessionDir(id), LEGACY_SESSION_FILENAME);
|
|
66
|
+
}
|
|
67
|
+
function logPath(id) {
|
|
68
|
+
return path.join(sessionDir(id), 'output.log');
|
|
69
|
+
}
|
|
70
|
+
function modelsDir(id) {
|
|
71
|
+
return path.join(sessionDir(id), MODELS_DIRNAME);
|
|
72
|
+
}
|
|
73
|
+
function modelJsonPath(id, model) {
|
|
74
|
+
const slug = safeModelSlug(model);
|
|
75
|
+
return path.join(modelsDir(id), `${slug}${MODEL_JSON_EXTENSION}`);
|
|
76
|
+
}
|
|
77
|
+
function modelLogPath(id, model) {
|
|
78
|
+
const slug = safeModelSlug(model);
|
|
79
|
+
return path.join(modelsDir(id), `${slug}${MODEL_LOG_EXTENSION}`);
|
|
80
|
+
}
|
|
81
|
+
async function fileExists(targetPath) {
|
|
82
|
+
try {
|
|
83
|
+
await fs.access(targetPath);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function ensureUniqueSessionId(baseSlug) {
|
|
91
|
+
let candidate = baseSlug;
|
|
92
|
+
let suffix = 2;
|
|
93
|
+
while (await fileExists(sessionDir(candidate))) {
|
|
94
|
+
candidate = `${baseSlug}-${suffix}`;
|
|
95
|
+
suffix += 1;
|
|
96
|
+
}
|
|
97
|
+
return candidate;
|
|
98
|
+
}
|
|
99
|
+
async function listModelRunFiles(sessionId) {
|
|
100
|
+
const dir = modelsDir(sessionId);
|
|
101
|
+
const entries = await fs.readdir(dir).catch(() => []);
|
|
102
|
+
const result = [];
|
|
103
|
+
for (const entry of entries) {
|
|
104
|
+
if (!entry.endsWith(MODEL_JSON_EXTENSION)) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const jsonPath = path.join(dir, entry);
|
|
108
|
+
try {
|
|
109
|
+
const raw = await fs.readFile(jsonPath, 'utf8');
|
|
110
|
+
const parsed = JSON.parse(raw);
|
|
111
|
+
const normalized = ensureModelLogReference(sessionId, parsed);
|
|
112
|
+
result.push(normalized);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// ignore malformed model files
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
function ensureModelLogReference(sessionId, record) {
|
|
121
|
+
const logPathRelative = record.log?.path ?? path.relative(sessionDir(sessionId), modelLogPath(sessionId, record.model));
|
|
122
|
+
return {
|
|
123
|
+
...record,
|
|
124
|
+
log: { path: logPathRelative, bytes: record.log?.bytes },
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async function readModelRunFile(sessionId, model) {
|
|
128
|
+
try {
|
|
129
|
+
const raw = await fs.readFile(modelJsonPath(sessionId, model), 'utf8');
|
|
130
|
+
const parsed = JSON.parse(raw);
|
|
131
|
+
return ensureModelLogReference(sessionId, parsed);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export async function updateModelRunMetadata(sessionId, model, updates) {
|
|
138
|
+
await ensureDir(modelsDir(sessionId));
|
|
139
|
+
const existing = (await readModelRunFile(sessionId, model)) ?? {
|
|
140
|
+
model,
|
|
141
|
+
status: 'pending',
|
|
142
|
+
};
|
|
143
|
+
const next = ensureModelLogReference(sessionId, {
|
|
144
|
+
...existing,
|
|
145
|
+
...updates,
|
|
146
|
+
model,
|
|
147
|
+
});
|
|
148
|
+
await fs.writeFile(modelJsonPath(sessionId, model), JSON.stringify(next, null, 2), 'utf8');
|
|
149
|
+
return next;
|
|
150
|
+
}
|
|
151
|
+
export async function readModelRunMetadata(sessionId, model) {
|
|
152
|
+
return readModelRunFile(sessionId, model);
|
|
153
|
+
}
|
|
154
|
+
export async function initializeSession(options, cwd, notifications, baseSlugOverride) {
|
|
155
|
+
await ensureSessionStorage();
|
|
156
|
+
const baseSlug = baseSlugOverride || createSessionId(options.prompt || DEFAULT_SLUG, options.slug);
|
|
157
|
+
const sessionId = await ensureUniqueSessionId(baseSlug);
|
|
158
|
+
const dir = sessionDir(sessionId);
|
|
159
|
+
await ensureDir(dir);
|
|
160
|
+
const mode = options.mode ?? 'api';
|
|
161
|
+
const browserConfig = options.browserConfig;
|
|
162
|
+
const modelList = Array.isArray(options.models) && options.models.length > 0
|
|
163
|
+
? options.models
|
|
164
|
+
: options.model
|
|
165
|
+
? [options.model]
|
|
166
|
+
: [];
|
|
167
|
+
const metadata = {
|
|
168
|
+
id: sessionId,
|
|
169
|
+
createdAt: new Date().toISOString(),
|
|
170
|
+
status: 'pending',
|
|
171
|
+
promptPreview: (options.prompt || '').slice(0, 160),
|
|
172
|
+
model: modelList[0] ?? options.model,
|
|
173
|
+
models: modelList.map((modelName) => ({
|
|
174
|
+
model: modelName,
|
|
175
|
+
status: 'pending',
|
|
176
|
+
})),
|
|
177
|
+
cwd,
|
|
178
|
+
mode,
|
|
179
|
+
browser: browserConfig ? { config: browserConfig } : undefined,
|
|
180
|
+
notifications,
|
|
181
|
+
options: {
|
|
182
|
+
prompt: options.prompt,
|
|
183
|
+
file: options.file ?? [],
|
|
184
|
+
model: options.model,
|
|
185
|
+
models: modelList,
|
|
186
|
+
effectiveModelId: options.effectiveModelId,
|
|
187
|
+
maxInput: options.maxInput,
|
|
188
|
+
system: options.system,
|
|
189
|
+
maxOutput: options.maxOutput,
|
|
190
|
+
silent: options.silent,
|
|
191
|
+
filesReport: options.filesReport,
|
|
192
|
+
slug: sessionId,
|
|
193
|
+
mode,
|
|
194
|
+
browserConfig,
|
|
195
|
+
verbose: options.verbose,
|
|
196
|
+
heartbeatIntervalMs: options.heartbeatIntervalMs,
|
|
197
|
+
browserAttachments: options.browserAttachments,
|
|
198
|
+
browserInlineFiles: options.browserInlineFiles,
|
|
199
|
+
browserBundleFiles: options.browserBundleFiles,
|
|
200
|
+
background: options.background,
|
|
201
|
+
search: options.search,
|
|
202
|
+
baseUrl: options.baseUrl,
|
|
203
|
+
azure: options.azure,
|
|
204
|
+
timeoutSeconds: options.timeoutSeconds,
|
|
205
|
+
httpTimeoutMs: options.httpTimeoutMs,
|
|
206
|
+
zombieTimeoutMs: options.zombieTimeoutMs,
|
|
207
|
+
zombieUseLastActivity: options.zombieUseLastActivity,
|
|
208
|
+
writeOutputPath: options.writeOutputPath,
|
|
209
|
+
waitPreference: options.waitPreference,
|
|
210
|
+
youtube: options.youtube,
|
|
211
|
+
generateImage: options.generateImage,
|
|
212
|
+
editImage: options.editImage,
|
|
213
|
+
outputPath: options.outputPath,
|
|
214
|
+
aspectRatio: options.aspectRatio,
|
|
215
|
+
geminiShowThoughts: options.geminiShowThoughts,
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
await ensureDir(modelsDir(sessionId));
|
|
219
|
+
await fs.writeFile(metaPath(sessionId), JSON.stringify(metadata, null, 2), 'utf8');
|
|
220
|
+
await Promise.all((modelList.length > 0 ? modelList : [metadata.model ?? DEFAULT_MODEL]).map(async (modelName) => {
|
|
221
|
+
const jsonPath = modelJsonPath(sessionId, modelName);
|
|
222
|
+
const logFilePath = modelLogPath(sessionId, modelName);
|
|
223
|
+
const modelRecord = {
|
|
224
|
+
model: modelName,
|
|
225
|
+
status: 'pending',
|
|
226
|
+
log: { path: path.relative(sessionDir(sessionId), logFilePath) },
|
|
227
|
+
};
|
|
228
|
+
await fs.writeFile(jsonPath, JSON.stringify(modelRecord, null, 2), 'utf8');
|
|
229
|
+
await fs.writeFile(logFilePath, '', 'utf8');
|
|
230
|
+
}));
|
|
231
|
+
await fs.writeFile(logPath(sessionId), '', 'utf8');
|
|
232
|
+
return metadata;
|
|
233
|
+
}
|
|
234
|
+
export async function readSessionMetadata(sessionId) {
|
|
235
|
+
const modern = await readModernSessionMetadata(sessionId);
|
|
236
|
+
if (modern) {
|
|
237
|
+
return modern;
|
|
238
|
+
}
|
|
239
|
+
const legacy = await readLegacySessionMetadata(sessionId);
|
|
240
|
+
if (legacy) {
|
|
241
|
+
return legacy;
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
export async function updateSessionMetadata(sessionId, updates) {
|
|
246
|
+
const existing = (await readModernSessionMetadata(sessionId)) ??
|
|
247
|
+
(await readLegacySessionMetadata(sessionId)) ??
|
|
248
|
+
{ id: sessionId };
|
|
249
|
+
const next = { ...existing, ...updates };
|
|
250
|
+
await fs.writeFile(metaPath(sessionId), JSON.stringify(next, null, 2), 'utf8');
|
|
251
|
+
return next;
|
|
252
|
+
}
|
|
253
|
+
async function readModernSessionMetadata(sessionId) {
|
|
254
|
+
try {
|
|
255
|
+
const raw = await fs.readFile(metaPath(sessionId), 'utf8');
|
|
256
|
+
const parsed = JSON.parse(raw);
|
|
257
|
+
if (!isSessionMetadataRecord(parsed)) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
const enriched = await attachModelRuns(parsed, sessionId);
|
|
261
|
+
const runtimeChecked = await markDeadBrowser(enriched, { persist: false });
|
|
262
|
+
return await markZombie(runtimeChecked, { persist: false });
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async function readLegacySessionMetadata(sessionId) {
|
|
269
|
+
try {
|
|
270
|
+
const raw = await fs.readFile(legacySessionPath(sessionId), 'utf8');
|
|
271
|
+
const parsed = JSON.parse(raw);
|
|
272
|
+
const enriched = await attachModelRuns(parsed, sessionId);
|
|
273
|
+
const runtimeChecked = await markDeadBrowser(enriched, { persist: false });
|
|
274
|
+
return await markZombie(runtimeChecked, { persist: false });
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function isSessionMetadataRecord(value) {
|
|
281
|
+
return Boolean(value && typeof value.id === 'string' && value.status);
|
|
282
|
+
}
|
|
283
|
+
async function attachModelRuns(meta, sessionId) {
|
|
284
|
+
const runs = await listModelRunFiles(sessionId);
|
|
285
|
+
if (runs.length === 0) {
|
|
286
|
+
return meta;
|
|
287
|
+
}
|
|
288
|
+
return { ...meta, models: runs };
|
|
289
|
+
}
|
|
290
|
+
export function createSessionLogWriter(sessionId, model) {
|
|
291
|
+
const targetPath = model ? modelLogPath(sessionId, model) : logPath(sessionId);
|
|
292
|
+
if (model) {
|
|
293
|
+
void ensureDir(modelsDir(sessionId));
|
|
294
|
+
}
|
|
295
|
+
const stream = createWriteStream(targetPath, { flags: 'a' });
|
|
296
|
+
const logLine = (line = '') => {
|
|
297
|
+
stream.write(`${line}\n`);
|
|
298
|
+
};
|
|
299
|
+
const writeChunk = (chunk) => {
|
|
300
|
+
stream.write(chunk);
|
|
301
|
+
return true;
|
|
302
|
+
};
|
|
303
|
+
return { stream, logLine, writeChunk, logPath: targetPath };
|
|
304
|
+
}
|
|
305
|
+
export async function listSessionsMetadata() {
|
|
306
|
+
await ensureSessionStorage();
|
|
307
|
+
const entries = await fs.readdir(getSessionsDir()).catch(() => []);
|
|
308
|
+
const metas = [];
|
|
309
|
+
for (const entry of entries) {
|
|
310
|
+
let meta = await readSessionMetadata(entry);
|
|
311
|
+
if (meta) {
|
|
312
|
+
meta = await markDeadBrowser(meta, { persist: true });
|
|
313
|
+
meta = await markZombie(meta, { persist: true }); // keep stored metadata consistent with zombie detection
|
|
314
|
+
metas.push(meta);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return metas.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
318
|
+
}
|
|
319
|
+
export function filterSessionsByRange(metas, { hours = 24, includeAll = false, limit = 100 }) {
|
|
320
|
+
const maxLimit = Math.min(limit, MAX_STATUS_LIMIT);
|
|
321
|
+
let filtered = metas;
|
|
322
|
+
if (!includeAll) {
|
|
323
|
+
const cutoff = Date.now() - hours * 60 * 60 * 1000;
|
|
324
|
+
filtered = metas.filter((meta) => new Date(meta.createdAt).getTime() >= cutoff);
|
|
325
|
+
}
|
|
326
|
+
const limited = filtered.slice(0, maxLimit);
|
|
327
|
+
const truncated = filtered.length > maxLimit;
|
|
328
|
+
return { entries: limited, truncated, total: filtered.length };
|
|
329
|
+
}
|
|
330
|
+
export async function readSessionLog(sessionId) {
|
|
331
|
+
const runs = await listModelRunFiles(sessionId);
|
|
332
|
+
if (runs.length === 0) {
|
|
333
|
+
try {
|
|
334
|
+
return await fs.readFile(logPath(sessionId), 'utf8');
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
return '';
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const sections = [];
|
|
341
|
+
let hasContent = false;
|
|
342
|
+
const ordered = runs
|
|
343
|
+
.slice()
|
|
344
|
+
.sort((a, b) => (a.startedAt && b.startedAt ? a.startedAt.localeCompare(b.startedAt) : a.model.localeCompare(b.model)));
|
|
345
|
+
for (const run of ordered) {
|
|
346
|
+
const logFile = run.log?.path
|
|
347
|
+
? path.isAbsolute(run.log.path)
|
|
348
|
+
? run.log.path
|
|
349
|
+
: path.join(sessionDir(sessionId), run.log.path)
|
|
350
|
+
: modelLogPath(sessionId, run.model);
|
|
351
|
+
let body = '';
|
|
352
|
+
try {
|
|
353
|
+
body = await fs.readFile(logFile, 'utf8');
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
body = '';
|
|
357
|
+
}
|
|
358
|
+
if (body.length > 0) {
|
|
359
|
+
hasContent = true;
|
|
360
|
+
}
|
|
361
|
+
sections.push(`=== ${run.model} ===\n${body}`.trimEnd());
|
|
362
|
+
}
|
|
363
|
+
if (!hasContent) {
|
|
364
|
+
try {
|
|
365
|
+
return await fs.readFile(logPath(sessionId), 'utf8');
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
// ignore and return structured header-only log
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return sections.join('\n\n');
|
|
372
|
+
}
|
|
373
|
+
export async function readModelLog(sessionId, model) {
|
|
374
|
+
try {
|
|
375
|
+
return await fs.readFile(modelLogPath(sessionId, model), 'utf8');
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
return '';
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
export async function readSessionRequest(sessionId) {
|
|
382
|
+
const modern = await readModernSessionMetadata(sessionId);
|
|
383
|
+
if (modern?.options) {
|
|
384
|
+
return modern.options;
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
const raw = await fs.readFile(requestPath(sessionId), 'utf8');
|
|
388
|
+
const parsed = JSON.parse(raw);
|
|
389
|
+
if (isSessionMetadataRecord(parsed)) {
|
|
390
|
+
return parsed.options ?? null;
|
|
391
|
+
}
|
|
392
|
+
return parsed;
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
export async function deleteSessionsOlderThan({ hours = 24, includeAll = false, } = {}) {
|
|
399
|
+
await ensureSessionStorage();
|
|
400
|
+
const entries = await fs.readdir(getSessionsDir()).catch(() => []);
|
|
401
|
+
if (!entries.length) {
|
|
402
|
+
return { deleted: 0, remaining: 0 };
|
|
403
|
+
}
|
|
404
|
+
const cutoff = includeAll ? Number.NEGATIVE_INFINITY : Date.now() - hours * 60 * 60 * 1000;
|
|
405
|
+
let deleted = 0;
|
|
406
|
+
for (const entry of entries) {
|
|
407
|
+
const dir = sessionDir(entry);
|
|
408
|
+
let createdMs;
|
|
409
|
+
const meta = await readSessionMetadata(entry);
|
|
410
|
+
if (meta?.createdAt) {
|
|
411
|
+
const parsed = Date.parse(meta.createdAt);
|
|
412
|
+
if (!Number.isNaN(parsed)) {
|
|
413
|
+
createdMs = parsed;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (createdMs == null) {
|
|
417
|
+
try {
|
|
418
|
+
const stats = await fs.stat(dir);
|
|
419
|
+
createdMs = stats.birthtimeMs || stats.mtimeMs;
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (includeAll || (createdMs != null && createdMs < cutoff)) {
|
|
426
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
427
|
+
deleted += 1;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const remaining = Math.max(entries.length - deleted, 0);
|
|
431
|
+
return { deleted, remaining };
|
|
432
|
+
}
|
|
433
|
+
export async function wait(ms) {
|
|
434
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
435
|
+
}
|
|
436
|
+
export { MAX_STATUS_LIMIT };
|
|
437
|
+
export { ZOMBIE_MAX_AGE_MS };
|
|
438
|
+
export async function getSessionPaths(sessionId) {
|
|
439
|
+
const dir = sessionDir(sessionId);
|
|
440
|
+
const metadata = metaPath(sessionId);
|
|
441
|
+
const log = logPath(sessionId);
|
|
442
|
+
const request = requestPath(sessionId);
|
|
443
|
+
const required = [metadata, log];
|
|
444
|
+
const missing = [];
|
|
445
|
+
for (const file of required) {
|
|
446
|
+
if (!(await fileExists(file))) {
|
|
447
|
+
missing.push(path.basename(file));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (missing.length > 0) {
|
|
451
|
+
throw new Error(`Session "${sessionId}" is missing: ${missing.join(', ')}`);
|
|
452
|
+
}
|
|
453
|
+
return { dir, metadata, log, request };
|
|
454
|
+
}
|
|
455
|
+
async function markZombie(meta, { persist }) {
|
|
456
|
+
if (!(await isZombie(meta))) {
|
|
457
|
+
return meta;
|
|
458
|
+
}
|
|
459
|
+
if (meta.mode === 'browser') {
|
|
460
|
+
const runtime = meta.browser?.runtime;
|
|
461
|
+
if (runtime) {
|
|
462
|
+
const signals = [];
|
|
463
|
+
if (runtime.chromePid) {
|
|
464
|
+
signals.push(isProcessAlive(runtime.chromePid));
|
|
465
|
+
}
|
|
466
|
+
if (runtime.chromePort) {
|
|
467
|
+
const host = runtime.chromeHost ?? '127.0.0.1';
|
|
468
|
+
signals.push(await isPortOpen(host, runtime.chromePort));
|
|
469
|
+
}
|
|
470
|
+
if (signals.some(Boolean)) {
|
|
471
|
+
return meta;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const maxAgeMs = resolveZombieMaxAgeMs(meta);
|
|
476
|
+
const updated = {
|
|
477
|
+
...meta,
|
|
478
|
+
status: 'error',
|
|
479
|
+
errorMessage: `Session marked as zombie (> ${formatElapsed(maxAgeMs)} stale)`,
|
|
480
|
+
completedAt: new Date().toISOString(),
|
|
481
|
+
};
|
|
482
|
+
if (persist) {
|
|
483
|
+
await fs.writeFile(metaPath(meta.id), JSON.stringify(updated, null, 2), 'utf8');
|
|
484
|
+
}
|
|
485
|
+
return updated;
|
|
486
|
+
}
|
|
487
|
+
async function markDeadBrowser(meta, { persist }) {
|
|
488
|
+
if (meta.status !== 'running' || meta.mode !== 'browser') {
|
|
489
|
+
return meta;
|
|
490
|
+
}
|
|
491
|
+
const runtime = meta.browser?.runtime;
|
|
492
|
+
if (!runtime) {
|
|
493
|
+
return meta;
|
|
494
|
+
}
|
|
495
|
+
const signals = [];
|
|
496
|
+
if (runtime.chromePid) {
|
|
497
|
+
signals.push(isProcessAlive(runtime.chromePid));
|
|
498
|
+
}
|
|
499
|
+
if (runtime.chromePort) {
|
|
500
|
+
const host = runtime.chromeHost ?? '127.0.0.1';
|
|
501
|
+
signals.push(await isPortOpen(host, runtime.chromePort));
|
|
502
|
+
}
|
|
503
|
+
if (signals.length === 0 || signals.some(Boolean)) {
|
|
504
|
+
return meta;
|
|
505
|
+
}
|
|
506
|
+
const response = meta.response
|
|
507
|
+
? {
|
|
508
|
+
...meta.response,
|
|
509
|
+
status: 'error',
|
|
510
|
+
incompleteReason: meta.response.incompleteReason ?? 'chrome-disconnected',
|
|
511
|
+
}
|
|
512
|
+
: { status: 'error', incompleteReason: 'chrome-disconnected' };
|
|
513
|
+
const updated = {
|
|
514
|
+
...meta,
|
|
515
|
+
status: 'error',
|
|
516
|
+
errorMessage: 'Browser session ended (Chrome is no longer reachable)',
|
|
517
|
+
completedAt: new Date().toISOString(),
|
|
518
|
+
response,
|
|
519
|
+
};
|
|
520
|
+
if (persist) {
|
|
521
|
+
await fs.writeFile(metaPath(meta.id), JSON.stringify(updated, null, 2), 'utf8');
|
|
522
|
+
}
|
|
523
|
+
return updated;
|
|
524
|
+
}
|
|
525
|
+
async function isZombie(meta) {
|
|
526
|
+
if (meta.status !== 'running') {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
const reference = meta.startedAt ?? meta.createdAt;
|
|
530
|
+
if (!reference) {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
const startedMs = Date.parse(reference);
|
|
534
|
+
if (Number.isNaN(startedMs)) {
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
const useLastActivity = meta.options?.zombieUseLastActivity === true;
|
|
538
|
+
const lastActivityMs = useLastActivity ? await getLastActivityMs(meta) : null;
|
|
539
|
+
const anchorMs = lastActivityMs ?? startedMs;
|
|
540
|
+
const maxAgeMs = resolveZombieMaxAgeMs(meta);
|
|
541
|
+
return Date.now() - anchorMs > maxAgeMs;
|
|
542
|
+
}
|
|
543
|
+
function resolveZombieMaxAgeMs(meta) {
|
|
544
|
+
const explicit = meta.options?.zombieTimeoutMs;
|
|
545
|
+
const hasExplicit = typeof explicit === 'number' && Number.isFinite(explicit) && explicit > 0;
|
|
546
|
+
let maxAgeMs = hasExplicit ? explicit : ZOMBIE_MAX_AGE_MS;
|
|
547
|
+
if (!hasExplicit) {
|
|
548
|
+
const timeoutSeconds = meta.options?.timeoutSeconds;
|
|
549
|
+
if (typeof timeoutSeconds === 'number' && Number.isFinite(timeoutSeconds) && timeoutSeconds > 0) {
|
|
550
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
551
|
+
if (timeoutMs > maxAgeMs) {
|
|
552
|
+
maxAgeMs = timeoutMs;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return maxAgeMs;
|
|
557
|
+
}
|
|
558
|
+
async function getLastActivityMs(meta) {
|
|
559
|
+
const candidates = new Set();
|
|
560
|
+
candidates.add(logPath(meta.id));
|
|
561
|
+
const modelNames = new Set();
|
|
562
|
+
if (typeof meta.model === 'string' && meta.model.length > 0) {
|
|
563
|
+
modelNames.add(meta.model);
|
|
564
|
+
}
|
|
565
|
+
if (Array.isArray(meta.models)) {
|
|
566
|
+
for (const entry of meta.models) {
|
|
567
|
+
if (entry?.model) {
|
|
568
|
+
modelNames.add(entry.model);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
for (const modelName of modelNames) {
|
|
573
|
+
candidates.add(modelLogPath(meta.id, modelName));
|
|
574
|
+
}
|
|
575
|
+
let latest = 0;
|
|
576
|
+
let sawStat = false;
|
|
577
|
+
for (const candidate of candidates) {
|
|
578
|
+
try {
|
|
579
|
+
const stats = await fs.stat(candidate);
|
|
580
|
+
const mtimeMs = Number.isFinite(stats.mtimeMs) ? stats.mtimeMs : stats.mtime.getTime();
|
|
581
|
+
if (Number.isFinite(mtimeMs)) {
|
|
582
|
+
latest = Math.max(latest, mtimeMs);
|
|
583
|
+
sawStat = true;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
// ignore missing logs; fallback to startedAt
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return sawStat ? latest : null;
|
|
591
|
+
}
|
|
592
|
+
function isProcessAlive(pid) {
|
|
593
|
+
if (!pid)
|
|
594
|
+
return false;
|
|
595
|
+
try {
|
|
596
|
+
process.kill(pid, 0);
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
const code = error instanceof Error ? error.code : undefined;
|
|
601
|
+
if (code === 'ESRCH' || code === 'EINVAL') {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
if (code === 'EPERM') {
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
return true;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
async function isPortOpen(host, port) {
|
|
611
|
+
if (!port || port <= 0 || port > 65535) {
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
return new Promise((resolve) => {
|
|
615
|
+
const socket = net.createConnection({ host, port });
|
|
616
|
+
let settled = false;
|
|
617
|
+
const cleanup = (result) => {
|
|
618
|
+
if (settled)
|
|
619
|
+
return;
|
|
620
|
+
settled = true;
|
|
621
|
+
socket.removeAllListeners();
|
|
622
|
+
socket.end();
|
|
623
|
+
socket.destroy();
|
|
624
|
+
socket.unref();
|
|
625
|
+
resolve(result);
|
|
626
|
+
};
|
|
627
|
+
const timer = setTimeout(() => cleanup(false), CHROME_RUNTIME_TIMEOUT_MS);
|
|
628
|
+
socket.once('connect', () => {
|
|
629
|
+
clearTimeout(timer);
|
|
630
|
+
cleanup(true);
|
|
631
|
+
});
|
|
632
|
+
socket.once('error', () => {
|
|
633
|
+
clearTimeout(timer);
|
|
634
|
+
cleanup(false);
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ensureSessionStorage, initializeSession, readSessionMetadata, updateSessionMetadata, createSessionLogWriter, readSessionLog, readModelLog, readSessionRequest, listSessionsMetadata, filterSessionsByRange, deleteSessionsOlderThan, updateModelRunMetadata, getSessionPaths, getSessionsDir, } from './sessionManager.js';
|
|
2
|
+
class FileSessionStore {
|
|
3
|
+
ensureStorage() {
|
|
4
|
+
return ensureSessionStorage();
|
|
5
|
+
}
|
|
6
|
+
createSession(options, cwd, notifications, baseSlugOverride) {
|
|
7
|
+
return initializeSession(options, cwd, notifications, baseSlugOverride);
|
|
8
|
+
}
|
|
9
|
+
readSession(sessionId) {
|
|
10
|
+
return readSessionMetadata(sessionId);
|
|
11
|
+
}
|
|
12
|
+
updateSession(sessionId, updates) {
|
|
13
|
+
return updateSessionMetadata(sessionId, updates);
|
|
14
|
+
}
|
|
15
|
+
createLogWriter(sessionId, model) {
|
|
16
|
+
return createSessionLogWriter(sessionId, model);
|
|
17
|
+
}
|
|
18
|
+
updateModelRun(sessionId, model, updates) {
|
|
19
|
+
return updateModelRunMetadata(sessionId, model, updates);
|
|
20
|
+
}
|
|
21
|
+
readLog(sessionId) {
|
|
22
|
+
return readSessionLog(sessionId);
|
|
23
|
+
}
|
|
24
|
+
readModelLog(sessionId, model) {
|
|
25
|
+
return readModelLog(sessionId, model);
|
|
26
|
+
}
|
|
27
|
+
readRequest(sessionId) {
|
|
28
|
+
return readSessionRequest(sessionId);
|
|
29
|
+
}
|
|
30
|
+
listSessions() {
|
|
31
|
+
return listSessionsMetadata();
|
|
32
|
+
}
|
|
33
|
+
filterSessions(metas, options) {
|
|
34
|
+
return filterSessionsByRange(metas, options);
|
|
35
|
+
}
|
|
36
|
+
deleteOlderThan(options) {
|
|
37
|
+
return deleteSessionsOlderThan(options);
|
|
38
|
+
}
|
|
39
|
+
getPaths(sessionId) {
|
|
40
|
+
return getSessionPaths(sessionId);
|
|
41
|
+
}
|
|
42
|
+
sessionsDir() {
|
|
43
|
+
return getSessionsDir();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export const sessionStore = new FileSessionStore();
|
|
47
|
+
export { wait } from './sessionManager.js';
|
|
48
|
+
export async function pruneOldSessions(hours, log) {
|
|
49
|
+
if (typeof hours !== 'number' || Number.isNaN(hours) || hours <= 0) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const result = await sessionStore.deleteOlderThan({ hours });
|
|
53
|
+
if (result.deleted > 0) {
|
|
54
|
+
log?.(`Pruned ${result.deleted} stored sessions older than ${hours}h.`);
|
|
55
|
+
}
|
|
56
|
+
}
|