orizu 0.5.11 → 0.5.13
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/dist/credentials.js +37 -0
- package/dist/deploy-key-merge-core.js +258 -0
- package/dist/hosted-boot.js +19 -0
- package/dist/hosted-session-cli.js +6 -1
- package/dist/index.js +15 -12
- package/dist/manifests-cli.js +77 -5
- package/dist/merge-job.js +325 -0
- package/dist/vercel-sandbox-provider.js +2 -2
- package/dist/workbench-cli.js +78 -2
- package/package.json +1 -1
- package/src/credentials.ts +40 -0
- package/src/deploy-key-merge-core.ts +359 -0
- package/src/hosted-boot.ts +23 -1
- package/src/hosted-session-cli.ts +6 -1
- package/src/index.ts +21 -14
- package/src/manifests-cli.ts +102 -9
- package/src/merge-job.ts +462 -0
- package/src/vercel-sandbox-provider.ts +12 -2
- package/src/workbench-cli.ts +100 -4
- package/vendor/orizu-gepa-python/src/orizu_gepa/optimizer.py +5 -3
- package/vendor/skills/orizu-cli/SKILL.md +1 -0
- package/vendor/skills/orizu-cli/references/cli-reference.md +1 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `orizu internal merge-job` — the one-shot merge sandbox ENTRYPOINT
|
|
3
|
+
* (ALI-1084, merge-sandbox-job Phase 2). The MergeJobCoordinator DO launches
|
|
4
|
+
* this inside a minimal, AGENT-FREE sandbox booted from the hosted snapshot;
|
|
5
|
+
* from here the sandbox pulls its own credentials under the established
|
|
6
|
+
* boot-secret PULL model and runs the SHARED merge core
|
|
7
|
+
* (deploy-key-merge-core.ts — the exact code the server path runs; no forked
|
|
8
|
+
* merge logic).
|
|
9
|
+
*
|
|
10
|
+
* FLOW (plan §2.2 step 4):
|
|
11
|
+
* a. exchange ORIZU_BOOT_SECRET at the DO for a single-use JOB TOKEN
|
|
12
|
+
* (`GET {coordinator}/merge-jobs/:jobKey/job-token`) — the token transits
|
|
13
|
+
* Cloudflare; the DEPLOY KEY never does;
|
|
14
|
+
* b. pull `{spec, privateKeyPem}` DIRECTLY from the control plane
|
|
15
|
+
* (`GET {orizu}/api/coordinator/merge-jobs/:jobId/payload`, job-token
|
|
16
|
+
* auth) — single-use, audited server-side before the key is returned;
|
|
17
|
+
* c. run the shared merge core (tmp worktree, 0600 key file, pinned GitHub
|
|
18
|
+
* host keys, expected-head verification, argv-asserted --no-ff merge)
|
|
19
|
+
* with the D8 PRE-PUSH GATE wired in: immediately before `git push` the
|
|
20
|
+
* core calls `GET .../gate`; `proceed: false` aborts WITHOUT pushing
|
|
21
|
+
* (terminal `superseded`). An UNREACHABLE gate also refuses the push
|
|
22
|
+
* (fail closed) but reports `failed` (retryable) — "unknown" is not
|
|
23
|
+
* "rejected";
|
|
24
|
+
* d. POST the terminal result to the DO (boot-secret auth). Reporting the
|
|
25
|
+
* terminal state — not the merge outcome — is this command's success:
|
|
26
|
+
* exit 0 on ANY reported terminal (conflict/superseded included), exit 1
|
|
27
|
+
* only when the env is invalid or the report itself failed.
|
|
28
|
+
*
|
|
29
|
+
* FROZEN ENV CONTRACT (the DO's merge-job sandbox env plan):
|
|
30
|
+
* ORIZU_BOOT_SECRET per-sandbox durable bootstrap secret;
|
|
31
|
+
* ORIZU_MERGE_JOB_ID the job's DO addressing key
|
|
32
|
+
* (`merge:<manifestId>:<expectedHeadSha>` — the
|
|
33
|
+
* control-plane job uuid rides the token response);
|
|
34
|
+
* ORIZU_COORDINATOR_URL the coordinator Worker origin;
|
|
35
|
+
* ORIZU_BASE_URL the Orizu control-plane origin.
|
|
36
|
+
*
|
|
37
|
+
* REDACTION: every reported detail passes through `redactSecrets` with the
|
|
38
|
+
* boot secret, the job token, and the private key PEM on the exact-match list
|
|
39
|
+
* (plus the shape-pattern net). Nothing here ever logs a secret.
|
|
40
|
+
*/
|
|
41
|
+
import { mergeBranchWithDeployKey, } from './deploy-key-merge-core.js';
|
|
42
|
+
import { redactSecrets } from './secret-redaction.js';
|
|
43
|
+
// -- Frozen env contract --------------------------------------------------------
|
|
44
|
+
export const REQUIRED_MERGE_JOB_ENV_VARS = [
|
|
45
|
+
'ORIZU_BOOT_SECRET',
|
|
46
|
+
'ORIZU_MERGE_JOB_ID',
|
|
47
|
+
'ORIZU_COORDINATOR_URL',
|
|
48
|
+
'ORIZU_BASE_URL',
|
|
49
|
+
];
|
|
50
|
+
export function resolveMergeJobEnv(env) {
|
|
51
|
+
const missing = [];
|
|
52
|
+
const req = (name) => {
|
|
53
|
+
const value = env[name]?.trim();
|
|
54
|
+
if (!value)
|
|
55
|
+
missing.push(name);
|
|
56
|
+
return value ?? '';
|
|
57
|
+
};
|
|
58
|
+
const bootSecret = req('ORIZU_BOOT_SECRET');
|
|
59
|
+
const jobKey = req('ORIZU_MERGE_JOB_ID');
|
|
60
|
+
const coordinatorUrl = req('ORIZU_COORDINATOR_URL');
|
|
61
|
+
const baseUrl = req('ORIZU_BASE_URL');
|
|
62
|
+
if (missing.length > 0)
|
|
63
|
+
return { ok: false, missing };
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
value: {
|
|
67
|
+
bootSecret,
|
|
68
|
+
jobKey,
|
|
69
|
+
coordinatorUrl: coordinatorUrl.replace(/\/+$/, ''),
|
|
70
|
+
baseUrl: baseUrl.replace(/\/+$/, ''),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Map the shared core's outcome to the job's terminal status. */
|
|
75
|
+
export function terminalFromMergeResult(result) {
|
|
76
|
+
if (result.superseded) {
|
|
77
|
+
return {
|
|
78
|
+
status: 'superseded',
|
|
79
|
+
result: null,
|
|
80
|
+
errorDetail: 'pre-push gate refused: manifest no longer appliable',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (result.conflict) {
|
|
84
|
+
return {
|
|
85
|
+
status: 'conflict',
|
|
86
|
+
result: { merged: false, sha: null, alreadyMerged: false, conflict: true },
|
|
87
|
+
errorDetail: null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
status: 'succeeded',
|
|
92
|
+
result: {
|
|
93
|
+
merged: result.merged,
|
|
94
|
+
sha: result.sha,
|
|
95
|
+
alreadyMerged: result.alreadyMerged,
|
|
96
|
+
conflict: false,
|
|
97
|
+
},
|
|
98
|
+
errorDetail: null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export async function exchangeJobToken(opts) {
|
|
102
|
+
const attempts = Math.max(1, opts.attempts ?? 5);
|
|
103
|
+
const baseBackoff = opts.backoffMs ?? 500;
|
|
104
|
+
const sleep = opts.sleep ?? ((ms) => new Promise(resolve => setTimeout(resolve, ms)));
|
|
105
|
+
const url = `${opts.coordinatorUrl}/merge-jobs/${encodeURIComponent(opts.jobKey)}/job-token`;
|
|
106
|
+
let lastDetail = 'no attempt made';
|
|
107
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
108
|
+
try {
|
|
109
|
+
const res = await opts.fetchImpl(url, {
|
|
110
|
+
method: 'GET',
|
|
111
|
+
headers: { Authorization: `Bearer ${opts.bootSecret}` },
|
|
112
|
+
});
|
|
113
|
+
if (res.ok) {
|
|
114
|
+
const data = (await res.json().catch(() => null));
|
|
115
|
+
const token = data && typeof data.token === 'string' ? data.token : '';
|
|
116
|
+
const jobId = data && typeof data.jobId === 'string' ? data.jobId : '';
|
|
117
|
+
if (token && jobId) {
|
|
118
|
+
return { token, jobId };
|
|
119
|
+
}
|
|
120
|
+
lastDetail = 'job-token response carried no token/jobId';
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
lastDetail = `job-token exchange returned ${res.status}`;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
lastDetail = error instanceof Error ? error.message : String(error);
|
|
128
|
+
}
|
|
129
|
+
if (attempt < attempts - 1) {
|
|
130
|
+
const delay = baseBackoff * 2 ** attempt;
|
|
131
|
+
opts.log?.(`job-token exchange attempt ${attempt + 1}/${attempts} failed (${lastDetail}); retrying in ${delay}ms`);
|
|
132
|
+
await sleep(delay);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`job-token exchange failed after ${attempts} attempts: ${lastDetail}`);
|
|
136
|
+
}
|
|
137
|
+
export async function fetchMergeJobPayload(opts) {
|
|
138
|
+
const url = `${opts.baseUrl}/api/coordinator/merge-jobs/${encodeURIComponent(opts.jobId)}/payload`;
|
|
139
|
+
const res = await opts.fetchImpl(url, {
|
|
140
|
+
method: 'GET',
|
|
141
|
+
headers: { Authorization: `Bearer ${opts.token}` },
|
|
142
|
+
});
|
|
143
|
+
if (!res.ok) {
|
|
144
|
+
throw new Error(`payload pull returned ${res.status}`);
|
|
145
|
+
}
|
|
146
|
+
const data = (await res.json().catch(() => null));
|
|
147
|
+
const spec = data?.spec;
|
|
148
|
+
const privateKeyPem = typeof data?.privateKeyPem === 'string' ? data.privateKeyPem : '';
|
|
149
|
+
if (!spec || typeof spec !== 'object' || !privateKeyPem) {
|
|
150
|
+
throw new Error('payload response was missing spec/privateKeyPem');
|
|
151
|
+
}
|
|
152
|
+
if (spec.op !== 'merge') {
|
|
153
|
+
// D9 op discriminator: this entrypoint implements ONLY the merge op.
|
|
154
|
+
throw new Error(`unsupported job op: ${String(spec.op)}`);
|
|
155
|
+
}
|
|
156
|
+
for (const field of ['repoFullName', 'base', 'sourceBranch', 'expectedHeadSha', 'commitMessage']) {
|
|
157
|
+
if (typeof spec[field] !== 'string' || spec[field].length === 0) {
|
|
158
|
+
throw new Error(`payload spec is missing ${field}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { spec: spec, privateKeyPem };
|
|
162
|
+
}
|
|
163
|
+
export async function checkPrePushGate(opts) {
|
|
164
|
+
const url = `${opts.baseUrl}/api/coordinator/merge-jobs/${encodeURIComponent(opts.jobId)}/gate`;
|
|
165
|
+
try {
|
|
166
|
+
const res = await opts.fetchImpl(url, {
|
|
167
|
+
method: 'GET',
|
|
168
|
+
headers: { Authorization: `Bearer ${opts.token}` },
|
|
169
|
+
});
|
|
170
|
+
if (!res.ok) {
|
|
171
|
+
return 'unavailable';
|
|
172
|
+
}
|
|
173
|
+
const data = (await res.json().catch(() => null));
|
|
174
|
+
if (data && data.proceed === true)
|
|
175
|
+
return 'proceed';
|
|
176
|
+
if (data && data.proceed === false)
|
|
177
|
+
return 'superseded';
|
|
178
|
+
return 'unavailable';
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return 'unavailable';
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// -- Step d: report the terminal state to the DO (boot-secret auth) --------------
|
|
185
|
+
export async function postMergeJobResult(opts) {
|
|
186
|
+
const attempts = Math.max(1, opts.attempts ?? 3);
|
|
187
|
+
const sleep = opts.sleep ?? ((ms) => new Promise(resolve => setTimeout(resolve, ms)));
|
|
188
|
+
const url = `${opts.coordinatorUrl}/merge-jobs/${encodeURIComponent(opts.jobKey)}/result`;
|
|
189
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
190
|
+
try {
|
|
191
|
+
const res = await opts.fetchImpl(url, {
|
|
192
|
+
method: 'POST',
|
|
193
|
+
headers: {
|
|
194
|
+
Authorization: `Bearer ${opts.bootSecret}`,
|
|
195
|
+
'Content-Type': 'application/json',
|
|
196
|
+
},
|
|
197
|
+
body: JSON.stringify({
|
|
198
|
+
status: opts.report.status,
|
|
199
|
+
...(opts.report.result ? { result: opts.report.result } : {}),
|
|
200
|
+
...(opts.report.errorDetail ? { errorDetail: opts.report.errorDetail } : {}),
|
|
201
|
+
}),
|
|
202
|
+
});
|
|
203
|
+
if (res.ok) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
opts.log?.(`result report attempt ${attempt + 1}/${attempts} returned ${res.status}`);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
opts.log?.(`result report attempt ${attempt + 1}/${attempts} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
210
|
+
}
|
|
211
|
+
if (attempt < attempts - 1) {
|
|
212
|
+
await sleep(500 * 2 ** attempt);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
// -- Orchestration ----------------------------------------------------------------
|
|
218
|
+
const MAX_DETAIL_LENGTH = 1500;
|
|
219
|
+
export async function runMergeJob(opts) {
|
|
220
|
+
const env = opts.env;
|
|
221
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
222
|
+
const log = opts.log ?? (() => { });
|
|
223
|
+
const secrets = [env.bootSecret];
|
|
224
|
+
const redact = (value) => redactSecrets(value, { secrets }).slice(0, MAX_DETAIL_LENGTH);
|
|
225
|
+
let report;
|
|
226
|
+
try {
|
|
227
|
+
// a. boot secret → single-use job token (+ the control-plane job id).
|
|
228
|
+
const exchanged = await exchangeJobToken({
|
|
229
|
+
coordinatorUrl: env.coordinatorUrl,
|
|
230
|
+
jobKey: env.jobKey,
|
|
231
|
+
bootSecret: env.bootSecret,
|
|
232
|
+
fetchImpl,
|
|
233
|
+
attempts: opts.tokenAttempts,
|
|
234
|
+
backoffMs: opts.tokenBackoffMs,
|
|
235
|
+
sleep: opts.sleep,
|
|
236
|
+
log,
|
|
237
|
+
});
|
|
238
|
+
secrets.push(exchanged.token);
|
|
239
|
+
log(`job token exchanged (job ${exchanged.jobId})`);
|
|
240
|
+
// b. pull spec + key directly from the control plane (single-use release).
|
|
241
|
+
const payload = await fetchMergeJobPayload({
|
|
242
|
+
baseUrl: env.baseUrl,
|
|
243
|
+
jobId: exchanged.jobId,
|
|
244
|
+
token: exchanged.token,
|
|
245
|
+
fetchImpl,
|
|
246
|
+
});
|
|
247
|
+
secrets.push(payload.privateKeyPem);
|
|
248
|
+
log(`payload pulled for ${payload.spec.repoFullName} (${payload.spec.sourceBranch} → ${payload.spec.base})`);
|
|
249
|
+
// c. the SHARED merge core with the D8 gate immediately pre-push.
|
|
250
|
+
// Object wrapper: TS cannot track closure writes to a let across the
|
|
251
|
+
// await below and would narrow the literal type at the read site.
|
|
252
|
+
const gate = { outcome: 'proceed' };
|
|
253
|
+
const result = await mergeBranchWithDeployKey(payload.spec.repoFullName, {
|
|
254
|
+
base: payload.spec.base,
|
|
255
|
+
sourceBranch: payload.spec.sourceBranch,
|
|
256
|
+
expectedHeadSha: payload.spec.expectedHeadSha,
|
|
257
|
+
commitMessage: payload.spec.commitMessage,
|
|
258
|
+
privateKeyPem: payload.privateKeyPem,
|
|
259
|
+
}, {
|
|
260
|
+
runGit: opts.runGit,
|
|
261
|
+
prePushGate: async () => {
|
|
262
|
+
gate.outcome = await checkPrePushGate({
|
|
263
|
+
baseUrl: env.baseUrl,
|
|
264
|
+
jobId: exchanged.jobId,
|
|
265
|
+
token: exchanged.token,
|
|
266
|
+
fetchImpl,
|
|
267
|
+
});
|
|
268
|
+
log(`pre-push gate: ${gate.outcome}`);
|
|
269
|
+
// FAIL CLOSED on the push for both 'superseded' and 'unavailable'.
|
|
270
|
+
return gate.outcome === 'proceed';
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
if (result.superseded && gate.outcome === 'unavailable') {
|
|
274
|
+
// The gate could not answer — "unknown" is not "rejected". No push
|
|
275
|
+
// happened (fail closed); report a retryable failure, not superseded.
|
|
276
|
+
report = {
|
|
277
|
+
status: 'failed',
|
|
278
|
+
result: null,
|
|
279
|
+
errorDetail: 'pre-push gate unreachable; aborted without pushing',
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
report = terminalFromMergeResult(result);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
288
|
+
report = { status: 'failed', result: null, errorDetail: redact(message) };
|
|
289
|
+
}
|
|
290
|
+
if (report.errorDetail) {
|
|
291
|
+
report = { ...report, errorDetail: redact(report.errorDetail) };
|
|
292
|
+
}
|
|
293
|
+
const reported = await postMergeJobResult({
|
|
294
|
+
coordinatorUrl: env.coordinatorUrl,
|
|
295
|
+
jobKey: env.jobKey,
|
|
296
|
+
bootSecret: env.bootSecret,
|
|
297
|
+
report,
|
|
298
|
+
fetchImpl,
|
|
299
|
+
sleep: opts.sleep,
|
|
300
|
+
log,
|
|
301
|
+
});
|
|
302
|
+
log(`terminal ${report.status} ${reported ? 'reported' : 'REPORT FAILED'}`);
|
|
303
|
+
return { reported, status: report.status, detail: report.errorDetail };
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Read the frozen env contract and run the job. Exit 0 on ANY reported
|
|
307
|
+
* terminal outcome (the REPORT, not the merge, is the command's success —
|
|
308
|
+
* a conflict is a correctly-completed job); exit 1 when the env is invalid
|
|
309
|
+
* or the terminal report never reached the DO (the DO's job wall is the
|
|
310
|
+
* backstop then).
|
|
311
|
+
*/
|
|
312
|
+
export async function mergeJobCommand(io) {
|
|
313
|
+
const resolved = resolveMergeJobEnv(process.env);
|
|
314
|
+
if (!resolved.ok) {
|
|
315
|
+
io.printErr?.(`merge-job: missing required env: ${resolved.missing.join(', ')} ` +
|
|
316
|
+
'(the MergeJobCoordinator sandbox env contract)');
|
|
317
|
+
return 1;
|
|
318
|
+
}
|
|
319
|
+
const log = (line) => io.printErr?.(`[merge-job] ${line}`);
|
|
320
|
+
const outcome = await runMergeJob({ env: resolved.value, log });
|
|
321
|
+
io.print(io.json
|
|
322
|
+
? JSON.stringify({ ok: outcome.reported, status: outcome.status, detail: outcome.detail })
|
|
323
|
+
: `merge-job finished: ${outcome.status}${outcome.detail ? ` (${outcome.detail})` : ''}${outcome.reported ? '' : ' [terminal report failed]'}`);
|
|
324
|
+
return outcome.reported ? 0 : 1;
|
|
325
|
+
}
|
|
@@ -54,10 +54,10 @@ export const VERCEL_IMAGE_NOT_READY_BACKOFF_MS = 5000;
|
|
|
54
54
|
// constant-fold `import(specifier)` into a static dependency on the SDK — the
|
|
55
55
|
// same discipline as the ALI-1031 server provider.
|
|
56
56
|
const VERCEL_SDK_SPECIFIER = ['@vercel', 'sandbox'].join('/');
|
|
57
|
-
async function loadVercelModule() {
|
|
57
|
+
export async function loadVercelModule(importModule = specifier => import(specifier)) {
|
|
58
58
|
try {
|
|
59
59
|
const specifier = VERCEL_SDK_SPECIFIER;
|
|
60
|
-
const mod = (await
|
|
60
|
+
const mod = (await importModule(specifier));
|
|
61
61
|
if (!mod || typeof mod.Sandbox?.create !== 'function') {
|
|
62
62
|
throw new Error('module did not export a Sandbox class with create()');
|
|
63
63
|
}
|
package/dist/workbench-cli.js
CHANGED
|
@@ -528,9 +528,82 @@ function formatSessionStart(result) {
|
|
|
528
528
|
}
|
|
529
529
|
return lines.join('\n');
|
|
530
530
|
}
|
|
531
|
+
// -- Cost/duration rendering (ALI-1089) ---------------------------------------
|
|
532
|
+
function finiteOrNull(value) {
|
|
533
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
534
|
+
}
|
|
535
|
+
function formatUsd(value) {
|
|
536
|
+
return `$${value.toFixed(4)}`;
|
|
537
|
+
}
|
|
538
|
+
/** `tokens: in 2, out 181, cache-read 126968, ...` — only the reported fields. */
|
|
539
|
+
function formatTokenBreakdown(costs) {
|
|
540
|
+
const parts = [];
|
|
541
|
+
const push = (label, value) => {
|
|
542
|
+
const parsed = finiteOrNull(value);
|
|
543
|
+
if (parsed !== null) {
|
|
544
|
+
parts.push(`${label} ${parsed}`);
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
push('in', costs.tokensInput);
|
|
548
|
+
push('out', costs.tokensOutput);
|
|
549
|
+
push('cache-read', costs.tokensCacheRead);
|
|
550
|
+
push('cache-write', costs.tokensCacheWrite);
|
|
551
|
+
push('reasoning', costs.tokensReasoning);
|
|
552
|
+
return parts.length > 0 ? `tokens: ${parts.join(', ')}` : null;
|
|
553
|
+
}
|
|
554
|
+
/** `cost $0.0697 tokens: ...` or null when nothing was reported. */
|
|
555
|
+
function formatCostSummary(costs) {
|
|
556
|
+
const cost = finiteOrNull(costs.modelCostUsd);
|
|
557
|
+
const tokens = formatTokenBreakdown(costs);
|
|
558
|
+
if (cost === null && tokens === null) {
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
const parts = [];
|
|
562
|
+
if (cost !== null) {
|
|
563
|
+
parts.push(`cost ${formatUsd(cost)}`);
|
|
564
|
+
}
|
|
565
|
+
if (tokens) {
|
|
566
|
+
parts.push(tokens);
|
|
567
|
+
}
|
|
568
|
+
return parts.join(' ');
|
|
569
|
+
}
|
|
570
|
+
function formatDurationMs(ms) {
|
|
571
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
572
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
573
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
574
|
+
const seconds = totalSeconds % 60;
|
|
575
|
+
if (hours > 0)
|
|
576
|
+
return `${hours}h ${minutes}m ${seconds}s`;
|
|
577
|
+
if (minutes > 0)
|
|
578
|
+
return `${minutes}m ${seconds}s`;
|
|
579
|
+
return `${seconds}s`;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Session wall-clock from the existing startedAt/endedAt columns — no cost
|
|
583
|
+
* math, just duration (infra $ estimation is a later phase). Active sessions
|
|
584
|
+
* measure up to `now` and say so.
|
|
585
|
+
*/
|
|
586
|
+
function formatSessionDuration(session, now = Date.now()) {
|
|
587
|
+
const startedAt = typeof session.startedAt === 'string' ? Date.parse(session.startedAt) : Number.NaN;
|
|
588
|
+
if (!Number.isFinite(startedAt)) {
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
const endedAt = typeof session.endedAt === 'string' ? Date.parse(session.endedAt) : Number.NaN;
|
|
592
|
+
return Number.isFinite(endedAt)
|
|
593
|
+
? `duration ${formatDurationMs(endedAt - startedAt)}`
|
|
594
|
+
: `duration ${formatDurationMs(now - startedAt)} (so far)`;
|
|
595
|
+
}
|
|
531
596
|
function formatSessionStatus(result) {
|
|
532
597
|
if (result.session) {
|
|
533
598
|
const lines = [`session ${result.session.id} ${result.session.status}`];
|
|
599
|
+
const duration = formatSessionDuration(result.session);
|
|
600
|
+
if (duration) {
|
|
601
|
+
lines.push(` ${duration}`);
|
|
602
|
+
}
|
|
603
|
+
const rollup = result.session.costs ? formatCostSummary(result.session.costs) : null;
|
|
604
|
+
if (rollup) {
|
|
605
|
+
lines.push(` ${rollup}`);
|
|
606
|
+
}
|
|
534
607
|
const sync = result.branchSync;
|
|
535
608
|
if (sync) {
|
|
536
609
|
lines.push(sync.ahead !== null && sync.behind !== null
|
|
@@ -538,7 +611,9 @@ function formatSessionStatus(result) {
|
|
|
538
611
|
: ` branch ${sync.branch}`);
|
|
539
612
|
}
|
|
540
613
|
for (const run of result.session.runs ?? []) {
|
|
541
|
-
|
|
614
|
+
const cost = finiteOrNull(run.modelCostUsd);
|
|
615
|
+
const suffix = cost !== null ? ` ${formatUsd(cost)}` : '';
|
|
616
|
+
lines.push(` run ${run.id} ${run.status}${suffix}`);
|
|
542
617
|
}
|
|
543
618
|
return lines.join('\n');
|
|
544
619
|
}
|
|
@@ -575,7 +650,8 @@ function formatSessionFinishOutcome(result) {
|
|
|
575
650
|
}
|
|
576
651
|
function formatRunStatus(run) {
|
|
577
652
|
const sequence = typeof run.latestSequence === 'number' ? ` latestSequence=${run.latestSequence}` : '';
|
|
578
|
-
|
|
653
|
+
const costLine = formatCostSummary(run);
|
|
654
|
+
return `run ${run.id} ${run.status}${sequence}${costLine ? `\n ${costLine}` : ''}`;
|
|
579
655
|
}
|
|
580
656
|
function formatEventLine(event) {
|
|
581
657
|
const payload = event.payload && Object.keys(event.payload).length > 0 ? ` ${JSON.stringify(event.payload)}` : '';
|
package/package.json
CHANGED
package/src/credentials.ts
CHANGED
|
@@ -256,6 +256,46 @@ export function resolveEnvBearerToken(): string | null {
|
|
|
256
256
|
return null
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
/**
|
|
260
|
+
* THE uniform bearer resolution for every CLI command (ALI-1090). Exactly the
|
|
261
|
+
* order `authedFetch` uses: `ORIZU_TOKEN` env → `ORIZU_TOKEN_FILE` (both via
|
|
262
|
+
* resolveEnvBearerToken, read FRESH per call — never cached, so hosted token
|
|
263
|
+
* rotations are picked up) → stored credentials.json. Commands that need a raw
|
|
264
|
+
* bearer (`orizu env`, the run-gepa wrapper) MUST use this instead of reading
|
|
265
|
+
* credentials.json directly, or they break in hosted sandboxes that are
|
|
266
|
+
* pre-authenticated via ORIZU_TOKEN_FILE and never ran `orizu login`.
|
|
267
|
+
*/
|
|
268
|
+
export function resolveAuthTokenForBaseUrl(baseUrl: string): string {
|
|
269
|
+
const envBearer = resolveEnvBearerToken()
|
|
270
|
+
if (envBearer) {
|
|
271
|
+
return envBearer
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const credentials = getServerCredentials(baseUrl)
|
|
275
|
+
if (!credentials) {
|
|
276
|
+
throw new Error(`Not logged in for ${baseUrl}. Run \`orizu login --server ${baseUrl}\` (or \`--local\`) first.`)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return 'accessToken' in credentials ? credentials.accessToken : credentials.apiKey
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Non-throwing "is any auth available?" check (login --no-prompt-if-logged-in,
|
|
284
|
+
* setup status). Mirrors resolveAuthTokenForBaseUrl's order so an env bearer
|
|
285
|
+
* counts as signed in. A misconfigured ORIZU_TOKEN_FILE reports false here —
|
|
286
|
+
* actual requests still fail loudly via resolveEnvBearerToken.
|
|
287
|
+
*/
|
|
288
|
+
export function hasResolvableAuth(baseUrl: string): boolean {
|
|
289
|
+
try {
|
|
290
|
+
if (resolveEnvBearerToken()) {
|
|
291
|
+
return true
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
// fall through to stored credentials
|
|
295
|
+
}
|
|
296
|
+
return getServerCredentials(baseUrl) !== null
|
|
297
|
+
}
|
|
298
|
+
|
|
259
299
|
export function setActiveBaseUrl(baseUrl: string | null) {
|
|
260
300
|
const config = loadCredentialsConfigForWrite()
|
|
261
301
|
config.activeBaseUrl = baseUrl
|