llm-cli-gateway 1.17.4 → 1.17.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +1 -1
- package/dist/approval-manager.js +0 -8
- package/dist/async-job-manager.d.ts +0 -113
- package/dist/async-job-manager.js +6 -124
- package/dist/cache-stats.d.ts +0 -89
- package/dist/cache-stats.js +0 -62
- package/dist/claude-mcp-config.js +0 -1
- package/dist/cli-updater.d.ts +0 -8
- package/dist/cli-updater.js +0 -12
- package/dist/codex-json-parser.d.ts +0 -20
- package/dist/codex-json-parser.js +0 -21
- package/dist/config.d.ts +0 -31
- package/dist/config.js +2 -72
- package/dist/db.d.ts +0 -18
- package/dist/db.js +0 -22
- package/dist/doctor.d.ts +0 -49
- package/dist/doctor.js +0 -47
- package/dist/endpoint-exposure.js +0 -1
- package/dist/executor.d.ts +0 -19
- package/dist/executor.js +3 -38
- package/dist/flight-recorder.d.ts +0 -26
- package/dist/flight-recorder.js +1 -70
- package/dist/gemini-json-parser.d.ts +0 -25
- package/dist/gemini-json-parser.js +0 -28
- package/dist/health.d.ts +0 -3
- package/dist/health.js +0 -3
- package/dist/index.d.ts +1 -221
- package/dist/index.js +14 -563
- package/dist/job-store.d.ts +0 -74
- package/dist/job-store.js +1 -73
- package/dist/logger.d.ts +0 -7
- package/dist/logger.js +0 -6
- package/dist/migrate-sessions.d.ts +0 -3
- package/dist/migrate-sessions.js +0 -16
- package/dist/migrate.js +1 -18
- package/dist/mistral-meta-json-parser.js +0 -67
- package/dist/model-registry.js +0 -13
- package/dist/pricing.d.ts +0 -46
- package/dist/pricing.js +0 -47
- package/dist/process-monitor.d.ts +0 -15
- package/dist/process-monitor.js +2 -31
- package/dist/prompt-parts.d.ts +0 -25
- package/dist/prompt-parts.js +0 -11
- package/dist/provider-status.d.ts +0 -8
- package/dist/provider-status.js +0 -11
- package/dist/request-helpers.d.ts +0 -334
- package/dist/request-helpers.js +1 -229
- package/dist/resources.d.ts +0 -20
- package/dist/resources.js +1 -34
- package/dist/retry.d.ts +0 -45
- package/dist/retry.js +3 -40
- package/dist/session-manager-pg.d.ts +0 -32
- package/dist/session-manager-pg.js +0 -32
- package/dist/session-manager.d.ts +0 -21
- package/dist/session-manager.js +1 -15
- package/dist/stream-json-parser.d.ts +0 -18
- package/dist/stream-json-parser.js +0 -22
- package/dist/upstream-contracts.d.ts +0 -55
- package/dist/upstream-contracts.js +0 -77
- package/dist/validation-orchestrator.js +0 -3
- package/dist/worktree-manager.d.ts +0 -9
- package/dist/worktree-manager.js +0 -21
- package/package.json +1 -1
package/dist/job-store.d.ts
CHANGED
|
@@ -23,10 +23,6 @@ export declare function resolveJobStoreDbPath(): string | null;
|
|
|
23
23
|
export declare function resolveJobRetentionMs(): number;
|
|
24
24
|
export declare function resolveDedupWindowMs(): number;
|
|
25
25
|
export declare function computeRequestKey(cli: string, args: string[], extra?: string): string;
|
|
26
|
-
/**
|
|
27
|
-
* Public surface every backend (sqlite/postgres/memory) must implement. The
|
|
28
|
-
* AsyncJobManager talks to this interface only.
|
|
29
|
-
*/
|
|
30
26
|
export interface JobStore {
|
|
31
27
|
recordStart(input: {
|
|
32
28
|
id: string;
|
|
@@ -51,15 +47,6 @@ export interface JobStore {
|
|
|
51
47
|
}): void;
|
|
52
48
|
getById(id: string): JobRecord | null;
|
|
53
49
|
findByRequestKey(requestKey: string): JobRecord | null;
|
|
54
|
-
/**
|
|
55
|
-
* Flip every `status='running'` row to `'orphaned'` at gateway boot.
|
|
56
|
-
*
|
|
57
|
-
* Returns the row count AND a snapshot of every row that was flipped, so
|
|
58
|
-
* AsyncJobManager can write a flight-recorder logComplete with the full
|
|
59
|
-
* sync-helper-equivalent payload (response from stderr||stdout,
|
|
60
|
-
* durationMs from startedAt). Pre-slice-1.5 rows that never wrote a
|
|
61
|
-
* logStart degrade silently to a no-op UPDATE inside the FR.
|
|
62
|
-
*/
|
|
63
50
|
markOrphanedOnStartup(): {
|
|
64
51
|
count: number;
|
|
65
52
|
orphaned: Array<OrphanedJobSnapshot>;
|
|
@@ -67,11 +54,6 @@ export interface JobStore {
|
|
|
67
54
|
evictExpired(): number;
|
|
68
55
|
close(): void;
|
|
69
56
|
}
|
|
70
|
-
/**
|
|
71
|
-
* Per-orphan snapshot returned by `markOrphanedOnStartup` so the
|
|
72
|
-
* AsyncJobManager constructor can build a faithful FlightLogResult for
|
|
73
|
-
* each row it flipped.
|
|
74
|
-
*/
|
|
75
57
|
export interface OrphanedJobSnapshot {
|
|
76
58
|
id: string;
|
|
77
59
|
correlationId: string;
|
|
@@ -80,10 +62,6 @@ export interface OrphanedJobSnapshot {
|
|
|
80
62
|
stderr: string;
|
|
81
63
|
exitCode: number | null;
|
|
82
64
|
}
|
|
83
|
-
/**
|
|
84
|
-
* SQLite-backed job store. Default backend for production. Durable across
|
|
85
|
-
* gateway restarts; safe for single-instance deployments.
|
|
86
|
-
*/
|
|
87
65
|
export declare class SqliteJobStore implements JobStore {
|
|
88
66
|
private logger;
|
|
89
67
|
private db;
|
|
@@ -101,9 +79,6 @@ export declare class SqliteJobStore implements JobStore {
|
|
|
101
79
|
retentionMs?: number;
|
|
102
80
|
dedupWindowMs?: number;
|
|
103
81
|
});
|
|
104
|
-
/**
|
|
105
|
-
* Insert a new running job row. Caller has already computed requestKey.
|
|
106
|
-
*/
|
|
107
82
|
recordStart(input: {
|
|
108
83
|
id: string;
|
|
109
84
|
correlationId: string;
|
|
@@ -114,13 +89,7 @@ export declare class SqliteJobStore implements JobStore {
|
|
|
114
89
|
startedAt: string;
|
|
115
90
|
pid: number | null;
|
|
116
91
|
}): void;
|
|
117
|
-
/**
|
|
118
|
-
* Batched output flush. Cheap to call repeatedly; better-sqlite3 is sync.
|
|
119
|
-
*/
|
|
120
92
|
recordOutput(id: string, stdout: string, stderr: string, outputTruncated: boolean): void;
|
|
121
|
-
/**
|
|
122
|
-
* Mark a job as completed/failed/canceled. Sets expires_at = now + retention.
|
|
123
|
-
*/
|
|
124
93
|
recordComplete(input: {
|
|
125
94
|
id: string;
|
|
126
95
|
status: Exclude<JobStoreStatus, "running">;
|
|
@@ -132,43 +101,15 @@ export declare class SqliteJobStore implements JobStore {
|
|
|
132
101
|
finishedAt: string;
|
|
133
102
|
}): void;
|
|
134
103
|
getById(id: string): JobRecord | null;
|
|
135
|
-
/**
|
|
136
|
-
* Returns the most recent matching job within the dedup window, if any.
|
|
137
|
-
* Caller pre-filters out forceRefresh requests.
|
|
138
|
-
*/
|
|
139
104
|
findByRequestKey(requestKey: string): JobRecord | null;
|
|
140
|
-
/**
|
|
141
|
-
* On gateway boot, flip any jobs that were 'running' to 'orphaned'.
|
|
142
|
-
* The child processes were detached but can't be reattached to in this process.
|
|
143
|
-
*
|
|
144
|
-
* Returns the row count + a per-orphan snapshot so AsyncJobManager can
|
|
145
|
-
* write a flight-recorder logComplete with proper audit data
|
|
146
|
-
* (durationMs from startedAt, response from stderr||stdout).
|
|
147
|
-
*/
|
|
148
105
|
markOrphanedOnStartup(): {
|
|
149
106
|
count: number;
|
|
150
107
|
orphaned: Array<OrphanedJobSnapshot>;
|
|
151
108
|
};
|
|
152
|
-
/**
|
|
153
|
-
* Delete rows whose expires_at has passed. Returns number of rows deleted.
|
|
154
|
-
*/
|
|
155
109
|
evictExpired(): number;
|
|
156
110
|
close(): void;
|
|
157
111
|
}
|
|
158
|
-
/**
|
|
159
|
-
* Backwards-compatibility alias. Older code and tests construct `new JobStore(path)`
|
|
160
|
-
* directly; that surface now resolves to the SQLite implementation. Prefer
|
|
161
|
-
* `createJobStore(config)` in new code.
|
|
162
|
-
*
|
|
163
|
-
* @deprecated Use `SqliteJobStore` directly, or `createJobStore(persistenceConfig)`.
|
|
164
|
-
*/
|
|
165
112
|
export declare const JobStoreClass: typeof SqliteJobStore;
|
|
166
|
-
/**
|
|
167
|
-
* In-process job store. Same semantics as SqliteJobStore but state lives in a
|
|
168
|
-
* Map and is lost on process exit. Use for tests and ephemeral/CI gateways
|
|
169
|
-
* that have explicitly acknowledged the trade-off via
|
|
170
|
-
* `[persistence].acknowledgeEphemeral = true`.
|
|
171
|
-
*/
|
|
172
113
|
export declare class MemoryJobStore implements JobStore {
|
|
173
114
|
private rows;
|
|
174
115
|
private retentionMs;
|
|
@@ -200,10 +141,6 @@ export declare class MemoryJobStore implements JobStore {
|
|
|
200
141
|
}): void;
|
|
201
142
|
getById(id: string): JobRecord | null;
|
|
202
143
|
findByRequestKey(requestKey: string): JobRecord | null;
|
|
203
|
-
/**
|
|
204
|
-
* In-memory stores have no cross-process state, so any "running" rows here
|
|
205
|
-
* came from this very process and aren't actually orphaned. No-op.
|
|
206
|
-
*/
|
|
207
144
|
markOrphanedOnStartup(): {
|
|
208
145
|
count: number;
|
|
209
146
|
orphaned: Array<OrphanedJobSnapshot>;
|
|
@@ -211,12 +148,6 @@ export declare class MemoryJobStore implements JobStore {
|
|
|
211
148
|
evictExpired(): number;
|
|
212
149
|
close(): void;
|
|
213
150
|
}
|
|
214
|
-
/**
|
|
215
|
-
* Stub for the planned Postgres backend. The interface and config surface ship
|
|
216
|
-
* now so multi-instance deployments can plan around them, but the
|
|
217
|
-
* implementation is intentionally not yet provided — calling code must select
|
|
218
|
-
* `sqlite` or `memory` until a real impl lands.
|
|
219
|
-
*/
|
|
220
151
|
export declare class PostgresJobStore implements JobStore {
|
|
221
152
|
constructor(_dsn: string, _logger?: Logger);
|
|
222
153
|
recordStart(): void;
|
|
@@ -231,9 +162,4 @@ export declare class PostgresJobStore implements JobStore {
|
|
|
231
162
|
evictExpired(): number;
|
|
232
163
|
close(): void;
|
|
233
164
|
}
|
|
234
|
-
/**
|
|
235
|
-
* Construct the JobStore appropriate to the resolved PersistenceConfig.
|
|
236
|
-
* Returns `null` when `backend = "none"` — callers must not register
|
|
237
|
-
* `*_request_async` tools in that case (use `config.asyncJobsEnabled`).
|
|
238
|
-
*/
|
|
239
165
|
export declare function createJobStore(config: PersistenceConfig, logger?: Logger): JobStore | null;
|
package/dist/job-store.js
CHANGED
|
@@ -25,7 +25,7 @@ export function resolveJobRetentionMs() {
|
|
|
25
25
|
}
|
|
26
26
|
return days * 24 * 60 * 60 * 1000;
|
|
27
27
|
}
|
|
28
|
-
const DEFAULT_DEDUP_WINDOW_MS = 60 * 60 * 1000;
|
|
28
|
+
const DEFAULT_DEDUP_WINDOW_MS = 60 * 60 * 1000;
|
|
29
29
|
export function resolveDedupWindowMs() {
|
|
30
30
|
const raw = process.env.LLM_GATEWAY_DEDUP_WINDOW_MS;
|
|
31
31
|
if (raw === undefined)
|
|
@@ -59,10 +59,6 @@ function rowToRecord(row) {
|
|
|
59
59
|
expiresAt: row.expires_at,
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
-
/**
|
|
63
|
-
* SQLite-backed job store. Default backend for production. Durable across
|
|
64
|
-
* gateway restarts; safe for single-instance deployments.
|
|
65
|
-
*/
|
|
66
62
|
export class SqliteJobStore {
|
|
67
63
|
logger;
|
|
68
64
|
db;
|
|
@@ -116,7 +112,6 @@ export class SqliteJobStore {
|
|
|
116
112
|
chmodSync(dbPath, 0o600);
|
|
117
113
|
}
|
|
118
114
|
catch {
|
|
119
|
-
// Best effort permissions hardening.
|
|
120
115
|
}
|
|
121
116
|
}
|
|
122
117
|
this.retentionMs = options.retentionMs ?? resolveJobRetentionMs();
|
|
@@ -140,8 +135,6 @@ export class SqliteJobStore {
|
|
|
140
135
|
WHERE id = @id
|
|
141
136
|
`);
|
|
142
137
|
this.getByIdStmt = this.db.prepare(`SELECT * FROM jobs WHERE id = ?`);
|
|
143
|
-
// Dedup query: most recent non-orphaned job with matching request_key, started within window.
|
|
144
|
-
// Exclude orphaned/canceled/failed-with-error from dedup so a broken run isn't reused.
|
|
145
138
|
this.findByRequestKeyStmt = this.db.prepare(`
|
|
146
139
|
SELECT * FROM jobs
|
|
147
140
|
WHERE request_key = ?
|
|
@@ -150,12 +143,6 @@ export class SqliteJobStore {
|
|
|
150
143
|
ORDER BY started_at DESC
|
|
151
144
|
LIMIT 1
|
|
152
145
|
`);
|
|
153
|
-
// Snapshot every in-flight row's audit data BEFORE the orphan-flip
|
|
154
|
-
// UPDATE so AsyncJobManager can construct a full FlightLogResult per
|
|
155
|
-
// orphan. No transaction wrapper required: gateway boot is
|
|
156
|
-
// single-threaded before any new jobs can arrive, so no
|
|
157
|
-
// status='running' row can be inserted between this SELECT and the
|
|
158
|
-
// UPDATE below.
|
|
159
146
|
this.selectRunningOrphansStmt = this.db.prepare(`
|
|
160
147
|
SELECT id, correlation_id, started_at, stdout, stderr, exit_code
|
|
161
148
|
FROM jobs WHERE status = 'running'
|
|
@@ -170,9 +157,6 @@ export class SqliteJobStore {
|
|
|
170
157
|
`);
|
|
171
158
|
this.deleteExpiredStmt = this.db.prepare(`DELETE FROM jobs WHERE expires_at < ?`);
|
|
172
159
|
}
|
|
173
|
-
/**
|
|
174
|
-
* Insert a new running job row. Caller has already computed requestKey.
|
|
175
|
-
*/
|
|
176
160
|
recordStart(input) {
|
|
177
161
|
this.insertStmt.run({
|
|
178
162
|
id: input.id,
|
|
@@ -190,13 +174,9 @@ export class SqliteJobStore {
|
|
|
190
174
|
started_at: input.startedAt,
|
|
191
175
|
finished_at: null,
|
|
192
176
|
pid: input.pid,
|
|
193
|
-
// Running jobs never expire — only completed/failed/canceled do.
|
|
194
177
|
expires_at: FAR_FUTURE_ISO,
|
|
195
178
|
});
|
|
196
179
|
}
|
|
197
|
-
/**
|
|
198
|
-
* Batched output flush. Cheap to call repeatedly; better-sqlite3 is sync.
|
|
199
|
-
*/
|
|
200
180
|
recordOutput(id, stdout, stderr, outputTruncated) {
|
|
201
181
|
this.updateOutputStmt.run({
|
|
202
182
|
id,
|
|
@@ -205,9 +185,6 @@ export class SqliteJobStore {
|
|
|
205
185
|
output_truncated: outputTruncated ? 1 : 0,
|
|
206
186
|
});
|
|
207
187
|
}
|
|
208
|
-
/**
|
|
209
|
-
* Mark a job as completed/failed/canceled. Sets expires_at = now + retention.
|
|
210
|
-
*/
|
|
211
188
|
recordComplete(input) {
|
|
212
189
|
const expiresAt = new Date(Date.parse(input.finishedAt) + this.retentionMs).toISOString();
|
|
213
190
|
this.updateCompleteStmt.run({
|
|
@@ -226,30 +203,14 @@ export class SqliteJobStore {
|
|
|
226
203
|
const row = this.getByIdStmt.get(id);
|
|
227
204
|
return row ? rowToRecord(row) : null;
|
|
228
205
|
}
|
|
229
|
-
/**
|
|
230
|
-
* Returns the most recent matching job within the dedup window, if any.
|
|
231
|
-
* Caller pre-filters out forceRefresh requests.
|
|
232
|
-
*/
|
|
233
206
|
findByRequestKey(requestKey) {
|
|
234
207
|
const cutoff = new Date(Date.now() - this.dedupWindowMs).toISOString();
|
|
235
208
|
const row = this.findByRequestKeyStmt.get(requestKey, cutoff);
|
|
236
209
|
return row ? rowToRecord(row) : null;
|
|
237
210
|
}
|
|
238
|
-
/**
|
|
239
|
-
* On gateway boot, flip any jobs that were 'running' to 'orphaned'.
|
|
240
|
-
* The child processes were detached but can't be reattached to in this process.
|
|
241
|
-
*
|
|
242
|
-
* Returns the row count + a per-orphan snapshot so AsyncJobManager can
|
|
243
|
-
* write a flight-recorder logComplete with proper audit data
|
|
244
|
-
* (durationMs from startedAt, response from stderr||stdout).
|
|
245
|
-
*/
|
|
246
211
|
markOrphanedOnStartup() {
|
|
247
212
|
const now = new Date().toISOString();
|
|
248
|
-
// Orphaned jobs retain a short window so callers can collect the partial output,
|
|
249
|
-
// then evict. Reuse the standard retention.
|
|
250
213
|
const expiresAt = new Date(Date.now() + this.retentionMs).toISOString();
|
|
251
|
-
// SELECT before UPDATE — gateway boot is single-threaded so no row can
|
|
252
|
-
// appear in 'running' between the two statements.
|
|
253
214
|
const rows = (this.selectRunningOrphansStmt.all?.() ?? []);
|
|
254
215
|
const orphaned = rows.map(row => ({
|
|
255
216
|
id: row.id,
|
|
@@ -262,9 +223,6 @@ export class SqliteJobStore {
|
|
|
262
223
|
const result = this.markOrphanedStmt.run(now, expiresAt);
|
|
263
224
|
return { count: result?.changes ?? 0, orphaned };
|
|
264
225
|
}
|
|
265
|
-
/**
|
|
266
|
-
* Delete rows whose expires_at has passed. Returns number of rows deleted.
|
|
267
|
-
*/
|
|
268
226
|
evictExpired() {
|
|
269
227
|
const now = new Date().toISOString();
|
|
270
228
|
const result = this.deleteExpiredStmt.run(now);
|
|
@@ -279,20 +237,7 @@ export class SqliteJobStore {
|
|
|
279
237
|
}
|
|
280
238
|
}
|
|
281
239
|
}
|
|
282
|
-
/**
|
|
283
|
-
* Backwards-compatibility alias. Older code and tests construct `new JobStore(path)`
|
|
284
|
-
* directly; that surface now resolves to the SQLite implementation. Prefer
|
|
285
|
-
* `createJobStore(config)` in new code.
|
|
286
|
-
*
|
|
287
|
-
* @deprecated Use `SqliteJobStore` directly, or `createJobStore(persistenceConfig)`.
|
|
288
|
-
*/
|
|
289
240
|
export const JobStoreClass = SqliteJobStore;
|
|
290
|
-
/**
|
|
291
|
-
* In-process job store. Same semantics as SqliteJobStore but state lives in a
|
|
292
|
-
* Map and is lost on process exit. Use for tests and ephemeral/CI gateways
|
|
293
|
-
* that have explicitly acknowledged the trade-off via
|
|
294
|
-
* `[persistence].acknowledgeEphemeral = true`.
|
|
295
|
-
*/
|
|
296
241
|
export class MemoryJobStore {
|
|
297
242
|
rows = new Map();
|
|
298
243
|
retentionMs;
|
|
@@ -362,10 +307,6 @@ export class MemoryJobStore {
|
|
|
362
307
|
}
|
|
363
308
|
return best ? { ...best } : null;
|
|
364
309
|
}
|
|
365
|
-
/**
|
|
366
|
-
* In-memory stores have no cross-process state, so any "running" rows here
|
|
367
|
-
* came from this very process and aren't actually orphaned. No-op.
|
|
368
|
-
*/
|
|
369
310
|
markOrphanedOnStartup() {
|
|
370
311
|
return { count: 0, orphaned: [] };
|
|
371
312
|
}
|
|
@@ -384,12 +325,6 @@ export class MemoryJobStore {
|
|
|
384
325
|
this.rows.clear();
|
|
385
326
|
}
|
|
386
327
|
}
|
|
387
|
-
/**
|
|
388
|
-
* Stub for the planned Postgres backend. The interface and config surface ship
|
|
389
|
-
* now so multi-instance deployments can plan around them, but the
|
|
390
|
-
* implementation is intentionally not yet provided — calling code must select
|
|
391
|
-
* `sqlite` or `memory` until a real impl lands.
|
|
392
|
-
*/
|
|
393
328
|
export class PostgresJobStore {
|
|
394
329
|
constructor(_dsn, _logger = noopLogger) {
|
|
395
330
|
throw new Error("PostgresJobStore is not yet implemented. Use backend = 'sqlite' (single-instance) or " +
|
|
@@ -417,14 +352,8 @@ export class PostgresJobStore {
|
|
|
417
352
|
throw new Error("not implemented");
|
|
418
353
|
}
|
|
419
354
|
close() {
|
|
420
|
-
/* no-op */
|
|
421
355
|
}
|
|
422
356
|
}
|
|
423
|
-
/**
|
|
424
|
-
* Construct the JobStore appropriate to the resolved PersistenceConfig.
|
|
425
|
-
* Returns `null` when `backend = "none"` — callers must not register
|
|
426
|
-
* `*_request_async` tools in that case (use `config.asyncJobsEnabled`).
|
|
427
|
-
*/
|
|
428
357
|
export function createJobStore(config, logger = noopLogger) {
|
|
429
358
|
const opts = {
|
|
430
359
|
retentionMs: config.retentionDays * 24 * 60 * 60 * 1000,
|
|
@@ -436,7 +365,6 @@ export function createJobStore(config, logger = noopLogger) {
|
|
|
436
365
|
case "memory":
|
|
437
366
|
return new MemoryJobStore(opts);
|
|
438
367
|
case "postgres":
|
|
439
|
-
// Throws today; design surface is honest so callers can react.
|
|
440
368
|
return new PostgresJobStore(config.dsn ?? "", logger);
|
|
441
369
|
case "sqlite":
|
|
442
370
|
default:
|
package/dist/logger.d.ts
CHANGED
|
@@ -2,14 +2,7 @@ export interface Logger {
|
|
|
2
2
|
info(message: string, meta?: unknown): void;
|
|
3
3
|
error(message: string, meta?: unknown): void;
|
|
4
4
|
debug(message: string, meta?: unknown): void;
|
|
5
|
-
/** Optional: callers that want explicit WARN routing can implement this. */
|
|
6
5
|
warn?(message: string, meta?: unknown): void;
|
|
7
6
|
}
|
|
8
7
|
export declare const noopLogger: Logger;
|
|
9
|
-
/**
|
|
10
|
-
* Emit a warning through whichever logger surface is available. Some Logger
|
|
11
|
-
* implementations (legacy) only provide `info`/`error`/`debug`; in that case
|
|
12
|
-
* the message is prefixed with `[WARN]` and routed through `info` so it still
|
|
13
|
-
* reaches stderr.
|
|
14
|
-
*/
|
|
15
8
|
export declare function logWarn(logger: Logger, message: string, meta?: unknown): void;
|
package/dist/logger.js
CHANGED
|
@@ -4,12 +4,6 @@ export const noopLogger = {
|
|
|
4
4
|
debug: () => { },
|
|
5
5
|
warn: () => { },
|
|
6
6
|
};
|
|
7
|
-
/**
|
|
8
|
-
* Emit a warning through whichever logger surface is available. Some Logger
|
|
9
|
-
* implementations (legacy) only provide `info`/`error`/`debug`; in that case
|
|
10
|
-
* the message is prefixed with `[WARN]` and routed through `info` so it still
|
|
11
|
-
* reaches stderr.
|
|
12
|
-
*/
|
|
13
7
|
export function logWarn(logger, message, meta) {
|
|
14
8
|
if (typeof logger.warn === "function") {
|
|
15
9
|
logger.warn(message, meta);
|
|
@@ -5,8 +5,5 @@ interface MigrationResult {
|
|
|
5
5
|
failed: number;
|
|
6
6
|
errors: string[];
|
|
7
7
|
}
|
|
8
|
-
/**
|
|
9
|
-
* Migrate sessions from file-based storage to PostgreSQL
|
|
10
|
-
*/
|
|
11
8
|
export declare function migrateFromFile(filePath: string, pgManager: PostgreSQLSessionManager): Promise<MigrationResult>;
|
|
12
9
|
export {};
|
package/dist/migrate-sessions.js
CHANGED
|
@@ -5,22 +5,17 @@ import { join } from "path";
|
|
|
5
5
|
import { PostgreSQLSessionManager } from "./session-manager-pg.js";
|
|
6
6
|
import { loadConfig } from "./config.js";
|
|
7
7
|
import { createDatabaseConnection } from "./db.js";
|
|
8
|
-
// Simple console logger for migration script
|
|
9
8
|
const logger = {
|
|
10
9
|
info: (message, meta) => console.error(`[INFO] ${message}`, meta || ""),
|
|
11
10
|
error: (message, meta) => console.error(`[ERROR] ${message}`, meta || ""),
|
|
12
11
|
debug: (message, meta) => console.error(`[DEBUG] ${message}`, meta || ""),
|
|
13
12
|
};
|
|
14
|
-
/**
|
|
15
|
-
* Migrate sessions from file-based storage to PostgreSQL
|
|
16
|
-
*/
|
|
17
13
|
export async function migrateFromFile(filePath, pgManager) {
|
|
18
14
|
const result = {
|
|
19
15
|
migrated: 0,
|
|
20
16
|
failed: 0,
|
|
21
17
|
errors: [],
|
|
22
18
|
};
|
|
23
|
-
// Read file-based sessions
|
|
24
19
|
let fileData;
|
|
25
20
|
try {
|
|
26
21
|
const fileContent = readFileSync(filePath, "utf-8");
|
|
@@ -30,11 +25,9 @@ export async function migrateFromFile(filePath, pgManager) {
|
|
|
30
25
|
throw new Error(`Failed to read sessions file: ${error instanceof Error ? error.message : String(error)}`);
|
|
31
26
|
}
|
|
32
27
|
console.error(`Found ${Object.keys(fileData.sessions).length} sessions to migrate`);
|
|
33
|
-
// Migrate sessions
|
|
34
28
|
for (const [id, session] of Object.entries(fileData.sessions)) {
|
|
35
29
|
try {
|
|
36
30
|
await pgManager.createSession(session.cli, session.description, session.id);
|
|
37
|
-
// Migrate metadata if present
|
|
38
31
|
if (session.metadata) {
|
|
39
32
|
await pgManager.updateSessionMetadata(id, session.metadata);
|
|
40
33
|
}
|
|
@@ -48,7 +41,6 @@ export async function migrateFromFile(filePath, pgManager) {
|
|
|
48
41
|
console.error(`✗ ${errorMsg}`);
|
|
49
42
|
}
|
|
50
43
|
}
|
|
51
|
-
// Restore active sessions
|
|
52
44
|
console.error("\nRestoring active sessions...");
|
|
53
45
|
for (const [cli, sessionId] of Object.entries(fileData.activeSession)) {
|
|
54
46
|
if (sessionId) {
|
|
@@ -65,9 +57,6 @@ export async function migrateFromFile(filePath, pgManager) {
|
|
|
65
57
|
}
|
|
66
58
|
return result;
|
|
67
59
|
}
|
|
68
|
-
/**
|
|
69
|
-
* Main CLI entry point
|
|
70
|
-
*/
|
|
71
60
|
async function main() {
|
|
72
61
|
const args = process.argv.slice(2);
|
|
73
62
|
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
@@ -85,7 +74,6 @@ Environment Variables:
|
|
|
85
74
|
`);
|
|
86
75
|
process.exit(args[0] === "--help" || args[0] === "-h" ? 0 : 1);
|
|
87
76
|
}
|
|
88
|
-
// Parse arguments
|
|
89
77
|
let filePath = join(homedir(), ".llm-cli-gateway", "sessions.json");
|
|
90
78
|
for (let i = 0; i < args.length; i++) {
|
|
91
79
|
if (args[i] === "--from" && args[i + 1]) {
|
|
@@ -97,19 +85,16 @@ Environment Variables:
|
|
|
97
85
|
console.error(` Source: ${filePath}`);
|
|
98
86
|
console.error(` DATABASE_URL: ${process.env.DATABASE_URL ? "[set]" : "[not set]"}`);
|
|
99
87
|
console.error("");
|
|
100
|
-
// Load config
|
|
101
88
|
const config = loadConfig();
|
|
102
89
|
if (!config.database) {
|
|
103
90
|
console.error("ERROR: DATABASE_URL must be set");
|
|
104
91
|
process.exit(1);
|
|
105
92
|
}
|
|
106
|
-
// Connect to database
|
|
107
93
|
console.error("Connecting to database...");
|
|
108
94
|
const db = await createDatabaseConnection(config, logger);
|
|
109
95
|
const pgManager = new PostgreSQLSessionManager(db.getPool());
|
|
110
96
|
console.error("✓ Connected to database\n");
|
|
111
97
|
try {
|
|
112
|
-
// Run migration
|
|
113
98
|
console.error("Starting migration...\n");
|
|
114
99
|
const result = await migrateFromFile(filePath, pgManager);
|
|
115
100
|
console.error("\n" + "=".repeat(50));
|
|
@@ -137,7 +122,6 @@ Environment Variables:
|
|
|
137
122
|
await db.disconnect();
|
|
138
123
|
}
|
|
139
124
|
}
|
|
140
|
-
// Run if executed directly
|
|
141
125
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
142
126
|
main();
|
|
143
127
|
}
|
package/dist/migrate.js
CHANGED
|
@@ -4,14 +4,11 @@ import { join, dirname } from "path";
|
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = dirname(__filename);
|
|
7
|
-
/**
|
|
8
|
-
* Load all migration files from migrations directory
|
|
9
|
-
*/
|
|
10
7
|
function loadMigrations() {
|
|
11
8
|
const migrationsDir = join(__dirname, "..", "migrations");
|
|
12
9
|
const files = readdirSync(migrationsDir)
|
|
13
10
|
.filter(f => f.endsWith(".sql"))
|
|
14
|
-
.sort();
|
|
11
|
+
.sort();
|
|
15
12
|
return files.map(file => {
|
|
16
13
|
const match = file.match(/^(\d+)_(.+)\.sql$/);
|
|
17
14
|
if (!match) {
|
|
@@ -23,11 +20,7 @@ function loadMigrations() {
|
|
|
23
20
|
return { version, name, sql };
|
|
24
21
|
});
|
|
25
22
|
}
|
|
26
|
-
/**
|
|
27
|
-
* Get list of applied migrations
|
|
28
|
-
*/
|
|
29
23
|
async function getAppliedMigrations(pool) {
|
|
30
|
-
// First, ensure schema_migrations table exists
|
|
31
24
|
await pool.query(`
|
|
32
25
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
33
26
|
version INTEGER PRIMARY KEY,
|
|
@@ -38,9 +31,6 @@ async function getAppliedMigrations(pool) {
|
|
|
38
31
|
const result = await pool.query("SELECT version FROM schema_migrations ORDER BY version");
|
|
39
32
|
return result.rows.map(row => row.version);
|
|
40
33
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Run a single migration
|
|
43
|
-
*/
|
|
44
34
|
async function runMigration(pool, migration) {
|
|
45
35
|
console.error(`Running migration ${migration.version}: ${migration.name}...`);
|
|
46
36
|
const client = await pool.connect();
|
|
@@ -58,9 +48,6 @@ async function runMigration(pool, migration) {
|
|
|
58
48
|
client.release();
|
|
59
49
|
}
|
|
60
50
|
}
|
|
61
|
-
/**
|
|
62
|
-
* Main migration runner
|
|
63
|
-
*/
|
|
64
51
|
async function main() {
|
|
65
52
|
const databaseUrl = process.env.DATABASE_URL;
|
|
66
53
|
if (!databaseUrl) {
|
|
@@ -70,20 +57,16 @@ async function main() {
|
|
|
70
57
|
const { Pool } = await importOptionalPg();
|
|
71
58
|
const pool = new Pool({ connectionString: databaseUrl });
|
|
72
59
|
try {
|
|
73
|
-
// Load migrations
|
|
74
60
|
const migrations = loadMigrations();
|
|
75
61
|
console.error(`Found ${migrations.length} migration(s)`);
|
|
76
|
-
// Get applied migrations
|
|
77
62
|
const applied = await getAppliedMigrations(pool);
|
|
78
63
|
console.error(`${applied.length} migration(s) already applied`);
|
|
79
|
-
// Filter pending migrations
|
|
80
64
|
const pending = migrations.filter(m => !applied.includes(m.version));
|
|
81
65
|
if (pending.length === 0) {
|
|
82
66
|
console.error("✓ All migrations up to date");
|
|
83
67
|
return;
|
|
84
68
|
}
|
|
85
69
|
console.error(`Running ${pending.length} pending migration(s)...`);
|
|
86
|
-
// Run pending migrations
|
|
87
70
|
for (const migration of pending) {
|
|
88
71
|
await runMigration(pool, migration);
|
|
89
72
|
}
|
|
@@ -1,37 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Phase 4 slice β — Mistral Vibe `meta.json` parser.
|
|
3
|
-
*
|
|
4
|
-
* Vibe writes per-session telemetry to
|
|
5
|
-
*
|
|
6
|
-
* ~/.vibe/logs/session/session_<YYYYMMDD>_<HHMMSS>_<first8hex>/meta.json
|
|
7
|
-
*
|
|
8
|
-
* where `<first8hex>` is the first 8 lowercase hex characters of the full
|
|
9
|
-
* session UUID. Inside the file:
|
|
10
|
-
*
|
|
11
|
-
* {
|
|
12
|
-
* "session_id": "<full-uuid>",
|
|
13
|
-
* "stats": {
|
|
14
|
-
* "session_prompt_tokens": <number> → inputTokens
|
|
15
|
-
* "session_completion_tokens": <number> → outputTokens
|
|
16
|
-
* "session_cost": <number> → costUsd
|
|
17
|
-
* }
|
|
18
|
-
* }
|
|
19
|
-
*
|
|
20
|
-
* The gateway's mistral session-id surface accepts the full UUID (so does
|
|
21
|
-
* `vibe --resume <uuid>`). To find the right directory we glob for
|
|
22
|
-
* `session_*_<first8>` and disambiguate by reading each candidate's
|
|
23
|
-
* `session_id` field. If callers happen to pass the directory basename
|
|
24
|
-
* itself we still honour that — useful for tests and for forward-compat if
|
|
25
|
-
* Vibe ever changes its dir naming scheme.
|
|
26
|
-
*
|
|
27
|
-
* Cache-token surfaces are not exposed by Vibe today, so `cacheReadTokens`
|
|
28
|
-
* and `cacheCreationTokens` are intentionally absent.
|
|
29
|
-
*
|
|
30
|
-
* Best-effort by design: any failure (missing file, bad JSON, missing
|
|
31
|
-
* fields, gateway-generated `gw-*` sessionId, unresolvable UUID, path
|
|
32
|
-
* outside the session log root) returns `{}` so the flight-recorder row
|
|
33
|
-
* simply lacks usage data.
|
|
34
|
-
*/
|
|
35
1
|
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "fs";
|
|
36
2
|
import { join, resolve, sep } from "path";
|
|
37
3
|
import { GATEWAY_SESSION_PREFIX } from "./request-helpers.js";
|
|
@@ -41,12 +7,6 @@ function asPositiveNumber(value) {
|
|
|
41
7
|
}
|
|
42
8
|
return value;
|
|
43
9
|
}
|
|
44
|
-
/**
|
|
45
|
-
* Read a file only if its realpath lives under `realBase`. Returns undefined
|
|
46
|
-
* on any error, missing file, or out-of-tree symlink target. This is the one
|
|
47
|
-
* place that calls `readFileSync` for meta.json content — the rest of the
|
|
48
|
-
* module routes through it so the security boundary is uniform.
|
|
49
|
-
*/
|
|
50
10
|
function readInBase(realBase, candidate) {
|
|
51
11
|
if (!existsSync(candidate))
|
|
52
12
|
return undefined;
|
|
@@ -67,30 +27,12 @@ function readInBase(realBase, candidate) {
|
|
|
67
27
|
return undefined;
|
|
68
28
|
}
|
|
69
29
|
}
|
|
70
|
-
// UUID v4-ish (Vibe's own session UUIDs are not strictly v4, so we
|
|
71
|
-
// validate against the broader 8-4-4-4-12 lowercase-hex shape) OR
|
|
72
|
-
// Vibe's session_<digits>_<digits>_<first8> directory basename.
|
|
73
30
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
74
31
|
const DIRNAME_RE = /^session_\d{8}_\d{6}_[0-9a-f]{8}$/;
|
|
75
|
-
/**
|
|
76
|
-
* Resolve the session-log directory basename for a given gateway sessionId.
|
|
77
|
-
* Returns undefined when no candidate can be found or the input is
|
|
78
|
-
* unsuitable. Pure with respect to side-effects on the caller — only reads
|
|
79
|
-
* the filesystem.
|
|
80
|
-
*
|
|
81
|
-
* Security invariants enforced here:
|
|
82
|
-
* - Inputs are charset-gated (UUID or DIRNAME) before any filesystem read.
|
|
83
|
-
* - For UUID input, the chosen candidate's meta.json MUST advertise the
|
|
84
|
-
* same `session_id` — single-candidate is NOT trusted, because two
|
|
85
|
-
* UUIDs sharing the first 8 hex chars would otherwise cross-attribute
|
|
86
|
-
* usage (and leak telemetry to the caller of the other session).
|
|
87
|
-
*/
|
|
88
32
|
function resolveVibeSessionDirname(baseDir, realBase, sessionId) {
|
|
89
|
-
// 1. Caller already supplied the directory name verbatim.
|
|
90
33
|
if (DIRNAME_RE.test(sessionId) && existsSync(join(baseDir, sessionId, "meta.json"))) {
|
|
91
34
|
return sessionId;
|
|
92
35
|
}
|
|
93
|
-
// 2. Treat the input as a full session UUID.
|
|
94
36
|
if (!UUID_RE.test(sessionId))
|
|
95
37
|
return undefined;
|
|
96
38
|
const short = sessionId.slice(0, 8).toLowerCase();
|
|
@@ -101,8 +43,6 @@ function resolveVibeSessionDirname(baseDir, realBase, sessionId) {
|
|
|
101
43
|
catch {
|
|
102
44
|
return undefined;
|
|
103
45
|
}
|
|
104
|
-
// Filter to candidates matching `session_*_<short>`. Sort newest-first
|
|
105
|
-
// by mtime; we still require an exact session_id match below.
|
|
106
46
|
const candidates = entries
|
|
107
47
|
.filter(name => DIRNAME_RE.test(name) && name.endsWith(`_${short}`))
|
|
108
48
|
.map(name => {
|
|
@@ -111,7 +51,6 @@ function resolveVibeSessionDirname(baseDir, realBase, sessionId) {
|
|
|
111
51
|
mtimeMs = statSync(join(baseDir, name)).mtimeMs;
|
|
112
52
|
}
|
|
113
53
|
catch {
|
|
114
|
-
/* ignore */
|
|
115
54
|
}
|
|
116
55
|
return { name, mtimeMs };
|
|
117
56
|
})
|
|
@@ -127,7 +66,6 @@ function resolveVibeSessionDirname(baseDir, realBase, sessionId) {
|
|
|
127
66
|
}
|
|
128
67
|
}
|
|
129
68
|
catch {
|
|
130
|
-
/* ignore and continue */
|
|
131
69
|
}
|
|
132
70
|
}
|
|
133
71
|
return undefined;
|
|
@@ -136,7 +74,6 @@ export function parseVibeMetaJson(home, sessionId) {
|
|
|
136
74
|
if (!sessionId)
|
|
137
75
|
return {};
|
|
138
76
|
if (sessionId.startsWith(GATEWAY_SESSION_PREFIX)) {
|
|
139
|
-
// gw-* IDs are gateway internal — Vibe never wrote a meta.json under that name.
|
|
140
77
|
return {};
|
|
141
78
|
}
|
|
142
79
|
const baseDir = resolve(join(home, ".vibe", "logs", "session"));
|
|
@@ -150,10 +87,6 @@ export function parseVibeMetaJson(home, sessionId) {
|
|
|
150
87
|
const dirname = resolveVibeSessionDirname(baseDir, realBase, sessionId);
|
|
151
88
|
if (!dirname)
|
|
152
89
|
return {};
|
|
153
|
-
// `readInBase` is the security boundary: it realpath-resolves the file
|
|
154
|
-
// and rejects anything whose target lives outside `realBase`. Re-routing
|
|
155
|
-
// the final read through it (instead of a bespoke readFileSync) keeps
|
|
156
|
-
// the in-tree-only invariant in one place.
|
|
157
90
|
const text = readInBase(realBase, join(baseDir, dirname, "meta.json"));
|
|
158
91
|
if (text === undefined)
|
|
159
92
|
return {};
|