@narumitw/pi-codex-usage 0.12.0 → 0.13.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/README.md +3 -2
- package/package.json +1 -1
- package/src/app-server-client.ts +216 -0
- package/src/codex-usage.ts +27 -898
- package/src/format.ts +264 -0
- package/src/normalize.ts +210 -0
- package/src/query.ts +179 -0
- package/src/types.ts +121 -0
package/README.md
CHANGED
|
@@ -98,14 +98,15 @@ The extension does not read Pi or Codex auth files directly, and it does not exp
|
|
|
98
98
|
```txt
|
|
99
99
|
extensions/pi-codex-usage/
|
|
100
100
|
├── src/
|
|
101
|
-
│
|
|
101
|
+
│ ├── codex-usage.ts # Pi entrypoint and cache/lifecycle orchestration
|
|
102
|
+
│ └── *.ts # Package-local query, RPC, normalization, and format modules
|
|
102
103
|
├── README.md
|
|
103
104
|
├── LICENSE
|
|
104
105
|
├── tsconfig.json
|
|
105
106
|
└── package.json
|
|
106
107
|
```
|
|
107
108
|
|
|
108
|
-
The package exposes its Pi extension through `package.json`:
|
|
109
|
+
Only `codex-usage.ts` is a Pi entrypoint; the other source modules are internal. The package exposes its Pi extension through `package.json`:
|
|
109
110
|
|
|
110
111
|
```json
|
|
111
112
|
{
|
package/package.json
CHANGED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { normalizeAppServerResponse } from "./normalize.js";
|
|
4
|
+
import type {
|
|
5
|
+
AppServerRateLimitResponse,
|
|
6
|
+
CodexUsageReport,
|
|
7
|
+
PendingRpc,
|
|
8
|
+
RpcResponse,
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
|
|
11
|
+
const MAX_ERROR_BODY_CHARS = 600;
|
|
12
|
+
|
|
13
|
+
export async function queryViaCodexAppServer(timeoutMs: number): Promise<CodexUsageReport> {
|
|
14
|
+
const client = new CodexAppServerClient(timeoutMs);
|
|
15
|
+
try {
|
|
16
|
+
await client.start();
|
|
17
|
+
await client.request("initialize", {
|
|
18
|
+
clientInfo: {
|
|
19
|
+
name: "pi_codex_usage",
|
|
20
|
+
title: "Pi Codex Usage",
|
|
21
|
+
version: "0.1.0",
|
|
22
|
+
},
|
|
23
|
+
capabilities: {
|
|
24
|
+
experimentalApi: false,
|
|
25
|
+
requestAttestation: false,
|
|
26
|
+
optOutNotificationMethods: [],
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
client.notify("initialized");
|
|
30
|
+
const result = await client.request("account/rateLimits/read", undefined);
|
|
31
|
+
return normalizeAppServerResponse(
|
|
32
|
+
assertObject(result, "account/rateLimits/read result") as AppServerRateLimitResponse,
|
|
33
|
+
Date.now(),
|
|
34
|
+
);
|
|
35
|
+
} finally {
|
|
36
|
+
client.dispose();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class CodexAppServerClient {
|
|
41
|
+
private child?: ChildProcessWithoutNullStreams;
|
|
42
|
+
private nextId = 1;
|
|
43
|
+
private stderr = "";
|
|
44
|
+
private readonly pending = new Map<number, PendingRpc>();
|
|
45
|
+
private startPromise?: Promise<void>;
|
|
46
|
+
private exitError?: Error;
|
|
47
|
+
private readonly timeoutMs: number;
|
|
48
|
+
|
|
49
|
+
constructor(timeoutMs: number) {
|
|
50
|
+
this.timeoutMs = timeoutMs;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
start(): Promise<void> {
|
|
54
|
+
if (this.startPromise) return this.startPromise;
|
|
55
|
+
|
|
56
|
+
this.startPromise = new Promise((resolve, reject) => {
|
|
57
|
+
const child = spawn("codex", ["app-server", "--listen", "stdio://"], {
|
|
58
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
59
|
+
});
|
|
60
|
+
this.child = child;
|
|
61
|
+
|
|
62
|
+
const startupTimeout = setTimeout(() => {
|
|
63
|
+
reject(
|
|
64
|
+
new Error(
|
|
65
|
+
`Timed out after ${Math.round(this.timeoutMs / 1000)}s starting codex app-server.`,
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
}, this.timeoutMs);
|
|
69
|
+
|
|
70
|
+
child.once("spawn", () => {
|
|
71
|
+
clearTimeout(startupTimeout);
|
|
72
|
+
resolve();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
child.once("error", (error) => {
|
|
76
|
+
clearTimeout(startupTimeout);
|
|
77
|
+
reject(new Error(`Failed to start codex app-server: ${error.message}`));
|
|
78
|
+
this.rejectAll(error);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
child.once("exit", (code, signal) => {
|
|
82
|
+
const suffix = this.stderr ? ` stderr: ${redactErrorBody(this.stderr)}` : "";
|
|
83
|
+
this.exitError = new Error(
|
|
84
|
+
`codex app-server exited before completing the request (code ${code ?? "unknown"}, signal ${signal ?? "none"}).${suffix}`,
|
|
85
|
+
);
|
|
86
|
+
this.rejectAll(this.exitError);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
child.stderr.setEncoding("utf8");
|
|
90
|
+
child.stderr.on("data", (chunk: string) => {
|
|
91
|
+
this.stderr = truncateEnd(this.stderr + chunk, MAX_ERROR_BODY_CHARS);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const lines = createInterface({ input: child.stdout });
|
|
95
|
+
lines.on("line", (line) => this.handleLine(line));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return this.startPromise;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
request(method: string, params: unknown): Promise<unknown> {
|
|
102
|
+
const child = this.child;
|
|
103
|
+
if (!child?.stdin.writable) {
|
|
104
|
+
throw new Error("codex app-server is not running.");
|
|
105
|
+
}
|
|
106
|
+
if (this.exitError) throw this.exitError;
|
|
107
|
+
|
|
108
|
+
const id = this.nextId++;
|
|
109
|
+
const payload = params === undefined ? { method, id } : { method, id, params };
|
|
110
|
+
const response = new Promise<unknown>((resolve, reject) => {
|
|
111
|
+
const timeout = setTimeout(() => {
|
|
112
|
+
this.pending.delete(id);
|
|
113
|
+
reject(
|
|
114
|
+
new Error(`Timed out after ${Math.round(this.timeoutMs / 1000)}s waiting for ${method}.`),
|
|
115
|
+
);
|
|
116
|
+
}, this.timeoutMs);
|
|
117
|
+
|
|
118
|
+
this.pending.set(id, {
|
|
119
|
+
resolve: (value) => {
|
|
120
|
+
clearTimeout(timeout);
|
|
121
|
+
resolve(value);
|
|
122
|
+
},
|
|
123
|
+
reject: (error) => {
|
|
124
|
+
clearTimeout(timeout);
|
|
125
|
+
reject(error);
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
child.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
131
|
+
return response;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
notify(method: string): void {
|
|
135
|
+
const child = this.child;
|
|
136
|
+
if (!child?.stdin.writable) return;
|
|
137
|
+
child.stdin.write(`${JSON.stringify({ method })}\n`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
dispose(): void {
|
|
141
|
+
for (const [id, pending] of this.pending) {
|
|
142
|
+
pending.reject(new Error(`codex app-server request ${id} cancelled.`));
|
|
143
|
+
}
|
|
144
|
+
this.pending.clear();
|
|
145
|
+
|
|
146
|
+
const child = this.child;
|
|
147
|
+
if (!child) return;
|
|
148
|
+
child.stdin.end();
|
|
149
|
+
if (!child.killed) child.kill();
|
|
150
|
+
this.child = undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private handleLine(line: string): void {
|
|
154
|
+
let parsed: RpcResponse;
|
|
155
|
+
try {
|
|
156
|
+
parsed = JSON.parse(line) as RpcResponse;
|
|
157
|
+
} catch {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (typeof parsed.id !== "number") return;
|
|
162
|
+
const pending = this.pending.get(parsed.id);
|
|
163
|
+
if (!pending) return;
|
|
164
|
+
this.pending.delete(parsed.id);
|
|
165
|
+
|
|
166
|
+
if (parsed.error) {
|
|
167
|
+
const message =
|
|
168
|
+
typeof parsed.error.message === "string" ? parsed.error.message : "unknown error";
|
|
169
|
+
pending.reject(new Error(`codex app-server request failed: ${message}`));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
pending.resolve(parsed.result);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private rejectAll(error: Error): void {
|
|
177
|
+
for (const pending of this.pending.values()) pending.reject(error);
|
|
178
|
+
this.pending.clear();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function parseJsonObject(text: string, description: string): Record<string, unknown> {
|
|
183
|
+
let parsed: unknown;
|
|
184
|
+
try {
|
|
185
|
+
parsed = JSON.parse(text) as unknown;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
throw new Error(`${description} was not valid JSON: ${errorMessage(error)}`);
|
|
188
|
+
}
|
|
189
|
+
return assertObject(parsed, description);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function assertObject(value: unknown, description: string): Record<string, unknown> {
|
|
193
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
194
|
+
throw new Error(`${description} was not an object.`);
|
|
195
|
+
}
|
|
196
|
+
return value as Record<string, unknown>;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function redactErrorBody(body: string): string {
|
|
200
|
+
return truncateEnd(
|
|
201
|
+
body
|
|
202
|
+
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer <redacted>")
|
|
203
|
+
.replace(/"access_token"\s*:\s*"[^"]+"/gi, '"access_token":"<redacted>"')
|
|
204
|
+
.trim(),
|
|
205
|
+
MAX_ERROR_BODY_CHARS,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function truncateEnd(value: string, maxChars: number): string {
|
|
210
|
+
if (value.length <= maxChars) return value;
|
|
211
|
+
return `${value.slice(0, maxChars - 1)}…`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function errorMessage(error: unknown): string {
|
|
215
|
+
return error instanceof Error ? error.message : String(error);
|
|
216
|
+
}
|