ondeckllm 1.3.0 → 1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ondeckllm",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Localhost dashboard for managing LLM providers, model routing, and batting-order fallback chains",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
@@ -11,7 +11,16 @@
11
11
  "start": "node src/server.js",
12
12
  "dev": "node src/server.js"
13
13
  },
14
- "keywords": ["llm", "ai", "dashboard", "model-routing", "openai", "anthropic", "ollama", "ondeckllm"],
14
+ "keywords": [
15
+ "llm",
16
+ "ai",
17
+ "dashboard",
18
+ "model-routing",
19
+ "openai",
20
+ "anthropic",
21
+ "ollama",
22
+ "ondeckllm"
23
+ ],
15
24
  "author": "Canonflip",
16
25
  "license": "MIT",
17
26
  "dependencies": {
@@ -196,92 +196,115 @@ export function getUsageSummary(range = 'all') {
196
196
  };
197
197
  }
198
198
 
199
- // ── OpenClaw Session Import ──
199
+ // ── OpenClaw Session Import (Continuous) ──
200
200
 
201
- export async function importOpenClawSessions() {
202
- ensureDataDir();
201
+ const SYNC_STATE_FILE = join(DATA_DIR, 'openclaw-sync-state.json');
202
+ const OPENCLAW_AGENTS_DIR = join(homedir(), '.openclaw', 'agents');
203
203
 
204
- // Don't re-import if already done
205
- if (existsSync(IMPORT_MARKER)) return { imported: 0, skipped: true };
204
+ function loadSyncState() {
205
+ ensureDataDir();
206
+ if (!existsSync(SYNC_STATE_FILE)) return { files: {} };
207
+ try {
208
+ return JSON.parse(readFileSync(SYNC_STATE_FILE, 'utf-8'));
209
+ } catch { return { files: {} }; }
210
+ }
206
211
 
207
- if (!existsSync(OPENCLAW_SESSIONS_DIR)) return { imported: 0, error: 'No sessions directory' };
212
+ function saveSyncState(state) {
213
+ ensureDataDir();
214
+ writeFileSync(SYNC_STATE_FILE, JSON.stringify(state));
215
+ }
208
216
 
217
+ export async function importOpenClawSessions() {
218
+ ensureDataDir();
219
+ const state = loadSyncState();
209
220
  let imported = 0;
221
+ let filesScanned = 0;
210
222
 
223
+ // Scan all agent session dirs
224
+ const agentDirs = [];
211
225
  try {
212
- const files = readdirSync(OPENCLAW_SESSIONS_DIR);
213
- const jsonFiles = files.filter(f => f.endsWith('.json') || f.endsWith('.jsonl'));
226
+ if (existsSync(OPENCLAW_AGENTS_DIR)) {
227
+ for (const agent of readdirSync(OPENCLAW_AGENTS_DIR)) {
228
+ const sessDir = join(OPENCLAW_AGENTS_DIR, agent, 'sessions');
229
+ if (existsSync(sessDir)) agentDirs.push(sessDir);
230
+ }
231
+ }
232
+ } catch { /* ignore */ }
233
+
234
+ // Also check legacy path
235
+ if (existsSync(OPENCLAW_SESSIONS_DIR)) agentDirs.push(OPENCLAW_SESSIONS_DIR);
214
236
 
215
- for (const file of jsonFiles) {
237
+ for (const sessDir of agentDirs) {
238
+ let files;
239
+ try { files = readdirSync(sessDir).filter(f => f.endsWith('.jsonl')); } catch { continue; }
240
+
241
+ for (const file of files) {
242
+ const filePath = join(sessDir, file);
243
+ filesScanned++;
244
+
245
+ // Get file size to detect changes
246
+ let fileSize;
247
+ try {
248
+ const { statSync } = await import('fs');
249
+ fileSize = statSync(filePath).size;
250
+ } catch { continue; }
251
+
252
+ const prevOffset = state.files[filePath]?.offset || 0;
253
+ if (fileSize <= prevOffset) continue; // No new data
254
+
255
+ // Read only new bytes
216
256
  try {
217
- const filePath = join(OPENCLAW_SESSIONS_DIR, file);
218
- const raw = readFileSync(filePath, 'utf-8');
257
+ const fd = (await import('fs')).openSync(filePath, 'r');
258
+ const buf = Buffer.alloc(fileSize - prevOffset);
259
+ (await import('fs')).readSync(fd, buf, 0, buf.length, prevOffset);
260
+ (await import('fs')).closeSync(fd);
261
+
262
+ const newData = buf.toString('utf-8');
263
+ const lines = newData.split('\n');
219
264
 
220
- // Try JSONL first (one JSON per line)
221
- const lines = raw.trim().split('\n');
222
265
  for (const line of lines) {
266
+ if (!line.trim()) continue;
223
267
  try {
224
268
  const data = JSON.parse(line);
225
-
226
- // OpenClaw session format — look for usage/token data
227
- if (data.usage || data.tokenUsage || data.tokens) {
228
- const usage = data.usage || data.tokenUsage || data.tokens || {};
229
- const inputTokens = usage.input_tokens || usage.inputTokens || usage.prompt_tokens || 0;
230
- const outputTokens = usage.output_tokens || usage.outputTokens || usage.completion_tokens || 0;
231
-
232
- if (inputTokens > 0 || outputTokens > 0) {
233
- const model = data.model || data.modelId || 'unknown';
234
- const provider = data.provider || guessProvider(model);
235
- const ts = data.timestamp ? new Date(data.timestamp).getTime() :
236
- data.createdAt ? new Date(data.createdAt).getTime() :
237
- data.ts || Date.now();
238
-
239
- logUsage({
240
- ts,
241
- provider,
242
- model,
243
- inputTokens,
244
- outputTokens,
245
- cost: calculateCost(model, inputTokens, outputTokens),
246
- latencyMs: data.latencyMs || data.latency || 0
247
- });
248
- imported++;
249
- }
250
- }
251
-
252
- // Also check for nested message arrays with usage
253
- if (Array.isArray(data.messages)) {
254
- for (const msg of data.messages) {
255
- if (msg.usage) {
256
- const inputTokens = msg.usage.input_tokens || msg.usage.prompt_tokens || 0;
257
- const outputTokens = msg.usage.output_tokens || msg.usage.completion_tokens || 0;
258
- if (inputTokens > 0 || outputTokens > 0) {
259
- const model = msg.model || data.model || 'unknown';
260
- const provider = msg.provider || data.provider || guessProvider(model);
261
- logUsage({
262
- ts: msg.timestamp ? new Date(msg.timestamp).getTime() : Date.now(),
263
- provider,
264
- model,
265
- inputTokens,
266
- outputTokens,
267
- cost: calculateCost(model, inputTokens, outputTokens),
268
- latencyMs: msg.latencyMs || 0
269
- });
270
- imported++;
271
- }
272
- }
273
- }
274
- }
269
+ // Only process assistant messages with usage data
270
+ if (data.type !== 'message') continue;
271
+ const msg = data.message;
272
+ if (!msg || msg.role !== 'assistant' || !msg.usage) continue;
273
+
274
+ const usage = msg.usage;
275
+ const inputTokens = usage.input || usage.input_tokens || 0;
276
+ const outputTokens = usage.output || usage.output_tokens || 0;
277
+ const cacheRead = usage.cacheRead || 0;
278
+ const cacheWrite = usage.cacheWrite || 0;
279
+
280
+ if (inputTokens === 0 && outputTokens === 0 && cacheRead === 0 && cacheWrite === 0) continue;
281
+
282
+ const model = msg.model || 'unknown';
283
+ const provider = msg.provider || guessProvider(model);
284
+ const cost = usage.cost?.total ?? calculateCost(model, inputTokens, outputTokens);
285
+ const ts = data.timestamp ? new Date(data.timestamp).getTime() : Date.now();
286
+
287
+ logUsage({
288
+ ts,
289
+ provider,
290
+ model,
291
+ inputTokens: inputTokens + cacheRead + cacheWrite,
292
+ outputTokens,
293
+ cost,
294
+ latencyMs: 0,
295
+ source: 'openclaw'
296
+ });
297
+ imported++;
275
298
  } catch { /* skip unparseable lines */ }
276
299
  }
300
+
301
+ state.files[filePath] = { offset: fileSize, lastSync: Date.now() };
277
302
  } catch { /* skip unreadable files */ }
278
303
  }
279
- } catch { /* sessions dir read error */ }
280
-
281
- // Mark as imported
282
- writeFileSync(IMPORT_MARKER, new Date().toISOString());
304
+ }
283
305
 
284
- return { imported, skipped: false };
306
+ saveSyncState(state);
307
+ return { imported, filesScanned, totalTracked: Object.keys(state.files).length };
285
308
  }
286
309
 
287
310
  function guessProvider(model) {