@meterbility/codex-cli-adapter 0.3.1
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/LICENSE +33 -0
- package/README.md +11 -0
- package/dist/index.d.ts +124 -0
- package/dist/index.js +394 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Meterbility authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
----------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
Note: This MIT license covers everything in this repository EXCEPT the
|
|
26
|
+
contents of the /ee directory (when present), which are licensed under the
|
|
27
|
+
Elastic License 2.0 (ELv2) — see /ee/LICENSE.
|
|
28
|
+
|
|
29
|
+
The /ee directory is reserved for Enterprise Edition modules: multi-tenant
|
|
30
|
+
fleet orchestration, SSO/RBAC, audit logs, and long-retention features.
|
|
31
|
+
|
|
32
|
+
Nothing under /ee today; the directory is created as a forward-compatible
|
|
33
|
+
marker so the licensing boundary is visible before any /ee code lands.
|
package/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @meterbility/codex-cli-adapter
|
|
2
|
+
|
|
3
|
+
Part of [Meterbility](https://github.com/HoneycombHairDevelopers/Meterbility) — the debugger for AI agents. Capture every run, inspect every decision, pause and inject live, fork from any step.
|
|
4
|
+
|
|
5
|
+
Ingests Codex CLI rollout sessions from `~/.codex/sessions`.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @meterbility/codex-cli-adapter
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
See the [Meterbility documentation](https://github.com/HoneycombHairDevelopers/Meterbility#readme) for the full guide. MIT licensed.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Store } from '@meterbility/collector';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Codex Desktop / CLI session JSONL schema. Three top-level types live
|
|
5
|
+
* inside the file; the discriminator is `type` and the per-record body
|
|
6
|
+
* lives under `payload`.
|
|
7
|
+
*/
|
|
8
|
+
interface CodexSessionMetaPayload {
|
|
9
|
+
id: string;
|
|
10
|
+
timestamp: string;
|
|
11
|
+
cwd?: string;
|
|
12
|
+
originator?: string;
|
|
13
|
+
cli_version?: string;
|
|
14
|
+
source?: string;
|
|
15
|
+
model_provider?: string;
|
|
16
|
+
base_instructions?: {
|
|
17
|
+
text?: string;
|
|
18
|
+
};
|
|
19
|
+
git?: {
|
|
20
|
+
branch?: string;
|
|
21
|
+
commit?: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
interface CodexResponseItemMessage {
|
|
25
|
+
type: "message";
|
|
26
|
+
role: "user" | "assistant";
|
|
27
|
+
content: Array<{
|
|
28
|
+
type: "input_text";
|
|
29
|
+
text: string;
|
|
30
|
+
} | {
|
|
31
|
+
type: "output_text";
|
|
32
|
+
text: string;
|
|
33
|
+
} | {
|
|
34
|
+
type: string;
|
|
35
|
+
[k: string]: unknown;
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
38
|
+
interface CodexResponseItemFunctionCall {
|
|
39
|
+
type: "function_call";
|
|
40
|
+
call_id?: string;
|
|
41
|
+
id?: string;
|
|
42
|
+
name?: string;
|
|
43
|
+
arguments?: string;
|
|
44
|
+
}
|
|
45
|
+
interface CodexResponseItemFunctionCallOutput {
|
|
46
|
+
type: "function_call_output";
|
|
47
|
+
call_id?: string;
|
|
48
|
+
output?: string;
|
|
49
|
+
}
|
|
50
|
+
type CodexResponseItem = CodexResponseItemMessage | CodexResponseItemFunctionCall | CodexResponseItemFunctionCallOutput | {
|
|
51
|
+
type: string;
|
|
52
|
+
[k: string]: unknown;
|
|
53
|
+
};
|
|
54
|
+
interface CodexEventMsgPayload {
|
|
55
|
+
type: string;
|
|
56
|
+
[k: string]: unknown;
|
|
57
|
+
}
|
|
58
|
+
type CodexRecord = {
|
|
59
|
+
type: "session_meta";
|
|
60
|
+
timestamp?: string;
|
|
61
|
+
payload: CodexSessionMetaPayload;
|
|
62
|
+
} | {
|
|
63
|
+
type: "response_item";
|
|
64
|
+
timestamp?: string;
|
|
65
|
+
payload: CodexResponseItem;
|
|
66
|
+
} | {
|
|
67
|
+
type: "event_msg";
|
|
68
|
+
timestamp?: string;
|
|
69
|
+
payload: CodexEventMsgPayload;
|
|
70
|
+
};
|
|
71
|
+
declare function isResponseItem(r: CodexRecord): r is {
|
|
72
|
+
type: "response_item";
|
|
73
|
+
timestamp?: string;
|
|
74
|
+
payload: CodexResponseItem;
|
|
75
|
+
};
|
|
76
|
+
declare function isMessage(p: CodexResponseItem): p is CodexResponseItemMessage;
|
|
77
|
+
declare function isFunctionCall(p: CodexResponseItem): p is CodexResponseItemFunctionCall;
|
|
78
|
+
declare function isFunctionCallOutput(p: CodexResponseItem): p is CodexResponseItemFunctionCallOutput;
|
|
79
|
+
declare function textOfMessage(m: CodexResponseItemMessage): string;
|
|
80
|
+
|
|
81
|
+
interface ParsedCodexRecord {
|
|
82
|
+
record: CodexRecord;
|
|
83
|
+
offset: number;
|
|
84
|
+
length: number;
|
|
85
|
+
}
|
|
86
|
+
declare function readCodexSession(path: string, startOffset?: number): Promise<ParsedCodexRecord[]>;
|
|
87
|
+
declare function parseBuffer(buf: Buffer, startOffset?: number): ParsedCodexRecord[];
|
|
88
|
+
declare function endOffset(records: ParsedCodexRecord[], fallback?: number): number;
|
|
89
|
+
|
|
90
|
+
interface CodexIngestResult {
|
|
91
|
+
run_id: string;
|
|
92
|
+
steps_added: number;
|
|
93
|
+
bytes_read: number;
|
|
94
|
+
status: "ok" | "empty";
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Ingest a Codex Desktop / Codex CLI rollout file. Maps each assistant
|
|
98
|
+
* `response_item.message` (or `function_call`) to one Meterbility Step. The
|
|
99
|
+
* preceding user message becomes the conversation history. If a
|
|
100
|
+
* subsequent function_call_output matches a function_call's call_id, we
|
|
101
|
+
* attach it as the Outcome.
|
|
102
|
+
*
|
|
103
|
+
* Codex doesn't expose token usage in the rollout JSONL, so token
|
|
104
|
+
* counts are zero and steps are tagged `cost:approx`. The fork engine
|
|
105
|
+
* still works — the conversation history is captured verbatim.
|
|
106
|
+
*/
|
|
107
|
+
declare function ingestCodexSession(store: Store, path: string): Promise<CodexIngestResult>;
|
|
108
|
+
|
|
109
|
+
interface DiscoveredCodexSession {
|
|
110
|
+
path: string;
|
|
111
|
+
session_id: string;
|
|
112
|
+
date_dir: string;
|
|
113
|
+
size_bytes: number;
|
|
114
|
+
mtime: Date;
|
|
115
|
+
}
|
|
116
|
+
declare function codexHome(): string;
|
|
117
|
+
declare function codexSessionsRoot(): string;
|
|
118
|
+
/**
|
|
119
|
+
* Walk `~/.codex/sessions/<year>/<month>/<day>/rollout-*.jsonl` and
|
|
120
|
+
* return every session log Meterbility can ingest, newest first.
|
|
121
|
+
*/
|
|
122
|
+
declare function discoverCodexSessions(): Promise<DiscoveredCodexSession[]>;
|
|
123
|
+
|
|
124
|
+
export { type CodexEventMsgPayload, type CodexIngestResult, type CodexRecord, type CodexResponseItem, type CodexResponseItemFunctionCall, type CodexResponseItemFunctionCallOutput, type CodexResponseItemMessage, type CodexSessionMetaPayload, type DiscoveredCodexSession, type ParsedCodexRecord, codexHome, codexSessionsRoot, discoverCodexSessions, endOffset, ingestCodexSession, isFunctionCall, isFunctionCallOutput, isMessage, isResponseItem, parseBuffer, readCodexSession, textOfMessage };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
function isResponseItem(r) {
|
|
3
|
+
return r.type === "response_item";
|
|
4
|
+
}
|
|
5
|
+
function isMessage(p) {
|
|
6
|
+
return p.type === "message";
|
|
7
|
+
}
|
|
8
|
+
function isFunctionCall(p) {
|
|
9
|
+
return p.type === "function_call";
|
|
10
|
+
}
|
|
11
|
+
function isFunctionCallOutput(p) {
|
|
12
|
+
return p.type === "function_call_output";
|
|
13
|
+
}
|
|
14
|
+
function textOfMessage(m) {
|
|
15
|
+
return m.content.map((c) => {
|
|
16
|
+
if (typeof c === "object" && c && "text" in c && typeof c.text === "string") {
|
|
17
|
+
return c.text;
|
|
18
|
+
}
|
|
19
|
+
return "";
|
|
20
|
+
}).filter((s) => s.length > 0).join("\n");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/parser.ts
|
|
24
|
+
import { readFile } from "fs/promises";
|
|
25
|
+
async function readCodexSession(path, startOffset = 0) {
|
|
26
|
+
const buf = await readFile(path);
|
|
27
|
+
return parseBuffer(buf, startOffset);
|
|
28
|
+
}
|
|
29
|
+
function parseBuffer(buf, startOffset = 0) {
|
|
30
|
+
const records = [];
|
|
31
|
+
let offset = startOffset;
|
|
32
|
+
if (startOffset > 0) {
|
|
33
|
+
while (offset < buf.length && buf[offset - 1] !== 10) offset += 1;
|
|
34
|
+
}
|
|
35
|
+
let lineStart = offset;
|
|
36
|
+
for (let i = offset; i <= buf.length; i++) {
|
|
37
|
+
if (i === buf.length || buf[i] === 10) {
|
|
38
|
+
const line = buf.subarray(lineStart, i).toString("utf-8").trim();
|
|
39
|
+
if (line.length > 0) {
|
|
40
|
+
try {
|
|
41
|
+
records.push({
|
|
42
|
+
record: JSON.parse(line),
|
|
43
|
+
offset: lineStart,
|
|
44
|
+
length: i - lineStart + (i < buf.length ? 1 : 0)
|
|
45
|
+
});
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.warn(
|
|
48
|
+
`[meter/codex] skipping malformed JSONL at ${lineStart}: ${err.message}`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
lineStart = i + 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return records;
|
|
56
|
+
}
|
|
57
|
+
function endOffset(records, fallback = 0) {
|
|
58
|
+
if (records.length === 0) return fallback;
|
|
59
|
+
const last = records[records.length - 1];
|
|
60
|
+
return last.offset + last.length;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/ingest.ts
|
|
64
|
+
import { randomUUID } from "crypto";
|
|
65
|
+
import { stat } from "fs/promises";
|
|
66
|
+
import { hashJson } from "@meterbility/shared";
|
|
67
|
+
import {
|
|
68
|
+
getIngestOffset,
|
|
69
|
+
getRunBySessionId,
|
|
70
|
+
insertRun,
|
|
71
|
+
insertStep,
|
|
72
|
+
recordContextSnapshot,
|
|
73
|
+
setIngestOffset,
|
|
74
|
+
setRunStatus,
|
|
75
|
+
updateRunTotals,
|
|
76
|
+
upsertAgent,
|
|
77
|
+
upsertProjectByCwd
|
|
78
|
+
} from "@meterbility/collector";
|
|
79
|
+
var SOURCE_RUNTIME = "codex-cli";
|
|
80
|
+
async function ingestCodexSession(store, path) {
|
|
81
|
+
const offset = getIngestOffset(store, SOURCE_RUNTIME, path);
|
|
82
|
+
const tail = await readCodexSession(path, offset);
|
|
83
|
+
if (tail.length === 0) {
|
|
84
|
+
return { run_id: "", steps_added: 0, bytes_read: 0, status: "empty" };
|
|
85
|
+
}
|
|
86
|
+
const fileStat = await stat(path);
|
|
87
|
+
const fullSize = fileStat.size;
|
|
88
|
+
const all = offset === 0 ? tail : await readCodexSession(path, 0);
|
|
89
|
+
const meta = inferMeta(all);
|
|
90
|
+
const sessionId = meta.id;
|
|
91
|
+
const project = upsertProjectByCwd(store, meta.cwd ?? "(unknown)");
|
|
92
|
+
const agent = upsertAgent(store, project.project_id, "codex-cli");
|
|
93
|
+
const existing = sessionId ? getRunBySessionId(store, sessionId) : void 0;
|
|
94
|
+
let runId;
|
|
95
|
+
if (existing) {
|
|
96
|
+
runId = existing.run_id;
|
|
97
|
+
} else {
|
|
98
|
+
runId = `run_${randomUUID()}`;
|
|
99
|
+
const run = {
|
|
100
|
+
run_id: runId,
|
|
101
|
+
agent_id: agent.agent_id,
|
|
102
|
+
project_id: project.project_id,
|
|
103
|
+
source_session_id: sessionId,
|
|
104
|
+
source_runtime: SOURCE_RUNTIME,
|
|
105
|
+
title: inferTitle(all),
|
|
106
|
+
status: "in_progress",
|
|
107
|
+
started_at: meta.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
108
|
+
git_branch: meta.git?.branch,
|
|
109
|
+
cwd: meta.cwd,
|
|
110
|
+
tokens_total_input: 0,
|
|
111
|
+
tokens_total_output: 0,
|
|
112
|
+
tokens_total_cached: 0,
|
|
113
|
+
cost_cents: 0,
|
|
114
|
+
step_count: 0,
|
|
115
|
+
tags: ["cost:approx"]
|
|
116
|
+
};
|
|
117
|
+
insertRun(store, run);
|
|
118
|
+
}
|
|
119
|
+
let stepsAdded = 0;
|
|
120
|
+
for await (const step of buildSteps(all, runId, store, meta)) {
|
|
121
|
+
insertStep(store, step);
|
|
122
|
+
stepsAdded += 1;
|
|
123
|
+
}
|
|
124
|
+
const last = lastTimestamp(all);
|
|
125
|
+
setRunStatus(store, runId, finalStatus(all), last);
|
|
126
|
+
updateRunTotals(store, runId);
|
|
127
|
+
setIngestOffset(store, SOURCE_RUNTIME, path, endOffset(tail, fullSize));
|
|
128
|
+
return {
|
|
129
|
+
run_id: runId,
|
|
130
|
+
steps_added: stepsAdded,
|
|
131
|
+
bytes_read: fullSize - offset,
|
|
132
|
+
status: "ok"
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function inferMeta(records) {
|
|
136
|
+
for (const r of records) {
|
|
137
|
+
if (r.record.type === "session_meta") return r.record.payload;
|
|
138
|
+
}
|
|
139
|
+
return { id: "", timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
140
|
+
}
|
|
141
|
+
function inferTitle(records) {
|
|
142
|
+
for (const r of records) {
|
|
143
|
+
if (!isResponseItem(r.record)) continue;
|
|
144
|
+
if (!isMessage(r.record.payload)) continue;
|
|
145
|
+
if (r.record.payload.role !== "user") continue;
|
|
146
|
+
const text = textOfMessage(r.record.payload);
|
|
147
|
+
if (text.trim().length === 0) continue;
|
|
148
|
+
return text.split("\n")[0].slice(0, 80);
|
|
149
|
+
}
|
|
150
|
+
return void 0;
|
|
151
|
+
}
|
|
152
|
+
function lastTimestamp(records) {
|
|
153
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
154
|
+
const ts = records[i].record.timestamp;
|
|
155
|
+
if (ts) return ts;
|
|
156
|
+
}
|
|
157
|
+
return void 0;
|
|
158
|
+
}
|
|
159
|
+
function finalStatus(records) {
|
|
160
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
161
|
+
const r = records[i].record;
|
|
162
|
+
if (r.type === "event_msg" && r.payload.type === "task_complete") {
|
|
163
|
+
return "ok";
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return "in_progress";
|
|
167
|
+
}
|
|
168
|
+
async function* buildSteps(records, runId, store, meta) {
|
|
169
|
+
const systemPromptText = meta.base_instructions?.text;
|
|
170
|
+
let systemPromptRef;
|
|
171
|
+
if (systemPromptText) {
|
|
172
|
+
systemPromptRef = await store.blobs.putString(systemPromptText);
|
|
173
|
+
}
|
|
174
|
+
const history = [];
|
|
175
|
+
let sequence = 0;
|
|
176
|
+
let prevStepId;
|
|
177
|
+
const outputByCallId = /* @__PURE__ */ new Map();
|
|
178
|
+
for (const r of records) {
|
|
179
|
+
if (!isResponseItem(r.record)) continue;
|
|
180
|
+
if (isFunctionCallOutput(r.record.payload) && r.record.payload.call_id) {
|
|
181
|
+
outputByCallId.set(r.record.payload.call_id, r.record.payload);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
for (let i = 0; i < records.length; i++) {
|
|
185
|
+
const r = records[i];
|
|
186
|
+
if (!isResponseItem(r.record)) continue;
|
|
187
|
+
const payload = r.record.payload;
|
|
188
|
+
if (isMessage(payload) && payload.role === "user") {
|
|
189
|
+
const text = textOfMessage(payload);
|
|
190
|
+
const ref = await store.blobs.putString(text);
|
|
191
|
+
history.push({ role: "user", content_ref: ref });
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (isMessage(payload) && payload.role === "assistant") {
|
|
195
|
+
const text = textOfMessage(payload);
|
|
196
|
+
const action = parseActionFromAgentText(text);
|
|
197
|
+
const decisionRef = await store.blobs.putJson(payload.content);
|
|
198
|
+
const components = await snapshotComponents(
|
|
199
|
+
store,
|
|
200
|
+
systemPromptRef,
|
|
201
|
+
history
|
|
202
|
+
);
|
|
203
|
+
const snapshot = {
|
|
204
|
+
id: hashJson(components),
|
|
205
|
+
components
|
|
206
|
+
};
|
|
207
|
+
const snapshotBlobRef = await store.blobs.putJson(snapshot);
|
|
208
|
+
recordContextSnapshot(
|
|
209
|
+
store,
|
|
210
|
+
snapshot.id,
|
|
211
|
+
snapshotBlobRef,
|
|
212
|
+
snapshot.components.length
|
|
213
|
+
);
|
|
214
|
+
const stepId = `stp_${randomUUID()}`;
|
|
215
|
+
const outcome = { status: "ok" };
|
|
216
|
+
const step = {
|
|
217
|
+
step_id: stepId,
|
|
218
|
+
run_id: runId,
|
|
219
|
+
parent_step_id: prevStepId,
|
|
220
|
+
sequence,
|
|
221
|
+
timestamp: r.record.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
222
|
+
model: meta.model_provider ?? "codex-unknown",
|
|
223
|
+
context_snapshot_id: snapshot.id,
|
|
224
|
+
decision_ref: decisionRef,
|
|
225
|
+
action,
|
|
226
|
+
outcome,
|
|
227
|
+
tokens: {
|
|
228
|
+
input: 0,
|
|
229
|
+
output: 0,
|
|
230
|
+
cached_read: 0,
|
|
231
|
+
cache_creation: 0
|
|
232
|
+
},
|
|
233
|
+
latency_ms: 0,
|
|
234
|
+
cost_cents: 0,
|
|
235
|
+
tags: ["cost:approx", "codex"],
|
|
236
|
+
status: "ok"
|
|
237
|
+
};
|
|
238
|
+
yield step;
|
|
239
|
+
const historyRef = await store.blobs.putString(text);
|
|
240
|
+
history.push({ role: "assistant", content_ref: historyRef });
|
|
241
|
+
sequence += 1;
|
|
242
|
+
prevStepId = stepId;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (isFunctionCall(payload)) {
|
|
246
|
+
const action = {
|
|
247
|
+
kind: "tool_call",
|
|
248
|
+
tool_name: payload.name ?? "function",
|
|
249
|
+
tool_use_id: payload.call_id ?? payload.id,
|
|
250
|
+
tool_input: safeParseJson(payload.arguments)
|
|
251
|
+
};
|
|
252
|
+
const decisionRef = await store.blobs.putJson(payload);
|
|
253
|
+
const matchedOutput = payload.call_id ? outputByCallId.get(payload.call_id) : void 0;
|
|
254
|
+
const outcome = matchedOutput ? {
|
|
255
|
+
status: "ok",
|
|
256
|
+
tool_result_ref: await store.blobs.putString(
|
|
257
|
+
matchedOutput.output ?? ""
|
|
258
|
+
),
|
|
259
|
+
summary: matchedOutput.output?.split("\n")[0]?.slice(0, 200)
|
|
260
|
+
} : { status: "pending" };
|
|
261
|
+
const components = await snapshotComponents(
|
|
262
|
+
store,
|
|
263
|
+
systemPromptRef,
|
|
264
|
+
history
|
|
265
|
+
);
|
|
266
|
+
const snapshot = {
|
|
267
|
+
id: hashJson(components),
|
|
268
|
+
components
|
|
269
|
+
};
|
|
270
|
+
const snapshotBlobRef = await store.blobs.putJson(snapshot);
|
|
271
|
+
recordContextSnapshot(
|
|
272
|
+
store,
|
|
273
|
+
snapshot.id,
|
|
274
|
+
snapshotBlobRef,
|
|
275
|
+
snapshot.components.length
|
|
276
|
+
);
|
|
277
|
+
const stepId = `stp_${randomUUID()}`;
|
|
278
|
+
yield {
|
|
279
|
+
step_id: stepId,
|
|
280
|
+
run_id: runId,
|
|
281
|
+
parent_step_id: prevStepId,
|
|
282
|
+
sequence,
|
|
283
|
+
timestamp: r.record.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
284
|
+
model: meta.model_provider ?? "codex-unknown",
|
|
285
|
+
context_snapshot_id: snapshot.id,
|
|
286
|
+
decision_ref: decisionRef,
|
|
287
|
+
action,
|
|
288
|
+
outcome,
|
|
289
|
+
tokens: { input: 0, output: 0, cached_read: 0, cache_creation: 0 },
|
|
290
|
+
latency_ms: 0,
|
|
291
|
+
cost_cents: 0,
|
|
292
|
+
tags: ["cost:approx", "codex"],
|
|
293
|
+
status: outcome.status === "pending" ? "in_progress" : "ok"
|
|
294
|
+
};
|
|
295
|
+
sequence += 1;
|
|
296
|
+
prevStepId = stepId;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async function snapshotComponents(store, systemPromptRef, history) {
|
|
302
|
+
const components = [];
|
|
303
|
+
if (systemPromptRef) {
|
|
304
|
+
components.push({ type: "system_prompt", content_ref: systemPromptRef });
|
|
305
|
+
}
|
|
306
|
+
if (history.length > 0) {
|
|
307
|
+
components.push({ type: "conversation_history", messages: [...history] });
|
|
308
|
+
}
|
|
309
|
+
return components;
|
|
310
|
+
}
|
|
311
|
+
var TOOL_CALL_RE = /\[external_agent_tool_call:\s*(\w+)\]/;
|
|
312
|
+
function parseActionFromAgentText(text) {
|
|
313
|
+
const m = text.match(TOOL_CALL_RE);
|
|
314
|
+
if (m) {
|
|
315
|
+
return {
|
|
316
|
+
kind: "tool_call",
|
|
317
|
+
tool_name: m[1],
|
|
318
|
+
text
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
return text.trim().length > 0 ? { kind: "message", text } : { kind: "thinking_only" };
|
|
322
|
+
}
|
|
323
|
+
function safeParseJson(s) {
|
|
324
|
+
if (!s) return null;
|
|
325
|
+
try {
|
|
326
|
+
return JSON.parse(s);
|
|
327
|
+
} catch {
|
|
328
|
+
return s;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/discover.ts
|
|
333
|
+
import { readdir, stat as stat2 } from "fs/promises";
|
|
334
|
+
import { homedir } from "os";
|
|
335
|
+
import { join } from "path";
|
|
336
|
+
function codexHome() {
|
|
337
|
+
return process.env.CODEX_HOME ?? join(homedir(), ".codex");
|
|
338
|
+
}
|
|
339
|
+
function codexSessionsRoot() {
|
|
340
|
+
return join(codexHome(), "sessions");
|
|
341
|
+
}
|
|
342
|
+
async function discoverCodexSessions() {
|
|
343
|
+
const root = codexSessionsRoot();
|
|
344
|
+
const out = [];
|
|
345
|
+
await walk(root, "", out);
|
|
346
|
+
out.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
async function walk(dir, rel, out) {
|
|
350
|
+
let entries;
|
|
351
|
+
try {
|
|
352
|
+
entries = await readdir(dir);
|
|
353
|
+
} catch {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
for (const e of entries) {
|
|
357
|
+
const full = join(dir, e);
|
|
358
|
+
let s;
|
|
359
|
+
try {
|
|
360
|
+
s = await stat2(full);
|
|
361
|
+
} catch {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (s.isDirectory()) {
|
|
365
|
+
await walk(full, rel ? `${rel}/${e}` : e, out);
|
|
366
|
+
} else if (e.startsWith("rollout-") && e.endsWith(".jsonl")) {
|
|
367
|
+
out.push({
|
|
368
|
+
path: full,
|
|
369
|
+
session_id: extractSessionId(e),
|
|
370
|
+
date_dir: rel,
|
|
371
|
+
size_bytes: s.size,
|
|
372
|
+
mtime: s.mtime
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function extractSessionId(filename) {
|
|
378
|
+
const m = filename.match(/rollout-[\d-]+T[\d-]+-([0-9a-f-]+)\.jsonl$/);
|
|
379
|
+
return m?.[1] ?? filename.replace(/\.jsonl$/, "");
|
|
380
|
+
}
|
|
381
|
+
export {
|
|
382
|
+
codexHome,
|
|
383
|
+
codexSessionsRoot,
|
|
384
|
+
discoverCodexSessions,
|
|
385
|
+
endOffset,
|
|
386
|
+
ingestCodexSession,
|
|
387
|
+
isFunctionCall,
|
|
388
|
+
isFunctionCallOutput,
|
|
389
|
+
isMessage,
|
|
390
|
+
isResponseItem,
|
|
391
|
+
parseBuffer,
|
|
392
|
+
readCodexSession,
|
|
393
|
+
textOfMessage
|
|
394
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@meterbility/codex-cli-adapter",
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@meterbility/shared": "^0.3.0",
|
|
15
|
+
"@meterbility/spec": "^0.3.0",
|
|
16
|
+
"@meterbility/collector": "^0.3.0"
|
|
17
|
+
},
|
|
18
|
+
"description": "Codex CLI session capture adapter for Meterbility",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/HoneycombHairDevelopers/Meterbility.git",
|
|
22
|
+
"directory": "adapters/codex-cli"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/HoneycombHairDevelopers/Meterbility#readme",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/HoneycombHairDevelopers/Meterbility/issues"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20.6"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist"
|
|
36
|
+
],
|
|
37
|
+
"types": "dist/index.d.ts",
|
|
38
|
+
"tsup": {
|
|
39
|
+
"entry": [
|
|
40
|
+
"src/index.ts"
|
|
41
|
+
],
|
|
42
|
+
"format": [
|
|
43
|
+
"esm"
|
|
44
|
+
],
|
|
45
|
+
"dts": true,
|
|
46
|
+
"clean": true
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsup"
|
|
50
|
+
}
|
|
51
|
+
}
|