ai-native-profile 0.1.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/bin/anp.mjs +214 -0
- package/package.json +19 -0
- package/src/codex-app-server.ts +34 -0
package/bin/anp.mjs
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHmac, randomUUID } from 'node:crypto';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, rmSync, readdirSync, statSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { createInterface } from 'node:readline';
|
|
8
|
+
|
|
9
|
+
const VERSION = '0.1.0';
|
|
10
|
+
const DEFAULT_API_URL = 'https://ai-native-profile.vercel.app';
|
|
11
|
+
const configDir = join(homedir(), '.config', 'ai-native-profile');
|
|
12
|
+
const configFile = join(configDir, 'config.json');
|
|
13
|
+
const paths = {
|
|
14
|
+
codex: [join(homedir(), '.codex', 'state_5.sqlite'), join(homedir(), '.codex', 'sessions')],
|
|
15
|
+
claude_code: [join(homedir(), '.claude', 'stats-cache.json'), join(homedir(), '.claude', 'projects')],
|
|
16
|
+
cursor: [join(homedir(), 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage', 'state.vscdb'), join(homedir(), '.config', 'Cursor', 'User', 'globalStorage', 'state.vscdb')],
|
|
17
|
+
gemini_cli: [join(homedir(), '.gemini', 'tmp')],
|
|
18
|
+
copilot_cli: [join(homedir(), '.copilot', 'session-state')],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const sourceNames = { codex: 'Codex', claude_code: 'Claude Code', cursor: 'Cursor', gemini_cli: 'Gemini CLI', copilot_cli: 'GitHub Copilot CLI' };
|
|
22
|
+
const detectedSources = () => Object.entries(paths).map(([source, candidates]) => ({ source, detected: candidates.some(existsSync) }));
|
|
23
|
+
const loadConfig = () => { try { return JSON.parse(readFileSync(configFile, 'utf8')); } catch { return {}; } };
|
|
24
|
+
const saveConfig = (value) => { mkdirSync(configDir, { recursive: true, mode: 0o700 }); writeFileSync(configFile, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 }); };
|
|
25
|
+
|
|
26
|
+
function canonicalJson(value) {
|
|
27
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
|
|
28
|
+
if (value && typeof value === 'object') {
|
|
29
|
+
const entries = Object.entries(value).filter(([, child]) => child !== undefined).sort(([left], [right]) => left.localeCompare(right));
|
|
30
|
+
return `{${entries.map(([key, child]) => `${JSON.stringify(key)}:${canonicalJson(child)}`).join(',')}}`;
|
|
31
|
+
}
|
|
32
|
+
return JSON.stringify(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function claudeAggregate() {
|
|
36
|
+
const file = paths.claude_code.find((path) => path.endsWith('stats-cache.json') && existsSync(path));
|
|
37
|
+
if (!file) return null;
|
|
38
|
+
try {
|
|
39
|
+
const stats = JSON.parse(readFileSync(file, 'utf8'));
|
|
40
|
+
const tokenRows = new Map((stats.dailyModelTokens ?? []).map((row) => [row.date, Object.values(row.tokensByModel ?? {}).reduce((sum, value) => sum + Number(value || 0), 0)]));
|
|
41
|
+
return (stats.dailyActivity ?? []).filter((row) => /^\d{4}-\d{2}-\d{2}$/.test(row.date ?? '')).map((row) => ({
|
|
42
|
+
date: row.date, source: 'claude_code', category: 'coding',
|
|
43
|
+
metrics: [
|
|
44
|
+
['sessions', row.sessionCount ?? 0], ['turns', row.messageCount ?? 0], ['tool_calls', row.toolCallCount ?? 0], ['tokens', tokenRows.get(row.date) ?? 0],
|
|
45
|
+
].filter(([, value]) => Number(value) > 0).map(([name, value]) => ({ name, value:Number(value), unit:'count', provenance:'local_exact', source:'claude_code', coverageStart:row.date, coverageEnd:row.date, competitiveEligible:true })),
|
|
46
|
+
}));
|
|
47
|
+
} catch { return null; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function localFiles(candidates, limit = 300) {
|
|
51
|
+
const files = [];
|
|
52
|
+
const visit = (path) => {
|
|
53
|
+
if (files.length >= limit || !existsSync(path)) return;
|
|
54
|
+
let info; try { info = statSync(path); } catch { return; }
|
|
55
|
+
if (info.isFile()) { if (/\.(jsonl|json)$/i.test(path)) files.push(path); return; }
|
|
56
|
+
if (!info.isDirectory()) return;
|
|
57
|
+
let entries = []; try { entries = readdirSync(path); } catch { return; }
|
|
58
|
+
for (const entry of entries) visit(join(path, entry));
|
|
59
|
+
};
|
|
60
|
+
for (const path of candidates) visit(path);
|
|
61
|
+
return files;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function genericLocalAggregate(source, candidates) {
|
|
65
|
+
const rows = new Map();
|
|
66
|
+
for (const file of localFiles(candidates)) {
|
|
67
|
+
let content; try { content = readFileSync(file, 'utf8'); } catch { continue; }
|
|
68
|
+
let events = [];
|
|
69
|
+
try { const parsed = JSON.parse(content); events = Array.isArray(parsed) ? parsed : [parsed]; }
|
|
70
|
+
catch { events = content.split(/\r?\n/).flatMap((line) => { try { return line.trim() ? [JSON.parse(line)] : []; } catch { return []; } }); }
|
|
71
|
+
for (const event of events) {
|
|
72
|
+
const stamp = event?.timestamp ?? event?.time ?? event?.created_at ?? event?.createdAt ?? event?.start_time;
|
|
73
|
+
const instant = new Date(stamp);
|
|
74
|
+
if (Number.isNaN(instant.getTime())) continue;
|
|
75
|
+
const date = instant.toISOString().slice(0, 10);
|
|
76
|
+
const name = String(event?.type ?? event?.event ?? event?.name ?? 'turn');
|
|
77
|
+
const current = rows.get(date) ?? { sessions:0, turns:0, tools:0 };
|
|
78
|
+
current.turns += 1;
|
|
79
|
+
if (/session.*(start|create)|conversation.*start/i.test(name)) current.sessions += 1;
|
|
80
|
+
if (/tool.*(call|use)|function.*call/i.test(name)) current.tools += 1;
|
|
81
|
+
rows.set(date, current);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return [...rows.entries()].sort().map(([date, row]) => ({ date, source, category:'coding', metrics:[['sessions', row.sessions], ['turns', row.turns], ['tool_calls', row.tools]].filter(([, value]) => value > 0).map(([name, value]) => ({ name, value, unit:'count', provenance:'local_derived', source, coverageStart:date, coverageEnd:date, competitiveEligible:true })) }));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function cursorAggregate() {
|
|
88
|
+
const database = paths.cursor.find(existsSync);
|
|
89
|
+
if (!database) return [];
|
|
90
|
+
try {
|
|
91
|
+
const { DatabaseSync } = await import('node:sqlite');
|
|
92
|
+
const db = new DatabaseSync(database, { readOnly:true });
|
|
93
|
+
const row = db.prepare(`SELECT COUNT(*) AS count FROM ItemTable WHERE lower(key) LIKE '%composer%' OR lower(key) LIKE '%ai%session%'`).get();
|
|
94
|
+
db.close();
|
|
95
|
+
const count = Number(row?.count ?? 0);
|
|
96
|
+
if (!count) return [];
|
|
97
|
+
const date = statSync(database).mtime.toISOString().slice(0, 10);
|
|
98
|
+
return [{ date, source:'cursor', category:'coding', metrics:[{ name:'sessions', value:count, unit:'count', provenance:'estimated', source:'cursor', coverageStart:date, coverageEnd:date, competitiveEligible:false }] }];
|
|
99
|
+
} catch { return []; }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function codexAggregate() {
|
|
103
|
+
const child = spawn('codex', ['app-server'], { stdio: ['pipe', 'pipe', process.env.ANP_DEBUG === '1' ? 'inherit' : 'ignore'] });
|
|
104
|
+
const lines = createInterface({ input: child.stdout });
|
|
105
|
+
const pending = new Map();
|
|
106
|
+
lines.on('line', (line) => { try { const message = JSON.parse(line); if (typeof message.id === 'number') pending.get(message.id)?.(message); } catch {} });
|
|
107
|
+
const request = (id, method, params = {}) => new Promise((resolve, reject) => {
|
|
108
|
+
const timer = setTimeout(() => reject(new Error(`${method} timed out`)), 8000);
|
|
109
|
+
pending.set(id, (response) => { clearTimeout(timer); pending.delete(id); if (response.error) reject(new Error(response.error.message ?? `${method} failed`)); else resolve(response.result); });
|
|
110
|
+
child.stdin.write(`${JSON.stringify({ id, method, params })}\n`);
|
|
111
|
+
});
|
|
112
|
+
try {
|
|
113
|
+
await request(1, 'initialize', { clientInfo: { name:'ai_native_profile', title:'AI Native Profile', version:VERSION } });
|
|
114
|
+
child.stdin.write(`${JSON.stringify({ method:'initialized', params:{} })}\n`);
|
|
115
|
+
const usage = await request(2, 'account/usage/read');
|
|
116
|
+
const buckets = usage?.dailyUsageBuckets ?? usage?.daily_usage_buckets ?? [];
|
|
117
|
+
return buckets.flatMap((row) => {
|
|
118
|
+
const date = row.startDate ?? row.start_date;
|
|
119
|
+
const tokens = Number(row.tokens ?? row.tokenCount ?? row.token_count ?? 0);
|
|
120
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date ?? '') || !Number.isFinite(tokens) || tokens < 0) return [];
|
|
121
|
+
return [{ date, source:'codex', category:'coding', metrics:[{ name:'tokens', value:tokens, unit:'count', provenance:'official_account_api', source:'codex', coverageStart:date, coverageEnd:date, competitiveEligible:true }] }];
|
|
122
|
+
});
|
|
123
|
+
} finally { child.kill('SIGTERM'); lines.close(); }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function createBatch() {
|
|
127
|
+
let codexDaily = [];
|
|
128
|
+
try { codexDaily = await codexAggregate(); } catch (error) { if (process.env.ANP_DEBUG === '1') console.error(`Codex usage unavailable: ${error instanceof Error ? error.message : String(error)}`); }
|
|
129
|
+
const daily = [...codexDaily, ...(claudeAggregate() ?? []), ...(await cursorAggregate()), ...genericLocalAggregate('gemini_cli', paths.gemini_cli), ...genericLocalAggregate('copilot_cli', paths.copilot_cli)];
|
|
130
|
+
const dates = daily.map((day) => day.date).sort();
|
|
131
|
+
const sources = [...new Set(daily.map((day) => day.source))];
|
|
132
|
+
const batch = {
|
|
133
|
+
schemaVersion:'anp.activity.v1', batchId:randomUUID(), deviceId:loadConfig().deviceId ?? randomUUID(), generatedAt:new Date().toISOString(), collectorVersion:VERSION,
|
|
134
|
+
daily,
|
|
135
|
+
coverage: sources.map((source) => { const sourceDays = daily.filter((day) => day.source === source); const sourceDates = sourceDays.map((day) => day.date).sort(); const competitiveEligible = sourceDays.some((day) => day.metrics.some((metric) => metric.competitiveEligible)); return { source, coverageStart:sourceDates[0] ?? dates[0], coverageEnd:sourceDates.at(-1) ?? dates.at(-1), lastSyncedAt:new Date().toISOString(), completeness:source === 'codex' || source === 'claude_code' ? 'full' : 'partial', competitiveEligible, note:source === 'cursor' ? 'Estimated from supported local database metadata; excluded from rankings.' : source === 'gemini_cli' || source === 'copilot_cli' ? 'Derived from locally retained CLI telemetry; prompt content is never emitted.' : undefined }; }),
|
|
136
|
+
signature:'pending',
|
|
137
|
+
};
|
|
138
|
+
const secret = loadConfig().deviceToken ?? 'preview-only';
|
|
139
|
+
batch.signature = createHmac('sha256', secret).update(canonicalJson({ ...batch, signature: undefined })).digest('base64url');
|
|
140
|
+
return batch;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function printSources() {
|
|
144
|
+
console.log('AI Native Profile source check\n');
|
|
145
|
+
for (const item of detectedSources()) console.log(`${item.detected ? '●' : '○'} ${sourceNames[item.source].padEnd(20)} ${item.detected ? 'detected' : 'not detected'}`);
|
|
146
|
+
console.log('\nOnly aggregate activity leaves this device. Run `anp preview` to inspect it.');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function connect() {
|
|
150
|
+
const apiUrl = option('api-url') ?? process.env.ANP_API_URL ?? DEFAULT_API_URL;
|
|
151
|
+
if (!/^https?:\/\//.test(apiUrl)) throw new Error('The API URL must start with https:// or http://.');
|
|
152
|
+
const response = await fetch(`${apiUrl.replace(/\/$/, '')}/api/v1/device/pair`, { method:'POST', headers:{ 'content-type':'application/json' }, body:JSON.stringify({ deviceName:process.env.USER ?? 'Developer device', collectorVersion:VERSION }) });
|
|
153
|
+
if (!response.ok) throw new Error(`Pairing failed (${response.status}).`);
|
|
154
|
+
const pair = await response.json();
|
|
155
|
+
saveConfig({ apiUrl, deviceId:pair.deviceId, deviceCode:pair.deviceCode, pairingCode:pair.userCode });
|
|
156
|
+
console.log(`Open ${pair.verificationUrl} and enter ${pair.userCode}`);
|
|
157
|
+
console.log('Waiting for approval (the code expires in 10 minutes)…');
|
|
158
|
+
const deadline = Date.now() + 10 * 60 * 1000;
|
|
159
|
+
while (Date.now() < deadline) {
|
|
160
|
+
await new Promise((resolve) => setTimeout(resolve, 2500));
|
|
161
|
+
const tokenResponse = await fetch(`${apiUrl.replace(/\/$/, '')}/api/v1/device/token`, { method:'POST', headers:{ 'content-type':'application/json' }, body:JSON.stringify({ deviceId:pair.deviceId, deviceCode:pair.deviceCode }) });
|
|
162
|
+
if (tokenResponse.status === 202) continue;
|
|
163
|
+
const tokenBody = await tokenResponse.json().catch(() => ({}));
|
|
164
|
+
if (tokenResponse.ok && tokenBody.deviceToken) {
|
|
165
|
+
saveConfig({ apiUrl, deviceId:pair.deviceId, deviceToken:tokenBody.deviceToken });
|
|
166
|
+
console.log('Connected. Automatic sync remains off until you run `anp watch`.');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
throw new Error(tokenBody.error ?? `Pairing failed (${tokenResponse.status}).`);
|
|
170
|
+
}
|
|
171
|
+
throw new Error('Pairing code expired. Run `anp connect` again.');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function sync() {
|
|
175
|
+
const config = loadConfig();
|
|
176
|
+
if (!config.apiUrl || !config.deviceToken) throw new Error('This device is not approved yet. Complete `anp connect` first.');
|
|
177
|
+
const batch = await createBatch();
|
|
178
|
+
const response = await fetch(`${config.apiUrl.replace(/\/$/, '')}/api/v1/metrics/batches`, { method:'POST', headers:{ 'content-type':'application/json', authorization:`Bearer ${config.deviceToken}` }, body:JSON.stringify(batch) });
|
|
179
|
+
if (!response.ok) throw new Error(`Sync failed (${response.status}): ${await response.text()}`);
|
|
180
|
+
console.log(`Synced ${batch.daily.length} aggregate day${batch.daily.length === 1 ? '' : 's'}.`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function help() {
|
|
184
|
+
console.log(`AI Native Profile collector ${VERSION}\n\nUsage: anp <command> [options]\n\n connect Pair this device with the cloud dashboard\n --api-url <url> overrides the hosted dashboard\n sources Detect supported coding tools\n preview Print the exact aggregate payload\n sync Send one aggregate batch\n watch Sync every 15 minutes until stopped\n doctor Check configuration and sources\n sessions Explain selected-session sharing\n share Publish a selected sanitized session\n unshare Revoke a shared session\n export Alias for preview\n disconnect Remove the local platform pairing\n`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function option(name) { const index = process.argv.indexOf(`--${name}`); return index >= 0 ? process.argv[index + 1] : undefined; }
|
|
188
|
+
async function shareSession() {
|
|
189
|
+
const config = loadConfig(); if (!config.apiUrl || !config.deviceToken) throw new Error('Connect this device first.');
|
|
190
|
+
const title = option('title') ?? ''; const summary = option('summary') ?? ''; const source = option('source') ?? 'codex'; const visibility = option('visibility') === 'public' ? 'public' : 'unlisted'; const tags = (option('tags') ?? '').split(',').map((tag) => tag.trim()).filter(Boolean);
|
|
191
|
+
if (!title || !summary) throw new Error('Use --title "…" and --summary "…". Content is never inferred from session logs.');
|
|
192
|
+
const preview = { source, title, summary, tags, visibility };
|
|
193
|
+
console.log('Selected-session preview (this is the exact content that would be sent):\n'); console.log(JSON.stringify(preview, null, 2));
|
|
194
|
+
if (!process.argv.includes('--confirm')) { console.log('\nNothing was sent. Re-run with --confirm after reviewing the preview.'); return; }
|
|
195
|
+
const response = await fetch(`${config.apiUrl.replace(/\/$/, '')}/api/v1/stories`, { method:'POST', headers:{ 'content-type':'application/json', authorization:`Bearer ${config.deviceToken}` }, body:JSON.stringify(preview) }); const body = await response.json().catch(() => ({})); if (!response.ok) throw new Error(body.error ?? `Publishing failed (${response.status}).`); console.log(`Published ${body.visibility}: ${body.url}`);
|
|
196
|
+
}
|
|
197
|
+
async function listSessions() { const config = loadConfig(); if (!config.apiUrl || !config.deviceToken) throw new Error('Connect this device first.'); const response = await fetch(`${config.apiUrl.replace(/\/$/, '')}/api/v1/stories`, { headers:{ authorization:`Bearer ${config.deviceToken}` } }); const body = await response.json().catch(() => ({})); if (!response.ok) throw new Error(body.error ?? 'Could not list shared sessions.'); for (const story of body.stories ?? []) console.log(`${story.revoked_at ? 'revoked' : story.visibility}\t${story.id}\t${story.title}`); }
|
|
198
|
+
async function unshareSession() { const id = process.argv[3]; const config = loadConfig(); if (!id) throw new Error('Use `anp unshare <story-id>`.'); if (!config.apiUrl || !config.deviceToken) throw new Error('Connect this device first.'); const response = await fetch(`${config.apiUrl.replace(/\/$/, '')}/api/v1/stories/${encodeURIComponent(id)}`, { method:'DELETE', headers:{ authorization:`Bearer ${config.deviceToken}` } }); const body = await response.json().catch(() => ({})); if (!response.ok) throw new Error(body.error ?? 'Revocation failed.'); console.log(`Revoked ${id}.`); }
|
|
199
|
+
|
|
200
|
+
const command = process.argv[2] ?? 'help';
|
|
201
|
+
try {
|
|
202
|
+
if (command === 'help' || command === '--help' || command === '-h') help();
|
|
203
|
+
else if (command === 'sources') printSources();
|
|
204
|
+
else if (command === 'preview' || command === 'export') console.log(JSON.stringify(await createBatch(), null, 2));
|
|
205
|
+
else if (command === 'connect') await connect();
|
|
206
|
+
else if (command === 'sync') await sync();
|
|
207
|
+
else if (command === 'watch') { await sync(); console.log('Watching every 15 minutes. Press Ctrl+C to stop.'); setInterval(() => sync().catch((error) => console.error(error.message)), 15 * 60 * 1000); }
|
|
208
|
+
else if (command === 'doctor') { printSources(); const config = loadConfig(); console.log(`\nCloud pairing: ${config.deviceId ? 'configured' : 'not configured'}`); }
|
|
209
|
+
else if (command === 'sessions') await listSessions();
|
|
210
|
+
else if (command === 'share') await shareSession();
|
|
211
|
+
else if (command === 'unshare') await unshareSession();
|
|
212
|
+
else if (command === 'disconnect') { rmSync(configFile, { force:true }); console.log('Removed the local platform pairing. Provider sign-ins and source data were not changed.'); }
|
|
213
|
+
else { console.error(`Unknown command: ${command}\n`); help(); process.exitCode = 1; }
|
|
214
|
+
} catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; }
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-native-profile",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Privacy-first collector for AI Native Profile",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": { "anp": "bin/anp.mjs", "ai-native-profile": "bin/anp.mjs" },
|
|
8
|
+
"files": ["bin", "src"],
|
|
9
|
+
"engines": { "node": ">=22.13.0" },
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/Gaurav890/ai-native-profile.git",
|
|
13
|
+
"directory": "packages/cli"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://ai-native-profile.vercel.app",
|
|
16
|
+
"bugs": { "url": "https://github.com/Gaurav890/ai-native-profile/issues" },
|
|
17
|
+
"publishConfig": { "access": "public", "provenance": true },
|
|
18
|
+
"keywords": ["developer-tools", "analytics", "activity", "profile", "privacy"]
|
|
19
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
|
|
4
|
+
type RpcResponse = { id?: number; result?: unknown; error?: { message?: string } };
|
|
5
|
+
|
|
6
|
+
export async function readCodexAccountUsage(timeoutMs = 8_000): Promise<unknown> {
|
|
7
|
+
const child = spawn('codex', ['app-server'], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
8
|
+
const lines = createInterface({ input: child.stdout });
|
|
9
|
+
const pending = new Map<number, (response: RpcResponse) => void>();
|
|
10
|
+
lines.on('line', (line) => {
|
|
11
|
+
try {
|
|
12
|
+
const message = JSON.parse(line) as RpcResponse;
|
|
13
|
+
if (typeof message.id === 'number') pending.get(message.id)?.(message);
|
|
14
|
+
} catch { /* Ignore diagnostics that are not JSON-RPC. */ }
|
|
15
|
+
});
|
|
16
|
+
const request = (id: number, method: string, params: object = {}) => new Promise<unknown>((resolve, reject) => {
|
|
17
|
+
const timer = setTimeout(() => reject(new Error(`Codex App Server timed out during ${method}.`)), timeoutMs);
|
|
18
|
+
pending.set(id, (response) => {
|
|
19
|
+
clearTimeout(timer);
|
|
20
|
+
pending.delete(id);
|
|
21
|
+
if (response.error) reject(new Error(response.error.message ?? `${method} failed.`));
|
|
22
|
+
else resolve(response.result);
|
|
23
|
+
});
|
|
24
|
+
child.stdin.write(`${JSON.stringify({ id, method, params })}\n`);
|
|
25
|
+
});
|
|
26
|
+
try {
|
|
27
|
+
await request(1, 'initialize', { clientInfo: { name: 'ai_native_profile', title: 'AI Native Profile', version: '0.1.0' } });
|
|
28
|
+
child.stdin.write(`${JSON.stringify({ method: 'initialized', params: {} })}\n`);
|
|
29
|
+
return await request(2, 'account/usage/read');
|
|
30
|
+
} finally {
|
|
31
|
+
child.kill('SIGTERM');
|
|
32
|
+
lines.close();
|
|
33
|
+
}
|
|
34
|
+
}
|