@onyx-robotics/agent 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/LICENSE +202 -0
- package/README.md +72 -0
- package/bin/onyx.ts +4 -0
- package/package.json +52 -0
- package/scripts/install.sh +115 -0
- package/skills/onyx/SKILL.md +150 -0
- package/src/commands/agent.ts +23 -0
- package/src/commands/branch.ts +96 -0
- package/src/commands/exp.ts +432 -0
- package/src/commands/listen.ts +327 -0
- package/src/commands/login.ts +198 -0
- package/src/commands/profile.ts +112 -0
- package/src/commands/sync.ts +88 -0
- package/src/install.test.ts +38 -0
- package/src/lib/api.ts +227 -0
- package/src/lib/args.ts +68 -0
- package/src/lib/config.ts +148 -0
- package/src/lib/events.ts +97 -0
- package/src/lib/git.ts +57 -0
- package/src/lib/history.ts +272 -0
- package/src/lib/login.ts +233 -0
- package/src/lib/markdown.ts +148 -0
- package/src/lib/metrics.ts +41 -0
- package/src/lib/outbox.ts +173 -0
- package/src/lib/process.ts +73 -0
- package/src/lib/project.ts +42 -0
- package/src/lib/skill-content.ts +1 -0
- package/src/lib/skill.ts +50 -0
- package/src/lib/sync.ts +294 -0
- package/src/lib/tui.ts +364 -0
- package/src/main.ts +84 -0
- package/src/onyx.test.ts +952 -0
- package/src/onyx.ts +92 -0
- package/src/profile.test.ts +472 -0
- package/src/protocol/index.ts +2 -0
- package/src/protocol/local-research.ts +152 -0
- package/src/protocol/research.ts +75 -0
package/src/lib/tui.ts
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
// Pure rendering helpers shared by `onyx exp list` and `onyx listen`.
|
|
2
|
+
// Everything is string-in/string-out (no I/O, no terminal state) so layout
|
|
3
|
+
// stays unit-testable; commands/listen.ts owns the actual terminal lifecycle.
|
|
4
|
+
// Column set and status vocabulary mirror the web app's tree view
|
|
5
|
+
// (apps/web/components/views/tree-view.tsx) so both surfaces read the same.
|
|
6
|
+
|
|
7
|
+
import type { LocalResearchHistoryRecord } from "../protocol"
|
|
8
|
+
|
|
9
|
+
const CSI = "\x1b["
|
|
10
|
+
|
|
11
|
+
function paint(code: string, text: string, color: boolean) {
|
|
12
|
+
return color ? `${CSI}${code}m${text}${CSI}0m` : text
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const dim = (text: string, color: boolean) => paint("2", text, color)
|
|
16
|
+
export const bold = (text: string, color: boolean) => paint("1", text, color)
|
|
17
|
+
export const green = (text: string, color: boolean) => paint("32", text, color)
|
|
18
|
+
export const red = (text: string, color: boolean) => paint("31", text, color)
|
|
19
|
+
export const yellow = (text: string, color: boolean) => paint("33", text, color)
|
|
20
|
+
export const cyan = (text: string, color: boolean) => paint("36", text, color)
|
|
21
|
+
|
|
22
|
+
/** Strips ANSI color/style sequences (for length math and tests). */
|
|
23
|
+
export function stripAnsi(text: string) {
|
|
24
|
+
// eslint-disable-next-line no-control-regex -- matching ANSI escapes is the point
|
|
25
|
+
return text.replaceAll(/\x1b\[\d+m/g, "")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Truncates by visible length, passing ANSI sequences through untouched. */
|
|
29
|
+
export function truncateAnsi(line: string, width: number) {
|
|
30
|
+
if (stripAnsi(line).length <= width) return line
|
|
31
|
+
let visible = 0
|
|
32
|
+
let out = ""
|
|
33
|
+
let i = 0
|
|
34
|
+
while (i < line.length && visible < width - 1) {
|
|
35
|
+
if (line[i] === "\x1b") {
|
|
36
|
+
const end = line.indexOf("m", i)
|
|
37
|
+
if (end === -1) break
|
|
38
|
+
out += line.slice(i, end + 1)
|
|
39
|
+
i = end + 1
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
out += line[i]
|
|
43
|
+
visible += 1
|
|
44
|
+
i += 1
|
|
45
|
+
}
|
|
46
|
+
return `${out}…${CSI}0m`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Truncates to width, appending an ellipsis when text was cut. */
|
|
50
|
+
export function truncate(text: string, width: number) {
|
|
51
|
+
if (width <= 0) return ""
|
|
52
|
+
if (text.length <= width) return text
|
|
53
|
+
if (width === 1) return "…"
|
|
54
|
+
return `${text.slice(0, width - 1)}…`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Pads (and truncates) to an exact width. */
|
|
58
|
+
export function pad(
|
|
59
|
+
text: string,
|
|
60
|
+
width: number,
|
|
61
|
+
align: "left" | "right" = "left"
|
|
62
|
+
) {
|
|
63
|
+
const cut = truncate(text, width)
|
|
64
|
+
const fill = " ".repeat(width - cut.length)
|
|
65
|
+
return align === "left" ? `${cut}${fill}` : `${fill}${cut}`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Compact relative age: now, 42s, 5m, 2h, 3d. */
|
|
69
|
+
export function formatAge(iso: string | null | undefined, nowMs: number) {
|
|
70
|
+
if (!iso) return "—"
|
|
71
|
+
const then = Date.parse(iso)
|
|
72
|
+
if (!Number.isFinite(then)) return "—"
|
|
73
|
+
const seconds = Math.max(0, Math.floor((nowMs - then) / 1000))
|
|
74
|
+
if (seconds < 5) return "now"
|
|
75
|
+
if (seconds < 60) return `${seconds}s`
|
|
76
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`
|
|
77
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`
|
|
78
|
+
return `${Math.floor(seconds / 86400)}d`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function formatMetricValue(value: number | null | undefined) {
|
|
82
|
+
if (value === null || value === undefined) return "—"
|
|
83
|
+
return value.toLocaleString(undefined, { maximumFractionDigits: 4 })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** `name=value`, truncating the name (never the value) to fit the width. */
|
|
87
|
+
export function formatMetricCell(
|
|
88
|
+
name: string,
|
|
89
|
+
value: number | null | undefined,
|
|
90
|
+
width: number
|
|
91
|
+
) {
|
|
92
|
+
if (value === null || value === undefined) return "—"
|
|
93
|
+
const formatted = formatMetricValue(value)
|
|
94
|
+
const full = `${name}=${formatted}`
|
|
95
|
+
if (full.length <= width) return full
|
|
96
|
+
return `${truncate(name, Math.max(1, width - formatted.length - 1))}=${formatted}`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const pad2 = (n: number) => String(n).padStart(2, "0")
|
|
100
|
+
|
|
101
|
+
/** `MM-DD HH:mm:ss` — the web tree view's created column, plus seconds. */
|
|
102
|
+
export function formatDateTime(iso: string | null | undefined) {
|
|
103
|
+
if (!iso) return "—"
|
|
104
|
+
const d = new Date(iso)
|
|
105
|
+
if (Number.isNaN(d.getTime())) return "—"
|
|
106
|
+
return `${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Duration formatting matching the web tree view. */
|
|
110
|
+
export function formatDuration(
|
|
111
|
+
startedAt: string | null | undefined,
|
|
112
|
+
completedAt: string | null | undefined,
|
|
113
|
+
nowMs: number
|
|
114
|
+
) {
|
|
115
|
+
if (!startedAt) return "—"
|
|
116
|
+
const start = Date.parse(startedAt)
|
|
117
|
+
if (!Number.isFinite(start)) return "—"
|
|
118
|
+
const end = completedAt ? Date.parse(completedAt) : nowMs
|
|
119
|
+
const totalSec = Math.max(0, (end - start) / 1000)
|
|
120
|
+
if (totalSec < 60) return `${totalSec.toFixed(4)}s`
|
|
121
|
+
const totalMin = Math.floor(totalSec / 60)
|
|
122
|
+
const s = (totalSec - totalMin * 60).toFixed(4)
|
|
123
|
+
if (totalMin < 60) return `${totalMin}m ${s}s`
|
|
124
|
+
const h = Math.floor(totalMin / 60)
|
|
125
|
+
const remM = totalMin % 60
|
|
126
|
+
return `${h}h ${remM}m ${s}s`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Status glyphs and labels mirror glyphFor() in the web tree view.
|
|
130
|
+
type StatusGlyph = {
|
|
131
|
+
char: string
|
|
132
|
+
label: string
|
|
133
|
+
colorize: (text: string, color: boolean) => string
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function glyphFor(status: string): StatusGlyph {
|
|
137
|
+
if (status === "running")
|
|
138
|
+
return { char: "◦", label: "running", colorize: dim }
|
|
139
|
+
if (status === "queued") return { char: "·", label: "queued", colorize: dim }
|
|
140
|
+
if (status === "failed") return { char: "✗", label: "failed", colorize: red }
|
|
141
|
+
if (status === "checks_failed")
|
|
142
|
+
return { char: "!", label: "checks", colorize: yellow }
|
|
143
|
+
if (status === "rejected")
|
|
144
|
+
return { char: "!", label: "rejected", colorize: yellow }
|
|
145
|
+
return { char: "•", label: "ok", colorize: green }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export type ExperimentRow = Pick<
|
|
149
|
+
LocalResearchHistoryRecord,
|
|
150
|
+
| "branchName"
|
|
151
|
+
| "name"
|
|
152
|
+
| "status"
|
|
153
|
+
| "primaryMetricName"
|
|
154
|
+
| "primaryMetricValue"
|
|
155
|
+
| "createdAt"
|
|
156
|
+
> & {
|
|
157
|
+
sequenceNumber?: number
|
|
158
|
+
source?: "local" | "api"
|
|
159
|
+
description?: string | null
|
|
160
|
+
startedAt?: string | null
|
|
161
|
+
completedAt?: string | null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const GLYPH_W = 1
|
|
165
|
+
const SEQ_W = 4
|
|
166
|
+
const BRANCH_W = 16
|
|
167
|
+
const METRIC_W = 10
|
|
168
|
+
const CREATED_W = 14
|
|
169
|
+
const DURATION_W = 14
|
|
170
|
+
const STATUS_W = 8
|
|
171
|
+
// Extra breathing room between the metric and created columns.
|
|
172
|
+
const METRIC_GAP = " "
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Renders the tree-view experiment table as aligned plain lines:
|
|
176
|
+
* glyph · #seq · name · description · metric · created · duration · status.
|
|
177
|
+
* Description and duration columns drop out on narrow widths. Cells are
|
|
178
|
+
* padded before color is applied so ANSI codes never skew alignment.
|
|
179
|
+
*/
|
|
180
|
+
export function renderExperimentTable(
|
|
181
|
+
rows: ExperimentRow[],
|
|
182
|
+
options: {
|
|
183
|
+
columns: number
|
|
184
|
+
color: boolean
|
|
185
|
+
nowMs: number
|
|
186
|
+
showBranch?: boolean
|
|
187
|
+
}
|
|
188
|
+
): string[] {
|
|
189
|
+
const { columns, color, nowMs } = options
|
|
190
|
+
const showBranch = options.showBranch ?? false
|
|
191
|
+
const showDescription = columns >= 90
|
|
192
|
+
const showDuration = columns >= 110
|
|
193
|
+
|
|
194
|
+
let fixed =
|
|
195
|
+
GLYPH_W + SEQ_W + METRIC_W + METRIC_GAP.length + CREATED_W + STATUS_W + 5 // gaps for base cols
|
|
196
|
+
if (showBranch) fixed += BRANCH_W + 1
|
|
197
|
+
if (showDuration) fixed += DURATION_W + 1
|
|
198
|
+
const flex = Math.max(showDescription ? 24 : 12, columns - fixed)
|
|
199
|
+
const nameWidth = showDescription ? Math.ceil(flex / 2) : flex
|
|
200
|
+
const descWidth = showDescription ? flex - nameWidth - 1 : 0
|
|
201
|
+
|
|
202
|
+
const cells = (parts: (string | null)[]) =>
|
|
203
|
+
parts.filter((part): part is string => part !== null).join(" ")
|
|
204
|
+
|
|
205
|
+
const header = cells([
|
|
206
|
+
" ".repeat(GLYPH_W),
|
|
207
|
+
pad("#", SEQ_W, "right"),
|
|
208
|
+
showBranch ? pad("BRANCH", BRANCH_W) : null,
|
|
209
|
+
pad("NAME", nameWidth),
|
|
210
|
+
showDescription ? pad("DESCRIPTION", descWidth) : null,
|
|
211
|
+
`${pad("METRIC", METRIC_W, "right")}${METRIC_GAP}`,
|
|
212
|
+
pad("CREATED", CREATED_W, "right"),
|
|
213
|
+
showDuration ? pad("DURATION", DURATION_W, "right") : null,
|
|
214
|
+
pad("STATUS", STATUS_W, "right"),
|
|
215
|
+
])
|
|
216
|
+
|
|
217
|
+
const lines = [dim(truncate(header, columns), color)]
|
|
218
|
+
for (const row of rows) {
|
|
219
|
+
const glyph = glyphFor(row.status)
|
|
220
|
+
const seq =
|
|
221
|
+
row.sequenceNumber !== undefined ? `#${row.sequenceNumber}` : "·"
|
|
222
|
+
const statusPadded = pad(glyph.label, STATUS_W, "right")
|
|
223
|
+
lines.push(
|
|
224
|
+
truncateAnsi(
|
|
225
|
+
cells([
|
|
226
|
+
glyph.colorize(glyph.char, color),
|
|
227
|
+
dim(pad(seq, SEQ_W, "right"), color),
|
|
228
|
+
showBranch ? pad(row.branchName, BRANCH_W) : null,
|
|
229
|
+
pad(row.name, nameWidth),
|
|
230
|
+
showDescription
|
|
231
|
+
? dim(pad(row.description?.trim() || "—", descWidth), color)
|
|
232
|
+
: null,
|
|
233
|
+
`${pad(formatMetricValue(row.primaryMetricValue), METRIC_W, "right")}${METRIC_GAP}`,
|
|
234
|
+
dim(pad(formatDateTime(row.createdAt), CREATED_W, "right"), color),
|
|
235
|
+
showDuration
|
|
236
|
+
? dim(
|
|
237
|
+
pad(
|
|
238
|
+
formatDuration(row.startedAt, row.completedAt, nowMs),
|
|
239
|
+
DURATION_W,
|
|
240
|
+
"right"
|
|
241
|
+
),
|
|
242
|
+
color
|
|
243
|
+
)
|
|
244
|
+
: null,
|
|
245
|
+
statusPadded.replace(glyph.label, glyph.colorize(glyph.label, color)),
|
|
246
|
+
]),
|
|
247
|
+
columns
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
return lines
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Braille square (⣿) with one border dot removed per frame; the gap orbits
|
|
255
|
+
// the square clockwise (top-left → top-right → down the right side → bottom
|
|
256
|
+
// → up the left side). A terminal cell renders in a single color, so the
|
|
257
|
+
// "lit" element is the moving notch in the otherwise-grey square.
|
|
258
|
+
const SPINNER_FRAMES = ["⣾", "⣷", "⣯", "⣟", "⡿", "⢿", "⣻", "⣽"]
|
|
259
|
+
const SPINNER_TICK_MS = 120
|
|
260
|
+
|
|
261
|
+
/** Deterministic spinner frame for a timestamp (pure — testable). */
|
|
262
|
+
export function spinnerChar(nowMs: number) {
|
|
263
|
+
return SPINNER_FRAMES[
|
|
264
|
+
Math.floor(nowMs / SPINNER_TICK_MS) % SPINNER_FRAMES.length
|
|
265
|
+
]!
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export type ListenModel = {
|
|
269
|
+
projectName: string | null
|
|
270
|
+
branchName: string | null
|
|
271
|
+
metricName: string | null
|
|
272
|
+
metricUnit: string | null
|
|
273
|
+
metricDirection: "maximize" | "minimize" | null
|
|
274
|
+
bestValue: number | null
|
|
275
|
+
activity: string | null
|
|
276
|
+
/** True while an agent session is live — animates the activity spinner. */
|
|
277
|
+
active: boolean
|
|
278
|
+
/** Ascending by recency — the most recent experiment renders at the bottom. */
|
|
279
|
+
rows: ExperimentRow[]
|
|
280
|
+
pendingOutbox: number
|
|
281
|
+
syncedCount: number
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Composes the full `onyx listen` frame as plain lines (no cursor control —
|
|
286
|
+
* commands/listen.ts adds per-line clears): a rounded box around the
|
|
287
|
+
* experiment table, the `ONYX | repo | branch` title interrupting the top
|
|
288
|
+
* border on the left (best metric on the right), with the activity line and
|
|
289
|
+
* sync footer below the box. Clipped to the terminal size.
|
|
290
|
+
*/
|
|
291
|
+
export function renderFrame(
|
|
292
|
+
model: ListenModel,
|
|
293
|
+
size: { columns: number; rows: number; nowMs: number; color?: boolean }
|
|
294
|
+
): string[] {
|
|
295
|
+
const columns = Math.max(40, size.columns)
|
|
296
|
+
const color = size.color ?? true
|
|
297
|
+
const inner = columns - 4
|
|
298
|
+
|
|
299
|
+
// Top border: ╭─ ONYX | repo | branch ───────── best ─╮
|
|
300
|
+
const best =
|
|
301
|
+
model.bestValue !== null && model.metricName
|
|
302
|
+
? `best ${formatMetricValue(model.bestValue)} ${
|
|
303
|
+
model.metricDirection === "minimize" ? "↓" : "↑"
|
|
304
|
+
} ${model.metricName}${model.metricUnit ? ` ${model.metricUnit}` : ""}`
|
|
305
|
+
: ""
|
|
306
|
+
const title = truncate(
|
|
307
|
+
`ONYX | ${model.projectName ?? "(unlinked)"} | ${model.branchName ?? "(no onyx branch)"}`,
|
|
308
|
+
Math.max(8, columns - best.length - 12)
|
|
309
|
+
)
|
|
310
|
+
const fill = Math.max(
|
|
311
|
+
1,
|
|
312
|
+
columns - 3 - title.length - 2 - (best ? best.length + 4 : 1)
|
|
313
|
+
)
|
|
314
|
+
const top = `${dim("╭─ ", color)}${bold(title, color)}${dim(
|
|
315
|
+
` ${"─".repeat(fill)}${best ? `─ ${best} ─` : "─"}╮`,
|
|
316
|
+
color
|
|
317
|
+
)}`
|
|
318
|
+
|
|
319
|
+
const boxLine = (line: string) => {
|
|
320
|
+
const padding = " ".repeat(Math.max(0, inner - stripAnsi(line).length))
|
|
321
|
+
return `${dim("│ ", color)}${line}${padding}${dim(" │", color)}`
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Newest at the bottom: keep the tail when the table outgrows the screen.
|
|
325
|
+
const tableRows = Math.max(1, size.rows - 8)
|
|
326
|
+
const table = renderExperimentTable(model.rows.slice(-tableRows), {
|
|
327
|
+
columns: inner,
|
|
328
|
+
color,
|
|
329
|
+
nowMs: size.nowMs,
|
|
330
|
+
})
|
|
331
|
+
const body =
|
|
332
|
+
model.rows.length === 0
|
|
333
|
+
? [table[0]!, dim("no experiments yet", color)]
|
|
334
|
+
: table
|
|
335
|
+
|
|
336
|
+
const bottom = dim(`╰${"─".repeat(columns - 2)}╯`, color)
|
|
337
|
+
|
|
338
|
+
// The notch orbits only while an agent is live; idle shows the full square.
|
|
339
|
+
const spinner = model.active ? spinnerChar(size.nowMs) : "⣿"
|
|
340
|
+
const activity = ` ${dim(spinner, color)} ${truncate(
|
|
341
|
+
`Research Agent: ${model.activity ?? "waiting for activity…"}`,
|
|
342
|
+
columns - 4
|
|
343
|
+
)}`
|
|
344
|
+
const footer = dim(
|
|
345
|
+
truncate(
|
|
346
|
+
` outbox ${model.pendingOutbox} pending · ${model.syncedCount} synced · q quit`,
|
|
347
|
+
columns
|
|
348
|
+
),
|
|
349
|
+
color
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
// Blank line above the box; blank box line below the title border; blank
|
|
353
|
+
// line above the footer.
|
|
354
|
+
return [
|
|
355
|
+
"",
|
|
356
|
+
top,
|
|
357
|
+
boxLine(""),
|
|
358
|
+
...body.map(boxLine),
|
|
359
|
+
bottom,
|
|
360
|
+
activity,
|
|
361
|
+
"",
|
|
362
|
+
footer,
|
|
363
|
+
]
|
|
364
|
+
}
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import packageJson from "../package.json"
|
|
2
|
+
|
|
3
|
+
import { commandAgent } from "./commands/agent"
|
|
4
|
+
import { parseArgs } from "./lib/args"
|
|
5
|
+
import { commandExpList, commandExpLog, commandExpRun } from "./commands/exp"
|
|
6
|
+
import { commandBranchCreate } from "./commands/branch"
|
|
7
|
+
import { commandListen } from "./commands/listen"
|
|
8
|
+
import { commandLogin } from "./commands/login"
|
|
9
|
+
import { commandProfile } from "./commands/profile"
|
|
10
|
+
import { commandPush, commandStatus, commandSync } from "./commands/sync"
|
|
11
|
+
|
|
12
|
+
export const USAGE = `onyx - research workflow CLI
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
onyx --version
|
|
16
|
+
onyx login [--api-url <url>] [--print-url] [--refresh] [--port <port>] [--timeout <ms>]
|
|
17
|
+
onyx agent skill-path
|
|
18
|
+
onyx agent install-skill [--dir <path>] [--quiet]
|
|
19
|
+
onyx profile list
|
|
20
|
+
onyx profile use <name>
|
|
21
|
+
onyx profile set-api-key-env <name> <ENV_VAR>
|
|
22
|
+
onyx branch create --name <name> --metric <name> [--unit <unit>] [--direction maximize|minimize] [--description <text>] [--project-path <path>]
|
|
23
|
+
onyx exp run [--branch <name>] [--timeout <seconds>] [--checks-timeout <seconds>] [--project-path <path>] [--no-log]
|
|
24
|
+
onyx exp log [--branch <name>] [--name <name>] [--description <text>] [--agent-notes <json-or-text>] [--commit <sha>] [--metric <value>] [--metric-name <name>] [--status succeeded|failed|checks_failed|accepted|rejected|running|queued] [--project-path <path>]
|
|
25
|
+
onyx exp list [--branch <name>] [--status <status>] [--grep <regex>] [--limit <n>] [--json]
|
|
26
|
+
onyx listen
|
|
27
|
+
onyx status
|
|
28
|
+
onyx push
|
|
29
|
+
onyx sync [--project <id>] [--repository-url <url>] [--project-path <path>]
|
|
30
|
+
|
|
31
|
+
Results are reported to the Onyx app and queued in a local outbox
|
|
32
|
+
(.git/onyx/outbox.jsonl) when offline; \`onyx push\` and \`onyx sync\` flush the
|
|
33
|
+
queue and push the branch. \`onyx sync\` also refreshes the local history cache
|
|
34
|
+
(.git/onyx/history.jsonl) that \`onyx exp list\` searches offline; \`onyx listen\`
|
|
35
|
+
is a live read-only view of the current repo's research session.
|
|
36
|
+
|
|
37
|
+
Env:
|
|
38
|
+
ONYX_API_KEY overrides the selected profile API key
|
|
39
|
+
ONYX_API_URL overrides the selected profile API URL
|
|
40
|
+
Profiles may store a key locally or read it from apiKeyEnv
|
|
41
|
+
`
|
|
42
|
+
|
|
43
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
44
|
+
const args = parseArgs(argv)
|
|
45
|
+
const command = args.positional[0]
|
|
46
|
+
const sub = args.positional[1]
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
if (command === "login") return commandLogin(args)
|
|
50
|
+
if (command === "agent") return commandAgent(args)
|
|
51
|
+
if (command === "profile") return commandProfile(args)
|
|
52
|
+
if (command === "branch" && sub === "create")
|
|
53
|
+
return commandBranchCreate(args)
|
|
54
|
+
if (command === "exp" && sub === "run") return commandExpRun(args)
|
|
55
|
+
if (command === "exp" && sub === "log") return commandExpLog(args)
|
|
56
|
+
if (command === "exp" && sub === "list") return commandExpList(args)
|
|
57
|
+
if (command === "listen") return commandListen()
|
|
58
|
+
if (command === "status") return commandStatus(args)
|
|
59
|
+
if (command === "push") return commandPush(args)
|
|
60
|
+
if (command === "sync") return commandSync(args)
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
args.options.version === "true" ||
|
|
64
|
+
command === "--version" ||
|
|
65
|
+
command === "-v" ||
|
|
66
|
+
command === "version"
|
|
67
|
+
) {
|
|
68
|
+
console.log(packageJson.version)
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (command === "help" || command === "--help" || !command) {
|
|
73
|
+
console.log(USAGE)
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.error(`Unknown command: ${args.positional.join(" ")}`)
|
|
78
|
+
console.error(USAGE)
|
|
79
|
+
process.exit(1)
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error(error instanceof Error ? error.message : String(error))
|
|
82
|
+
process.exit(1)
|
|
83
|
+
}
|
|
84
|
+
}
|