llm-cli-gateway 1.17.3 → 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 +45 -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 +12 -208
- package/dist/index.js +116 -588
- 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 +6 -31
- 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 +4 -316
- package/dist/request-helpers.js +13 -231
- 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 +86 -64
- 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/retry.js
CHANGED
|
@@ -1,72 +1,35 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A module for adding retry and circuit breaker logic to asynchronous operations.
|
|
3
|
-
*
|
|
4
|
-
* @module retry
|
|
5
|
-
*/
|
|
6
|
-
/**
|
|
7
|
-
* Defines the possible states of the circuit breaker.
|
|
8
|
-
*/
|
|
9
1
|
export var CircuitBreakerState;
|
|
10
2
|
(function (CircuitBreakerState) {
|
|
11
|
-
/** The circuit is closed and allows operations to execute. */
|
|
12
3
|
CircuitBreakerState["CLOSED"] = "CLOSED";
|
|
13
|
-
/** The circuit is open and fails operations immediately. */
|
|
14
4
|
CircuitBreakerState["OPEN"] = "OPEN";
|
|
15
|
-
/** The circuit is half-open and allows a single trial operation. */
|
|
16
5
|
CircuitBreakerState["HALF_OPEN"] = "HALF_OPEN";
|
|
17
6
|
})(CircuitBreakerState || (CircuitBreakerState = {}));
|
|
18
|
-
/**
|
|
19
|
-
* Default function to determine if an error is transient.
|
|
20
|
-
* Retries on timeout (exit code 124) and common network errors.
|
|
21
|
-
* Does not retry on file-not-found (ENOENT) or other errors.
|
|
22
|
-
* @param error The error object.
|
|
23
|
-
* @returns True if the error is considered transient.
|
|
24
|
-
*/
|
|
25
7
|
const isDefaultTransient = (error) => {
|
|
26
8
|
if (!error) {
|
|
27
9
|
return false;
|
|
28
10
|
}
|
|
29
|
-
// Shell command-related errors
|
|
30
11
|
if (error.code === 124) {
|
|
31
|
-
// wall-clock timeout (explicit, caller-set) — transient
|
|
32
12
|
return true;
|
|
33
13
|
}
|
|
34
|
-
// Note: exit code 125 = idle timeout (stuck process) — intentionally non-transient
|
|
35
14
|
if (error.code === "ENOENT") {
|
|
36
|
-
// command not found
|
|
37
15
|
return false;
|
|
38
16
|
}
|
|
39
|
-
// Node.js network errors
|
|
40
17
|
const transientErrorCodes = ["ECONNRESET", "ETIMEDOUT", "ECONNREFUSED", "EPIPE"];
|
|
41
18
|
if (transientErrorCodes.includes(error.code)) {
|
|
42
19
|
return true;
|
|
43
20
|
}
|
|
44
21
|
return false;
|
|
45
22
|
};
|
|
46
|
-
/**
|
|
47
|
-
* Creates a new CircuitBreaker instance with default settings.
|
|
48
|
-
* @param options Partial options to override defaults.
|
|
49
|
-
* @returns A new CircuitBreaker instance.
|
|
50
|
-
*/
|
|
51
23
|
export function createCircuitBreaker(options) {
|
|
52
24
|
return {
|
|
53
25
|
state: CircuitBreakerState.CLOSED,
|
|
54
26
|
failures: 0,
|
|
55
27
|
lastFailureTime: null,
|
|
56
|
-
resetTimeout: options?.resetTimeout ?? 60000,
|
|
28
|
+
resetTimeout: options?.resetTimeout ?? 60000,
|
|
57
29
|
failureThreshold: options?.failureThreshold ?? 5,
|
|
58
30
|
onStateChange: options?.onStateChange,
|
|
59
31
|
};
|
|
60
32
|
}
|
|
61
|
-
/**
|
|
62
|
-
* Wraps an asynchronous operation with retry and circuit breaker logic.
|
|
63
|
-
*
|
|
64
|
-
* @template T The return type of the operation.
|
|
65
|
-
* @param {() => Promise<T>} operation The asynchronous operation to execute.
|
|
66
|
-
* @param {CircuitBreaker} circuitBreaker The circuit breaker instance to use.
|
|
67
|
-
* @param {Partial<RetryOptions>} [retryOptions] Options for retry behavior.
|
|
68
|
-
* @returns {Promise<T>} A promise that resolves with the result of the operation.
|
|
69
|
-
*/
|
|
70
33
|
export async function withRetry(operation, circuitBreaker, retryOptions, logger) {
|
|
71
34
|
const wrapError = (message, error) => {
|
|
72
35
|
const wrapped = new Error(message);
|
|
@@ -82,8 +45,8 @@ export async function withRetry(operation, circuitBreaker, retryOptions, logger)
|
|
|
82
45
|
return wrapped;
|
|
83
46
|
};
|
|
84
47
|
const options = {
|
|
85
|
-
initialDelay: 1000,
|
|
86
|
-
maxDelay: 30000,
|
|
48
|
+
initialDelay: 1000,
|
|
49
|
+
maxDelay: 30000,
|
|
87
50
|
factor: 2,
|
|
88
51
|
isTransient: isDefaultTransient,
|
|
89
52
|
onRetry: (error, attempt, delay) => {
|
|
@@ -1,48 +1,16 @@
|
|
|
1
1
|
import type { Pool } from "pg";
|
|
2
2
|
import { Session, CliType } from "./session-manager.js";
|
|
3
3
|
export type { Logger } from "./logger.js";
|
|
4
|
-
/**
|
|
5
|
-
* PostgreSQL-backed session manager. PostgreSQL is the source of truth and
|
|
6
|
-
* the only required service for this backend.
|
|
7
|
-
*/
|
|
8
4
|
export declare class PostgreSQLSessionManager {
|
|
9
5
|
private pool;
|
|
10
6
|
constructor(pool: Pool);
|
|
11
|
-
/**
|
|
12
|
-
* Create a new session.
|
|
13
|
-
*/
|
|
14
7
|
createSession(cli: CliType, description?: string, sessionId?: string): Promise<Session>;
|
|
15
|
-
/**
|
|
16
|
-
* Get session by ID.
|
|
17
|
-
*/
|
|
18
8
|
getSession(sessionId: string): Promise<Session | null>;
|
|
19
|
-
/**
|
|
20
|
-
* List all sessions, optionally filtered by CLI.
|
|
21
|
-
*/
|
|
22
9
|
listSessions(cli?: CliType): Promise<Session[]>;
|
|
23
|
-
/**
|
|
24
|
-
* Delete a session.
|
|
25
|
-
*/
|
|
26
10
|
deleteSession(sessionId: string): Promise<boolean>;
|
|
27
|
-
/**
|
|
28
|
-
* Set active session for a CLI. The row-level update is serialized by
|
|
29
|
-
* PostgreSQL and the session FK keeps stale IDs from being recorded.
|
|
30
|
-
*/
|
|
31
11
|
setActiveSession(cli: CliType, sessionId: string | null): Promise<boolean>;
|
|
32
|
-
/**
|
|
33
|
-
* Get active session for a CLI.
|
|
34
|
-
*/
|
|
35
12
|
getActiveSession(cli: CliType): Promise<Session | null>;
|
|
36
|
-
/**
|
|
37
|
-
* Update session usage timestamp.
|
|
38
|
-
*/
|
|
39
13
|
updateSessionUsage(sessionId: string): Promise<void>;
|
|
40
|
-
/**
|
|
41
|
-
* Update session metadata using PostgreSQL's atomic JSONB merge.
|
|
42
|
-
*/
|
|
43
14
|
updateSessionMetadata(sessionId: string, metadata: Record<string, any>): Promise<boolean>;
|
|
44
|
-
/**
|
|
45
|
-
* Clear all sessions, optionally filtered by CLI.
|
|
46
|
-
*/
|
|
47
15
|
clearAllSessions(cli?: CliType): Promise<number>;
|
|
48
16
|
}
|
|
@@ -6,18 +6,11 @@ const DEFAULT_SESSION_DESCRIPTIONS = {
|
|
|
6
6
|
grok: "Grok Session",
|
|
7
7
|
mistral: "Mistral Session",
|
|
8
8
|
};
|
|
9
|
-
/**
|
|
10
|
-
* PostgreSQL-backed session manager. PostgreSQL is the source of truth and
|
|
11
|
-
* the only required service for this backend.
|
|
12
|
-
*/
|
|
13
9
|
export class PostgreSQLSessionManager {
|
|
14
10
|
pool;
|
|
15
11
|
constructor(pool) {
|
|
16
12
|
this.pool = pool;
|
|
17
13
|
}
|
|
18
|
-
/**
|
|
19
|
-
* Create a new session.
|
|
20
|
-
*/
|
|
21
14
|
async createSession(cli, description, sessionId) {
|
|
22
15
|
const id = sessionId || randomUUID();
|
|
23
16
|
const sessionDescription = description ?? DEFAULT_SESSION_DESCRIPTIONS[cli];
|
|
@@ -47,18 +40,12 @@ export class PostgreSQLSessionManager {
|
|
|
47
40
|
client.release();
|
|
48
41
|
}
|
|
49
42
|
}
|
|
50
|
-
/**
|
|
51
|
-
* Get session by ID.
|
|
52
|
-
*/
|
|
53
43
|
async getSession(sessionId) {
|
|
54
44
|
const result = await this.pool.query(`SELECT id, cli, description, metadata, created_at AS "createdAt", last_used_at AS "lastUsedAt"
|
|
55
45
|
FROM sessions
|
|
56
46
|
WHERE id = $1`, [sessionId]);
|
|
57
47
|
return result.rows[0] ?? null;
|
|
58
48
|
}
|
|
59
|
-
/**
|
|
60
|
-
* List all sessions, optionally filtered by CLI.
|
|
61
|
-
*/
|
|
62
49
|
async listSessions(cli) {
|
|
63
50
|
const query = cli
|
|
64
51
|
? `SELECT id, cli, description, metadata, created_at AS "createdAt", last_used_at AS "lastUsedAt"
|
|
@@ -73,9 +60,6 @@ export class PostgreSQLSessionManager {
|
|
|
73
60
|
: await this.pool.query(query);
|
|
74
61
|
return result.rows;
|
|
75
62
|
}
|
|
76
|
-
/**
|
|
77
|
-
* Delete a session.
|
|
78
|
-
*/
|
|
79
63
|
async deleteSession(sessionId) {
|
|
80
64
|
const session = await this.getSession(sessionId);
|
|
81
65
|
if (!session) {
|
|
@@ -84,10 +68,6 @@ export class PostgreSQLSessionManager {
|
|
|
84
68
|
const result = await this.pool.query("DELETE FROM sessions WHERE id = $1", [sessionId]);
|
|
85
69
|
return result.rowCount !== 0;
|
|
86
70
|
}
|
|
87
|
-
/**
|
|
88
|
-
* Set active session for a CLI. The row-level update is serialized by
|
|
89
|
-
* PostgreSQL and the session FK keeps stale IDs from being recorded.
|
|
90
|
-
*/
|
|
91
71
|
async setActiveSession(cli, sessionId) {
|
|
92
72
|
if (sessionId !== null) {
|
|
93
73
|
const session = await this.getSession(sessionId);
|
|
@@ -101,9 +81,6 @@ export class PostgreSQLSessionManager {
|
|
|
101
81
|
ON CONFLICT (cli) DO UPDATE SET session_id = $2, updated_at = $3`, [cli, sessionId, now]);
|
|
102
82
|
return true;
|
|
103
83
|
}
|
|
104
|
-
/**
|
|
105
|
-
* Get active session for a CLI.
|
|
106
|
-
*/
|
|
107
84
|
async getActiveSession(cli) {
|
|
108
85
|
const result = await this.pool.query("SELECT session_id FROM active_sessions WHERE cli = $1", [cli]);
|
|
109
86
|
const sessionId = result.rows[0]?.session_id;
|
|
@@ -112,16 +89,10 @@ export class PostgreSQLSessionManager {
|
|
|
112
89
|
}
|
|
113
90
|
return await this.getSession(sessionId);
|
|
114
91
|
}
|
|
115
|
-
/**
|
|
116
|
-
* Update session usage timestamp.
|
|
117
|
-
*/
|
|
118
92
|
async updateSessionUsage(sessionId) {
|
|
119
93
|
const now = new Date().toISOString();
|
|
120
94
|
await this.pool.query("UPDATE sessions SET last_used_at = $1 WHERE id = $2", [now, sessionId]);
|
|
121
95
|
}
|
|
122
|
-
/**
|
|
123
|
-
* Update session metadata using PostgreSQL's atomic JSONB merge.
|
|
124
|
-
*/
|
|
125
96
|
async updateSessionMetadata(sessionId, metadata) {
|
|
126
97
|
const result = await this.pool.query(`UPDATE sessions
|
|
127
98
|
SET metadata = COALESCE(metadata, '{}'::jsonb) || $1::jsonb
|
|
@@ -129,9 +100,6 @@ export class PostgreSQLSessionManager {
|
|
|
129
100
|
RETURNING id`, [JSON.stringify(metadata), sessionId]);
|
|
130
101
|
return result.rowCount !== 0;
|
|
131
102
|
}
|
|
132
|
-
/**
|
|
133
|
-
* Clear all sessions, optionally filtered by CLI.
|
|
134
|
-
*/
|
|
135
103
|
async clearAllSessions(cli) {
|
|
136
104
|
const query = cli ? "DELETE FROM sessions WHERE cli = $1" : "DELETE FROM sessions";
|
|
137
105
|
const result = cli ? await this.pool.query(query, [cli]) : await this.pool.query(query);
|
|
@@ -15,15 +15,6 @@ export interface SessionStorage {
|
|
|
15
15
|
sessions: Record<string, Session>;
|
|
16
16
|
activeSession: Record<CliType, string | null>;
|
|
17
17
|
}
|
|
18
|
-
/**
|
|
19
|
-
* Slice λ: callback invoked before a session record is removed (whether via
|
|
20
|
-
* explicit `deleteSession`, TTL eviction, or `clearAllSessions`). Used to
|
|
21
|
-
* tear down per-session resources owned by the gateway — currently git
|
|
22
|
-
* worktrees registered on `session.metadata.worktreePath`. The hook
|
|
23
|
-
* receives the path; promise failures are logged but do not block session
|
|
24
|
-
* removal (gateway-owned-lifecycle invariant: `session_delete` must always
|
|
25
|
-
* succeed for the caller).
|
|
26
|
-
*/
|
|
27
18
|
export type SessionCleanupHook = (session: Session) => void | Promise<void>;
|
|
28
19
|
export declare class FileSessionManager {
|
|
29
20
|
private storagePath;
|
|
@@ -52,11 +43,6 @@ export declare class FileSessionManager {
|
|
|
52
43
|
clearAllSessions(cli?: CliType): number;
|
|
53
44
|
}
|
|
54
45
|
export declare const SessionManager: typeof FileSessionManager;
|
|
55
|
-
/**
|
|
56
|
-
* Session manager interface supporting both sync (file) and async (PostgreSQL) backends.
|
|
57
|
-
* Methods return T | Promise<T> so both backends satisfy the contract.
|
|
58
|
-
* Callers must always use `await` for uniform handling.
|
|
59
|
-
*/
|
|
60
46
|
export interface ISessionManager {
|
|
61
47
|
createSession(cli: CliType, description?: string, sessionId?: string): Session | Promise<Session>;
|
|
62
48
|
getSession(sessionId: string): Session | null | Promise<Session | null>;
|
|
@@ -68,13 +54,6 @@ export interface ISessionManager {
|
|
|
68
54
|
updateSessionMetadata(sessionId: string, metadata: Record<string, any>): boolean | Promise<boolean>;
|
|
69
55
|
clearAllSessions(cli?: CliType): number | Promise<number>;
|
|
70
56
|
}
|
|
71
|
-
/**
|
|
72
|
-
* Factory function to create session manager
|
|
73
|
-
* Returns PostgreSQLSessionManager if config present, otherwise FileSessionManager
|
|
74
|
-
* @param config - Configuration object
|
|
75
|
-
* @param db - Optional pre-existing DatabaseConnection (avoids creating duplicate connections)
|
|
76
|
-
* @param logger - Logger instance for structured logging
|
|
77
|
-
*/
|
|
78
57
|
export declare function createSessionManager(config?: Config, db?: DatabaseConnection, logger?: Logger, opts?: {
|
|
79
58
|
cleanupHook?: SessionCleanupHook;
|
|
80
59
|
}): Promise<ISessionManager>;
|
package/dist/session-manager.js
CHANGED
|
@@ -45,7 +45,7 @@ export class FileSessionManager {
|
|
|
45
45
|
isExpired(session) {
|
|
46
46
|
const ts = new Date(session.lastUsedAt).getTime();
|
|
47
47
|
if (!Number.isFinite(ts))
|
|
48
|
-
return true;
|
|
48
|
+
return true;
|
|
49
49
|
return Date.now() - ts > this.sessionTtlMs;
|
|
50
50
|
}
|
|
51
51
|
evictExpiredSessions() {
|
|
@@ -77,7 +77,6 @@ export class FileSessionManager {
|
|
|
77
77
|
this.storage = JSON.parse(data);
|
|
78
78
|
}
|
|
79
79
|
catch {
|
|
80
|
-
// If file is corrupted, start fresh
|
|
81
80
|
this.storage = { sessions: {}, activeSession: createEmptyActiveSessions() };
|
|
82
81
|
}
|
|
83
82
|
}
|
|
@@ -113,7 +112,6 @@ export class FileSessionManager {
|
|
|
113
112
|
description: sessionDescription,
|
|
114
113
|
};
|
|
115
114
|
this.storage.sessions[id] = session;
|
|
116
|
-
// Set as active session if none exists for this CLI
|
|
117
115
|
if (!this.storage.activeSession[cli]) {
|
|
118
116
|
this.storage.activeSession[cli] = id;
|
|
119
117
|
}
|
|
@@ -145,7 +143,6 @@ export class FileSessionManager {
|
|
|
145
143
|
const session = this.storage.sessions[sessionId];
|
|
146
144
|
this.invokeCleanupHook(session);
|
|
147
145
|
delete this.storage.sessions[sessionId];
|
|
148
|
-
// If this was the active session, clear it
|
|
149
146
|
if (this.storage.activeSession[session.cli] === sessionId) {
|
|
150
147
|
this.storage.activeSession[session.cli] = null;
|
|
151
148
|
}
|
|
@@ -220,20 +217,10 @@ export class FileSessionManager {
|
|
|
220
217
|
return sessionsToDelete.length;
|
|
221
218
|
}
|
|
222
219
|
}
|
|
223
|
-
// Maintain backward compatibility
|
|
224
220
|
export const SessionManager = FileSessionManager;
|
|
225
|
-
/**
|
|
226
|
-
* Factory function to create session manager
|
|
227
|
-
* Returns PostgreSQLSessionManager if config present, otherwise FileSessionManager
|
|
228
|
-
* @param config - Configuration object
|
|
229
|
-
* @param db - Optional pre-existing DatabaseConnection (avoids creating duplicate connections)
|
|
230
|
-
* @param logger - Logger instance for structured logging
|
|
231
|
-
*/
|
|
232
221
|
export async function createSessionManager(config, db, logger, opts) {
|
|
233
222
|
if (config?.database) {
|
|
234
|
-
// Import dynamically to avoid loading pg if not needed.
|
|
235
223
|
const { PostgreSQLSessionManager } = await import("./session-manager-pg.js");
|
|
236
|
-
// Use provided db connection or create new one
|
|
237
224
|
if (!db) {
|
|
238
225
|
const { createDatabaseConnection } = await import("./db.js");
|
|
239
226
|
db = await createDatabaseConnection(config, logger);
|
|
@@ -241,7 +228,6 @@ export async function createSessionManager(config, db, logger, opts) {
|
|
|
241
228
|
return new PostgreSQLSessionManager(db.getPool());
|
|
242
229
|
}
|
|
243
230
|
else {
|
|
244
|
-
// Use file-based storage with TTL from config
|
|
245
231
|
const sessionTtlMs = config?.sessionTtl
|
|
246
232
|
? config.sessionTtl * 1000
|
|
247
233
|
: DEFAULT_SESSION_TTL_SECONDS * 1000;
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* NDJSON parser for Claude `--output-format stream-json --include-partial-messages`.
|
|
3
|
-
*
|
|
4
|
-
* Each line of stdout is a complete JSON object. This parser extracts the
|
|
5
|
-
* final result text, cost, usage, and metadata from the stream.
|
|
6
|
-
*/
|
|
7
1
|
export interface StreamJsonUsage {
|
|
8
2
|
inputTokens: number;
|
|
9
3
|
outputTokens: number;
|
|
@@ -20,16 +14,4 @@ export interface StreamJsonResult {
|
|
|
20
14
|
isError: boolean;
|
|
21
15
|
numTurns: number | null;
|
|
22
16
|
}
|
|
23
|
-
/**
|
|
24
|
-
* Parse completed NDJSON stdout from `claude --output-format stream-json --include-partial-messages`.
|
|
25
|
-
*
|
|
26
|
-
* Parsing strategy:
|
|
27
|
-
* 1. Split by newlines, filter empty lines
|
|
28
|
-
* 2. JSON.parse each line, skip malformed lines
|
|
29
|
-
* 3. Find the `type=result` event — contains final text, cost, usage
|
|
30
|
-
* 4. Fall back to the last `type=assistant` event if no result event
|
|
31
|
-
* 5. Extract `model` from `type=system` (init) event
|
|
32
|
-
*
|
|
33
|
-
* No rawEvents stored — the stdout buffer is already in memory.
|
|
34
|
-
*/
|
|
35
17
|
export declare function parseStreamJson(stdout: string): StreamJsonResult;
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* NDJSON parser for Claude `--output-format stream-json --include-partial-messages`.
|
|
3
|
-
*
|
|
4
|
-
* Each line of stdout is a complete JSON object. This parser extracts the
|
|
5
|
-
* final result text, cost, usage, and metadata from the stream.
|
|
6
|
-
*/
|
|
7
1
|
function stringOrNull(value) {
|
|
8
2
|
return typeof value === "string" ? value : null;
|
|
9
3
|
}
|
|
@@ -13,18 +7,6 @@ function numberOrNull(value) {
|
|
|
13
7
|
function numberOrZero(value) {
|
|
14
8
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
15
9
|
}
|
|
16
|
-
/**
|
|
17
|
-
* Parse completed NDJSON stdout from `claude --output-format stream-json --include-partial-messages`.
|
|
18
|
-
*
|
|
19
|
-
* Parsing strategy:
|
|
20
|
-
* 1. Split by newlines, filter empty lines
|
|
21
|
-
* 2. JSON.parse each line, skip malformed lines
|
|
22
|
-
* 3. Find the `type=result` event — contains final text, cost, usage
|
|
23
|
-
* 4. Fall back to the last `type=assistant` event if no result event
|
|
24
|
-
* 5. Extract `model` from `type=system` (init) event
|
|
25
|
-
*
|
|
26
|
-
* No rawEvents stored — the stdout buffer is already in memory.
|
|
27
|
-
*/
|
|
28
10
|
export function parseStreamJson(stdout) {
|
|
29
11
|
const lines = stdout.split("\n").filter(line => line.trim().length > 0);
|
|
30
12
|
let resultEvent = null;
|
|
@@ -36,7 +18,6 @@ export function parseStreamJson(stdout) {
|
|
|
36
18
|
parsed = JSON.parse(line);
|
|
37
19
|
}
|
|
38
20
|
catch {
|
|
39
|
-
// Skip malformed lines
|
|
40
21
|
continue;
|
|
41
22
|
}
|
|
42
23
|
if (!parsed || typeof parsed !== "object") {
|
|
@@ -52,7 +33,6 @@ export function parseStreamJson(stdout) {
|
|
|
52
33
|
systemEvent = parsed;
|
|
53
34
|
}
|
|
54
35
|
}
|
|
55
|
-
// Extract from result event (preferred)
|
|
56
36
|
if (resultEvent) {
|
|
57
37
|
const usage = resultEvent.usage
|
|
58
38
|
? {
|
|
@@ -73,7 +53,6 @@ export function parseStreamJson(stdout) {
|
|
|
73
53
|
numTurns: numberOrNull(resultEvent.num_turns),
|
|
74
54
|
};
|
|
75
55
|
}
|
|
76
|
-
// Fallback: extract text from assistant event
|
|
77
56
|
if (assistantEvent) {
|
|
78
57
|
const message = assistantEvent.message;
|
|
79
58
|
let text = "";
|
|
@@ -97,7 +76,6 @@ export function parseStreamJson(stdout) {
|
|
|
97
76
|
numTurns: null,
|
|
98
77
|
};
|
|
99
78
|
}
|
|
100
|
-
// No result or assistant event found — return empty
|
|
101
79
|
return {
|
|
102
80
|
text: "",
|
|
103
81
|
costUsd: null,
|
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
import type { CliType } from "./session-manager.js";
|
|
2
|
-
/**
|
|
3
|
-
* `optional` (slice κ): consumes the next token as the flag's value
|
|
4
|
-
* ONLY if that token does not start with `-`. Used for Claude's
|
|
5
|
-
* `-p`/`--print`, which is a no-arg switch in claude-code 2.x but
|
|
6
|
-
* also doubles as the legacy `-p <prompt>` positional shorthand that
|
|
7
|
-
* the gateway has emitted since v0.x.
|
|
8
|
-
*/
|
|
9
2
|
export type CliFlagArity = "none" | "one" | "optional" | "variadic";
|
|
10
3
|
export interface CliFlagContract {
|
|
11
4
|
arity: CliFlagArity;
|
|
@@ -13,41 +6,12 @@ export interface CliFlagContract {
|
|
|
13
6
|
pattern?: RegExp;
|
|
14
7
|
description: string;
|
|
15
8
|
}
|
|
16
|
-
/**
|
|
17
|
-
* Pure upstream-tracking metadata for a provider CLI.
|
|
18
|
-
*
|
|
19
|
-
* IMPORTANT — non-duplication invariant: nothing here encodes mechanical
|
|
20
|
-
* behaviour. Flags, output modes, session/resume rules, permission modes,
|
|
21
|
-
* forbidden flags, env contracts, and positional limits live ONLY in the
|
|
22
|
-
* surrounding {@link CliContract} and are validated ONLY by
|
|
23
|
-
* {@link validateUpstreamCliArgs} / {@link validateUpstreamCliEnv}. The fields
|
|
24
|
-
* below are descriptive pointers used by the upstream changelog scanner
|
|
25
|
-
* (`scripts/upstream-scan.mjs`) and surfaced in the contract report — they
|
|
26
|
-
* never drive argv/env enforcement.
|
|
27
|
-
*
|
|
28
|
-
* `docs/upstream/provider-sources.dag.toml` mirrors `sourceUrls` and
|
|
29
|
-
* `watchCategories` for the scanner's offline scan plan; a unit test
|
|
30
|
-
* (`upstream-sources.test.ts`) asserts the TOML stays in sync with these
|
|
31
|
-
* fields so the two cannot drift. The TypeScript values here are authoritative;
|
|
32
|
-
* the TOML is scanner input only and is never consulted for contract
|
|
33
|
-
* enforcement.
|
|
34
|
-
*/
|
|
35
9
|
export interface CliUpstreamMetadata {
|
|
36
|
-
/** Canonical changelog / release-notes URLs the scanner retrieves with --live. */
|
|
37
10
|
sourceUrls: readonly string[];
|
|
38
|
-
/** Distribution package identifier (npm package name, PyPI project, …). */
|
|
39
11
|
packageName?: string;
|
|
40
|
-
/** Source repository URL, when distinct from the changelog source. */
|
|
41
12
|
repo?: string;
|
|
42
|
-
/** Human-facing install / getting-started docs. */
|
|
43
13
|
installDocsUrl?: string;
|
|
44
|
-
/** Distribution channel the gateway expects the CLI to ship through. */
|
|
45
14
|
releaseChannel?: "npm" | "pypi" | "github-release" | "vendor";
|
|
46
|
-
/**
|
|
47
|
-
* Contract surfaces worth watching in upstream release notes (e.g. "flags",
|
|
48
|
-
* "output-formats", "session-resume"). Descriptive labels for the scanner and
|
|
49
|
-
* report ONLY — never a validation input.
|
|
50
|
-
*/
|
|
51
15
|
watchCategories: readonly string[];
|
|
52
16
|
}
|
|
53
17
|
export interface CliContract {
|
|
@@ -68,7 +32,6 @@ export interface CliContract {
|
|
|
68
32
|
resumeMaxPositionals?: number;
|
|
69
33
|
resumeOnlyFlags?: readonly string[];
|
|
70
34
|
resumeForbiddenFlags?: readonly string[];
|
|
71
|
-
/** Non-mechanical upstream-tracking metadata. See {@link CliUpstreamMetadata}. */
|
|
72
35
|
upstreamMetadata?: CliUpstreamMetadata;
|
|
73
36
|
}
|
|
74
37
|
export interface CliContractFixture {
|
|
@@ -93,19 +56,6 @@ export declare function validateUpstreamCliArgs(cli: CliType, args: readonly str
|
|
|
93
56
|
export declare function assertUpstreamCliArgs(cli: CliType, args: readonly string[]): void;
|
|
94
57
|
export declare function validateUpstreamCliEnv(cli: CliType, env: Record<string, string> | undefined): ContractValidationResult;
|
|
95
58
|
export declare function assertUpstreamCliEnv(cli: CliType, env: Record<string, string> | undefined): void;
|
|
96
|
-
/**
|
|
97
|
-
* Best-effort, advisory-only extraction of long-form flags from raw --help text.
|
|
98
|
-
* Returns a sorted array of unique `--foo-bar` style flags discovered in the output.
|
|
99
|
-
*
|
|
100
|
-
* Heuristics:
|
|
101
|
-
* - Matches common option declaration lines emitted by clap, yargs, commander, custom TUIs, etc.
|
|
102
|
-
* - Lowercases for stable comparison against our contract keys.
|
|
103
|
-
* - Intentionally conservative: ignores obvious noise (URLs, prose in descriptions).
|
|
104
|
-
*
|
|
105
|
-
* This powers the bidirectional drift detector (extra flags the installed binary
|
|
106
|
-
* advertises that our contract does not yet allow). It is NEVER used for argv
|
|
107
|
-
* validation — only for the upstream scanner and `upstream_contracts` probe reports.
|
|
108
|
-
*/
|
|
109
59
|
export declare function extractDiscoveredFlags(helpText: string): readonly string[];
|
|
110
60
|
export interface InstalledCliContractProbe {
|
|
111
61
|
cli: CliType;
|
|
@@ -115,15 +65,10 @@ export interface InstalledCliContractProbe {
|
|
|
115
65
|
available: boolean;
|
|
116
66
|
checkedHelpCommands: string[][];
|
|
117
67
|
missingFlags: string[];
|
|
118
|
-
/** Flags present in the installed binary's --help but absent from the declared contract. */
|
|
119
68
|
extraFlags: readonly string[];
|
|
120
|
-
/** Sorted list of long flags discovered in the help text (for snapshot diffing). */
|
|
121
69
|
discoveredFlags: readonly string[];
|
|
122
|
-
/** Stable hash of the concatenated help output (detects subtle text changes even if flag set is stable). */
|
|
123
70
|
helpHash?: string;
|
|
124
|
-
/** Best-effort version string scraped from the help/version output (if present). */
|
|
125
71
|
versionHint?: string;
|
|
126
|
-
/** ISO timestamp when this probe was performed. */
|
|
127
72
|
probedAt: string;
|
|
128
73
|
warnings: string[];
|
|
129
74
|
}
|