@steipete/oracle 0.4.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.
Files changed (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/assets-oracle-icon.png +0 -0
  4. package/dist/bin/oracle-cli.js +954 -0
  5. package/dist/bin/oracle-mcp.js +6 -0
  6. package/dist/bin/oracle.js +683 -0
  7. package/dist/markdansi/types/index.js +4 -0
  8. package/dist/oracle/bin/oracle-cli.js +472 -0
  9. package/dist/oracle/src/browser/actions/assistantResponse.js +471 -0
  10. package/dist/oracle/src/browser/actions/attachments.js +82 -0
  11. package/dist/oracle/src/browser/actions/modelSelection.js +190 -0
  12. package/dist/oracle/src/browser/actions/navigation.js +75 -0
  13. package/dist/oracle/src/browser/actions/promptComposer.js +167 -0
  14. package/dist/oracle/src/browser/chromeLifecycle.js +104 -0
  15. package/dist/oracle/src/browser/config.js +33 -0
  16. package/dist/oracle/src/browser/constants.js +40 -0
  17. package/dist/oracle/src/browser/cookies.js +210 -0
  18. package/dist/oracle/src/browser/domDebug.js +36 -0
  19. package/dist/oracle/src/browser/index.js +331 -0
  20. package/dist/oracle/src/browser/pageActions.js +5 -0
  21. package/dist/oracle/src/browser/prompt.js +88 -0
  22. package/dist/oracle/src/browser/promptSummary.js +20 -0
  23. package/dist/oracle/src/browser/sessionRunner.js +80 -0
  24. package/dist/oracle/src/browser/types.js +1 -0
  25. package/dist/oracle/src/browser/utils.js +62 -0
  26. package/dist/oracle/src/browserMode.js +1 -0
  27. package/dist/oracle/src/cli/browserConfig.js +44 -0
  28. package/dist/oracle/src/cli/dryRun.js +59 -0
  29. package/dist/oracle/src/cli/engine.js +17 -0
  30. package/dist/oracle/src/cli/errorUtils.js +9 -0
  31. package/dist/oracle/src/cli/help.js +70 -0
  32. package/dist/oracle/src/cli/markdownRenderer.js +15 -0
  33. package/dist/oracle/src/cli/options.js +103 -0
  34. package/dist/oracle/src/cli/promptRequirement.js +14 -0
  35. package/dist/oracle/src/cli/rootAlias.js +30 -0
  36. package/dist/oracle/src/cli/sessionCommand.js +77 -0
  37. package/dist/oracle/src/cli/sessionDisplay.js +270 -0
  38. package/dist/oracle/src/cli/sessionRunner.js +94 -0
  39. package/dist/oracle/src/heartbeat.js +43 -0
  40. package/dist/oracle/src/oracle/client.js +48 -0
  41. package/dist/oracle/src/oracle/config.js +29 -0
  42. package/dist/oracle/src/oracle/errors.js +101 -0
  43. package/dist/oracle/src/oracle/files.js +220 -0
  44. package/dist/oracle/src/oracle/format.js +33 -0
  45. package/dist/oracle/src/oracle/fsAdapter.js +7 -0
  46. package/dist/oracle/src/oracle/oscProgress.js +60 -0
  47. package/dist/oracle/src/oracle/request.js +48 -0
  48. package/dist/oracle/src/oracle/run.js +444 -0
  49. package/dist/oracle/src/oracle/tokenStats.js +39 -0
  50. package/dist/oracle/src/oracle/types.js +1 -0
  51. package/dist/oracle/src/oracle.js +9 -0
  52. package/dist/oracle/src/sessionManager.js +205 -0
  53. package/dist/oracle/src/version.js +39 -0
  54. package/dist/scripts/browser-tools.js +536 -0
  55. package/dist/scripts/check.js +21 -0
  56. package/dist/scripts/chrome/browser-tools.js +295 -0
  57. package/dist/scripts/run-cli.js +14 -0
  58. package/dist/src/browser/actions/assistantResponse.js +555 -0
  59. package/dist/src/browser/actions/attachments.js +82 -0
  60. package/dist/src/browser/actions/modelSelection.js +300 -0
  61. package/dist/src/browser/actions/navigation.js +175 -0
  62. package/dist/src/browser/actions/promptComposer.js +167 -0
  63. package/dist/src/browser/actions/remoteFileTransfer.js +154 -0
  64. package/dist/src/browser/chromeCookies.js +274 -0
  65. package/dist/src/browser/chromeLifecycle.js +107 -0
  66. package/dist/src/browser/config.js +49 -0
  67. package/dist/src/browser/constants.js +42 -0
  68. package/dist/src/browser/cookies.js +130 -0
  69. package/dist/src/browser/domDebug.js +36 -0
  70. package/dist/src/browser/index.js +541 -0
  71. package/dist/src/browser/keytarShim.js +56 -0
  72. package/dist/src/browser/pageActions.js +5 -0
  73. package/dist/src/browser/policies.js +43 -0
  74. package/dist/src/browser/prompt.js +82 -0
  75. package/dist/src/browser/promptSummary.js +20 -0
  76. package/dist/src/browser/sessionRunner.js +96 -0
  77. package/dist/src/browser/types.js +1 -0
  78. package/dist/src/browser/utils.js +112 -0
  79. package/dist/src/browser/windowsCookies.js +218 -0
  80. package/dist/src/browserMode.js +1 -0
  81. package/dist/src/cli/browserConfig.js +193 -0
  82. package/dist/src/cli/bundleWarnings.js +9 -0
  83. package/dist/src/cli/clipboard.js +10 -0
  84. package/dist/src/cli/detach.js +11 -0
  85. package/dist/src/cli/dryRun.js +103 -0
  86. package/dist/src/cli/duplicatePromptGuard.js +14 -0
  87. package/dist/src/cli/engine.js +25 -0
  88. package/dist/src/cli/errorUtils.js +9 -0
  89. package/dist/src/cli/format.js +13 -0
  90. package/dist/src/cli/help.js +77 -0
  91. package/dist/src/cli/hiddenAliases.js +22 -0
  92. package/dist/src/cli/markdownBundle.js +17 -0
  93. package/dist/src/cli/markdownRenderer.js +97 -0
  94. package/dist/src/cli/notifier.js +300 -0
  95. package/dist/src/cli/options.js +193 -0
  96. package/dist/src/cli/oscUtils.js +20 -0
  97. package/dist/src/cli/promptRequirement.js +17 -0
  98. package/dist/src/cli/renderFlags.js +9 -0
  99. package/dist/src/cli/renderOutput.js +26 -0
  100. package/dist/src/cli/rootAlias.js +30 -0
  101. package/dist/src/cli/runOptions.js +62 -0
  102. package/dist/src/cli/sessionCommand.js +111 -0
  103. package/dist/src/cli/sessionDisplay.js +540 -0
  104. package/dist/src/cli/sessionRunner.js +419 -0
  105. package/dist/src/cli/tagline.js +258 -0
  106. package/dist/src/cli/tui/index.js +520 -0
  107. package/dist/src/cli/writeOutputPath.js +21 -0
  108. package/dist/src/config.js +27 -0
  109. package/dist/src/heartbeat.js +43 -0
  110. package/dist/src/mcp/server.js +36 -0
  111. package/dist/src/mcp/tools/consult.js +221 -0
  112. package/dist/src/mcp/tools/sessionResources.js +75 -0
  113. package/dist/src/mcp/tools/sessions.js +96 -0
  114. package/dist/src/mcp/types.js +18 -0
  115. package/dist/src/mcp/utils.js +27 -0
  116. package/dist/src/oracle/background.js +134 -0
  117. package/dist/src/oracle/claude.js +95 -0
  118. package/dist/src/oracle/client.js +87 -0
  119. package/dist/src/oracle/config.js +92 -0
  120. package/dist/src/oracle/errors.js +104 -0
  121. package/dist/src/oracle/files.js +371 -0
  122. package/dist/src/oracle/format.js +30 -0
  123. package/dist/src/oracle/fsAdapter.js +10 -0
  124. package/dist/src/oracle/gemini.js +185 -0
  125. package/dist/src/oracle/logging.js +36 -0
  126. package/dist/src/oracle/markdown.js +46 -0
  127. package/dist/src/oracle/multiModelRunner.js +164 -0
  128. package/dist/src/oracle/oscProgress.js +66 -0
  129. package/dist/src/oracle/promptAssembly.js +13 -0
  130. package/dist/src/oracle/request.js +49 -0
  131. package/dist/src/oracle/run.js +492 -0
  132. package/dist/src/oracle/runUtils.js +27 -0
  133. package/dist/src/oracle/tokenEstimate.js +37 -0
  134. package/dist/src/oracle/tokenStats.js +39 -0
  135. package/dist/src/oracle/tokenStringifier.js +24 -0
  136. package/dist/src/oracle/types.js +1 -0
  137. package/dist/src/oracle.js +12 -0
  138. package/dist/src/remote/client.js +128 -0
  139. package/dist/src/remote/server.js +294 -0
  140. package/dist/src/remote/types.js +1 -0
  141. package/dist/src/sessionManager.js +462 -0
  142. package/dist/src/sessionStore.js +56 -0
  143. package/dist/src/version.js +39 -0
  144. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  145. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  146. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  147. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  148. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  149. package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  150. package/dist/vendor/oracle-notifier/README.md +24 -0
  151. package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
  152. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  153. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  154. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  155. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  156. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  157. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.swift +45 -0
  158. package/dist/vendor/oracle-notifier/oracle-notifier/README.md +24 -0
  159. package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +93 -0
  160. package/package.json +102 -0
  161. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  162. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  163. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  164. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  165. package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  166. package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  167. package/vendor/oracle-notifier/README.md +24 -0
  168. package/vendor/oracle-notifier/build-notifier.sh +93 -0
@@ -0,0 +1,462 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs/promises';
4
+ import { createWriteStream } from 'node:fs';
5
+ import { DEFAULT_MODEL } from './oracle.js';
6
+ const ORACLE_HOME = process.env.ORACLE_HOME_DIR ?? path.join(os.homedir(), '.oracle');
7
+ const SESSIONS_DIR = path.join(ORACLE_HOME, 'sessions');
8
+ const METADATA_FILENAME = 'meta.json';
9
+ const LEGACY_SESSION_FILENAME = 'session.json';
10
+ const LEGACY_REQUEST_FILENAME = 'request.json';
11
+ const MODELS_DIRNAME = 'models';
12
+ const MODEL_JSON_EXTENSION = '.json';
13
+ const MODEL_LOG_EXTENSION = '.log';
14
+ const MAX_STATUS_LIMIT = 1000;
15
+ const ZOMBIE_MAX_AGE_MS = 60 * 60 * 1000; // 60 minutes
16
+ const DEFAULT_SLUG = 'session';
17
+ const MAX_SLUG_WORDS = 5;
18
+ const MIN_CUSTOM_SLUG_WORDS = 3;
19
+ const MAX_SLUG_WORD_LENGTH = 10;
20
+ async function ensureDir(dirPath) {
21
+ await fs.mkdir(dirPath, { recursive: true });
22
+ }
23
+ export async function ensureSessionStorage() {
24
+ await ensureDir(SESSIONS_DIR);
25
+ }
26
+ function slugify(text, maxWords = MAX_SLUG_WORDS) {
27
+ const normalized = text?.toLowerCase() ?? '';
28
+ const words = normalized.match(/[a-z0-9]+/g) ?? [];
29
+ const trimmed = words
30
+ .slice(0, maxWords)
31
+ .map((word) => word.slice(0, MAX_SLUG_WORD_LENGTH));
32
+ return trimmed.length > 0 ? trimmed.join('-') : DEFAULT_SLUG;
33
+ }
34
+ function countSlugWords(slug) {
35
+ return slug.split('-').filter(Boolean).length;
36
+ }
37
+ function normalizeCustomSlug(candidate) {
38
+ const slug = slugify(candidate, MAX_SLUG_WORDS);
39
+ const wordCount = countSlugWords(slug);
40
+ if (wordCount < MIN_CUSTOM_SLUG_WORDS || wordCount > MAX_SLUG_WORDS) {
41
+ throw new Error(`Custom slug must include between ${MIN_CUSTOM_SLUG_WORDS} and ${MAX_SLUG_WORDS} words.`);
42
+ }
43
+ return slug;
44
+ }
45
+ export function createSessionId(prompt, customSlug) {
46
+ if (customSlug) {
47
+ return normalizeCustomSlug(customSlug);
48
+ }
49
+ return slugify(prompt);
50
+ }
51
+ function sessionDir(id) {
52
+ return path.join(SESSIONS_DIR, id);
53
+ }
54
+ function metaPath(id) {
55
+ return path.join(sessionDir(id), METADATA_FILENAME);
56
+ }
57
+ function requestPath(id) {
58
+ return path.join(sessionDir(id), LEGACY_REQUEST_FILENAME);
59
+ }
60
+ function legacySessionPath(id) {
61
+ return path.join(sessionDir(id), LEGACY_SESSION_FILENAME);
62
+ }
63
+ function logPath(id) {
64
+ return path.join(sessionDir(id), 'output.log');
65
+ }
66
+ function modelsDir(id) {
67
+ return path.join(sessionDir(id), MODELS_DIRNAME);
68
+ }
69
+ function modelJsonPath(id, model) {
70
+ return path.join(modelsDir(id), `${model}${MODEL_JSON_EXTENSION}`);
71
+ }
72
+ function modelLogPath(id, model) {
73
+ return path.join(modelsDir(id), `${model}${MODEL_LOG_EXTENSION}`);
74
+ }
75
+ async function fileExists(targetPath) {
76
+ try {
77
+ await fs.access(targetPath);
78
+ return true;
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ }
84
+ async function ensureUniqueSessionId(baseSlug) {
85
+ let candidate = baseSlug;
86
+ let suffix = 2;
87
+ while (await fileExists(sessionDir(candidate))) {
88
+ candidate = `${baseSlug}-${suffix}`;
89
+ suffix += 1;
90
+ }
91
+ return candidate;
92
+ }
93
+ async function listModelRunFiles(sessionId) {
94
+ const dir = modelsDir(sessionId);
95
+ const entries = await fs.readdir(dir).catch(() => []);
96
+ const result = [];
97
+ for (const entry of entries) {
98
+ if (!entry.endsWith(MODEL_JSON_EXTENSION)) {
99
+ continue;
100
+ }
101
+ const jsonPath = path.join(dir, entry);
102
+ try {
103
+ const raw = await fs.readFile(jsonPath, 'utf8');
104
+ const parsed = JSON.parse(raw);
105
+ const normalized = ensureModelLogReference(sessionId, parsed);
106
+ result.push(normalized);
107
+ }
108
+ catch {
109
+ // ignore malformed model files
110
+ }
111
+ }
112
+ return result;
113
+ }
114
+ function ensureModelLogReference(sessionId, record) {
115
+ const logPathRelative = record.log?.path ?? path.relative(sessionDir(sessionId), modelLogPath(sessionId, record.model));
116
+ return {
117
+ ...record,
118
+ log: { path: logPathRelative, bytes: record.log?.bytes },
119
+ };
120
+ }
121
+ async function readModelRunFile(sessionId, model) {
122
+ try {
123
+ const raw = await fs.readFile(modelJsonPath(sessionId, model), 'utf8');
124
+ const parsed = JSON.parse(raw);
125
+ return ensureModelLogReference(sessionId, parsed);
126
+ }
127
+ catch {
128
+ return null;
129
+ }
130
+ }
131
+ export async function updateModelRunMetadata(sessionId, model, updates) {
132
+ await ensureDir(modelsDir(sessionId));
133
+ const existing = (await readModelRunFile(sessionId, model)) ?? {
134
+ model,
135
+ status: 'pending',
136
+ };
137
+ const next = ensureModelLogReference(sessionId, {
138
+ ...existing,
139
+ ...updates,
140
+ model,
141
+ });
142
+ await fs.writeFile(modelJsonPath(sessionId, model), JSON.stringify(next, null, 2), 'utf8');
143
+ return next;
144
+ }
145
+ export async function readModelRunMetadata(sessionId, model) {
146
+ return readModelRunFile(sessionId, model);
147
+ }
148
+ export async function initializeSession(options, cwd, notifications) {
149
+ await ensureSessionStorage();
150
+ const baseSlug = createSessionId(options.prompt || DEFAULT_SLUG, options.slug);
151
+ const sessionId = await ensureUniqueSessionId(baseSlug);
152
+ const dir = sessionDir(sessionId);
153
+ await ensureDir(dir);
154
+ const mode = options.mode ?? 'api';
155
+ const browserConfig = options.browserConfig;
156
+ const modelList = Array.isArray(options.models) && options.models.length > 0
157
+ ? options.models
158
+ : options.model
159
+ ? [options.model]
160
+ : [];
161
+ const metadata = {
162
+ id: sessionId,
163
+ createdAt: new Date().toISOString(),
164
+ status: 'pending',
165
+ promptPreview: (options.prompt || '').slice(0, 160),
166
+ model: modelList[0] ?? options.model,
167
+ models: modelList.map((modelName) => ({
168
+ model: modelName,
169
+ status: 'pending',
170
+ })),
171
+ cwd,
172
+ mode,
173
+ browser: browserConfig ? { config: browserConfig } : undefined,
174
+ notifications,
175
+ options: {
176
+ prompt: options.prompt,
177
+ file: options.file ?? [],
178
+ model: options.model,
179
+ models: modelList,
180
+ effectiveModelId: options.effectiveModelId,
181
+ maxInput: options.maxInput,
182
+ system: options.system,
183
+ maxOutput: options.maxOutput,
184
+ silent: options.silent,
185
+ filesReport: options.filesReport,
186
+ slug: sessionId,
187
+ mode,
188
+ browserConfig,
189
+ verbose: options.verbose,
190
+ heartbeatIntervalMs: options.heartbeatIntervalMs,
191
+ browserInlineFiles: options.browserInlineFiles,
192
+ browserBundleFiles: options.browserBundleFiles,
193
+ background: options.background,
194
+ search: options.search,
195
+ baseUrl: options.baseUrl,
196
+ azure: options.azure,
197
+ writeOutputPath: options.writeOutputPath,
198
+ },
199
+ };
200
+ await ensureDir(modelsDir(sessionId));
201
+ await fs.writeFile(metaPath(sessionId), JSON.stringify(metadata, null, 2), 'utf8');
202
+ await Promise.all((modelList.length > 0 ? modelList : [metadata.model ?? DEFAULT_MODEL]).map(async (modelName) => {
203
+ const jsonPath = modelJsonPath(sessionId, modelName);
204
+ const logFilePath = modelLogPath(sessionId, modelName);
205
+ const modelRecord = {
206
+ model: modelName,
207
+ status: 'pending',
208
+ log: { path: path.relative(sessionDir(sessionId), logFilePath) },
209
+ };
210
+ await fs.writeFile(jsonPath, JSON.stringify(modelRecord, null, 2), 'utf8');
211
+ await fs.writeFile(logFilePath, '', 'utf8');
212
+ }));
213
+ await fs.writeFile(logPath(sessionId), '', 'utf8');
214
+ return metadata;
215
+ }
216
+ export async function readSessionMetadata(sessionId) {
217
+ const modern = await readModernSessionMetadata(sessionId);
218
+ if (modern) {
219
+ return modern;
220
+ }
221
+ const legacy = await readLegacySessionMetadata(sessionId);
222
+ if (legacy) {
223
+ return legacy;
224
+ }
225
+ return null;
226
+ }
227
+ export async function updateSessionMetadata(sessionId, updates) {
228
+ const existing = (await readModernSessionMetadata(sessionId)) ??
229
+ (await readLegacySessionMetadata(sessionId)) ??
230
+ { id: sessionId };
231
+ const next = { ...existing, ...updates };
232
+ await fs.writeFile(metaPath(sessionId), JSON.stringify(next, null, 2), 'utf8');
233
+ return next;
234
+ }
235
+ async function readModernSessionMetadata(sessionId) {
236
+ try {
237
+ const raw = await fs.readFile(metaPath(sessionId), 'utf8');
238
+ const parsed = JSON.parse(raw);
239
+ if (!isSessionMetadataRecord(parsed)) {
240
+ return null;
241
+ }
242
+ const enriched = await attachModelRuns(parsed, sessionId);
243
+ return await markZombie(enriched, { persist: false });
244
+ }
245
+ catch {
246
+ return null;
247
+ }
248
+ }
249
+ async function readLegacySessionMetadata(sessionId) {
250
+ try {
251
+ const raw = await fs.readFile(legacySessionPath(sessionId), 'utf8');
252
+ const parsed = JSON.parse(raw);
253
+ const enriched = await attachModelRuns(parsed, sessionId);
254
+ return await markZombie(enriched, { persist: false });
255
+ }
256
+ catch {
257
+ return null;
258
+ }
259
+ }
260
+ function isSessionMetadataRecord(value) {
261
+ return Boolean(value && typeof value.id === 'string' && value.status);
262
+ }
263
+ async function attachModelRuns(meta, sessionId) {
264
+ const runs = await listModelRunFiles(sessionId);
265
+ if (runs.length === 0) {
266
+ return meta;
267
+ }
268
+ return { ...meta, models: runs };
269
+ }
270
+ export function createSessionLogWriter(sessionId, model) {
271
+ const targetPath = model ? modelLogPath(sessionId, model) : logPath(sessionId);
272
+ if (model) {
273
+ void ensureDir(modelsDir(sessionId));
274
+ }
275
+ const stream = createWriteStream(targetPath, { flags: 'a' });
276
+ const logLine = (line = '') => {
277
+ stream.write(`${line}\n`);
278
+ };
279
+ const writeChunk = (chunk) => {
280
+ stream.write(chunk);
281
+ return true;
282
+ };
283
+ return { stream, logLine, writeChunk, logPath: targetPath };
284
+ }
285
+ export async function listSessionsMetadata() {
286
+ await ensureSessionStorage();
287
+ const entries = await fs.readdir(SESSIONS_DIR).catch(() => []);
288
+ const metas = [];
289
+ for (const entry of entries) {
290
+ let meta = await readSessionMetadata(entry);
291
+ if (meta) {
292
+ meta = await markZombie(meta, { persist: true }); // keep stored metadata consistent with zombie detection
293
+ metas.push(meta);
294
+ }
295
+ }
296
+ return metas.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
297
+ }
298
+ export function filterSessionsByRange(metas, { hours = 24, includeAll = false, limit = 100 }) {
299
+ const maxLimit = Math.min(limit, MAX_STATUS_LIMIT);
300
+ let filtered = metas;
301
+ if (!includeAll) {
302
+ const cutoff = Date.now() - hours * 60 * 60 * 1000;
303
+ filtered = metas.filter((meta) => new Date(meta.createdAt).getTime() >= cutoff);
304
+ }
305
+ const limited = filtered.slice(0, maxLimit);
306
+ const truncated = filtered.length > maxLimit;
307
+ return { entries: limited, truncated, total: filtered.length };
308
+ }
309
+ export async function readSessionLog(sessionId) {
310
+ const runs = await listModelRunFiles(sessionId);
311
+ if (runs.length === 0) {
312
+ try {
313
+ return await fs.readFile(logPath(sessionId), 'utf8');
314
+ }
315
+ catch {
316
+ return '';
317
+ }
318
+ }
319
+ const sections = [];
320
+ let hasContent = false;
321
+ const ordered = runs
322
+ .slice()
323
+ .sort((a, b) => (a.startedAt && b.startedAt ? a.startedAt.localeCompare(b.startedAt) : a.model.localeCompare(b.model)));
324
+ for (const run of ordered) {
325
+ const logFile = run.log?.path
326
+ ? path.isAbsolute(run.log.path)
327
+ ? run.log.path
328
+ : path.join(sessionDir(sessionId), run.log.path)
329
+ : modelLogPath(sessionId, run.model);
330
+ let body = '';
331
+ try {
332
+ body = await fs.readFile(logFile, 'utf8');
333
+ }
334
+ catch {
335
+ body = '';
336
+ }
337
+ if (body.length > 0) {
338
+ hasContent = true;
339
+ }
340
+ sections.push(`=== ${run.model} ===\n${body}`.trimEnd());
341
+ }
342
+ if (!hasContent) {
343
+ try {
344
+ return await fs.readFile(logPath(sessionId), 'utf8');
345
+ }
346
+ catch {
347
+ // ignore and return structured header-only log
348
+ }
349
+ }
350
+ return sections.join('\n\n');
351
+ }
352
+ export async function readModelLog(sessionId, model) {
353
+ try {
354
+ return await fs.readFile(modelLogPath(sessionId, model), 'utf8');
355
+ }
356
+ catch {
357
+ return '';
358
+ }
359
+ }
360
+ export async function readSessionRequest(sessionId) {
361
+ const modern = await readModernSessionMetadata(sessionId);
362
+ if (modern?.options) {
363
+ return modern.options;
364
+ }
365
+ try {
366
+ const raw = await fs.readFile(requestPath(sessionId), 'utf8');
367
+ const parsed = JSON.parse(raw);
368
+ if (isSessionMetadataRecord(parsed)) {
369
+ return parsed.options ?? null;
370
+ }
371
+ return parsed;
372
+ }
373
+ catch {
374
+ return null;
375
+ }
376
+ }
377
+ export async function deleteSessionsOlderThan({ hours = 24, includeAll = false, } = {}) {
378
+ await ensureSessionStorage();
379
+ const entries = await fs.readdir(SESSIONS_DIR).catch(() => []);
380
+ if (!entries.length) {
381
+ return { deleted: 0, remaining: 0 };
382
+ }
383
+ const cutoff = includeAll ? Number.NEGATIVE_INFINITY : Date.now() - hours * 60 * 60 * 1000;
384
+ let deleted = 0;
385
+ for (const entry of entries) {
386
+ const dir = sessionDir(entry);
387
+ let createdMs;
388
+ const meta = await readSessionMetadata(entry);
389
+ if (meta?.createdAt) {
390
+ const parsed = Date.parse(meta.createdAt);
391
+ if (!Number.isNaN(parsed)) {
392
+ createdMs = parsed;
393
+ }
394
+ }
395
+ if (createdMs == null) {
396
+ try {
397
+ const stats = await fs.stat(dir);
398
+ createdMs = stats.birthtimeMs || stats.mtimeMs;
399
+ }
400
+ catch {
401
+ continue;
402
+ }
403
+ }
404
+ if (includeAll || (createdMs != null && createdMs < cutoff)) {
405
+ await fs.rm(dir, { recursive: true, force: true });
406
+ deleted += 1;
407
+ }
408
+ }
409
+ const remaining = Math.max(entries.length - deleted, 0);
410
+ return { deleted, remaining };
411
+ }
412
+ export async function wait(ms) {
413
+ return new Promise((resolve) => setTimeout(resolve, ms));
414
+ }
415
+ export { ORACLE_HOME, SESSIONS_DIR, MAX_STATUS_LIMIT };
416
+ export { ZOMBIE_MAX_AGE_MS };
417
+ export async function getSessionPaths(sessionId) {
418
+ const dir = sessionDir(sessionId);
419
+ const metadata = metaPath(sessionId);
420
+ const log = logPath(sessionId);
421
+ const request = requestPath(sessionId);
422
+ const required = [metadata, log];
423
+ const missing = [];
424
+ for (const file of required) {
425
+ if (!(await fileExists(file))) {
426
+ missing.push(path.basename(file));
427
+ }
428
+ }
429
+ if (missing.length > 0) {
430
+ throw new Error(`Session "${sessionId}" is missing: ${missing.join(', ')}`);
431
+ }
432
+ return { dir, metadata, log, request };
433
+ }
434
+ async function markZombie(meta, { persist }) {
435
+ if (!isZombie(meta)) {
436
+ return meta;
437
+ }
438
+ const updated = {
439
+ ...meta,
440
+ status: 'error',
441
+ errorMessage: 'Session marked as zombie (>60m stale)',
442
+ completedAt: new Date().toISOString(),
443
+ };
444
+ if (persist) {
445
+ await fs.writeFile(metaPath(meta.id), JSON.stringify(updated, null, 2), 'utf8');
446
+ }
447
+ return updated;
448
+ }
449
+ function isZombie(meta) {
450
+ if (meta.status !== 'running') {
451
+ return false;
452
+ }
453
+ const reference = meta.startedAt ?? meta.createdAt;
454
+ if (!reference) {
455
+ return false;
456
+ }
457
+ const startedMs = Date.parse(reference);
458
+ if (Number.isNaN(startedMs)) {
459
+ return false;
460
+ }
461
+ return Date.now() - startedMs > ZOMBIE_MAX_AGE_MS;
462
+ }
@@ -0,0 +1,56 @@
1
+ import { ensureSessionStorage, initializeSession, readSessionMetadata, updateSessionMetadata, createSessionLogWriter, readSessionLog, readModelLog, readSessionRequest, listSessionsMetadata, filterSessionsByRange, deleteSessionsOlderThan, updateModelRunMetadata, getSessionPaths, SESSIONS_DIR, } from './sessionManager.js';
2
+ class FileSessionStore {
3
+ ensureStorage() {
4
+ return ensureSessionStorage();
5
+ }
6
+ createSession(options, cwd, notifications) {
7
+ return initializeSession(options, cwd, notifications);
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 SESSIONS_DIR;
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
+ }
@@ -0,0 +1,39 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ let cachedVersion = null;
5
+ export function getCliVersion() {
6
+ if (cachedVersion) {
7
+ return cachedVersion;
8
+ }
9
+ cachedVersion = readVersionFromPackage();
10
+ return cachedVersion;
11
+ }
12
+ function readVersionFromPackage() {
13
+ const modulePath = fileURLToPath(import.meta.url);
14
+ let currentDir = path.dirname(modulePath);
15
+ const filesystemRoot = path.parse(currentDir).root;
16
+ // biome-ignore lint/nursery/noUnnecessaryConditions: deliberate sentinel loop to walk up directories
17
+ while (true) {
18
+ const candidate = path.join(currentDir, 'package.json');
19
+ try {
20
+ const raw = readFileSync(candidate, 'utf8');
21
+ const parsed = JSON.parse(raw);
22
+ const version = typeof parsed.version === 'string' && parsed.version.trim().length > 0
23
+ ? parsed.version.trim()
24
+ : '0.0.0';
25
+ return version;
26
+ }
27
+ catch (error) {
28
+ const code = error instanceof Error && 'code' in error ? error.code : undefined;
29
+ if (code && code !== 'ENOENT') {
30
+ break;
31
+ }
32
+ }
33
+ if (currentDir === filesystemRoot) {
34
+ break;
35
+ }
36
+ currentDir = path.dirname(currentDir);
37
+ }
38
+ return '0.0.0';
39
+ }
@@ -0,0 +1,20 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleIdentifier</key>
6
+ <string>com.steipete.oracle.notifier</string>
7
+ <key>CFBundleName</key>
8
+ <string>OracleNotifier</string>
9
+ <key>CFBundleDisplayName</key>
10
+ <string>Oracle Notifier</string>
11
+ <key>CFBundleExecutable</key>
12
+ <string>OracleNotifier</string>
13
+ <key>CFBundleIconFile</key>
14
+ <string>OracleIcon</string>
15
+ <key>CFBundlePackageType</key>
16
+ <string>APPL</string>
17
+ <key>LSMinimumSystemVersion</key>
18
+ <string>13.0</string>
19
+ </dict>
20
+ </plist>