@ai-devkit/agent-manager 0.33.1 → 0.34.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/dist/__tests__/capacity/codex.test.js +2 -1
- package/dist/__tests__/capacity/codex.test.js.map +1 -1
- package/dist/__tests__/capacity/index.test.js +6 -3
- package/dist/__tests__/capacity/index.test.js.map +1 -1
- package/dist/__tests__/capacity/zai.test.js +213 -0
- package/dist/__tests__/capacity/zai.test.js.map +1 -0
- package/dist/__tests__/providers/opencode/OpenCodeAdapter.test.js +189 -0
- package/dist/__tests__/providers/opencode/OpenCodeAdapter.test.js.map +1 -0
- package/dist/__tests__/providers/opencode/OpenCodeAgentMapper.test.js +89 -0
- package/dist/__tests__/providers/opencode/OpenCodeAgentMapper.test.js.map +1 -0
- package/dist/__tests__/providers/opencode/OpenCodeSessionLocator.test.js +94 -0
- package/dist/__tests__/providers/opencode/OpenCodeSessionLocator.test.js.map +1 -0
- package/dist/__tests__/providers/opencode/OpenCodeSessionParser.test.js +147 -0
- package/dist/__tests__/providers/opencode/OpenCodeSessionParser.test.js.map +1 -0
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +1 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/capacity/codex.d.ts +1 -0
- package/dist/capacity/codex.d.ts.map +1 -1
- package/dist/capacity/codex.js +9 -4
- package/dist/capacity/codex.js.map +1 -1
- package/dist/capacity/index.d.ts +8 -0
- package/dist/capacity/index.d.ts.map +1 -1
- package/dist/capacity/index.js +10 -9
- package/dist/capacity/index.js.map +1 -1
- package/dist/capacity/types.d.ts +8 -0
- package/dist/capacity/types.d.ts.map +1 -1
- package/dist/capacity/types.js.map +1 -1
- package/dist/capacity/zai.d.ts +14 -0
- package/dist/capacity/zai.d.ts.map +1 -0
- package/dist/capacity/zai.js +203 -0
- package/dist/capacity/zai.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/{adapters → providers/opencode}/OpenCodeAdapter.d.ts +5 -12
- package/dist/providers/opencode/OpenCodeAdapter.d.ts.map +1 -0
- package/dist/providers/opencode/OpenCodeAdapter.js +86 -0
- package/dist/providers/opencode/OpenCodeAdapter.js.map +1 -0
- package/dist/providers/opencode/OpenCodeAgentMapper.d.ts +11 -0
- package/dist/providers/opencode/OpenCodeAgentMapper.d.ts.map +1 -0
- package/dist/providers/opencode/OpenCodeAgentMapper.js +50 -0
- package/dist/providers/opencode/OpenCodeAgentMapper.js.map +1 -0
- package/dist/providers/opencode/OpenCodeSessionLocator.d.ts +21 -0
- package/dist/providers/opencode/OpenCodeSessionLocator.d.ts.map +1 -0
- package/dist/providers/opencode/OpenCodeSessionLocator.js +99 -0
- package/dist/providers/opencode/OpenCodeSessionLocator.js.map +1 -0
- package/dist/providers/opencode/OpenCodeSessionParser.d.ts +18 -0
- package/dist/providers/opencode/OpenCodeSessionParser.d.ts.map +1 -0
- package/dist/providers/opencode/OpenCodeSessionParser.js +104 -0
- package/dist/providers/opencode/OpenCodeSessionParser.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/capacity/codex.test.ts +94 -26
- package/src/__tests__/capacity/fixtures/zai-auth.json +6 -0
- package/src/__tests__/capacity/fixtures/zai-quota-invalid.json +7 -0
- package/src/__tests__/capacity/fixtures/zai-quota.json +38 -0
- package/src/__tests__/capacity/index.test.ts +10 -3
- package/src/__tests__/capacity/zai.test.ts +186 -0
- package/src/__tests__/providers/opencode/OpenCodeAdapter.test.ts +197 -0
- package/src/__tests__/providers/opencode/OpenCodeAgentMapper.test.ts +83 -0
- package/src/__tests__/providers/opencode/OpenCodeSessionLocator.test.ts +84 -0
- package/src/__tests__/providers/opencode/OpenCodeSessionParser.test.ts +148 -0
- package/src/adapters/index.ts +1 -1
- package/src/capacity/codex.ts +96 -28
- package/src/capacity/index.ts +29 -11
- package/src/capacity/types.ts +8 -0
- package/src/capacity/zai.ts +237 -0
- package/src/index.ts +8 -3
- package/src/providers/opencode/OpenCodeAdapter.ts +112 -0
- package/src/providers/opencode/OpenCodeAgentMapper.ts +61 -0
- package/src/providers/opencode/OpenCodeSessionLocator.ts +123 -0
- package/src/providers/opencode/OpenCodeSessionParser.ts +145 -0
- package/dist/__tests__/adapters/OpenCodeAdapter.test.js +0 -636
- package/dist/__tests__/adapters/OpenCodeAdapter.test.js.map +0 -1
- package/dist/adapters/OpenCodeAdapter.d.ts.map +0 -1
- package/dist/adapters/OpenCodeAdapter.js +0 -289
- package/dist/adapters/OpenCodeAdapter.js.map +0 -1
- package/src/__tests__/adapters/OpenCodeAdapter.test.ts +0 -544
- package/src/adapters/OpenCodeAdapter.ts +0 -373
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { AgentStatus } from "../../adapters/AgentAdapter.js";
|
|
2
|
+
import { generateAgentName } from "../../utils/matching.js";
|
|
3
|
+
const SESSION_REF_SEP = "::";
|
|
4
|
+
const IDLE_THRESHOLD_MINUTES = 5;
|
|
5
|
+
function encodeSessionRef(dbPath, sessionId) {
|
|
6
|
+
return `${dbPath}${SESSION_REF_SEP}${sessionId}`;
|
|
7
|
+
}
|
|
8
|
+
export class OpenCodeAgentMapper {
|
|
9
|
+
dbPath;
|
|
10
|
+
constructor(dbPath){
|
|
11
|
+
this.dbPath = dbPath;
|
|
12
|
+
}
|
|
13
|
+
mapSessionToAgent(session, stats, proc) {
|
|
14
|
+
const lastActive = stats.lastTimeUpdated > 0 ? new Date(stats.lastTimeUpdated) : new Date(session.timeCreated);
|
|
15
|
+
return {
|
|
16
|
+
name: generateAgentName(session.directory || proc.cwd || "", proc.pid),
|
|
17
|
+
type: "opencode",
|
|
18
|
+
status: this.determineStatus(stats, lastActive),
|
|
19
|
+
summary: stats.summary || "OpenCode session active",
|
|
20
|
+
pid: proc.pid,
|
|
21
|
+
projectPath: session.directory || proc.cwd || "",
|
|
22
|
+
sessionId: session.sessionId,
|
|
23
|
+
lastActive,
|
|
24
|
+
sessionFilePath: encodeSessionRef(this.dbPath, session.sessionId)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
mapProcessOnlyAgent(proc) {
|
|
28
|
+
return {
|
|
29
|
+
name: generateAgentName(proc.cwd || "", proc.pid),
|
|
30
|
+
type: "opencode",
|
|
31
|
+
status: AgentStatus.RUNNING,
|
|
32
|
+
summary: "OpenCode process running",
|
|
33
|
+
pid: proc.pid,
|
|
34
|
+
projectPath: proc.cwd || "",
|
|
35
|
+
sessionId: `pid-${proc.pid}`,
|
|
36
|
+
lastActive: new Date()
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
determineStatus(stats, lastActive) {
|
|
40
|
+
const ageMin = (Date.now() - lastActive.getTime()) / 60000;
|
|
41
|
+
if (ageMin > IDLE_THRESHOLD_MINUTES) return AgentStatus.IDLE;
|
|
42
|
+
if (stats.lastRole === "assistant" && !stats.lastAssistantCompleted) {
|
|
43
|
+
return AgentStatus.RUNNING;
|
|
44
|
+
}
|
|
45
|
+
if (stats.lastRole === "assistant") return AgentStatus.WAITING;
|
|
46
|
+
return AgentStatus.RUNNING;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=OpenCodeAgentMapper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/providers/opencode/OpenCodeAgentMapper.ts"],"sourcesContent":["import type { AgentInfo, ProcessInfo } from \"../../adapters/AgentAdapter.js\";\nimport { AgentStatus } from \"../../adapters/AgentAdapter.js\";\nimport { generateAgentName } from \"../../utils/matching.js\";\nimport type { OpenCodeSession } from \"./OpenCodeSessionLocator.js\";\nimport type { OpenCodeSessionStats } from \"./OpenCodeSessionParser.js\";\n\nconst SESSION_REF_SEP = \"::\";\nconst IDLE_THRESHOLD_MINUTES = 5;\n\nfunction encodeSessionRef(dbPath: string, sessionId: string): string {\n return `${dbPath}${SESSION_REF_SEP}${sessionId}`;\n}\n\nexport class OpenCodeAgentMapper {\n constructor(private readonly dbPath: string) {}\n\n mapSessionToAgent(\n session: OpenCodeSession,\n stats: OpenCodeSessionStats,\n proc: ProcessInfo,\n ): AgentInfo {\n const lastActive =\n stats.lastTimeUpdated > 0 ? new Date(stats.lastTimeUpdated) : new Date(session.timeCreated);\n\n return {\n name: generateAgentName(session.directory || proc.cwd || \"\", proc.pid),\n type: \"opencode\",\n status: this.determineStatus(stats, lastActive),\n summary: stats.summary || \"OpenCode session active\",\n pid: proc.pid,\n projectPath: session.directory || proc.cwd || \"\",\n sessionId: session.sessionId,\n lastActive,\n sessionFilePath: encodeSessionRef(this.dbPath, session.sessionId),\n };\n }\n\n mapProcessOnlyAgent(proc: ProcessInfo): AgentInfo {\n return {\n name: generateAgentName(proc.cwd || \"\", proc.pid),\n type: \"opencode\",\n status: AgentStatus.RUNNING,\n summary: \"OpenCode process running\",\n pid: proc.pid,\n projectPath: proc.cwd || \"\",\n sessionId: `pid-${proc.pid}`,\n lastActive: new Date(),\n };\n }\n\n private determineStatus(stats: OpenCodeSessionStats, lastActive: Date): AgentStatus {\n const ageMin = (Date.now() - lastActive.getTime()) / 60000;\n if (ageMin > IDLE_THRESHOLD_MINUTES) return AgentStatus.IDLE;\n\n if (stats.lastRole === \"assistant\" && !stats.lastAssistantCompleted) {\n return AgentStatus.RUNNING;\n }\n if (stats.lastRole === \"assistant\") return AgentStatus.WAITING;\n return AgentStatus.RUNNING;\n }\n}\n"],"names":["AgentStatus","generateAgentName","SESSION_REF_SEP","IDLE_THRESHOLD_MINUTES","encodeSessionRef","dbPath","sessionId","OpenCodeAgentMapper","mapSessionToAgent","session","stats","proc","lastActive","lastTimeUpdated","Date","timeCreated","name","directory","cwd","pid","type","status","determineStatus","summary","projectPath","sessionFilePath","mapProcessOnlyAgent","RUNNING","ageMin","now","getTime","IDLE","lastRole","lastAssistantCompleted","WAITING"],"mappings":"AACA,SAASA,WAAW,QAAQ,iCAAiC;AAC7D,SAASC,iBAAiB,QAAQ,0BAA0B;AAI5D,MAAMC,kBAAkB;AACxB,MAAMC,yBAAyB;AAE/B,SAASC,iBAAiBC,MAAc,EAAEC,SAAiB;IACzD,OAAO,GAAGD,SAASH,kBAAkBI,WAAW;AAClD;AAEA,OAAO,MAAMC;;IACX,YAAY,AAAiBF,MAAc,CAAE;aAAhBA,SAAAA;IAAiB;IAE9CG,kBACEC,OAAwB,EACxBC,KAA2B,EAC3BC,IAAiB,EACN;QACX,MAAMC,aACJF,MAAMG,eAAe,GAAG,IAAI,IAAIC,KAAKJ,MAAMG,eAAe,IAAI,IAAIC,KAAKL,QAAQM,WAAW;QAE5F,OAAO;YACLC,MAAMf,kBAAkBQ,QAAQQ,SAAS,IAAIN,KAAKO,GAAG,IAAI,IAAIP,KAAKQ,GAAG;YACrEC,MAAM;YACNC,QAAQ,IAAI,CAACC,eAAe,CAACZ,OAAOE;YACpCW,SAASb,MAAMa,OAAO,IAAI;YAC1BJ,KAAKR,KAAKQ,GAAG;YACbK,aAAaf,QAAQQ,SAAS,IAAIN,KAAKO,GAAG,IAAI;YAC9CZ,WAAWG,QAAQH,SAAS;YAC5BM;YACAa,iBAAiBrB,iBAAiB,IAAI,CAACC,MAAM,EAAEI,QAAQH,SAAS;QAClE;IACF;IAEAoB,oBAAoBf,IAAiB,EAAa;QAChD,OAAO;YACLK,MAAMf,kBAAkBU,KAAKO,GAAG,IAAI,IAAIP,KAAKQ,GAAG;YAChDC,MAAM;YACNC,QAAQrB,YAAY2B,OAAO;YAC3BJ,SAAS;YACTJ,KAAKR,KAAKQ,GAAG;YACbK,aAAab,KAAKO,GAAG,IAAI;YACzBZ,WAAW,CAAC,IAAI,EAAEK,KAAKQ,GAAG,EAAE;YAC5BP,YAAY,IAAIE;QAClB;IACF;IAEQQ,gBAAgBZ,KAA2B,EAAEE,UAAgB,EAAe;QAClF,MAAMgB,SAAS,AAACd,CAAAA,KAAKe,GAAG,KAAKjB,WAAWkB,OAAO,EAAC,IAAK;QACrD,IAAIF,SAASzB,wBAAwB,OAAOH,YAAY+B,IAAI;QAE5D,IAAIrB,MAAMsB,QAAQ,KAAK,eAAe,CAACtB,MAAMuB,sBAAsB,EAAE;YACnE,OAAOjC,YAAY2B,OAAO;QAC5B;QACA,IAAIjB,MAAMsB,QAAQ,KAAK,aAAa,OAAOhC,YAAYkC,OAAO;QAC9D,OAAOlC,YAAY2B,OAAO;IAC5B;AACF"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import type { ListSessionsOptions, SessionSummary } from "../../adapters/AgentAdapter.js";
|
|
3
|
+
import { OpenCodeSessionParser } from "./OpenCodeSessionParser.js";
|
|
4
|
+
export interface OpenCodeSession {
|
|
5
|
+
sessionId: string;
|
|
6
|
+
directory: string;
|
|
7
|
+
timeCreated: number;
|
|
8
|
+
}
|
|
9
|
+
export declare class OpenCodeSessionLocator {
|
|
10
|
+
private readonly parser;
|
|
11
|
+
private readonly dbPath;
|
|
12
|
+
private db;
|
|
13
|
+
constructor(dbPath?: string, parser?: OpenCodeSessionParser);
|
|
14
|
+
static resolveDbPath(): string;
|
|
15
|
+
openDb(): Database.Database | null;
|
|
16
|
+
close(): void;
|
|
17
|
+
findSessionForDirectory(db: Database.Database, directory: string): OpenCodeSession | null;
|
|
18
|
+
listSessions(opts?: ListSessionsOptions): SessionSummary[];
|
|
19
|
+
get dbFilePath(): string;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=OpenCodeSessionLocator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OpenCodeSessionLocator.d.ts","sourceRoot":"","sources":["../../../src/providers/opencode/OpenCodeSessionLocator.ts"],"names":[],"mappings":"AAEA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAC1F,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAQnE,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,sBAAsB;IAM/B,OAAO,CAAC,QAAQ,CAAC,MAAM;IALzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,EAAE,CAAkC;gBAG1C,MAAM,SAAyC,EAC9B,MAAM,GAAE,qBAAmD;IAK9E,MAAM,CAAC,aAAa,IAAI,MAAM;IAO9B,MAAM,IAAI,QAAQ,CAAC,QAAQ,GAAG,IAAI;IAYlC,KAAK,IAAI,IAAI;IAWb,uBAAuB,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI;IAmBzF,YAAY,CAAC,IAAI,CAAC,EAAE,mBAAmB,GAAG,cAAc,EAAE;IAyC1D,IAAI,UAAU,IAAI,MAAM,CAEvB;CACF"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import Database from "better-sqlite3";
|
|
4
|
+
import { OpenCodeSessionParser } from "./OpenCodeSessionParser.js";
|
|
5
|
+
const SESSION_REF_SEP = "::";
|
|
6
|
+
function encodeOpenCodeSessionRef(dbPath, sessionId) {
|
|
7
|
+
return `${dbPath}${SESSION_REF_SEP}${sessionId}`;
|
|
8
|
+
}
|
|
9
|
+
export class OpenCodeSessionLocator {
|
|
10
|
+
parser;
|
|
11
|
+
dbPath;
|
|
12
|
+
db = null;
|
|
13
|
+
constructor(dbPath = OpenCodeSessionLocator.resolveDbPath(), parser = new OpenCodeSessionParser()){
|
|
14
|
+
this.parser = parser;
|
|
15
|
+
this.dbPath = dbPath;
|
|
16
|
+
}
|
|
17
|
+
static resolveDbPath() {
|
|
18
|
+
const xdg = process.env.XDG_DATA_HOME;
|
|
19
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
20
|
+
const base = xdg || path.join(home, ".local", "share");
|
|
21
|
+
return path.join(base, "opencode", "opencode.db");
|
|
22
|
+
}
|
|
23
|
+
openDb() {
|
|
24
|
+
if (this.db) return this.db;
|
|
25
|
+
if (!fs.existsSync(this.dbPath)) return null;
|
|
26
|
+
try {
|
|
27
|
+
this.db = new Database(this.dbPath, {
|
|
28
|
+
readonly: true
|
|
29
|
+
});
|
|
30
|
+
return this.db;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
close() {
|
|
36
|
+
if (this.db) {
|
|
37
|
+
try {
|
|
38
|
+
this.db.close();
|
|
39
|
+
} catch {
|
|
40
|
+
/* ignore */ }
|
|
41
|
+
this.db = null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
findSessionForDirectory(db, directory) {
|
|
45
|
+
try {
|
|
46
|
+
const row = db.prepare(`
|
|
47
|
+
SELECT id, directory, time_created
|
|
48
|
+
FROM session
|
|
49
|
+
WHERE directory = ?
|
|
50
|
+
ORDER BY time_created DESC
|
|
51
|
+
LIMIT 1
|
|
52
|
+
`).get(directory);
|
|
53
|
+
if (!row) return null;
|
|
54
|
+
return {
|
|
55
|
+
sessionId: row.id,
|
|
56
|
+
directory: row.directory,
|
|
57
|
+
timeCreated: row.time_created
|
|
58
|
+
};
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
listSessions(opts) {
|
|
64
|
+
const db = this.openDb();
|
|
65
|
+
if (!db) return [];
|
|
66
|
+
try {
|
|
67
|
+
const rows = db.prepare(`
|
|
68
|
+
SELECT id, directory, time_created AS timeCreated
|
|
69
|
+
FROM session
|
|
70
|
+
ORDER BY time_created DESC
|
|
71
|
+
`).all();
|
|
72
|
+
const summaries = [];
|
|
73
|
+
for (const row of rows){
|
|
74
|
+
if (opts?.cwd !== undefined && row.directory !== opts.cwd) continue;
|
|
75
|
+
const stats = this.parser.getSessionStats(db, row.id);
|
|
76
|
+
const lastActive = stats.lastTimeUpdated > 0 ? new Date(stats.lastTimeUpdated) : new Date(row.timeCreated);
|
|
77
|
+
const startedAt = new Date(row.timeCreated);
|
|
78
|
+
summaries.push({
|
|
79
|
+
type: "opencode",
|
|
80
|
+
sessionId: row.id,
|
|
81
|
+
cwd: row.directory,
|
|
82
|
+
firstUserMessage: stats.summary,
|
|
83
|
+
lastActive,
|
|
84
|
+
startedAt,
|
|
85
|
+
sessionFilePath: encodeOpenCodeSessionRef(this.dbPath, row.id)
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return summaries;
|
|
89
|
+
} catch {
|
|
90
|
+
this.close();
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
get dbFilePath() {
|
|
95
|
+
return this.dbPath;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
//# sourceMappingURL=OpenCodeSessionLocator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/providers/opencode/OpenCodeSessionLocator.ts"],"sourcesContent":["import * as fs from \"fs\";\nimport * as path from \"path\";\nimport Database from \"better-sqlite3\";\nimport type { ListSessionsOptions, SessionSummary } from \"../../adapters/AgentAdapter.js\";\nimport { OpenCodeSessionParser } from \"./OpenCodeSessionParser.js\";\n\nconst SESSION_REF_SEP = \"::\";\n\nfunction encodeOpenCodeSessionRef(dbPath: string, sessionId: string): string {\n return `${dbPath}${SESSION_REF_SEP}${sessionId}`;\n}\n\nexport interface OpenCodeSession {\n sessionId: string;\n directory: string;\n timeCreated: number;\n}\n\nexport class OpenCodeSessionLocator {\n private readonly dbPath: string;\n private db: Database.Database | null = null;\n\n constructor(\n dbPath = OpenCodeSessionLocator.resolveDbPath(),\n private readonly parser: OpenCodeSessionParser = new OpenCodeSessionParser(),\n ) {\n this.dbPath = dbPath;\n }\n\n static resolveDbPath(): string {\n const xdg = process.env.XDG_DATA_HOME;\n const home = process.env.HOME || process.env.USERPROFILE || \"\";\n const base = xdg || path.join(home, \".local\", \"share\");\n return path.join(base, \"opencode\", \"opencode.db\");\n }\n\n openDb(): Database.Database | null {\n if (this.db) return this.db;\n if (!fs.existsSync(this.dbPath)) return null;\n\n try {\n this.db = new Database(this.dbPath, { readonly: true });\n return this.db;\n } catch {\n return null;\n }\n }\n\n close(): void {\n if (this.db) {\n try {\n this.db.close();\n } catch {\n /* ignore */\n }\n this.db = null;\n }\n }\n\n findSessionForDirectory(db: Database.Database, directory: string): OpenCodeSession | null {\n try {\n const row = db\n .prepare<[string], { id: string; directory: string; time_created: number }>(`\n SELECT id, directory, time_created\n FROM session\n WHERE directory = ?\n ORDER BY time_created DESC\n LIMIT 1\n `)\n .get(directory);\n\n if (!row) return null;\n return { sessionId: row.id, directory: row.directory, timeCreated: row.time_created };\n } catch {\n return null;\n }\n }\n\n listSessions(opts?: ListSessionsOptions): SessionSummary[] {\n const db = this.openDb();\n if (!db) return [];\n\n try {\n const rows = db\n .prepare<[], { id: string; directory: string; timeCreated: number }>(`\n SELECT id, directory, time_created AS timeCreated\n FROM session\n ORDER BY time_created DESC\n `)\n .all();\n\n const summaries: SessionSummary[] = [];\n\n for (const row of rows) {\n if (opts?.cwd !== undefined && row.directory !== opts.cwd) continue;\n\n const stats = this.parser.getSessionStats(db, row.id);\n const lastActive =\n stats.lastTimeUpdated > 0 ? new Date(stats.lastTimeUpdated) : new Date(row.timeCreated);\n const startedAt = new Date(row.timeCreated);\n\n summaries.push({\n type: \"opencode\",\n sessionId: row.id,\n cwd: row.directory,\n firstUserMessage: stats.summary,\n lastActive,\n startedAt,\n sessionFilePath: encodeOpenCodeSessionRef(this.dbPath, row.id),\n });\n }\n\n return summaries;\n } catch {\n this.close();\n return [];\n }\n }\n\n get dbFilePath(): string {\n return this.dbPath;\n }\n}\n"],"names":["fs","path","Database","OpenCodeSessionParser","SESSION_REF_SEP","encodeOpenCodeSessionRef","dbPath","sessionId","OpenCodeSessionLocator","db","resolveDbPath","parser","xdg","process","env","XDG_DATA_HOME","home","HOME","USERPROFILE","base","join","openDb","existsSync","readonly","close","findSessionForDirectory","directory","row","prepare","get","id","timeCreated","time_created","listSessions","opts","rows","all","summaries","cwd","undefined","stats","getSessionStats","lastActive","lastTimeUpdated","Date","startedAt","push","type","firstUserMessage","summary","sessionFilePath","dbFilePath"],"mappings":"AAAA,YAAYA,QAAQ,KAAK;AACzB,YAAYC,UAAU,OAAO;AAC7B,OAAOC,cAAc,iBAAiB;AAEtC,SAASC,qBAAqB,QAAQ,6BAA6B;AAEnE,MAAMC,kBAAkB;AAExB,SAASC,yBAAyBC,MAAc,EAAEC,SAAiB;IACjE,OAAO,GAAGD,SAASF,kBAAkBG,WAAW;AAClD;AAQA,OAAO,MAAMC;;IACMF,OAAe;IACxBG,KAA+B,KAAK;IAE5C,YACEH,SAASE,uBAAuBE,aAAa,EAAE,EAC/C,AAAiBC,SAAgC,IAAIR,uBAAuB,CAC5E;aADiBQ,SAAAA;QAEjB,IAAI,CAACL,MAAM,GAAGA;IAChB;IAEA,OAAOI,gBAAwB;QAC7B,MAAME,MAAMC,QAAQC,GAAG,CAACC,aAAa;QACrC,MAAMC,OAAOH,QAAQC,GAAG,CAACG,IAAI,IAAIJ,QAAQC,GAAG,CAACI,WAAW,IAAI;QAC5D,MAAMC,OAAOP,OAAOX,KAAKmB,IAAI,CAACJ,MAAM,UAAU;QAC9C,OAAOf,KAAKmB,IAAI,CAACD,MAAM,YAAY;IACrC;IAEAE,SAAmC;QACjC,IAAI,IAAI,CAACZ,EAAE,EAAE,OAAO,IAAI,CAACA,EAAE;QAC3B,IAAI,CAACT,GAAGsB,UAAU,CAAC,IAAI,CAAChB,MAAM,GAAG,OAAO;QAExC,IAAI;YACF,IAAI,CAACG,EAAE,GAAG,IAAIP,SAAS,IAAI,CAACI,MAAM,EAAE;gBAAEiB,UAAU;YAAK;YACrD,OAAO,IAAI,CAACd,EAAE;QAChB,EAAE,OAAM;YACN,OAAO;QACT;IACF;IAEAe,QAAc;QACZ,IAAI,IAAI,CAACf,EAAE,EAAE;YACX,IAAI;gBACF,IAAI,CAACA,EAAE,CAACe,KAAK;YACf,EAAE,OAAM;YACN,UAAU,GACZ;YACA,IAAI,CAACf,EAAE,GAAG;QACZ;IACF;IAEAgB,wBAAwBhB,EAAqB,EAAEiB,SAAiB,EAA0B;QACxF,IAAI;YACF,MAAMC,MAAMlB,GACTmB,OAAO,CAAoE,CAAC;;;;;;YAMzE,CAAC,EACJC,GAAG,CAACH;YAEP,IAAI,CAACC,KAAK,OAAO;YACjB,OAAO;gBAAEpB,WAAWoB,IAAIG,EAAE;gBAAEJ,WAAWC,IAAID,SAAS;gBAAEK,aAAaJ,IAAIK,YAAY;YAAC;QACtF,EAAE,OAAM;YACN,OAAO;QACT;IACF;IAEAC,aAAaC,IAA0B,EAAoB;QACzD,MAAMzB,KAAK,IAAI,CAACY,MAAM;QACtB,IAAI,CAACZ,IAAI,OAAO,EAAE;QAElB,IAAI;YACF,MAAM0B,OAAO1B,GACVmB,OAAO,CAA6D,CAAC;;;;YAIlE,CAAC,EACJQ,GAAG;YAEN,MAAMC,YAA8B,EAAE;YAEtC,KAAK,MAAMV,OAAOQ,KAAM;gBACtB,IAAID,MAAMI,QAAQC,aAAaZ,IAAID,SAAS,KAAKQ,KAAKI,GAAG,EAAE;gBAE3D,MAAME,QAAQ,IAAI,CAAC7B,MAAM,CAAC8B,eAAe,CAAChC,IAAIkB,IAAIG,EAAE;gBACpD,MAAMY,aACJF,MAAMG,eAAe,GAAG,IAAI,IAAIC,KAAKJ,MAAMG,eAAe,IAAI,IAAIC,KAAKjB,IAAII,WAAW;gBACxF,MAAMc,YAAY,IAAID,KAAKjB,IAAII,WAAW;gBAE1CM,UAAUS,IAAI,CAAC;oBACbC,MAAM;oBACNxC,WAAWoB,IAAIG,EAAE;oBACjBQ,KAAKX,IAAID,SAAS;oBAClBsB,kBAAkBR,MAAMS,OAAO;oBAC/BP;oBACAG;oBACAK,iBAAiB7C,yBAAyB,IAAI,CAACC,MAAM,EAAEqB,IAAIG,EAAE;gBAC/D;YACF;YAEA,OAAOO;QACT,EAAE,OAAM;YACN,IAAI,CAACb,KAAK;YACV,OAAO,EAAE;QACX;IACF;IAEA,IAAI2B,aAAqB;QACvB,OAAO,IAAI,CAAC7C,MAAM;IACpB;AACF"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ConversationMessage } from "../../adapters/AgentAdapter.js";
|
|
2
|
+
import type Database from "better-sqlite3";
|
|
3
|
+
export interface OpenCodeSessionStats {
|
|
4
|
+
lastRole: string | null;
|
|
5
|
+
lastTimeUpdated: number;
|
|
6
|
+
/** OpenCode writes `time.completed` on the assistant message only when the turn finishes. */
|
|
7
|
+
lastAssistantCompleted: boolean;
|
|
8
|
+
lastAssistantErrored: boolean;
|
|
9
|
+
summary: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class OpenCodeSessionParser {
|
|
12
|
+
getConversation(db: Database.Database, sessionId: string, options?: {
|
|
13
|
+
verbose?: boolean;
|
|
14
|
+
}): ConversationMessage[];
|
|
15
|
+
getSessionStats(db: Database.Database, sessionId: string): OpenCodeSessionStats;
|
|
16
|
+
private parsePartData;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=OpenCodeSessionParser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OpenCodeSessionParser.d.ts","sourceRoot":"","sources":["../../../src/providers/opencode/OpenCodeSessionParser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAC1E,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAE3C,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,6FAA6F;IAC7F,sBAAsB,EAAE,OAAO,CAAC;IAChC,oBAAoB,EAAE,OAAO,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;CACjB;AAiBD,qBAAa,qBAAqB;IAChC,eAAe,CACb,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAC9B,mBAAmB,EAAE;IAyCxB,eAAe,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,oBAAoB;IAgE/E,OAAO,CAAC,aAAa;CAOtB"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const EMPTY_STATS = {
|
|
2
|
+
lastRole: null,
|
|
3
|
+
lastTimeUpdated: 0,
|
|
4
|
+
lastAssistantCompleted: false,
|
|
5
|
+
lastAssistantErrored: false,
|
|
6
|
+
summary: ""
|
|
7
|
+
};
|
|
8
|
+
export class OpenCodeSessionParser {
|
|
9
|
+
getConversation(db, sessionId, options) {
|
|
10
|
+
const verbose = options?.verbose ?? false;
|
|
11
|
+
try {
|
|
12
|
+
const rows = db.prepare(`
|
|
13
|
+
SELECT json_extract(m.data, '$.role') AS role,
|
|
14
|
+
p.data AS partData,
|
|
15
|
+
p.time_created AS timeCreated
|
|
16
|
+
FROM part p
|
|
17
|
+
JOIN message m ON p.message_id = m.id
|
|
18
|
+
WHERE p.session_id = ?
|
|
19
|
+
ORDER BY p.time_created ASC
|
|
20
|
+
`).all(sessionId);
|
|
21
|
+
const messages = [];
|
|
22
|
+
for (const row of rows){
|
|
23
|
+
const partData = this.parsePartData(row.partData);
|
|
24
|
+
if (!partData) continue;
|
|
25
|
+
const role = row.role === "user" ? "user" : "assistant";
|
|
26
|
+
if (partData.type === "text" && partData.text) {
|
|
27
|
+
messages.push({
|
|
28
|
+
role,
|
|
29
|
+
content: partData.text
|
|
30
|
+
});
|
|
31
|
+
} else if (partData.type === "reasoning" && verbose) {
|
|
32
|
+
const text = partData.reasoning || partData.text || "";
|
|
33
|
+
if (text) messages.push({
|
|
34
|
+
role: "assistant",
|
|
35
|
+
content: `[thinking] ${text}`
|
|
36
|
+
});
|
|
37
|
+
} else if (partData.type === "tool" && verbose) {
|
|
38
|
+
const toolName = partData.tool || "tool";
|
|
39
|
+
messages.push({
|
|
40
|
+
role: "assistant",
|
|
41
|
+
content: `[tool: ${toolName}]`
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return messages;
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
getSessionStats(db, sessionId) {
|
|
51
|
+
try {
|
|
52
|
+
// Order by time_created — time_updated can lag when OpenCode appends
|
|
53
|
+
// metadata (e.g. summary diffs) to user messages after a turn finishes.
|
|
54
|
+
const last = db.prepare(`
|
|
55
|
+
SELECT json_extract(data, '$.role') AS role,
|
|
56
|
+
time_updated AS timeUpdated
|
|
57
|
+
FROM message
|
|
58
|
+
WHERE session_id = ?
|
|
59
|
+
ORDER BY time_created DESC
|
|
60
|
+
LIMIT 1
|
|
61
|
+
`).get(sessionId);
|
|
62
|
+
const heartbeat = db.prepare(`
|
|
63
|
+
SELECT MAX(time_updated) AS maxUpdated FROM message WHERE session_id = ?
|
|
64
|
+
`).get(sessionId);
|
|
65
|
+
const lastAssistant = db.prepare(`
|
|
66
|
+
SELECT json_extract(data, '$.time.completed') AS completed,
|
|
67
|
+
json_extract(data, '$.time.error') AS errored
|
|
68
|
+
FROM message
|
|
69
|
+
WHERE session_id = ? AND json_extract(data, '$.role') = 'assistant'
|
|
70
|
+
ORDER BY time_created DESC
|
|
71
|
+
LIMIT 1
|
|
72
|
+
`).get(sessionId);
|
|
73
|
+
const first = db.prepare(`
|
|
74
|
+
SELECT json_extract(p.data, '$.text') AS text
|
|
75
|
+
FROM part p
|
|
76
|
+
JOIN message m ON p.message_id = m.id
|
|
77
|
+
WHERE p.session_id = ?
|
|
78
|
+
AND json_extract(m.data, '$.role') = 'user'
|
|
79
|
+
AND json_extract(p.data, '$.type') = 'text'
|
|
80
|
+
AND json_extract(p.data, '$.text') IS NOT NULL
|
|
81
|
+
ORDER BY p.time_created ASC
|
|
82
|
+
LIMIT 1
|
|
83
|
+
`).get(sessionId);
|
|
84
|
+
return {
|
|
85
|
+
lastRole: last?.role ?? null,
|
|
86
|
+
lastTimeUpdated: heartbeat?.maxUpdated ?? last?.timeUpdated ?? 0,
|
|
87
|
+
lastAssistantCompleted: lastAssistant?.completed != null,
|
|
88
|
+
lastAssistantErrored: lastAssistant?.errored != null,
|
|
89
|
+
summary: first?.text?.trim() ?? ""
|
|
90
|
+
};
|
|
91
|
+
} catch {
|
|
92
|
+
return EMPTY_STATS;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
parsePartData(partData) {
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(partData);
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
//# sourceMappingURL=OpenCodeSessionParser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/providers/opencode/OpenCodeSessionParser.ts"],"sourcesContent":["import type { ConversationMessage } from \"../../adapters/AgentAdapter.js\";\nimport type Database from \"better-sqlite3\";\n\nexport interface OpenCodeSessionStats {\n lastRole: string | null;\n lastTimeUpdated: number;\n /** OpenCode writes `time.completed` on the assistant message only when the turn finishes. */\n lastAssistantCompleted: boolean;\n lastAssistantErrored: boolean;\n summary: string;\n}\n\ninterface OpenCodePartData {\n type?: string;\n text?: string;\n reasoning?: string;\n tool?: string;\n}\n\nconst EMPTY_STATS: OpenCodeSessionStats = {\n lastRole: null,\n lastTimeUpdated: 0,\n lastAssistantCompleted: false,\n lastAssistantErrored: false,\n summary: \"\",\n};\n\nexport class OpenCodeSessionParser {\n getConversation(\n db: Database.Database,\n sessionId: string,\n options?: { verbose?: boolean },\n ): ConversationMessage[] {\n const verbose = options?.verbose ?? false;\n\n try {\n const rows = db\n .prepare<[string], { role: string; partData: string; timeCreated: number }>(`\n SELECT json_extract(m.data, '$.role') AS role,\n p.data AS partData,\n p.time_created AS timeCreated\n FROM part p\n JOIN message m ON p.message_id = m.id\n WHERE p.session_id = ?\n ORDER BY p.time_created ASC\n `)\n .all(sessionId);\n\n const messages: ConversationMessage[] = [];\n\n for (const row of rows) {\n const partData = this.parsePartData(row.partData);\n if (!partData) continue;\n\n const role = row.role === \"user\" ? \"user\" : \"assistant\";\n\n if (partData.type === \"text\" && partData.text) {\n messages.push({ role, content: partData.text });\n } else if (partData.type === \"reasoning\" && verbose) {\n const text = partData.reasoning || partData.text || \"\";\n if (text) messages.push({ role: \"assistant\", content: `[thinking] ${text}` });\n } else if (partData.type === \"tool\" && verbose) {\n const toolName = partData.tool || \"tool\";\n messages.push({ role: \"assistant\", content: `[tool: ${toolName}]` });\n }\n }\n\n return messages;\n } catch {\n return [];\n }\n }\n\n getSessionStats(db: Database.Database, sessionId: string): OpenCodeSessionStats {\n try {\n // Order by time_created — time_updated can lag when OpenCode appends\n // metadata (e.g. summary diffs) to user messages after a turn finishes.\n const last = db\n .prepare<[string], { role: string; timeUpdated: number }>(`\n SELECT json_extract(data, '$.role') AS role,\n time_updated AS timeUpdated\n FROM message\n WHERE session_id = ?\n ORDER BY time_created DESC\n LIMIT 1\n `)\n .get(sessionId);\n\n const heartbeat = db\n .prepare<[string], { maxUpdated: number }>(`\n SELECT MAX(time_updated) AS maxUpdated FROM message WHERE session_id = ?\n `)\n .get(sessionId);\n\n const lastAssistant = db\n .prepare<\n [string],\n {\n completed: number | null;\n errored: number | null;\n }\n >(`\n SELECT json_extract(data, '$.time.completed') AS completed,\n json_extract(data, '$.time.error') AS errored\n FROM message\n WHERE session_id = ? AND json_extract(data, '$.role') = 'assistant'\n ORDER BY time_created DESC\n LIMIT 1\n `)\n .get(sessionId);\n\n const first = db\n .prepare<[string], { text: string }>(`\n SELECT json_extract(p.data, '$.text') AS text\n FROM part p\n JOIN message m ON p.message_id = m.id\n WHERE p.session_id = ?\n AND json_extract(m.data, '$.role') = 'user'\n AND json_extract(p.data, '$.type') = 'text'\n AND json_extract(p.data, '$.text') IS NOT NULL\n ORDER BY p.time_created ASC\n LIMIT 1\n `)\n .get(sessionId);\n\n return {\n lastRole: last?.role ?? null,\n lastTimeUpdated: heartbeat?.maxUpdated ?? last?.timeUpdated ?? 0,\n lastAssistantCompleted: lastAssistant?.completed != null,\n lastAssistantErrored: lastAssistant?.errored != null,\n summary: first?.text?.trim() ?? \"\",\n };\n } catch {\n return EMPTY_STATS;\n }\n }\n\n private parsePartData(partData: string): OpenCodePartData | null {\n try {\n return JSON.parse(partData) as OpenCodePartData;\n } catch {\n return null;\n }\n }\n}\n"],"names":["EMPTY_STATS","lastRole","lastTimeUpdated","lastAssistantCompleted","lastAssistantErrored","summary","OpenCodeSessionParser","getConversation","db","sessionId","options","verbose","rows","prepare","all","messages","row","partData","parsePartData","role","type","text","push","content","reasoning","toolName","tool","getSessionStats","last","get","heartbeat","lastAssistant","first","maxUpdated","timeUpdated","completed","errored","trim","JSON","parse"],"mappings":"AAmBA,MAAMA,cAAoC;IACxCC,UAAU;IACVC,iBAAiB;IACjBC,wBAAwB;IACxBC,sBAAsB;IACtBC,SAAS;AACX;AAEA,OAAO,MAAMC;IACXC,gBACEC,EAAqB,EACrBC,SAAiB,EACjBC,OAA+B,EACR;QACvB,MAAMC,UAAUD,SAASC,WAAW;QAEpC,IAAI;YACF,MAAMC,OAAOJ,GACVK,OAAO,CAAoE,CAAC;;;;;;;;YAQzE,CAAC,EACJC,GAAG,CAACL;YAEP,MAAMM,WAAkC,EAAE;YAE1C,KAAK,MAAMC,OAAOJ,KAAM;gBACtB,MAAMK,WAAW,IAAI,CAACC,aAAa,CAACF,IAAIC,QAAQ;gBAChD,IAAI,CAACA,UAAU;gBAEf,MAAME,OAAOH,IAAIG,IAAI,KAAK,SAAS,SAAS;gBAE5C,IAAIF,SAASG,IAAI,KAAK,UAAUH,SAASI,IAAI,EAAE;oBAC7CN,SAASO,IAAI,CAAC;wBAAEH;wBAAMI,SAASN,SAASI,IAAI;oBAAC;gBAC/C,OAAO,IAAIJ,SAASG,IAAI,KAAK,eAAeT,SAAS;oBACnD,MAAMU,OAAOJ,SAASO,SAAS,IAAIP,SAASI,IAAI,IAAI;oBACpD,IAAIA,MAAMN,SAASO,IAAI,CAAC;wBAAEH,MAAM;wBAAaI,SAAS,CAAC,WAAW,EAAEF,MAAM;oBAAC;gBAC7E,OAAO,IAAIJ,SAASG,IAAI,KAAK,UAAUT,SAAS;oBAC9C,MAAMc,WAAWR,SAASS,IAAI,IAAI;oBAClCX,SAASO,IAAI,CAAC;wBAAEH,MAAM;wBAAaI,SAAS,CAAC,OAAO,EAAEE,SAAS,CAAC,CAAC;oBAAC;gBACpE;YACF;YAEA,OAAOV;QACT,EAAE,OAAM;YACN,OAAO,EAAE;QACX;IACF;IAEAY,gBAAgBnB,EAAqB,EAAEC,SAAiB,EAAwB;QAC9E,IAAI;YACF,qEAAqE;YACrE,wEAAwE;YACxE,MAAMmB,OAAOpB,GACVK,OAAO,CAAkD,CAAC;;;;;;;YAOvD,CAAC,EACJgB,GAAG,CAACpB;YAEP,MAAMqB,YAAYtB,GACfK,OAAO,CAAmC,CAAC;;YAExC,CAAC,EACJgB,GAAG,CAACpB;YAEP,MAAMsB,gBAAgBvB,GACnBK,OAAO,CAMN,CAAC;;;;;;;YAOC,CAAC,EACJgB,GAAG,CAACpB;YAEP,MAAMuB,QAAQxB,GACXK,OAAO,CAA6B,CAAC;;;;;;;;;;YAUlC,CAAC,EACJgB,GAAG,CAACpB;YAEP,OAAO;gBACLR,UAAU2B,MAAMT,QAAQ;gBACxBjB,iBAAiB4B,WAAWG,cAAcL,MAAMM,eAAe;gBAC/D/B,wBAAwB4B,eAAeI,aAAa;gBACpD/B,sBAAsB2B,eAAeK,WAAW;gBAChD/B,SAAS2B,OAAOX,MAAMgB,UAAU;YAClC;QACF,EAAE,OAAM;YACN,OAAOrC;QACT;IACF;IAEQkB,cAAcD,QAAgB,EAA2B;QAC/D,IAAI;YACF,OAAOqB,KAAKC,KAAK,CAACtB;QACpB,EAAE,OAAM;YACN,OAAO;QACT;IACF;AACF"}
|
package/package.json
CHANGED
|
@@ -13,7 +13,11 @@ const context = { installed: true, checkedAt };
|
|
|
13
13
|
function apiUsage(overrides: Record<string, unknown> = {}) {
|
|
14
14
|
return {
|
|
15
15
|
rate_limit: {
|
|
16
|
-
primary_window: {
|
|
16
|
+
primary_window: {
|
|
17
|
+
used_percent: 20,
|
|
18
|
+
limit_window_seconds: 18_000,
|
|
19
|
+
reset_at: 1_787_220_000,
|
|
20
|
+
},
|
|
17
21
|
secondary_window: {
|
|
18
22
|
used_percent: 60,
|
|
19
23
|
limit_window_seconds: 604_800,
|
|
@@ -39,13 +43,18 @@ function apiUsage(overrides: Record<string, unknown> = {}) {
|
|
|
39
43
|
|
|
40
44
|
describe("Codex auth resolution", () => {
|
|
41
45
|
it("uses CODEX_HOME before HOME", () => {
|
|
42
|
-
expect(
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
expect(
|
|
47
|
+
resolveCodexAuthPath({
|
|
48
|
+
CODEX_HOME: "/custom/codex",
|
|
49
|
+
HOME: "/users/test",
|
|
50
|
+
}),
|
|
51
|
+
).toBe("/custom/codex/auth.json");
|
|
45
52
|
});
|
|
46
53
|
|
|
47
54
|
it("falls back to ~/.codex/auth.json", () => {
|
|
48
|
-
expect(resolveCodexAuthPath({ HOME: "/users/test" })).toBe(
|
|
55
|
+
expect(resolveCodexAuthPath({ HOME: "/users/test" })).toBe(
|
|
56
|
+
"/users/test/.codex/auth.json",
|
|
57
|
+
);
|
|
49
58
|
});
|
|
50
59
|
});
|
|
51
60
|
|
|
@@ -53,7 +62,11 @@ describe("Codex API usage mapping", () => {
|
|
|
53
62
|
it("converts an API window without treating missing data as zero", () => {
|
|
54
63
|
expect(
|
|
55
64
|
toRateWindow(
|
|
56
|
-
{
|
|
65
|
+
{
|
|
66
|
+
used_percent: 25,
|
|
67
|
+
limit_window_seconds: 18_000,
|
|
68
|
+
reset_at: 1_787_220_000,
|
|
69
|
+
},
|
|
57
70
|
"session",
|
|
58
71
|
"Session",
|
|
59
72
|
),
|
|
@@ -64,7 +77,9 @@ describe("Codex API usage mapping", () => {
|
|
|
64
77
|
usedPercent: 25,
|
|
65
78
|
resetsAt: "2026-08-20T10:00:00.000Z",
|
|
66
79
|
});
|
|
67
|
-
expect(toRateWindow({}, "session", "Session")).toMatchObject({
|
|
80
|
+
expect(toRateWindow({}, "session", "Session")).toMatchObject({
|
|
81
|
+
usedPercent: null,
|
|
82
|
+
});
|
|
68
83
|
});
|
|
69
84
|
|
|
70
85
|
it("maps session, weekly, credits, and extra limits", () => {
|
|
@@ -89,16 +104,23 @@ describe("tiered Codex probing", () => {
|
|
|
89
104
|
const fetch = vi
|
|
90
105
|
.fn()
|
|
91
106
|
.mockResolvedValueOnce(
|
|
92
|
-
new Response(JSON.stringify({ chatgpt_account_id: "acct-1" }), {
|
|
107
|
+
new Response(JSON.stringify({ chatgpt_account_id: "acct-1" }), {
|
|
108
|
+
status: 200,
|
|
109
|
+
}),
|
|
93
110
|
)
|
|
94
|
-
.mockResolvedValueOnce(
|
|
111
|
+
.mockResolvedValueOnce(
|
|
112
|
+
new Response(JSON.stringify(apiUsage()), { status: 200 }),
|
|
113
|
+
);
|
|
95
114
|
const rpc = vi.fn();
|
|
96
115
|
const result = await probeCodexCapacity({
|
|
97
116
|
...context,
|
|
98
117
|
readFile: async () =>
|
|
99
118
|
JSON.stringify({
|
|
100
119
|
personal_access_token: "pat-secret",
|
|
101
|
-
tokens: {
|
|
120
|
+
tokens: {
|
|
121
|
+
access_token: "ignored-oauth",
|
|
122
|
+
account_id: "ignored-account",
|
|
123
|
+
},
|
|
102
124
|
}),
|
|
103
125
|
fetch,
|
|
104
126
|
rpc,
|
|
@@ -107,14 +129,17 @@ describe("tiered Codex probing", () => {
|
|
|
107
129
|
expect(fetch.mock.calls[0][0]).toBe(
|
|
108
130
|
"https://auth.openai.com/api/accounts/v1/user-auth-credential/whoami",
|
|
109
131
|
);
|
|
110
|
-
expect(fetch.mock.calls[1][0]).toBe(
|
|
132
|
+
expect(fetch.mock.calls[1][0]).toBe(
|
|
133
|
+
"https://chatgpt.com/backend-api/wham/usage",
|
|
134
|
+
);
|
|
111
135
|
expect(fetch.mock.calls[1][1].headers).toMatchObject({
|
|
112
136
|
Authorization: "Bearer pat-secret",
|
|
113
137
|
"ChatGPT-Account-Id": "acct-1",
|
|
114
138
|
});
|
|
115
139
|
expect(rpc).not.toHaveBeenCalled();
|
|
116
140
|
expect(result).toMatchObject({
|
|
117
|
-
|
|
141
|
+
harness: "codex",
|
|
142
|
+
provider: "openai",
|
|
118
143
|
available: "yes",
|
|
119
144
|
creditsRemaining: 12.5,
|
|
120
145
|
authenticated: true,
|
|
@@ -129,12 +154,18 @@ describe("tiered Codex probing", () => {
|
|
|
129
154
|
it("selects a fresh OAuth token without calling whoami", async () => {
|
|
130
155
|
const fetch = vi
|
|
131
156
|
.fn()
|
|
132
|
-
.mockResolvedValue(
|
|
157
|
+
.mockResolvedValue(
|
|
158
|
+
new Response(JSON.stringify(apiUsage()), { status: 200 }),
|
|
159
|
+
);
|
|
133
160
|
const result = await probeCodexCapacity({
|
|
134
161
|
...context,
|
|
135
162
|
readFile: async () =>
|
|
136
163
|
JSON.stringify({
|
|
137
|
-
tokens: {
|
|
164
|
+
tokens: {
|
|
165
|
+
access_token: "oauth-secret",
|
|
166
|
+
account_id: "acct-2",
|
|
167
|
+
expires_at: 1_800_000_000,
|
|
168
|
+
},
|
|
138
169
|
}),
|
|
139
170
|
fetch,
|
|
140
171
|
now: () => new Date("2026-08-20T10:00:00.000Z"),
|
|
@@ -158,23 +189,35 @@ describe("tiered Codex probing", () => {
|
|
|
158
189
|
"stale OAuth token",
|
|
159
190
|
async () =>
|
|
160
191
|
JSON.stringify({
|
|
161
|
-
tokens: {
|
|
192
|
+
tokens: {
|
|
193
|
+
access_token: "stale-secret",
|
|
194
|
+
account_id: "acct",
|
|
195
|
+
expires_at: 1,
|
|
196
|
+
},
|
|
162
197
|
}),
|
|
163
198
|
],
|
|
164
199
|
[
|
|
165
200
|
"OAuth 401",
|
|
166
201
|
async () =>
|
|
167
202
|
JSON.stringify({
|
|
168
|
-
tokens: {
|
|
203
|
+
tokens: {
|
|
204
|
+
access_token: "oauth-secret",
|
|
205
|
+
account_id: "acct",
|
|
206
|
+
expires_at: 1_800_000_000,
|
|
207
|
+
},
|
|
169
208
|
}),
|
|
170
209
|
],
|
|
171
210
|
])("falls back to the CLI for %s", async (name, readFile) => {
|
|
172
211
|
const fetch = vi
|
|
173
212
|
.fn()
|
|
174
|
-
.mockResolvedValue(
|
|
213
|
+
.mockResolvedValue(
|
|
214
|
+
new Response("", { status: name === "OAuth 401" ? 401 : 200 }),
|
|
215
|
+
);
|
|
175
216
|
const rpc = vi.fn(async () => ({
|
|
176
217
|
rateLimits: {
|
|
177
|
-
rateLimits: {
|
|
218
|
+
rateLimits: {
|
|
219
|
+
primary: { usedPercent: 5, windowDurationMins: 300, resetsAt: null },
|
|
220
|
+
},
|
|
178
221
|
},
|
|
179
222
|
account: { account: { type: "chatgpt" } },
|
|
180
223
|
}));
|
|
@@ -190,10 +233,14 @@ describe("tiered Codex probing", () => {
|
|
|
190
233
|
});
|
|
191
234
|
|
|
192
235
|
it("falls back to CLI if PAT requests fail", async () => {
|
|
193
|
-
const rpc = vi.fn(async () => ({
|
|
236
|
+
const rpc = vi.fn(async () => ({
|
|
237
|
+
rateLimits: {},
|
|
238
|
+
account: { account: null },
|
|
239
|
+
}));
|
|
194
240
|
const result = await probeCodexCapacity({
|
|
195
241
|
...context,
|
|
196
|
-
readFile: async () =>
|
|
242
|
+
readFile: async () =>
|
|
243
|
+
JSON.stringify({ personal_access_token: "pat-secret" }),
|
|
197
244
|
fetch: vi.fn().mockRejectedValue(new Error("network failure pat-secret")),
|
|
198
245
|
rpc,
|
|
199
246
|
});
|
|
@@ -205,14 +252,20 @@ describe("tiered Codex probing", () => {
|
|
|
205
252
|
const fetch = vi
|
|
206
253
|
.fn()
|
|
207
254
|
.mockRejectedValueOnce(new Error("PAT failed"))
|
|
208
|
-
.mockResolvedValueOnce(
|
|
255
|
+
.mockResolvedValueOnce(
|
|
256
|
+
new Response(JSON.stringify(apiUsage()), { status: 200 }),
|
|
257
|
+
);
|
|
209
258
|
const rpc = vi.fn();
|
|
210
259
|
const result = await probeCodexCapacity({
|
|
211
260
|
...context,
|
|
212
261
|
readFile: async () =>
|
|
213
262
|
JSON.stringify({
|
|
214
263
|
personal_access_token: "pat-secret",
|
|
215
|
-
tokens: {
|
|
264
|
+
tokens: {
|
|
265
|
+
access_token: "oauth-secret",
|
|
266
|
+
account_id: "acct",
|
|
267
|
+
expires_at: 1_800_000_000,
|
|
268
|
+
},
|
|
216
269
|
}),
|
|
217
270
|
fetch,
|
|
218
271
|
rpc,
|
|
@@ -226,7 +279,9 @@ describe("tiered Codex probing", () => {
|
|
|
226
279
|
it("uses hardened read-only app-server arguments and both account methods", async () => {
|
|
227
280
|
const rpc = vi.fn(async () => ({
|
|
228
281
|
rateLimits: {
|
|
229
|
-
rateLimits: {
|
|
282
|
+
rateLimits: {
|
|
283
|
+
primary: { usedPercent: 5, windowDurationMins: 300, resetsAt: null },
|
|
284
|
+
},
|
|
230
285
|
},
|
|
231
286
|
account: { account: { type: "chatgpt" } },
|
|
232
287
|
}));
|
|
@@ -239,7 +294,13 @@ describe("tiered Codex probing", () => {
|
|
|
239
294
|
"account/read",
|
|
240
295
|
]);
|
|
241
296
|
expect(JSON.stringify(messages)).not.toMatch(/prompt|turn\/start/);
|
|
242
|
-
expect(CODEX_APP_SERVER_ARGS).toEqual([
|
|
297
|
+
expect(CODEX_APP_SERVER_ARGS).toEqual([
|
|
298
|
+
"-s",
|
|
299
|
+
"read-only",
|
|
300
|
+
"-a",
|
|
301
|
+
"untrusted",
|
|
302
|
+
"app-server",
|
|
303
|
+
]);
|
|
243
304
|
});
|
|
244
305
|
|
|
245
306
|
it("uses account/read to distinguish logged-out CLI state", async () => {
|
|
@@ -248,11 +309,18 @@ describe("tiered Codex probing", () => {
|
|
|
248
309
|
readFile: async () => "{}",
|
|
249
310
|
rpc: async () => ({ rateLimits: {}, account: { account: null } }),
|
|
250
311
|
});
|
|
251
|
-
expect(result).toMatchObject({
|
|
312
|
+
expect(result).toMatchObject({
|
|
313
|
+
authenticated: false,
|
|
314
|
+
available: "unknown",
|
|
315
|
+
});
|
|
252
316
|
});
|
|
253
317
|
|
|
254
318
|
it("never exposes tokens or raw auth content through failures", async () => {
|
|
255
|
-
const secrets = [
|
|
319
|
+
const secrets = [
|
|
320
|
+
"pat-secret-value",
|
|
321
|
+
"oauth-secret-value",
|
|
322
|
+
"refresh-secret-value",
|
|
323
|
+
];
|
|
256
324
|
const result = await probeCodexCapacity({
|
|
257
325
|
...context,
|
|
258
326
|
readFile: async () =>
|