@token-dashboard/codex-usage-uploader 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/README.md +113 -0
- package/bin/codex-usage-uploader.js +9 -0
- package/package.json +20 -0
- package/src/auth.js +56 -0
- package/src/cli.js +596 -0
- package/src/collector.js +683 -0
- package/src/constants.js +35 -0
- package/src/install.js +101 -0
- package/src/launchd.js +151 -0
- package/src/parser.js +180 -0
- package/src/runtime-config.js +142 -0
- package/src/state-db.js +334 -0
- package/src/utils.js +53 -0
package/src/install.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { CLI_NAME } from './constants.js';
|
|
6
|
+
import { saveRuntimeConfig } from './runtime-config.js';
|
|
7
|
+
|
|
8
|
+
export function findPackageRoot(startUrl = import.meta.url) {
|
|
9
|
+
let current = path.dirname(fileURLToPath(startUrl));
|
|
10
|
+
while (true) {
|
|
11
|
+
const candidate = path.join(current, 'package.json');
|
|
12
|
+
if (fs.existsSync(candidate)) {
|
|
13
|
+
return current;
|
|
14
|
+
}
|
|
15
|
+
const parent = path.dirname(current);
|
|
16
|
+
if (parent === current) {
|
|
17
|
+
throw new Error('package.json not found from current runtime');
|
|
18
|
+
}
|
|
19
|
+
current = parent;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function resolveSelfPackageSpec(packageRoot, explicitSpec) {
|
|
24
|
+
if (explicitSpec) return explicitSpec;
|
|
25
|
+
return `file:${packageRoot}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function installCurrentPackage(runtime, { packageRoot, packageSpec, nodePath = process.execPath } = {}) {
|
|
29
|
+
const resolvedSpec = resolveSelfPackageSpec(packageRoot, packageSpec);
|
|
30
|
+
const appRoot = runtime.appRoot;
|
|
31
|
+
const stagingDir = path.join(appRoot, `.staging-${Date.now()}`);
|
|
32
|
+
fs.mkdirSync(stagingDir, { recursive: true });
|
|
33
|
+
fs.writeFileSync(
|
|
34
|
+
path.join(stagingDir, 'package.json'),
|
|
35
|
+
`${JSON.stringify({ private: true, name: 'codex-usage-uploader-runtime' }, null, 2)}\n`,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
39
|
+
const result = spawnSync(
|
|
40
|
+
npmCommand,
|
|
41
|
+
['install', '--no-save', '--omit=dev', '--no-package-lock', resolvedSpec],
|
|
42
|
+
{
|
|
43
|
+
cwd: stagingDir,
|
|
44
|
+
stdio: 'inherit',
|
|
45
|
+
env: { ...process.env, npm_config_fund: 'false', npm_config_audit: 'false' },
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
if (result.error) {
|
|
49
|
+
throw new Error(`failed to execute ${npmCommand}: ${result.error.message}`);
|
|
50
|
+
}
|
|
51
|
+
if (result.status !== 0) {
|
|
52
|
+
throw new Error(`npm install failed for ${resolvedSpec}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const sourcePkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
56
|
+
const installedPackageRoot = path.join(stagingDir, 'node_modules', ...String(sourcePkg.name).split('/'));
|
|
57
|
+
const installedPkg = JSON.parse(fs.readFileSync(path.join(installedPackageRoot, 'package.json'), 'utf8'));
|
|
58
|
+
const binField = installedPkg.bin;
|
|
59
|
+
const binRel = typeof binField === 'string'
|
|
60
|
+
? binField
|
|
61
|
+
: binField?.[CLI_NAME] ?? Object.values(binField ?? {})[0];
|
|
62
|
+
if (!binRel) {
|
|
63
|
+
throw new Error('installed package does not expose the expected CLI binary');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const currentDir = runtime.currentAppDir;
|
|
67
|
+
fs.rmSync(currentDir, { recursive: true, force: true });
|
|
68
|
+
fs.mkdirSync(path.dirname(currentDir), { recursive: true });
|
|
69
|
+
fs.renameSync(stagingDir, currentDir);
|
|
70
|
+
|
|
71
|
+
runtime.nodePath = nodePath;
|
|
72
|
+
runtime.packageSpec = resolvedSpec;
|
|
73
|
+
runtime.entryFile = path.join(currentDir, 'node_modules', ...String(sourcePkg.name).split('/'), binRel);
|
|
74
|
+
writeLocalWrapper(runtime);
|
|
75
|
+
ensureHomeBinLink(runtime);
|
|
76
|
+
saveRuntimeConfig(runtime);
|
|
77
|
+
return runtime;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function writeLocalWrapper(runtime) {
|
|
81
|
+
fs.mkdirSync(runtime.localBinDir, { recursive: true });
|
|
82
|
+
const script = `#!/bin/sh
|
|
83
|
+
set -eu
|
|
84
|
+
CONFIG_FILE="\${CODEX_USAGE_UPLOADER_CONFIG:-${runtime.configFile}}"
|
|
85
|
+
exec "${runtime.nodePath}" "${runtime.entryFile}" --config-file "$CONFIG_FILE" "$@"
|
|
86
|
+
`;
|
|
87
|
+
fs.writeFileSync(runtime.localBinPath, script, { mode: 0o755 });
|
|
88
|
+
fs.chmodSync(runtime.localBinPath, 0o755);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function ensureHomeBinLink(runtime) {
|
|
92
|
+
fs.mkdirSync(path.dirname(runtime.homeBinLink), { recursive: true });
|
|
93
|
+
try {
|
|
94
|
+
if (fs.existsSync(runtime.homeBinLink) || fs.lstatSync(runtime.homeBinLink).isSymbolicLink()) {
|
|
95
|
+
fs.rmSync(runtime.homeBinLink, { force: true });
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// ignore
|
|
99
|
+
}
|
|
100
|
+
fs.symlinkSync(runtime.localBinPath, runtime.homeBinLink);
|
|
101
|
+
}
|
package/src/launchd.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
export function buildLaunchdPlist(runtime) {
|
|
6
|
+
const args = [
|
|
7
|
+
runtime.nodePath,
|
|
8
|
+
runtime.entryFile,
|
|
9
|
+
'--config-file',
|
|
10
|
+
runtime.configFile,
|
|
11
|
+
'run',
|
|
12
|
+
];
|
|
13
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
14
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
15
|
+
<plist version="1.0">
|
|
16
|
+
<dict>
|
|
17
|
+
<key>Label</key>
|
|
18
|
+
<string>${escapeXml(runtime.launchdLabel)}</string>
|
|
19
|
+
<key>ProgramArguments</key>
|
|
20
|
+
<array>
|
|
21
|
+
${args.map((item) => ` <string>${escapeXml(item)}</string>`).join('\n')}
|
|
22
|
+
</array>
|
|
23
|
+
<key>RunAtLoad</key>
|
|
24
|
+
<true/>
|
|
25
|
+
<key>KeepAlive</key>
|
|
26
|
+
<true/>
|
|
27
|
+
<key>ProcessType</key>
|
|
28
|
+
<string>Background</string>
|
|
29
|
+
<key>WorkingDirectory</key>
|
|
30
|
+
<string>${escapeXml(runtime.installRoot)}</string>
|
|
31
|
+
<key>StandardOutPath</key>
|
|
32
|
+
<string>${escapeXml(runtime.stdoutLogPath)}</string>
|
|
33
|
+
<key>StandardErrorPath</key>
|
|
34
|
+
<string>${escapeXml(runtime.stderrLogPath)}</string>
|
|
35
|
+
<key>EnvironmentVariables</key>
|
|
36
|
+
<dict>
|
|
37
|
+
<key>NODE_NO_WARNINGS</key>
|
|
38
|
+
<string>1</string>
|
|
39
|
+
</dict>
|
|
40
|
+
</dict>
|
|
41
|
+
</plist>
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function escapeXml(value) {
|
|
46
|
+
return String(value)
|
|
47
|
+
.replaceAll('&', '&')
|
|
48
|
+
.replaceAll('<', '<')
|
|
49
|
+
.replaceAll('>', '>')
|
|
50
|
+
.replaceAll('"', '"')
|
|
51
|
+
.replaceAll("'", ''');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function parseLaunchctlPrint(output) {
|
|
55
|
+
const state = output.match(/^\s*state = ([^\n]+)$/m)?.[1]?.trim() ?? null;
|
|
56
|
+
const pidRaw = output.match(/^\s*pid = (\d+)$/m)?.[1] ?? null;
|
|
57
|
+
const exitRaw = output.match(/^\s*last exit code = (-?\d+)$/m)?.[1] ?? null;
|
|
58
|
+
return {
|
|
59
|
+
loaded: true,
|
|
60
|
+
running: state === 'running' || pidRaw != null,
|
|
61
|
+
pid: pidRaw ? Number(pidRaw) : null,
|
|
62
|
+
state,
|
|
63
|
+
lastExitCode: exitRaw ? Number(exitRaw) : null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class LaunchdServiceManager {
|
|
68
|
+
constructor(runtime) {
|
|
69
|
+
this.runtime = runtime;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
ensureMacos() {
|
|
73
|
+
if (process.platform !== 'darwin') {
|
|
74
|
+
throw new Error('launchd management is only supported on macOS.');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
domainTarget() {
|
|
79
|
+
return `gui/${process.getuid()}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
serviceTarget() {
|
|
83
|
+
return `${this.domainTarget()}/${this.runtime.launchdLabel}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
run(args, { check = true } = {}) {
|
|
87
|
+
const result = spawnSync(args[0], args.slice(1), { encoding: 'utf8' });
|
|
88
|
+
if (check && result.status !== 0) {
|
|
89
|
+
throw new Error(result.stderr?.trim() || result.stdout?.trim() || `command failed: ${args.join(' ')}`);
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
ensurePlist() {
|
|
95
|
+
this.ensureMacos();
|
|
96
|
+
if (!this.runtime.entryFile || !fs.existsSync(this.runtime.entryFile)) {
|
|
97
|
+
throw new Error(`installed runtime entry not found: ${this.runtime.entryFile || '(empty)'}`);
|
|
98
|
+
}
|
|
99
|
+
fs.mkdirSync(path.dirname(this.runtime.plistPath), { recursive: true });
|
|
100
|
+
fs.mkdirSync(path.dirname(this.runtime.stdoutLogPath), { recursive: true });
|
|
101
|
+
fs.writeFileSync(this.runtime.plistPath, buildLaunchdPlist(this.runtime));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
start() {
|
|
105
|
+
this.ensurePlist();
|
|
106
|
+
this.stop();
|
|
107
|
+
this.run(['launchctl', 'bootstrap', this.domainTarget(), this.runtime.plistPath]);
|
|
108
|
+
this.run(['launchctl', 'kickstart', '-k', this.serviceTarget()], { check: false });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
stop() {
|
|
112
|
+
this.ensureMacos();
|
|
113
|
+
this.run(['launchctl', 'bootout', this.domainTarget(), this.runtime.plistPath], { check: false });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
restart() {
|
|
117
|
+
this.start();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
status() {
|
|
121
|
+
this.ensureMacos();
|
|
122
|
+
const result = this.run(['launchctl', 'print', this.serviceTarget()], { check: false });
|
|
123
|
+
if (result.status !== 0) {
|
|
124
|
+
return {
|
|
125
|
+
loaded: false,
|
|
126
|
+
running: false,
|
|
127
|
+
pid: null,
|
|
128
|
+
state: null,
|
|
129
|
+
lastExitCode: null,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return parseLaunchctlPrint(result.stdout);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
uninstall() {
|
|
136
|
+
this.stop();
|
|
137
|
+
if (fs.existsSync(this.runtime.plistPath)) {
|
|
138
|
+
fs.unlinkSync(this.runtime.plistPath);
|
|
139
|
+
}
|
|
140
|
+
if (fs.existsSync(this.runtime.homeBinLink) && fs.lstatSync(this.runtime.homeBinLink).isSymbolicLink()) {
|
|
141
|
+
try {
|
|
142
|
+
const target = fs.realpathSync.native(this.runtime.homeBinLink);
|
|
143
|
+
if (target === this.runtime.localBinPath) {
|
|
144
|
+
fs.unlinkSync(this.runtime.homeBinLink);
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
fs.unlinkSync(this.runtime.homeBinLink);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
package/src/parser.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { computeEventUid, deriveRepoName, isoToEpochMs, stableStringify } from './utils.js';
|
|
2
|
+
|
|
3
|
+
export class RolloutParser {
|
|
4
|
+
constructor(collectorIdentity, relpath, initialState = {}) {
|
|
5
|
+
this.collectorIdentity = collectorIdentity;
|
|
6
|
+
this.relpath = relpath;
|
|
7
|
+
this.currentSession = initialState.current_session ?? null;
|
|
8
|
+
this.currentTurn = initialState.current_turn ?? null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
exportState() {
|
|
12
|
+
return {
|
|
13
|
+
current_session: this.currentSession,
|
|
14
|
+
current_turn: this.currentTurn,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
normalizeSession(payload) {
|
|
19
|
+
if (!payload?.id) return null;
|
|
20
|
+
const git = payload.git && typeof payload.git === 'object' ? payload.git : {};
|
|
21
|
+
const cwd = payload.cwd ?? null;
|
|
22
|
+
const repositoryUrl = git.repository_url ?? null;
|
|
23
|
+
return {
|
|
24
|
+
sessionId: String(payload.id),
|
|
25
|
+
sessionTimestamp: isoToEpochMs(payload.timestamp),
|
|
26
|
+
cwd,
|
|
27
|
+
originator: payload.originator ?? null,
|
|
28
|
+
source: payload.source ?? null,
|
|
29
|
+
cliVersion: payload.cli_version ?? null,
|
|
30
|
+
modelProvider: payload.model_provider ?? null,
|
|
31
|
+
repoName: deriveRepoName(repositoryUrl, cwd),
|
|
32
|
+
gitBranch: git.branch ?? null,
|
|
33
|
+
gitCommitHash: git.commit_hash ?? null,
|
|
34
|
+
repositoryUrl,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
normalizeTurn(payload, timestampMs) {
|
|
39
|
+
const turnId = payload?.turn_id;
|
|
40
|
+
const sessionId = this.currentSession?.sessionId;
|
|
41
|
+
if (!turnId || !sessionId) return null;
|
|
42
|
+
const sandbox = payload.sandbox_policy && typeof payload.sandbox_policy === 'object'
|
|
43
|
+
? payload.sandbox_policy
|
|
44
|
+
: {};
|
|
45
|
+
const writableRoots = Array.isArray(sandbox.writable_roots) ? sandbox.writable_roots : null;
|
|
46
|
+
const collaborationMode = payload.collaboration_mode && typeof payload.collaboration_mode === 'object'
|
|
47
|
+
? payload.collaboration_mode.mode
|
|
48
|
+
: null;
|
|
49
|
+
return {
|
|
50
|
+
turnId: String(turnId),
|
|
51
|
+
sessionId: String(sessionId),
|
|
52
|
+
eventTimestamp: timestampMs,
|
|
53
|
+
cwd: payload.cwd ?? null,
|
|
54
|
+
currentDate: payload.current_date ?? null,
|
|
55
|
+
timezone: payload.timezone ?? null,
|
|
56
|
+
approvalPolicy: payload.approval_policy ?? null,
|
|
57
|
+
sandboxPolicyType: sandbox.type ?? null,
|
|
58
|
+
sandboxNetworkAccess: sandbox.network_access ?? null,
|
|
59
|
+
sandboxWritableRootsJson: writableRoots ? stableStringify(writableRoots) : null,
|
|
60
|
+
sandboxPolicyJson: Object.keys(sandbox).length ? stableStringify(sandbox) : null,
|
|
61
|
+
model: payload.model ?? null,
|
|
62
|
+
personality: payload.personality ?? null,
|
|
63
|
+
collaborationMode: collaborationMode ?? null,
|
|
64
|
+
effort: payload.effort ?? null,
|
|
65
|
+
summary: payload.summary ?? null,
|
|
66
|
+
truncationPolicyJson: payload.truncation_policy != null
|
|
67
|
+
? stableStringify(payload.truncation_policy)
|
|
68
|
+
: null,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
normalizeTokenEvent(payload, timestampMs, lineNo) {
|
|
73
|
+
if (!this.currentSession || timestampMs == null) return null;
|
|
74
|
+
const info = payload?.info;
|
|
75
|
+
if (!info || typeof info !== 'object') return null;
|
|
76
|
+
const totalUsage = info.total_token_usage && typeof info.total_token_usage === 'object'
|
|
77
|
+
? info.total_token_usage
|
|
78
|
+
: {};
|
|
79
|
+
const lastUsage = info.last_token_usage && typeof info.last_token_usage === 'object'
|
|
80
|
+
? info.last_token_usage
|
|
81
|
+
: {};
|
|
82
|
+
const rateLimits = payload.rate_limits && typeof payload.rate_limits === 'object'
|
|
83
|
+
? payload.rate_limits
|
|
84
|
+
: {};
|
|
85
|
+
const primary = rateLimits.primary && typeof rateLimits.primary === 'object' ? rateLimits.primary : {};
|
|
86
|
+
const secondary = rateLimits.secondary && typeof rateLimits.secondary === 'object' ? rateLimits.secondary : {};
|
|
87
|
+
const session = this.currentSession;
|
|
88
|
+
const turn = this.currentTurn ?? {};
|
|
89
|
+
return {
|
|
90
|
+
eventUid: computeEventUid(this.collectorIdentity.collectorId, this.relpath, lineNo),
|
|
91
|
+
sessionId: session.sessionId,
|
|
92
|
+
turnId: turn.turnId ?? null,
|
|
93
|
+
sourceFileRelpath: this.relpath,
|
|
94
|
+
lineNo,
|
|
95
|
+
timestamp: timestampMs,
|
|
96
|
+
model: turn.model ?? null,
|
|
97
|
+
cwd: turn.cwd ?? session.cwd ?? null,
|
|
98
|
+
timezone: turn.timezone ?? null,
|
|
99
|
+
approvalPolicy: turn.approvalPolicy ?? null,
|
|
100
|
+
sandboxPolicyType: turn.sandboxPolicyType ?? null,
|
|
101
|
+
source: session.source ?? null,
|
|
102
|
+
originator: session.originator ?? null,
|
|
103
|
+
cliVersion: session.cliVersion ?? null,
|
|
104
|
+
repositoryUrl: session.repositoryUrl ?? null,
|
|
105
|
+
repoName: session.repoName ?? null,
|
|
106
|
+
gitBranch: session.gitBranch ?? null,
|
|
107
|
+
gitCommitHash: session.gitCommitHash ?? null,
|
|
108
|
+
totalInputTokens: totalUsage.input_tokens ?? null,
|
|
109
|
+
totalCachedInputTokens: totalUsage.cached_input_tokens ?? null,
|
|
110
|
+
totalOutputTokens: totalUsage.output_tokens ?? null,
|
|
111
|
+
totalReasoningOutputTokens: totalUsage.reasoning_output_tokens ?? null,
|
|
112
|
+
totalTokens: totalUsage.total_tokens ?? null,
|
|
113
|
+
lastInputTokens: lastUsage.input_tokens ?? null,
|
|
114
|
+
lastCachedInputTokens: lastUsage.cached_input_tokens ?? null,
|
|
115
|
+
lastOutputTokens: lastUsage.output_tokens ?? null,
|
|
116
|
+
lastReasoningOutputTokens: lastUsage.reasoning_output_tokens ?? null,
|
|
117
|
+
lastTotalTokens: lastUsage.total_tokens ?? null,
|
|
118
|
+
modelContextWindow: info.model_context_window ?? null,
|
|
119
|
+
rateLimitPlanType: rateLimits.plan_type ?? null,
|
|
120
|
+
primaryUsedPercent: primary.used_percent ?? null,
|
|
121
|
+
primaryWindowMinutes: primary.window_minutes ?? null,
|
|
122
|
+
primaryResetsAt: primary.resets_at ?? null,
|
|
123
|
+
secondaryUsedPercent: secondary.used_percent ?? null,
|
|
124
|
+
secondaryWindowMinutes: secondary.window_minutes ?? null,
|
|
125
|
+
secondaryResetsAt: secondary.resets_at ?? null,
|
|
126
|
+
credits: normalizeCredits(rateLimits.credits),
|
|
127
|
+
rawRateLimitsJson: Object.keys(rateLimits).length ? stableStringify(rateLimits) : null,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
processLine(lineNo, line) {
|
|
132
|
+
let record;
|
|
133
|
+
try {
|
|
134
|
+
record = JSON.parse(line);
|
|
135
|
+
} catch {
|
|
136
|
+
return { sessions: [], turns: [], events: [] };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const recordType = record.type;
|
|
140
|
+
const payload = record.payload ?? {};
|
|
141
|
+
const timestampMs = isoToEpochMs(record.timestamp);
|
|
142
|
+
const sessions = [];
|
|
143
|
+
const turns = [];
|
|
144
|
+
const events = [];
|
|
145
|
+
|
|
146
|
+
if (recordType === 'session_meta') {
|
|
147
|
+
const normalized = this.normalizeSession(payload);
|
|
148
|
+
if (normalized) {
|
|
149
|
+
this.currentSession = normalized;
|
|
150
|
+
sessions.push(normalized);
|
|
151
|
+
}
|
|
152
|
+
} else if (recordType === 'turn_context') {
|
|
153
|
+
const normalized = this.normalizeTurn(payload, timestampMs);
|
|
154
|
+
if (normalized) {
|
|
155
|
+
this.currentTurn = normalized;
|
|
156
|
+
turns.push(normalized);
|
|
157
|
+
}
|
|
158
|
+
} else if (recordType === 'event_msg' && payload.type === 'token_count') {
|
|
159
|
+
const normalized = this.normalizeTokenEvent(payload, timestampMs, lineNo);
|
|
160
|
+
if (normalized) {
|
|
161
|
+
events.push(normalized);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { sessions, turns, events };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function normalizeCredits(value) {
|
|
170
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
if (value && typeof value === 'object') {
|
|
174
|
+
const balance = value.balance;
|
|
175
|
+
if (typeof balance === 'number' && Number.isFinite(balance)) {
|
|
176
|
+
return balance;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
CLI_NAME,
|
|
5
|
+
DEFAULT_APP_ROOT,
|
|
6
|
+
DEFAULT_BACKEND_URL,
|
|
7
|
+
DEFAULT_CODEX_AUTH_PATH,
|
|
8
|
+
DEFAULT_CONFIG_FILE,
|
|
9
|
+
DEFAULT_CURRENT_APP_DIR,
|
|
10
|
+
DEFAULT_HOME_BIN_LINK,
|
|
11
|
+
DEFAULT_INSTALL_ROOT,
|
|
12
|
+
DEFAULT_LAUNCHD_LABEL,
|
|
13
|
+
DEFAULT_LOCAL_BIN_DIR,
|
|
14
|
+
DEFAULT_LOCAL_BIN_PATH,
|
|
15
|
+
DEFAULT_LOG_DIR,
|
|
16
|
+
DEFAULT_PLIST_PATH,
|
|
17
|
+
DEFAULT_SESSIONS_DIR,
|
|
18
|
+
DEFAULT_STATE_DB,
|
|
19
|
+
DEFAULT_STDERR_LOG_PATH,
|
|
20
|
+
DEFAULT_STDOUT_LOG_PATH,
|
|
21
|
+
STATUS_ONLINE_THRESHOLD_SECONDS,
|
|
22
|
+
} from './constants.js';
|
|
23
|
+
import { stableStringify } from './utils.js';
|
|
24
|
+
|
|
25
|
+
export function defaultRuntimeConfig(configFile = DEFAULT_CONFIG_FILE) {
|
|
26
|
+
const installRoot = path.dirname(configFile);
|
|
27
|
+
const appRoot = path.join(installRoot, 'app');
|
|
28
|
+
const currentAppDir = path.join(appRoot, 'current');
|
|
29
|
+
return {
|
|
30
|
+
configFile,
|
|
31
|
+
installRoot,
|
|
32
|
+
appRoot,
|
|
33
|
+
currentAppDir,
|
|
34
|
+
backendUrl: DEFAULT_BACKEND_URL,
|
|
35
|
+
intervalSeconds: 30,
|
|
36
|
+
codexAuthPath: DEFAULT_CODEX_AUTH_PATH,
|
|
37
|
+
sessionsDir: DEFAULT_SESSIONS_DIR,
|
|
38
|
+
stateDbPath: path.join(installRoot, path.basename(DEFAULT_STATE_DB)),
|
|
39
|
+
nodePath: process.execPath,
|
|
40
|
+
entryFile: '',
|
|
41
|
+
packageSpec: '',
|
|
42
|
+
localBinDir: path.join(installRoot, 'bin'),
|
|
43
|
+
localBinPath: path.join(installRoot, 'bin', CLI_NAME),
|
|
44
|
+
homeBinLink: DEFAULT_HOME_BIN_LINK,
|
|
45
|
+
stdoutLogPath: path.join(installRoot, 'logs', path.basename(DEFAULT_STDOUT_LOG_PATH)),
|
|
46
|
+
stderrLogPath: path.join(installRoot, 'logs', path.basename(DEFAULT_STDERR_LOG_PATH)),
|
|
47
|
+
launchdLabel: DEFAULT_LAUNCHD_LABEL,
|
|
48
|
+
plistPath: DEFAULT_PLIST_PATH,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function loadRuntimeConfig(configFile = DEFAULT_CONFIG_FILE) {
|
|
53
|
+
const runtime = defaultRuntimeConfig(configFile);
|
|
54
|
+
try {
|
|
55
|
+
const payload = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
56
|
+
if (!payload || typeof payload !== 'object') return runtime;
|
|
57
|
+
return normalizeRuntimeConfig({ ...runtime, ...payload, configFile });
|
|
58
|
+
} catch {
|
|
59
|
+
return runtime;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function normalizeRuntimeConfig(runtime) {
|
|
64
|
+
const installRoot = runtime.installRoot || DEFAULT_INSTALL_ROOT;
|
|
65
|
+
const appRoot = runtime.appRoot || path.join(installRoot, 'app');
|
|
66
|
+
const currentAppDir = runtime.currentAppDir || path.join(appRoot, 'current');
|
|
67
|
+
const localBinDir = runtime.localBinDir || DEFAULT_LOCAL_BIN_DIR;
|
|
68
|
+
return {
|
|
69
|
+
...runtime,
|
|
70
|
+
configFile: runtime.configFile || DEFAULT_CONFIG_FILE,
|
|
71
|
+
installRoot,
|
|
72
|
+
appRoot,
|
|
73
|
+
currentAppDir,
|
|
74
|
+
backendUrl: runtime.backendUrl?.trim() ? runtime.backendUrl.replace(/\/+$/, '') : null,
|
|
75
|
+
intervalSeconds: Number(runtime.intervalSeconds) || 30,
|
|
76
|
+
codexAuthPath: runtime.codexAuthPath || DEFAULT_CODEX_AUTH_PATH,
|
|
77
|
+
sessionsDir: runtime.sessionsDir || DEFAULT_SESSIONS_DIR,
|
|
78
|
+
stateDbPath: runtime.stateDbPath || path.join(installRoot, 'state.sqlite'),
|
|
79
|
+
nodePath: runtime.nodePath || process.execPath,
|
|
80
|
+
entryFile: runtime.entryFile || '',
|
|
81
|
+
packageSpec: runtime.packageSpec || '',
|
|
82
|
+
localBinDir,
|
|
83
|
+
localBinPath: runtime.localBinPath || path.join(localBinDir, CLI_NAME),
|
|
84
|
+
homeBinLink: runtime.homeBinLink || DEFAULT_HOME_BIN_LINK,
|
|
85
|
+
stdoutLogPath: runtime.stdoutLogPath || path.join(installRoot, 'logs', 'stdout.log'),
|
|
86
|
+
stderrLogPath: runtime.stderrLogPath || path.join(installRoot, 'logs', 'stderr.log'),
|
|
87
|
+
launchdLabel: runtime.launchdLabel || DEFAULT_LAUNCHD_LABEL,
|
|
88
|
+
plistPath: runtime.plistPath || DEFAULT_PLIST_PATH,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function saveRuntimeConfig(runtime) {
|
|
93
|
+
const normalized = normalizeRuntimeConfig(runtime);
|
|
94
|
+
fs.mkdirSync(path.dirname(normalized.configFile), { recursive: true });
|
|
95
|
+
fs.mkdirSync(path.dirname(normalized.stdoutLogPath), { recursive: true });
|
|
96
|
+
fs.writeFileSync(normalized.configFile, `${stableStringify({
|
|
97
|
+
backendUrl: normalized.backendUrl,
|
|
98
|
+
intervalSeconds: normalized.intervalSeconds,
|
|
99
|
+
codexAuthPath: normalized.codexAuthPath,
|
|
100
|
+
sessionsDir: normalized.sessionsDir,
|
|
101
|
+
stateDbPath: normalized.stateDbPath,
|
|
102
|
+
installRoot: normalized.installRoot,
|
|
103
|
+
appRoot: normalized.appRoot,
|
|
104
|
+
currentAppDir: normalized.currentAppDir,
|
|
105
|
+
nodePath: normalized.nodePath,
|
|
106
|
+
entryFile: normalized.entryFile,
|
|
107
|
+
packageSpec: normalized.packageSpec,
|
|
108
|
+
localBinDir: normalized.localBinDir,
|
|
109
|
+
localBinPath: normalized.localBinPath,
|
|
110
|
+
homeBinLink: normalized.homeBinLink,
|
|
111
|
+
stdoutLogPath: normalized.stdoutLogPath,
|
|
112
|
+
stderrLogPath: normalized.stderrLogPath,
|
|
113
|
+
launchdLabel: normalized.launchdLabel,
|
|
114
|
+
plistPath: normalized.plistPath,
|
|
115
|
+
})}\n`);
|
|
116
|
+
return normalized;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function mergeRuntimeConfig(configFile, overrides = {}) {
|
|
120
|
+
const base = loadRuntimeConfig(configFile);
|
|
121
|
+
return normalizeRuntimeConfig({ ...base, ...overrides, configFile });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function formatStatusOutput(runtime, status) {
|
|
125
|
+
return {
|
|
126
|
+
configExists: fs.existsSync(runtime.configFile),
|
|
127
|
+
loaded: status.loaded,
|
|
128
|
+
running: status.running,
|
|
129
|
+
pid: status.pid,
|
|
130
|
+
state: status.state,
|
|
131
|
+
lastExitCode: status.lastExitCode,
|
|
132
|
+
backendUrl: runtime.backendUrl,
|
|
133
|
+
intervalSeconds: runtime.intervalSeconds,
|
|
134
|
+
configFile: runtime.configFile,
|
|
135
|
+
stateDbPath: runtime.stateDbPath,
|
|
136
|
+
stdoutLogPath: runtime.stdoutLogPath,
|
|
137
|
+
stderrLogPath: runtime.stderrLogPath,
|
|
138
|
+
plistPath: runtime.plistPath,
|
|
139
|
+
label: runtime.launchdLabel,
|
|
140
|
+
onlineThresholdSeconds: STATUS_ONLINE_THRESHOLD_SECONDS,
|
|
141
|
+
};
|
|
142
|
+
}
|