@getmarrow/install 0.1.51 → 0.1.53
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 +17 -6
- package/bin/marrow-install.js +2 -1
- package/package.json +1 -1
- package/src/control-state.js +116 -0
- package/src/first-hour.js +71 -7
- package/src/governed-runner.js +92 -11
- package/src/installer.js +869 -42
- package/src/usage-telemetry.js +235 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
const { StringDecoder } = require('node:string_decoder');
|
|
3
|
+
|
|
4
|
+
const MAX_TOKEN_COUNT = 1_000_000_000;
|
|
5
|
+
const MAX_USAGE_EVENT_BYTES = 4_096;
|
|
6
|
+
const CODEX_USAGE_KEYS = new Set([
|
|
7
|
+
'cached_input_tokens',
|
|
8
|
+
'input_tokens',
|
|
9
|
+
'output_tokens',
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
function isBoundedTokenCount(value) {
|
|
13
|
+
return Number.isSafeInteger(value) && value >= 0 && value <= MAX_TOKEN_COUNT;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function exactKeys(value, allowed) {
|
|
17
|
+
const keys = Object.keys(value);
|
|
18
|
+
return keys.length === allowed.size && keys.every((key) => allowed.has(key));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeCodexTurnUsage(event) {
|
|
22
|
+
if (!event || typeof event !== 'object' || Array.isArray(event)) return null;
|
|
23
|
+
if (!exactKeys(event, new Set(['type', 'usage'])) || event.type !== 'turn.completed') return null;
|
|
24
|
+
|
|
25
|
+
const usage = event.usage;
|
|
26
|
+
if (!usage || typeof usage !== 'object' || Array.isArray(usage)) return null;
|
|
27
|
+
if (!exactKeys(usage, CODEX_USAGE_KEYS)) return null;
|
|
28
|
+
|
|
29
|
+
const inputTokens = usage.input_tokens;
|
|
30
|
+
const outputTokens = usage.output_tokens;
|
|
31
|
+
const cachedTokens = usage.cached_input_tokens;
|
|
32
|
+
if (![inputTokens, outputTokens, cachedTokens].every(isBoundedTokenCount)) return null;
|
|
33
|
+
if (cachedTokens > inputTokens) return null;
|
|
34
|
+
|
|
35
|
+
const totalTokens = inputTokens + outputTokens;
|
|
36
|
+
if (!Number.isSafeInteger(totalTokens) || totalTokens > MAX_TOKEN_COUNT) return null;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
provider: 'openai',
|
|
40
|
+
input_tokens: inputTokens,
|
|
41
|
+
output_tokens: outputTokens,
|
|
42
|
+
cached_tokens: cachedTokens,
|
|
43
|
+
total_tokens: totalTokens,
|
|
44
|
+
source: 'codex_exec_jsonl',
|
|
45
|
+
marrow_intervention: 'governed_runner_usage_capture',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isCodexJsonExecution(command) {
|
|
50
|
+
if (!Array.isArray(command) || command.length < 3) return false;
|
|
51
|
+
const executable = path.basename(String(command[0] || '')).toLowerCase().replace(/\.exe$/, '');
|
|
52
|
+
const execArgs = command.slice(2);
|
|
53
|
+
const terminatorIndex = execArgs.indexOf('--');
|
|
54
|
+
const optionArgs = terminatorIndex === -1 ? execArgs : execArgs.slice(0, terminatorIndex);
|
|
55
|
+
return executable === 'codex'
|
|
56
|
+
&& command[1] === 'exec'
|
|
57
|
+
&& optionArgs.includes('--json');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createCompletedTypeScanner() {
|
|
61
|
+
let depth = 0;
|
|
62
|
+
let inString = false;
|
|
63
|
+
let escaped = false;
|
|
64
|
+
let role = 'other';
|
|
65
|
+
let token = '';
|
|
66
|
+
let tokenInvalid = false;
|
|
67
|
+
let lastSignificant = '';
|
|
68
|
+
let expectTypeColon = false;
|
|
69
|
+
let expectTypeValue = false;
|
|
70
|
+
let completed = false;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
write(character) {
|
|
74
|
+
if (inString) {
|
|
75
|
+
if (escaped) {
|
|
76
|
+
escaped = false;
|
|
77
|
+
tokenInvalid = role !== 'other';
|
|
78
|
+
} else if (character === '\\') {
|
|
79
|
+
escaped = true;
|
|
80
|
+
} else if (character === '"') {
|
|
81
|
+
inString = false;
|
|
82
|
+
if (role === 'root_key') {
|
|
83
|
+
expectTypeColon = !tokenInvalid && token === 'type';
|
|
84
|
+
} else if (role === 'type_value') {
|
|
85
|
+
if (!tokenInvalid && token === 'turn.completed') completed = true;
|
|
86
|
+
expectTypeValue = false;
|
|
87
|
+
}
|
|
88
|
+
role = 'other';
|
|
89
|
+
token = '';
|
|
90
|
+
tokenInvalid = false;
|
|
91
|
+
lastSignificant = '"';
|
|
92
|
+
} else if (role !== 'other') {
|
|
93
|
+
if (token.length < 32) token += character;
|
|
94
|
+
else tokenInvalid = true;
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (/\s/.test(character)) return;
|
|
100
|
+
if (character === '"') {
|
|
101
|
+
role = depth === 1 && (lastSignificant === '{' || lastSignificant === ',')
|
|
102
|
+
? 'root_key'
|
|
103
|
+
: depth === 1 && expectTypeValue
|
|
104
|
+
? 'type_value'
|
|
105
|
+
: 'other';
|
|
106
|
+
token = '';
|
|
107
|
+
tokenInvalid = false;
|
|
108
|
+
inString = true;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (character === '{' || character === '[') {
|
|
112
|
+
depth += 1;
|
|
113
|
+
lastSignificant = character;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (character === '}' || character === ']') {
|
|
117
|
+
depth = Math.max(0, depth - 1);
|
|
118
|
+
lastSignificant = character;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (depth === 1 && expectTypeColon) {
|
|
122
|
+
expectTypeColon = false;
|
|
123
|
+
expectTypeValue = character === ':';
|
|
124
|
+
} else if (depth === 1 && expectTypeValue) {
|
|
125
|
+
expectTypeValue = false;
|
|
126
|
+
}
|
|
127
|
+
if (depth === 1 && character === ',') {
|
|
128
|
+
expectTypeColon = false;
|
|
129
|
+
expectTypeValue = false;
|
|
130
|
+
}
|
|
131
|
+
lastSignificant = character;
|
|
132
|
+
},
|
|
133
|
+
completed() {
|
|
134
|
+
return completed;
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function createCodexJsonlUsageCollector() {
|
|
140
|
+
const decoder = new StringDecoder('utf8');
|
|
141
|
+
let line = '';
|
|
142
|
+
let discardLine = false;
|
|
143
|
+
let acceptedUsage = null;
|
|
144
|
+
let usageEventCount = 0;
|
|
145
|
+
let invalidUsageEvent = false;
|
|
146
|
+
let typeScanner = createCompletedTypeScanner();
|
|
147
|
+
|
|
148
|
+
const resetLine = () => {
|
|
149
|
+
line = '';
|
|
150
|
+
discardLine = false;
|
|
151
|
+
typeScanner = createCompletedTypeScanner();
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const finishLine = () => {
|
|
155
|
+
if (discardLine || !line.trim()) {
|
|
156
|
+
if (discardLine && typeScanner.completed()) invalidUsageEvent = true;
|
|
157
|
+
resetLine();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
let parsed = null;
|
|
161
|
+
try {
|
|
162
|
+
parsed = JSON.parse(line);
|
|
163
|
+
} catch {
|
|
164
|
+
if (typeScanner.completed()) invalidUsageEvent = true;
|
|
165
|
+
resetLine();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || parsed.type !== 'turn.completed') {
|
|
169
|
+
resetLine();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
usageEventCount += 1;
|
|
174
|
+
const normalized = normalizeCodexTurnUsage(parsed);
|
|
175
|
+
if (!normalized || usageEventCount !== 1) invalidUsageEvent = true;
|
|
176
|
+
else acceptedUsage = normalized;
|
|
177
|
+
resetLine();
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const consume = (text) => {
|
|
181
|
+
for (const character of text) {
|
|
182
|
+
if (character === '\n') {
|
|
183
|
+
finishLine();
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
typeScanner.write(character);
|
|
187
|
+
if (discardLine) continue;
|
|
188
|
+
if (line.length >= MAX_USAGE_EVENT_BYTES) {
|
|
189
|
+
discardLine = true;
|
|
190
|
+
line = '';
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
line += character;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
write(chunk) {
|
|
199
|
+
consume(decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
200
|
+
},
|
|
201
|
+
finish() {
|
|
202
|
+
consume(decoder.end());
|
|
203
|
+
if (line || discardLine) finishLine();
|
|
204
|
+
return invalidUsageEvent || usageEventCount !== 1 ? null : acceptedUsage;
|
|
205
|
+
},
|
|
206
|
+
bufferedBytes() {
|
|
207
|
+
return Buffer.byteLength(line, 'utf8');
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function createHostUsageCapture(command) {
|
|
213
|
+
if (!isCodexJsonExecution(command)) {
|
|
214
|
+
return {
|
|
215
|
+
supported: false,
|
|
216
|
+
write() {},
|
|
217
|
+
finish() { return null; },
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const collector = createCodexJsonlUsageCollector();
|
|
221
|
+
return {
|
|
222
|
+
supported: true,
|
|
223
|
+
write: (chunk) => collector.write(chunk),
|
|
224
|
+
finish: () => collector.finish(),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
module.exports = {
|
|
229
|
+
MAX_TOKEN_COUNT,
|
|
230
|
+
MAX_USAGE_EVENT_BYTES,
|
|
231
|
+
createCodexJsonlUsageCollector,
|
|
232
|
+
createHostUsageCapture,
|
|
233
|
+
isCodexJsonExecution,
|
|
234
|
+
normalizeCodexTurnUsage,
|
|
235
|
+
};
|