@sixb/cli-core 0.1.9 → 0.2.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 +21 -16
- package/dist/api-client.d.ts +1 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/bin/sixb +26 -0
- package/dist/commands/data.d.ts +5 -0
- package/dist/commands/data.d.ts.map +1 -0
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/history.d.ts +7 -0
- package/dist/commands/history.d.ts.map +1 -0
- package/dist/commands/index.d.ts +1 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/memory.d.ts +3 -0
- package/dist/commands/memory.d.ts.map +1 -0
- package/dist/commands/metadata.d.ts +11 -4
- package/dist/commands/metadata.d.ts.map +1 -1
- package/dist/commands/runs.d.ts +8 -1
- package/dist/commands/runs.d.ts.map +1 -1
- package/dist/commands/shared.d.ts +2 -0
- package/dist/commands/shared.d.ts.map +1 -1
- package/dist/commands/system.d.ts +21 -0
- package/dist/commands/system.d.ts.map +1 -0
- package/dist/commands/users.d.ts.map +1 -1
- package/dist/generated/sixb.mjs +2658 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +298 -58
- package/dist/index.js.map +21 -17
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/memory.d.ts +21 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/policies.d.ts +14 -0
- package/dist/policies.d.ts.map +1 -1
- package/dist/run.d.ts +1 -0
- package/dist/run.d.ts.map +1 -1
- package/dist/sandbox-cli.d.ts +8 -0
- package/dist/sandbox-cli.d.ts.map +1 -0
- package/package.json +15 -1
- package/src/api-client.ts +70 -2
- package/src/bin/sixb +26 -0
- package/src/commands/actions.ts +3 -3
- package/src/commands/data.ts +90 -0
- package/src/commands/files.ts +27 -3
- package/src/commands/history.ts +204 -0
- package/src/commands/index.ts +16 -0
- package/src/commands/memory.ts +358 -0
- package/src/commands/metadata.ts +76 -29
- package/src/commands/models.ts +3 -3
- package/src/commands/ontology.ts +2 -2
- package/src/commands/runs.ts +22 -16
- package/src/commands/shared.ts +6 -0
- package/src/commands/system.ts +67 -0
- package/src/commands/telemetry.ts +2 -2
- package/src/commands/users.ts +22 -2
- package/src/commands/workflows.ts +3 -3
- package/src/generated/sixb.mjs +2658 -0
- package/src/index.ts +12 -3
- package/src/main.ts +41 -0
- package/src/memory.ts +30 -0
- package/src/policies.ts +5 -0
- package/src/run.ts +3 -2
- package/src/sandbox-cli.ts +18 -0
|
@@ -0,0 +1,2658 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/output.ts
|
|
4
|
+
var INSTANCE_CLI_VERSION = "1";
|
|
5
|
+
var EXIT_USAGE = 2;
|
|
6
|
+
var EXIT_API = 3;
|
|
7
|
+
var EXIT_AWAITING_APPROVAL = 4;
|
|
8
|
+
|
|
9
|
+
class CliError extends Error {
|
|
10
|
+
body;
|
|
11
|
+
exitCode;
|
|
12
|
+
constructor(body, exitCode = EXIT_USAGE) {
|
|
13
|
+
super(body.message);
|
|
14
|
+
this.name = "CliError";
|
|
15
|
+
this.body = body;
|
|
16
|
+
this.exitCode = exitCode;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function fail(message, code = "invalid_arguments", hint) {
|
|
20
|
+
throw new CliError({ code, message, ...hint ? { hint } : {} });
|
|
21
|
+
}
|
|
22
|
+
function writeJson(value) {
|
|
23
|
+
process.stdout.write(`${JSON.stringify(value)}
|
|
24
|
+
`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class CliExit extends Error {
|
|
28
|
+
exitCode;
|
|
29
|
+
constructor(exitCode) {
|
|
30
|
+
super(`The Sixb CLI exited with code ${exitCode}.`);
|
|
31
|
+
this.exitCode = exitCode;
|
|
32
|
+
this.name = "CliExit";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function writeRequestResult(value) {
|
|
36
|
+
writeJson(value);
|
|
37
|
+
const status = typeof value === "object" && value !== null ? Reflect.get(value, "status") : null;
|
|
38
|
+
if (status === "awaiting_approval")
|
|
39
|
+
throw new CliExit(EXIT_AWAITING_APPROVAL);
|
|
40
|
+
}
|
|
41
|
+
function writeText(value) {
|
|
42
|
+
process.stdout.write(value.endsWith(`
|
|
43
|
+
`) ? value : `${value}
|
|
44
|
+
`);
|
|
45
|
+
}
|
|
46
|
+
function reportError(error) {
|
|
47
|
+
if (error instanceof CliExit)
|
|
48
|
+
return error.exitCode;
|
|
49
|
+
const cliError = error instanceof CliError ? error : new CliError({
|
|
50
|
+
code: "internal_error",
|
|
51
|
+
message: error instanceof Error ? error.message : "The Sixb CLI failed unexpectedly."
|
|
52
|
+
}, EXIT_API);
|
|
53
|
+
process.stderr.write(`${JSON.stringify({ error: cliError.body })}
|
|
54
|
+
`);
|
|
55
|
+
return cliError.exitCode;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/arguments.ts
|
|
59
|
+
function parseCommandArgs(args, kinds, command, positionalCount = 0) {
|
|
60
|
+
const positionals = [];
|
|
61
|
+
const options = {};
|
|
62
|
+
let positionalOnly = false;
|
|
63
|
+
for (let index = 0;index < args.length; index++) {
|
|
64
|
+
const argument = args[index] ?? "";
|
|
65
|
+
if (!positionalOnly && argument === "--") {
|
|
66
|
+
positionalOnly = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (positionalOnly || !argument.startsWith("-") || argument === "-") {
|
|
70
|
+
positionals.push(argument);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const equals = argument.indexOf("=");
|
|
74
|
+
const flag = equals < 0 ? argument : argument.slice(0, equals);
|
|
75
|
+
if (!Object.hasOwn(kinds, flag))
|
|
76
|
+
fail(`Unknown ${command} option '${flag}'.`);
|
|
77
|
+
if (Object.hasOwn(options, flag) && kinds[flag] !== "strings") {
|
|
78
|
+
fail(`${flag} may only be provided once.`);
|
|
79
|
+
}
|
|
80
|
+
if (kinds[flag] === "boolean") {
|
|
81
|
+
if (equals >= 0)
|
|
82
|
+
fail(`${flag} does not accept a value.`);
|
|
83
|
+
options[flag] = true;
|
|
84
|
+
} else {
|
|
85
|
+
const value = equals < 0 ? args[++index] : argument.slice(equals + 1);
|
|
86
|
+
if (!value || equals < 0 && value.startsWith("--")) {
|
|
87
|
+
fail(`${flag} requires a value.`);
|
|
88
|
+
}
|
|
89
|
+
const previous = options[flag];
|
|
90
|
+
options[flag] = kinds[flag] === "strings" ? [...Array.isArray(previous) ? previous : [], value] : value;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const [minimum, maximum] = typeof positionalCount === "number" ? [positionalCount, positionalCount] : positionalCount;
|
|
94
|
+
if (positionals.length < minimum || positionals.length > maximum) {
|
|
95
|
+
fail(`Invalid arguments for '${command}'.`);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
positionals,
|
|
99
|
+
options
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function requestsHelp(args) {
|
|
103
|
+
const separator = args.indexOf("--");
|
|
104
|
+
const beforeSeparator = separator < 0 ? args : args.slice(0, separator);
|
|
105
|
+
return isHelp(args[0]) || beforeSeparator.includes("--help") || beforeSeparator.includes("-h");
|
|
106
|
+
}
|
|
107
|
+
var RFC3339_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
108
|
+
function isHelp(value) {
|
|
109
|
+
return value === "-h" || value === "--help" || value === "help";
|
|
110
|
+
}
|
|
111
|
+
function requireExact(args, count, message) {
|
|
112
|
+
if (args.length !== count)
|
|
113
|
+
fail(message);
|
|
114
|
+
}
|
|
115
|
+
function integerInRange(flag, value, minimum, maximum) {
|
|
116
|
+
if (!/^[0-9]+$/.test(value)) {
|
|
117
|
+
fail(`${flag} must be an integer from ${minimum} through ${maximum}.`);
|
|
118
|
+
}
|
|
119
|
+
const parsed = Number(value);
|
|
120
|
+
if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) {
|
|
121
|
+
fail(`${flag} must be an integer from ${minimum} through ${maximum}.`);
|
|
122
|
+
}
|
|
123
|
+
return parsed;
|
|
124
|
+
}
|
|
125
|
+
function nonNegativeInteger(flag, value) {
|
|
126
|
+
if (!/^[0-9]+$/.test(value)) {
|
|
127
|
+
fail(`${flag} must be a non-negative integer.`);
|
|
128
|
+
}
|
|
129
|
+
const parsed = Number(value);
|
|
130
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
131
|
+
fail(`${flag} must be a non-negative safe integer.`);
|
|
132
|
+
}
|
|
133
|
+
return parsed;
|
|
134
|
+
}
|
|
135
|
+
function rfc3339Value(flag, value) {
|
|
136
|
+
if (!RFC3339_TIMESTAMP.test(value) || Number.isNaN(Date.parse(value))) {
|
|
137
|
+
fail(`${flag} must be an RFC 3339 timestamp.`);
|
|
138
|
+
}
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
function requireOrderedRange(afterFlag, after, beforeFlag, before) {
|
|
142
|
+
if (after && before && Date.parse(after) > Date.parse(before)) {
|
|
143
|
+
fail(`${afterFlag} must be before or equal to ${beforeFlag}.`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function enumValue(flag, value, allowed) {
|
|
147
|
+
if (!allowed.includes(value))
|
|
148
|
+
fail(`${flag} must be ${formatAlternatives(allowed)}.`);
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
function formatAlternatives(values) {
|
|
152
|
+
if (values.length < 2)
|
|
153
|
+
return values[0] ?? "a supported value";
|
|
154
|
+
if (values.length === 2)
|
|
155
|
+
return `${values[0]} or ${values[1]}`;
|
|
156
|
+
return `${values.slice(0, -1).join(", ")}, or ${values.at(-1)}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/commands/actions.ts
|
|
160
|
+
import { randomUUID } from "node:crypto";
|
|
161
|
+
|
|
162
|
+
// src/policies.ts
|
|
163
|
+
var CLI_LIMITS = {
|
|
164
|
+
list: { default: 20, maximum: 1000 },
|
|
165
|
+
search: { default: 20, maximum: 50 },
|
|
166
|
+
vectorSearchText: { maximum: 8000 },
|
|
167
|
+
telemetryHistory: { default: 100, maximum: 1000 },
|
|
168
|
+
linkPage: { default: 100, maximum: 1000 },
|
|
169
|
+
users: { default: 20, maximum: 200 },
|
|
170
|
+
history: { default: 20, maximum: 100 },
|
|
171
|
+
datasetVersions: { default: 20, maximum: 100 },
|
|
172
|
+
datasetRows: { default: 100, maximum: 1000 },
|
|
173
|
+
inspect: {
|
|
174
|
+
depth: { default: 2, maximum: 3 },
|
|
175
|
+
objects: { default: 20, maximum: 100 },
|
|
176
|
+
links: { default: 50, maximum: 500 },
|
|
177
|
+
maximumPages: 10
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
var DEFAULT_LIST_ORDER = "desc";
|
|
181
|
+
var DEFAULT_OBJECT_ORDER_BY = "updatedAt";
|
|
182
|
+
var DEFAULT_TELEMETRY_ORDER = "desc";
|
|
183
|
+
|
|
184
|
+
// src/commands/metadata.ts
|
|
185
|
+
var SANDBOX_MAIN_HELP = `Sixb CLI
|
|
186
|
+
|
|
187
|
+
Usage:
|
|
188
|
+
sixb <command> [options]
|
|
189
|
+
|
|
190
|
+
The CLI uses the run's SIXB_API_BASE_URL, which adds its credentials. Do not configure authentication or another origin.
|
|
191
|
+
API commands emit JSON. Help, version, and examples emit text. Downloads write to --output and
|
|
192
|
+
emit a JSON receipt.
|
|
193
|
+
|
|
194
|
+
Discovery:
|
|
195
|
+
sixb status Check the sandbox and its API access
|
|
196
|
+
sixb context Print the current run context
|
|
197
|
+
sixb project show Show project metadata
|
|
198
|
+
sixb ontology list Compact visible type and relationship catalog
|
|
199
|
+
sixb ontology get <type> Inspect one object type, properties, links, and actions
|
|
200
|
+
|
|
201
|
+
When you do not know a type's properties or links, read its file under $SIXB_ONTOLOGY_DIR. Use
|
|
202
|
+
\`sixb ontology get\` for a type that has no file there.
|
|
203
|
+
|
|
204
|
+
Objects:
|
|
205
|
+
sixb objects inspect <type> <id> Inspect an object and its related graph in one command
|
|
206
|
+
sixb objects list [options] Browse materialized objects
|
|
207
|
+
sixb objects get <type> <id>... Exact lookup through opaque object references
|
|
208
|
+
sixb objects get-by <type> <property> <value>
|
|
209
|
+
The object holding a value of a unique property
|
|
210
|
+
sixb objects search <text> Search visible objects by id prefix and full text
|
|
211
|
+
sixb objects search <text> --type <type>
|
|
212
|
+
Semantic search through the type's vector profile
|
|
213
|
+
sixb objects query --file <file|-> Execute query IR from JSON
|
|
214
|
+
sixb objects count --file <file|-> Count a query without returning rows
|
|
215
|
+
sixb objects exists --file <file|-> Test whether a query has a match
|
|
216
|
+
sixb objects facets --file <file|-> Run a {query, facets} request
|
|
217
|
+
sixb objects links <type> <id> Read exact persisted links
|
|
218
|
+
|
|
219
|
+
Telemetry:
|
|
220
|
+
sixb telemetry latest <type> <id> <property>
|
|
221
|
+
sixb telemetry history <type> <id> <property> [options]
|
|
222
|
+
sixb telemetry query --file <file|->
|
|
223
|
+
|
|
224
|
+
Actions and workflows:
|
|
225
|
+
sixb actions list|get|request ...
|
|
226
|
+
sixb action-runs list|get ...
|
|
227
|
+
sixb workflows list|get|start ...
|
|
228
|
+
sixb workflow-runs list|get ...
|
|
229
|
+
|
|
230
|
+
Data:
|
|
231
|
+
sixb datasets list|get|versions|rows ...
|
|
232
|
+
sixb syncs list|get ... Syncs you may run, each with its latest run
|
|
233
|
+
sixb sync-runs list ...
|
|
234
|
+
sixb pipelines list|get ... Pipelines you may run, each with its latest run
|
|
235
|
+
sixb pipeline-runs list|get ...
|
|
236
|
+
|
|
237
|
+
Files:
|
|
238
|
+
sixb files upload|download ...
|
|
239
|
+
|
|
240
|
+
Members:
|
|
241
|
+
sixb users search <text> Find members by name or email
|
|
242
|
+
sixb users list|get ...
|
|
243
|
+
|
|
244
|
+
A user parameter or property is {"type":"user","id":"<user-id>"}. Find the id with
|
|
245
|
+
\`sixb users search <name>\`.
|
|
246
|
+
|
|
247
|
+
Run \`sixb <group> --help\` or \`sixb <group> <command> --help\` for exact arguments.
|
|
248
|
+
For query IR, run \`sixb objects query --help\` and \`sixb objects query --example list\`.`;
|
|
249
|
+
function renderInstanceHelp() {
|
|
250
|
+
return SANDBOX_MAIN_HELP;
|
|
251
|
+
}
|
|
252
|
+
var SEARCH_DETAILS = `Without --type, search matches primary-id prefixes and full-text fields across visible types.
|
|
253
|
+
With --type, the server embeds the text with the profile's model and ranks results by score,
|
|
254
|
+
a cosine similarity where higher is closer. \`sixb ontology list\` shows each type's
|
|
255
|
+
vectorProfiles. To combine semantic search with filters, run
|
|
256
|
+
\`sixb objects query --example vector\`.`;
|
|
257
|
+
var SEARCH_HELP = `Usage:
|
|
258
|
+
sixb objects search <text> [--limit <1-${CLI_LIMITS.search.maximum}>]
|
|
259
|
+
sixb objects search <text> --type <object-type> [--vector-profile <name>] [--limit <1-${CLI_LIMITS.search.maximum}>]
|
|
260
|
+
|
|
261
|
+
--limit defaults to ${CLI_LIMITS.search.default}. --vector-profile is required when the type declares several
|
|
262
|
+
profiles.
|
|
263
|
+
|
|
264
|
+
${SEARCH_DETAILS}`;
|
|
265
|
+
var GET_BY_HELP = `Usage:
|
|
266
|
+
sixb objects get-by <object-type> <property> <value> [--json]
|
|
267
|
+
|
|
268
|
+
Prints the object of exactly <object-type> holding <value> for a property marked unique in its
|
|
269
|
+
ontology doc, or null when no visible object holds it. Values match exactly, case included.
|
|
270
|
+
The value is a string; with --json it is parsed as JSON, for an integer (42) or a user
|
|
271
|
+
reference ({"type":"user","id":"usr_1"}).`;
|
|
272
|
+
var OBJECTS_HELP = `Usage:
|
|
273
|
+
sixb objects inspect <object-type> <primary-id> [options]
|
|
274
|
+
sixb objects list [options]
|
|
275
|
+
sixb objects get <object-type> <primary-id>...
|
|
276
|
+
sixb objects get-by <object-type> <property> <value> [--json]
|
|
277
|
+
sixb objects search <text> [--type <object-type> [--vector-profile <name>]] [--limit <n>]
|
|
278
|
+
sixb objects query --file <path|-> [--include-total|--no-total]
|
|
279
|
+
sixb objects query --example <name>
|
|
280
|
+
sixb objects count --file <path|->
|
|
281
|
+
sixb objects exists --file <path|->
|
|
282
|
+
sixb objects facets --file <path|->
|
|
283
|
+
sixb objects facets --example
|
|
284
|
+
sixb objects links <object-type> <primary-id> [options]
|
|
285
|
+
|
|
286
|
+
List options:
|
|
287
|
+
--type <id> Exact ontology object type id
|
|
288
|
+
--limit <1-${CLI_LIMITS.list.maximum}> Defaults to ${CLI_LIMITS.list.default}
|
|
289
|
+
--offset <n> Non-negative; defaults to 0
|
|
290
|
+
--order-by createdAt|updatedAt|primaryId Defaults to ${DEFAULT_OBJECT_ORDER_BY}
|
|
291
|
+
--order asc|desc Defaults to ${DEFAULT_LIST_ORDER}
|
|
292
|
+
--id-prefix <value> Primary-id prefix
|
|
293
|
+
--id-suffix <value> Primary-id suffix
|
|
294
|
+
--created-after|--created-before <RFC3339>
|
|
295
|
+
--updated-after|--updated-before <RFC3339>
|
|
296
|
+
|
|
297
|
+
Search options:
|
|
298
|
+
--limit <1-${CLI_LIMITS.search.maximum}> Defaults to ${CLI_LIMITS.search.default}
|
|
299
|
+
--type <id> Semantic search through this type's vector profile
|
|
300
|
+
--vector-profile <name> Required when the type declares several profiles
|
|
301
|
+
|
|
302
|
+
Links options:
|
|
303
|
+
--link <link-id>
|
|
304
|
+
--direction outgoing|incoming|both Defaults to both
|
|
305
|
+
--page-size <1-${CLI_LIMITS.linkPage.maximum}> Defaults to ${CLI_LIMITS.linkPage.default}
|
|
306
|
+
--page-token <token> Continue an edge page
|
|
307
|
+
--include-objects Include selected and current-page endpoint objects
|
|
308
|
+
|
|
309
|
+
Inspect options:
|
|
310
|
+
--depth <0-${CLI_LIMITS.inspect.depth.maximum}> Defaults to ${CLI_LIMITS.inspect.depth.default}; use 0 for only the object
|
|
311
|
+
--max-objects <1-${CLI_LIMITS.inspect.objects.maximum}> Defaults to ${CLI_LIMITS.inspect.objects.default}
|
|
312
|
+
--max-links <1-${CLI_LIMITS.inspect.links.maximum}> Defaults to ${CLI_LIMITS.inspect.links.default}
|
|
313
|
+
--full Include timestamps and encountered type definitions
|
|
314
|
+
|
|
315
|
+
Use \`objects get\` first when context provides an exact object reference. Use \`objects inspect\`
|
|
316
|
+
only when related objects are needed; it follows both relationship directions to depth 2 by
|
|
317
|
+
default and returns a bounded graph.
|
|
318
|
+
Inspect omits materialization timestamps and ontology definitions by default. Use \`--full\` when
|
|
319
|
+
storage timestamps, declared links, or available actions are needed.
|
|
320
|
+
|
|
321
|
+
${SEARCH_DETAILS}
|
|
322
|
+
|
|
323
|
+
\`objects get\` uses a refs query without identity URL paths. Opaque ids containing :, /, #, ?, or
|
|
324
|
+
% are safe. Identifiers are case-sensitive.`;
|
|
325
|
+
var QUERY_HELP = `Usage:
|
|
326
|
+
sixb objects query --file <path|-> [--include-total|--no-total]
|
|
327
|
+
sixb objects query --example <name>
|
|
328
|
+
sixb objects query --example list
|
|
329
|
+
|
|
330
|
+
Input is a query node. A full {"query": ...} request is also accepted.
|
|
331
|
+
|
|
332
|
+
Query nodes compose through input:
|
|
333
|
+
start {"kind":"start","objectTypeId":"Type","includeSubtypes":false}
|
|
334
|
+
refs {"kind":"refs","refs":[{"objectTypeId":"Type","primaryId":"opaque:id"}]}
|
|
335
|
+
filter {"kind":"filter","input":<query>,"predicate":<predicate>}
|
|
336
|
+
text {"kind":"text","input":<query>,"query":"words","fields":["name"]}
|
|
337
|
+
traverse {"kind":"traverse","input":<query>,"linkId":"link","direction":"outgoing"}
|
|
338
|
+
set {"kind":"set","op":"union","inputs":[<query>,<query>]} op: union, intersect, subtract
|
|
339
|
+
sort {"kind":"sort","input":<query>,"fields":[{"kind":"property","propertyId":"name","direction":"asc"}]}
|
|
340
|
+
limit {"kind":"limit","input":<query>,"limit":${CLI_LIMITS.list.default}}
|
|
341
|
+
page {"kind":"page","input":<query>,"pageSize":${CLI_LIMITS.list.default},"pageToken":"from nextPageToken"}
|
|
342
|
+
project {"kind":"project","input":<query>,"properties":["name","status"]}
|
|
343
|
+
expand {"kind":"expand","input":<query>,"expansions":[{"linkId":"link","direction":"outgoing","limit":5}],"users":["assignee"]}
|
|
344
|
+
vector {"kind":"vector","input":<start|filter>,"vector":"text","profile":"name","k":10}
|
|
345
|
+
rerank {"kind":"rerank","input":<vector>,"model":{"provider":"provider","modelId":"model"}}
|
|
346
|
+
|
|
347
|
+
Predicates use op, not kind:
|
|
348
|
+
{"op":"and","items":[<predicate>,<predicate>]} or: same shape
|
|
349
|
+
{"op":"not","item":<predicate>}
|
|
350
|
+
{"op":"eq","propertyId":"status","value":"open"} neq, lt, lte, gt, gte: same shape
|
|
351
|
+
{"op":"in","propertyId":"status","values":["open","blocked"]}
|
|
352
|
+
{"op":"exists","propertyId":"dueDate","value":true}
|
|
353
|
+
{"op":"contains","propertyId":"tags","value":"urgent"}
|
|
354
|
+
{"op":"eq","propertyId":"assignee","value":{"type":"currentUser"}} the caller, on a user reference: neq, in, contains too
|
|
355
|
+
|
|
356
|
+
Every field shown is required except includeSubtypes, fields, pageToken, properties, a sort
|
|
357
|
+
direction, an expansion's limit, and users. Sort by relevance after text or vector with
|
|
358
|
+
{"kind":"relevance"}. Wrap the outermost node in project to return only the properties you need.
|
|
359
|
+
|
|
360
|
+
An expand's users, or an expansion's, lists user-reference properties: each row then carries the
|
|
361
|
+
members' profiles under users (id, email, displayName, avatarUrl, status). Without access to the
|
|
362
|
+
member directory, users is empty.
|
|
363
|
+
|
|
364
|
+
Traversal and expansion directions are outgoing or incoming. For incoming relationships, the link
|
|
365
|
+
is declared on the child/source type; add sourceObjectTypeId when needed to disambiguate it.
|
|
366
|
+
|
|
367
|
+
Never guess property or link ids: run \`sixb ontology get <type>\` (in the agent sandbox, read
|
|
368
|
+
the type's file under $SIXB_ONTOLOGY_DIR). Use refs for exact
|
|
369
|
+
identities. Put limits and pages inside the query tree.
|
|
370
|
+
|
|
371
|
+
Vector profile names are the keys of the type's search.vectors. Vector input is a start node,
|
|
372
|
+
optionally under filters; k bounds the ranked matches and results carry a score.`;
|
|
373
|
+
var GROUP_HELP = {
|
|
374
|
+
status: "Usage: sixb status",
|
|
375
|
+
context: "Usage: sixb context",
|
|
376
|
+
history: `Usage:
|
|
377
|
+
sixb history [--offset <n>] [--limit <1-${CLI_LIMITS.history.maximum}>]
|
|
378
|
+
sixb history <seq>
|
|
379
|
+
|
|
380
|
+
Prints this conversation's messages as text, oldest first, including those summarized when it
|
|
381
|
+
was compacted: what was said, the files sent, and each tool call with its result. A page holds
|
|
382
|
+
${CLI_LIMITS.history.default} messages by default and cuts long text; \`sixb history <seq>\` prints one message whole.`,
|
|
383
|
+
project: "Usage: sixb project show",
|
|
384
|
+
ontology: `Usage:
|
|
385
|
+
sixb ontology list [--full]
|
|
386
|
+
sixb ontology get <object-type>`,
|
|
387
|
+
telemetry: `Usage:
|
|
388
|
+
sixb telemetry latest <object-type> <primary-id> <property-id>
|
|
389
|
+
sixb telemetry history <object-type> <primary-id> <property-id> [options]
|
|
390
|
+
sixb telemetry query --file <path|->
|
|
391
|
+
|
|
392
|
+
History options:
|
|
393
|
+
--from <RFC3339>
|
|
394
|
+
--to <RFC3339>
|
|
395
|
+
--limit <1-${CLI_LIMITS.telemetryHistory.maximum}> Defaults to ${CLI_LIMITS.telemetryHistory.default}
|
|
396
|
+
--order <asc|desc> Timestamp order; defaults to ${DEFAULT_TELEMETRY_ORDER}`,
|
|
397
|
+
actions: `Usage:
|
|
398
|
+
sixb actions list [--type <object-type>]
|
|
399
|
+
sixb actions get <action-id>
|
|
400
|
+
sixb actions request <action-id> [--subject-type <type> --subject-id <id>] [--file <path|->] [--run-id <id>]
|
|
401
|
+
|
|
402
|
+
\`actions get\` includes inputSchema, the exact JSON shape accepted by the Action. The JSON file
|
|
403
|
+
contains that parameter object; use - to read standard input. \`actions request\` runs the Action
|
|
404
|
+
and prints its finished run, with its status and any error. It waits at most 25 seconds: a run that
|
|
405
|
+
takes longer keeps running, and the error names the run to read with \`action-runs get\`. A request
|
|
406
|
+
that waits for the user's approval prints \`"status": "awaiting_approval"\` and exits with code 4.`,
|
|
407
|
+
"action-runs": `Usage:
|
|
408
|
+
sixb action-runs list [options]
|
|
409
|
+
sixb action-runs get <run-id>
|
|
410
|
+
|
|
411
|
+
List options:
|
|
412
|
+
--action <action-id>
|
|
413
|
+
--type <object-type>
|
|
414
|
+
--id <primary-id>
|
|
415
|
+
--status succeeded|failed
|
|
416
|
+
--started-after|--started-before <RFC3339>
|
|
417
|
+
--limit <1-${CLI_LIMITS.list.maximum}> Defaults to ${CLI_LIMITS.list.default}
|
|
418
|
+
--offset <n> Non-negative; defaults to 0
|
|
419
|
+
--order <asc|desc> Started-time order; defaults to ${DEFAULT_LIST_ORDER}`,
|
|
420
|
+
files: `Usage:
|
|
421
|
+
sixb files upload <local-path> [--logical-path <path>]
|
|
422
|
+
sixb files download object <type> <id> --path <json-pointer> --output <local-path>
|
|
423
|
+
sixb files download action-run|workflow-run <run-id> --path <json-pointer> --output <local-path>
|
|
424
|
+
sixb files download url <content-url> --output <local-path>`,
|
|
425
|
+
workflows: `Usage:
|
|
426
|
+
sixb workflows list
|
|
427
|
+
sixb workflows get <workflow-id>
|
|
428
|
+
sixb workflows start <workflow-id> [--file <path|->] [--run-id <id>]
|
|
429
|
+
|
|
430
|
+
The JSON file contains the workflow input object. Use - to read standard input. A start that waits
|
|
431
|
+
for the user's approval prints \`"status": "awaiting_approval"\` and exits with code 4.`,
|
|
432
|
+
users: `Usage:
|
|
433
|
+
sixb users list [options]
|
|
434
|
+
sixb users search <text> [options]
|
|
435
|
+
sixb users get <user-id|email>
|
|
436
|
+
|
|
437
|
+
Options:
|
|
438
|
+
--status active|suspended|all Defaults to active
|
|
439
|
+
--limit <1-${CLI_LIMITS.users.maximum}> Defaults to ${CLI_LIMITS.users.default}
|
|
440
|
+
--offset <n> Non-negative; defaults to 0
|
|
441
|
+
|
|
442
|
+
\`users search\` matches part of a display name or email, ignoring case; \`users get\` takes an
|
|
443
|
+
exact email, ignoring case, and finds suspended members too. Each member is
|
|
444
|
+
{"id","email","displayName","avatarUrl","status"}; a user parameter takes
|
|
445
|
+
{"type":"user","id":"<id>"}. Reading members requires can.view(project.users).`,
|
|
446
|
+
models: `Usage:
|
|
447
|
+
sixb models list [--kind language|image|speech|transcription]
|
|
448
|
+
sixb models get <model> [--kind <kind>]
|
|
449
|
+
sixb models text "<prompt>"|--file <path|-> [--model <tier|model>] [--instructions <text>]
|
|
450
|
+
[--max-output-tokens <n>] [--reasoning <level>]
|
|
451
|
+
sixb models image "<prompt>" --output <path> [--model <model>] [--n <n>]
|
|
452
|
+
[--size <size>] [--aspect-ratio <ratio>] [--quality <q>] [--style <s>]
|
|
453
|
+
[--seed <n>] [--image <file>]...
|
|
454
|
+
sixb models speech "<text>" --output <path> [--model <model>] [--voice <voice>]
|
|
455
|
+
[--format mp3|wav|ogg|aac|flac] [--instructions <text>]
|
|
456
|
+
[--speed <n>] [--language <code>]
|
|
457
|
+
sixb models transcribe <file> [--model <model>]
|
|
458
|
+
sixb models calls files <call-id> [--output <path>]
|
|
459
|
+
|
|
460
|
+
<model> is a model id, or provider:model-id when two providers share one; without --model, the
|
|
461
|
+
kind's default answers. \`list\` shows each model's kind, default, description and price; \`get\`
|
|
462
|
+
adds the sizes, voices and other options a request can use. Prices are in USD.
|
|
463
|
+
|
|
464
|
+
\`text\` also takes a tier (light, standard or heavy) for --model, and reads a long prompt from
|
|
465
|
+
--file: a path, or - for standard input.
|
|
466
|
+
|
|
467
|
+
--output is a file, numbered when a call returns several, or a directory ending in /. The
|
|
468
|
+
extension follows the file's type, and the result lists the paths written. --image uploads a
|
|
469
|
+
reference image; repeat it for several. \`calls files\` lists a past call's files, or downloads
|
|
470
|
+
them again.`,
|
|
471
|
+
"workflow-runs": `Usage:
|
|
472
|
+
sixb workflow-runs list [options]
|
|
473
|
+
sixb workflow-runs get <run-id>
|
|
474
|
+
|
|
475
|
+
List options:
|
|
476
|
+
--workflow <workflow-id>
|
|
477
|
+
--status queued|running|waiting|succeeded|failed|cancelled
|
|
478
|
+
--started-after|--started-before <RFC3339>
|
|
479
|
+
--limit <1-${CLI_LIMITS.list.maximum}> Defaults to ${CLI_LIMITS.list.default}
|
|
480
|
+
--offset <n> Non-negative; defaults to 0
|
|
481
|
+
--order <asc|desc> Started-time order; defaults to ${DEFAULT_LIST_ORDER}`,
|
|
482
|
+
datasets: `Usage:
|
|
483
|
+
sixb datasets list
|
|
484
|
+
sixb datasets get <dataset-id>
|
|
485
|
+
sixb datasets versions <dataset-id> [--limit <1-${CLI_LIMITS.datasetVersions.maximum}>]
|
|
486
|
+
sixb datasets rows <dataset-id> [options]
|
|
487
|
+
|
|
488
|
+
Rows options:
|
|
489
|
+
--version <version-id> Defaults to the latest version
|
|
490
|
+
--columns <a,b,...> Defaults to every column
|
|
491
|
+
--limit <1-${CLI_LIMITS.datasetRows.maximum}> Defaults to ${CLI_LIMITS.datasetRows.default}
|
|
492
|
+
--offset <n> Non-negative; defaults to 0
|
|
493
|
+
|
|
494
|
+
\`list\` and \`get\` show a dataset's schema, its latest version, and the syncs and pipelines you
|
|
495
|
+
may run that read or write it. \`versions\` lists the newest first. \`rows\` leaves out the
|
|
496
|
+
columns your clearances do not cover and names them under redactions.`,
|
|
497
|
+
syncs: `Usage:
|
|
498
|
+
sixb syncs list
|
|
499
|
+
sixb syncs get <sync-id>
|
|
500
|
+
|
|
501
|
+
Lists the syncs you may run, each with its latest run. \`sixb sync-runs list\` shows the history.`,
|
|
502
|
+
"sync-runs": `Usage:
|
|
503
|
+
sixb sync-runs list [options]
|
|
504
|
+
|
|
505
|
+
List options:
|
|
506
|
+
--sync <sync-id>
|
|
507
|
+
--dataset <dataset-id>
|
|
508
|
+
--status queued|running|succeeded|failed|cancelled
|
|
509
|
+
--started-after|--started-before <RFC3339>
|
|
510
|
+
--limit <1-${CLI_LIMITS.list.maximum}> Defaults to ${CLI_LIMITS.list.default}
|
|
511
|
+
--offset <n> Non-negative; defaults to 0
|
|
512
|
+
--order <asc|desc> Started-time order; defaults to ${DEFAULT_LIST_ORDER}`,
|
|
513
|
+
pipelines: `Usage:
|
|
514
|
+
sixb pipelines list
|
|
515
|
+
sixb pipelines get <pipeline-id>
|
|
516
|
+
|
|
517
|
+
Lists the pipelines you may run, each with its latest run. \`sixb pipeline-runs\` shows the
|
|
518
|
+
history and each run's steps.`,
|
|
519
|
+
"pipeline-runs": `Usage:
|
|
520
|
+
sixb pipeline-runs list [options]
|
|
521
|
+
sixb pipeline-runs get <run-id>
|
|
522
|
+
|
|
523
|
+
List options:
|
|
524
|
+
--pipeline <pipeline-id>
|
|
525
|
+
--status queued|running|succeeded|failed|cancelled
|
|
526
|
+
--started-after|--started-before <RFC3339>
|
|
527
|
+
--limit <1-${CLI_LIMITS.list.maximum}> Defaults to ${CLI_LIMITS.list.default}
|
|
528
|
+
--offset <n> Non-negative; defaults to 0
|
|
529
|
+
--order <asc|desc> Started-time order; defaults to ${DEFAULT_LIST_ORDER}
|
|
530
|
+
|
|
531
|
+
\`get\` includes the run's steps.`
|
|
532
|
+
};
|
|
533
|
+
var QUERY_EXAMPLES = {
|
|
534
|
+
exact: '{"kind":"refs","refs":[{"objectTypeId":"RepositoryIssue","primaryId":"github:issue:owner/repo#297"}]}',
|
|
535
|
+
filter: `{"kind":"limit","input":{"kind":"filter","input":{"kind":"start","objectTypeId":"Customer"},"predicate":{"op":"eq","propertyId":"status","value":"active"}},"limit":${CLI_LIMITS.list.default}}`,
|
|
536
|
+
and: `{"kind":"limit","input":{"kind":"filter","input":{"kind":"start","objectTypeId":"Customer"},"predicate":{"op":"and","items":[{"op":"eq","propertyId":"status","value":"active"},{"op":"gte","propertyId":"createdAt","value":"2026-01-01T00:00:00Z"}]}},"limit":${CLI_LIMITS.list.default}}`,
|
|
537
|
+
not: `{"kind":"limit","input":{"kind":"filter","input":{"kind":"start","objectTypeId":"Customer"},"predicate":{"op":"not","item":{"op":"eq","propertyId":"status","value":"churned"}}},"limit":${CLI_LIMITS.list.default}}`,
|
|
538
|
+
in: `{"kind":"limit","input":{"kind":"filter","input":{"kind":"start","objectTypeId":"Customer"},"predicate":{"op":"in","propertyId":"status","values":["active","trial"]}},"limit":${CLI_LIMITS.list.default}}`,
|
|
539
|
+
exists: `{"kind":"limit","input":{"kind":"filter","input":{"kind":"start","objectTypeId":"Customer"},"predicate":{"op":"exists","propertyId":"accountManager","value":false}},"limit":${CLI_LIMITS.list.default}}`,
|
|
540
|
+
mine: `{"kind":"limit","input":{"kind":"filter","input":{"kind":"start","objectTypeId":"Task"},"predicate":{"op":"eq","propertyId":"assignee","value":{"type":"currentUser"}}},"limit":${CLI_LIMITS.list.default}}`,
|
|
541
|
+
project: `{"kind":"project","input":{"kind":"limit","input":{"kind":"start","objectTypeId":"Customer"},"limit":${CLI_LIMITS.list.default}},"properties":["name","status"]}`,
|
|
542
|
+
incoming: '{"kind":"traverse","input":{"kind":"refs","refs":[{"objectTypeId":"RepositoryIssue","primaryId":"github:issue:owner/repo#297"}]},"linkId":"issue","direction":"incoming","sourceObjectTypeId":"RepositoryComment"}',
|
|
543
|
+
expand: `{"kind":"expand","input":{"kind":"refs","refs":[{"objectTypeId":"RepositoryIssue","primaryId":"github:issue:owner/repo#297"}]},"expansions":[{"linkId":"issue","direction":"incoming","sourceObjectTypeId":"RepositoryComment","limit":${CLI_LIMITS.list.default}}]}`,
|
|
544
|
+
users: `{"kind":"expand","input":{"kind":"limit","input":{"kind":"start","objectTypeId":"Task"},"limit":${CLI_LIMITS.list.default}},"expansions":[],"users":["assignee"]}`,
|
|
545
|
+
sort: `{"kind":"limit","input":{"kind":"sort","input":{"kind":"start","objectTypeId":"Customer"},"fields":[{"kind":"property","propertyId":"name","direction":"asc"}]},"limit":${CLI_LIMITS.list.default}}`,
|
|
546
|
+
page: `{"kind":"page","input":{"kind":"start","objectTypeId":"Customer"},"pageSize":${CLI_LIMITS.list.default}}`,
|
|
547
|
+
vector: `{"kind":"vector","input":{"kind":"filter","input":{"kind":"start","objectTypeId":"Product"},"predicate":{"op":"eq","propertyId":"status","value":"active"}},"vector":"waterproof trail shoes","profile":"content","k":${CLI_LIMITS.search.default}}`
|
|
548
|
+
};
|
|
549
|
+
var FACETS_EXAMPLE = '{"query":{"kind":"start","objectTypeId":"WorkOrder"},"facets":[{"propertyId":"status","limit":10}]}';
|
|
550
|
+
|
|
551
|
+
// src/commands/shared.ts
|
|
552
|
+
import { readFile } from "node:fs/promises";
|
|
553
|
+
function idSegment(value) {
|
|
554
|
+
if (!value || value === "." || value === "..")
|
|
555
|
+
fail(`'${value ?? ""}' is not an id.`);
|
|
556
|
+
return encodeURIComponent(value);
|
|
557
|
+
}
|
|
558
|
+
function parseQueryOptions(args, names, command) {
|
|
559
|
+
const kinds = Object.fromEntries(Object.keys(names).map((flag) => [flag, "string"]));
|
|
560
|
+
const { options } = parseCommandArgs(args, kinds, command);
|
|
561
|
+
const query = {};
|
|
562
|
+
for (const [flag, value] of Object.entries(options)) {
|
|
563
|
+
const name = names[flag];
|
|
564
|
+
if (name)
|
|
565
|
+
query[name] = value;
|
|
566
|
+
}
|
|
567
|
+
return query;
|
|
568
|
+
}
|
|
569
|
+
function normalizeWindowOptions(options, policy) {
|
|
570
|
+
const normalized = {
|
|
571
|
+
...options,
|
|
572
|
+
limit: String(integerInRange("--limit", options.limit ?? String(policy.defaultLimit), 1, policy.maximumLimit)),
|
|
573
|
+
order: enumValue("--order", options.order ?? policy.defaultOrder, ["asc", "desc"])
|
|
574
|
+
};
|
|
575
|
+
if (policy.offset && options.offset !== undefined) {
|
|
576
|
+
normalized.offset = String(nonNegativeInteger("--offset", options.offset));
|
|
577
|
+
}
|
|
578
|
+
return normalized;
|
|
579
|
+
}
|
|
580
|
+
function singleFileOption(args, command) {
|
|
581
|
+
const { options } = parseCommandArgs(args, { "--file": "string" }, command);
|
|
582
|
+
if (!options["--file"])
|
|
583
|
+
fail(`${command} requires --file <path|->.`);
|
|
584
|
+
return options["--file"];
|
|
585
|
+
}
|
|
586
|
+
async function readJson(source) {
|
|
587
|
+
const text = await readText(source, "JSON file");
|
|
588
|
+
try {
|
|
589
|
+
return JSON.parse(text);
|
|
590
|
+
} catch {
|
|
591
|
+
fail(source === "-" ? "Standard input is not valid JSON." : `JSON file '${source}' is not valid JSON.`, "invalid_json");
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
async function readText(source, noun) {
|
|
595
|
+
try {
|
|
596
|
+
return source === "-" ? await readStdin() : await readFile(source, "utf8");
|
|
597
|
+
} catch (error) {
|
|
598
|
+
if (isFileError(error, "ENOENT"))
|
|
599
|
+
fail(`${noun} '${source}' does not exist.`);
|
|
600
|
+
throw error;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
function asRecord(value) {
|
|
604
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
605
|
+
}
|
|
606
|
+
function asRecords(value) {
|
|
607
|
+
return Array.isArray(value) ? value.map(asRecord) : [];
|
|
608
|
+
}
|
|
609
|
+
function vectorProfileNames(objectType) {
|
|
610
|
+
return Object.keys(asRecord(asRecord(objectType.search).vectors));
|
|
611
|
+
}
|
|
612
|
+
function isFileError(error, code) {
|
|
613
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
614
|
+
}
|
|
615
|
+
async function readStdin() {
|
|
616
|
+
const chunks = [];
|
|
617
|
+
for await (const chunk of process.stdin) {
|
|
618
|
+
chunks.push(typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk);
|
|
619
|
+
}
|
|
620
|
+
return new TextDecoder().decode(Buffer.concat(chunks));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/commands/actions.ts
|
|
624
|
+
var ACTION_REQUEST_WAIT_MS = 25000;
|
|
625
|
+
async function actions(api, args, requestWaitMs = ACTION_REQUEST_WAIT_MS) {
|
|
626
|
+
const [sub, ...rest] = args;
|
|
627
|
+
if (!sub || isHelp(sub) || requestsHelp(rest))
|
|
628
|
+
return writeText(GROUP_HELP.actions);
|
|
629
|
+
if (sub === "get") {
|
|
630
|
+
const { positionals } = parseCommandArgs(rest, {}, "actions get", 1);
|
|
631
|
+
return writeJson(await api.get(`/api/actions/${idSegment(positionals[0])}`));
|
|
632
|
+
}
|
|
633
|
+
if (sub === "list") {
|
|
634
|
+
const options = parseQueryOptions(rest, { "--type": "objectTypeId" }, "actions list");
|
|
635
|
+
const response = await api.get("/api/actions");
|
|
636
|
+
if (!options.objectTypeId)
|
|
637
|
+
return writeJson(response);
|
|
638
|
+
if (!Array.isArray(response))
|
|
639
|
+
fail("The actions API returned an invalid response.");
|
|
640
|
+
return writeJson(response.filter((value) => asRecord(value).objectTypeId === options.objectTypeId));
|
|
641
|
+
}
|
|
642
|
+
if (sub === "request") {
|
|
643
|
+
const { positionals, options } = parseCommandArgs(rest, {
|
|
644
|
+
"--subject-type": "string",
|
|
645
|
+
"--subject-id": "string",
|
|
646
|
+
"--file": "string",
|
|
647
|
+
"--run-id": "string"
|
|
648
|
+
}, "actions request", 1);
|
|
649
|
+
const actionId = positionals[0] ?? "";
|
|
650
|
+
const subjectType = options["--subject-type"];
|
|
651
|
+
const subjectId = options["--subject-id"];
|
|
652
|
+
const paramsSource = options["--file"];
|
|
653
|
+
const runId = options["--run-id"];
|
|
654
|
+
if (Boolean(subjectType) !== Boolean(subjectId)) {
|
|
655
|
+
fail("--subject-type and --subject-id must be provided together.");
|
|
656
|
+
}
|
|
657
|
+
const params = paramsSource ? await readJson(paramsSource) : {};
|
|
658
|
+
if (Array.isArray(params) || typeof params !== "object" || params === null) {
|
|
659
|
+
fail("Action params must be a JSON object.");
|
|
660
|
+
}
|
|
661
|
+
const requestedRunId = runId ?? `act_${randomUUID()}`;
|
|
662
|
+
const waiting = AbortSignal.timeout(requestWaitMs);
|
|
663
|
+
try {
|
|
664
|
+
return writeRequestResult(await api.post(`/api/actions/${idSegment(actionId)}`, {
|
|
665
|
+
params,
|
|
666
|
+
...subjectType && subjectId ? { subject: { kind: "object", objectTypeId: subjectType, primaryId: subjectId } } : {},
|
|
667
|
+
runId: requestedRunId
|
|
668
|
+
}, { signal: waiting }));
|
|
669
|
+
} catch (error) {
|
|
670
|
+
if (!waiting.aborted)
|
|
671
|
+
throw error;
|
|
672
|
+
throw new CliError({
|
|
673
|
+
code: "action_wait_timeout",
|
|
674
|
+
message: `Action run '${requestedRunId}' did not finish within ${requestWaitMs / 1000} ` + "seconds. It was not cancelled.",
|
|
675
|
+
hint: `Read it with 'sixb action-runs get ${requestedRunId}': its record exists once it ` + `has ended. A repeated request must pass '--run-id ${requestedRunId}', or the ` + "Action runs twice."
|
|
676
|
+
}, EXIT_API);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
fail(`Unknown actions command '${sub}'.`);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// src/commands/data.ts
|
|
683
|
+
async function datasets(api, args) {
|
|
684
|
+
const [sub, ...rest] = args;
|
|
685
|
+
if (!sub || isHelp(sub) || requestsHelp(rest))
|
|
686
|
+
return writeText(GROUP_HELP.datasets);
|
|
687
|
+
if (sub === "list") {
|
|
688
|
+
parseCommandArgs(rest, {}, "datasets list");
|
|
689
|
+
return writeJson(await api.get("/api/datasets"));
|
|
690
|
+
}
|
|
691
|
+
if (sub === "get") {
|
|
692
|
+
const { positionals } = parseCommandArgs(rest, {}, "datasets get", 1);
|
|
693
|
+
return writeJson(await api.get(`/api/datasets/${idSegment(positionals[0])}`));
|
|
694
|
+
}
|
|
695
|
+
if (sub === "versions") {
|
|
696
|
+
const { positionals, options } = parseCommandArgs(rest, { "--limit": "string" }, "datasets versions", 1);
|
|
697
|
+
const { default: fallback, maximum } = CLI_LIMITS.datasetVersions;
|
|
698
|
+
const limit = integerInRange("--limit", options["--limit"] ?? String(fallback), 1, maximum);
|
|
699
|
+
return writeJson(await api.get(`/api/datasets/${idSegment(positionals[0])}/versions`, { limit: String(limit) }));
|
|
700
|
+
}
|
|
701
|
+
if (sub === "rows") {
|
|
702
|
+
const { positionals, options } = parseCommandArgs(rest, { "--version": "string", "--columns": "string", "--limit": "string", "--offset": "string" }, "datasets rows", 1);
|
|
703
|
+
const { default: fallback, maximum } = CLI_LIMITS.datasetRows;
|
|
704
|
+
const query = {
|
|
705
|
+
limit: String(integerInRange("--limit", options["--limit"] ?? String(fallback), 1, maximum)),
|
|
706
|
+
...options["--version"] === undefined ? {} : { versionId: options["--version"] },
|
|
707
|
+
...options["--columns"] === undefined ? {} : { columns: columnList(options["--columns"]) },
|
|
708
|
+
...options["--offset"] === undefined ? {} : { offset: String(nonNegativeInteger("--offset", options["--offset"])) }
|
|
709
|
+
};
|
|
710
|
+
return writeJson(await api.get(`/api/datasets/${idSegment(positionals[0])}/rows`, query));
|
|
711
|
+
}
|
|
712
|
+
fail(`Unknown datasets command '${sub}'.`);
|
|
713
|
+
}
|
|
714
|
+
async function runnables(api, group, args) {
|
|
715
|
+
const [sub, ...rest] = args;
|
|
716
|
+
if (!sub || isHelp(sub) || requestsHelp(rest))
|
|
717
|
+
return writeText(GROUP_HELP[group]);
|
|
718
|
+
if (sub === "list") {
|
|
719
|
+
parseCommandArgs(rest, {}, `${group} list`);
|
|
720
|
+
return writeJson(await api.get(`/api/${group}`));
|
|
721
|
+
}
|
|
722
|
+
if (sub === "get") {
|
|
723
|
+
const { positionals } = parseCommandArgs(rest, {}, `${group} get`, 1);
|
|
724
|
+
return writeJson(await api.get(`/api/${group}/${idSegment(positionals[0])}`));
|
|
725
|
+
}
|
|
726
|
+
fail(`Unknown ${group} command '${sub}'.`);
|
|
727
|
+
}
|
|
728
|
+
function columnList(value) {
|
|
729
|
+
const columns = value.split(",").map((column) => column.trim());
|
|
730
|
+
if (columns.some((column) => !column))
|
|
731
|
+
fail("--columns takes column names separated by commas.");
|
|
732
|
+
return columns.join(",");
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// src/commands/files.ts
|
|
736
|
+
import { access } from "node:fs/promises";
|
|
737
|
+
async function files(api, args) {
|
|
738
|
+
const [sub, ...rest] = args;
|
|
739
|
+
if (!sub || isHelp(sub) || requestsHelp(rest))
|
|
740
|
+
return writeText(GROUP_HELP.files);
|
|
741
|
+
if (sub === "upload") {
|
|
742
|
+
const { positionals, options } = parseCommandArgs(rest, { "--logical-path": "string" }, "files upload", 1);
|
|
743
|
+
const source = positionals[0] ?? "";
|
|
744
|
+
try {
|
|
745
|
+
await access(source);
|
|
746
|
+
} catch {
|
|
747
|
+
fail(`Upload file '${source}' does not exist.`);
|
|
748
|
+
}
|
|
749
|
+
return writeJson(await api.upload("/api/files", source, options["--logical-path"]));
|
|
750
|
+
}
|
|
751
|
+
if (sub === "download") {
|
|
752
|
+
const { positionals, options } = parseCommandArgs(rest, { "--path": "string", "--output": "string" }, "files download", [2, 3]);
|
|
753
|
+
const context = positionals[0];
|
|
754
|
+
const output = options["--output"];
|
|
755
|
+
if (context === "url") {
|
|
756
|
+
requireExact(positionals, 2, "files download url requires exactly one content URL.");
|
|
757
|
+
const url = parseContentUrl(positionals[1] ?? "");
|
|
758
|
+
if (!output)
|
|
759
|
+
fail("files download requires --output <local-path>.");
|
|
760
|
+
await api.download(url.pathname, output, Object.fromEntries(url.searchParams));
|
|
761
|
+
return writeJson({ downloaded: true, output });
|
|
762
|
+
}
|
|
763
|
+
let route;
|
|
764
|
+
if (context === "object") {
|
|
765
|
+
requireExact(positionals, 3, "files download object requires object type and primary id.");
|
|
766
|
+
const type = positionals[1] ?? "";
|
|
767
|
+
const id = positionals[2] ?? "";
|
|
768
|
+
route = `/api/objects/${idSegment(type)}/${idSegment(id)}/files/content`;
|
|
769
|
+
} else if (context === "action-run" || context === "workflow-run") {
|
|
770
|
+
requireExact(positionals, 2, `files download ${context} requires exactly one run id.`);
|
|
771
|
+
const id = positionals[1] ?? "";
|
|
772
|
+
route = `/api/${context}s/${idSegment(id)}/files/content`;
|
|
773
|
+
} else
|
|
774
|
+
fail(`Unknown file download context '${context}'.`);
|
|
775
|
+
const path = options["--path"];
|
|
776
|
+
if (!path)
|
|
777
|
+
fail("files download requires --path <json-pointer>.");
|
|
778
|
+
if (!output)
|
|
779
|
+
fail("files download requires --output <local-path>.");
|
|
780
|
+
await api.download(route, output, { path });
|
|
781
|
+
return writeJson({ downloaded: true, output });
|
|
782
|
+
}
|
|
783
|
+
fail(`Unknown files command '${sub}'.`);
|
|
784
|
+
}
|
|
785
|
+
function parseContentUrl(value) {
|
|
786
|
+
let url;
|
|
787
|
+
try {
|
|
788
|
+
url = new URL(value);
|
|
789
|
+
} catch {
|
|
790
|
+
fail(`'${value}' is not a URL.`);
|
|
791
|
+
}
|
|
792
|
+
if (!url.pathname.startsWith("/api/") || !url.pathname.endsWith("/files/content")) {
|
|
793
|
+
fail(`'${value}' is not a Sixb file content URL.`);
|
|
794
|
+
}
|
|
795
|
+
return url;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// src/commands/models.ts
|
|
799
|
+
import { access as access2, mkdir, stat } from "node:fs/promises";
|
|
800
|
+
import { dirname, extname, join, parse } from "node:path";
|
|
801
|
+
var MODEL_KINDS = ["language", "image", "speech", "transcription"];
|
|
802
|
+
async function models(api, args) {
|
|
803
|
+
const [sub, ...rest] = args;
|
|
804
|
+
if (!sub || isHelp(sub) || requestsHelp(rest))
|
|
805
|
+
return writeText(GROUP_HELP.models);
|
|
806
|
+
if (sub === "list") {
|
|
807
|
+
const { options } = parseCommandArgs(rest, { "--kind": "string" }, "models list", 0);
|
|
808
|
+
const kind = options["--kind"] === undefined ? undefined : modelKind(options["--kind"]);
|
|
809
|
+
const entries = catalogEntries(await api.get("/api/models"));
|
|
810
|
+
return writeJson(entries.filter((entry) => kind === undefined || entry.kind === kind).map(({ kind, entry }) => ({
|
|
811
|
+
kind,
|
|
812
|
+
model: entry.modelId,
|
|
813
|
+
provider: entry.provider,
|
|
814
|
+
...entry.tier ? { tier: entry.tier } : {},
|
|
815
|
+
...entry.isDefault ? { default: true } : {},
|
|
816
|
+
...entry.description ? { description: entry.description } : {},
|
|
817
|
+
price: describePrice(entry.rateCard)
|
|
818
|
+
})));
|
|
819
|
+
}
|
|
820
|
+
if (sub === "get") {
|
|
821
|
+
const { positionals, options } = parseCommandArgs(rest, { "--kind": "string" }, "models get", 1);
|
|
822
|
+
const kind = options["--kind"] === undefined ? undefined : modelKind(options["--kind"]);
|
|
823
|
+
const entries = catalogEntries(await api.get("/api/models"));
|
|
824
|
+
const { kind: found, entry } = findModel(entries, positionals[0] ?? "", kind);
|
|
825
|
+
const { provider, modelId, isDefault, rateCard, ...details } = entry;
|
|
826
|
+
return writeJson({
|
|
827
|
+
kind: found,
|
|
828
|
+
model: modelId,
|
|
829
|
+
provider,
|
|
830
|
+
default: isDefault,
|
|
831
|
+
...details,
|
|
832
|
+
price: describePrice(rateCard),
|
|
833
|
+
...rateCard ? { rateCard } : {}
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
if (sub === "text")
|
|
837
|
+
return generateText(api, rest);
|
|
838
|
+
if (sub === "image")
|
|
839
|
+
return generateImages(api, rest);
|
|
840
|
+
if (sub === "speech")
|
|
841
|
+
return generateSpeech(api, rest);
|
|
842
|
+
if (sub === "transcribe")
|
|
843
|
+
return transcribe(api, rest);
|
|
844
|
+
if (sub === "calls")
|
|
845
|
+
return calls(api, rest);
|
|
846
|
+
fail(`Unknown models command '${sub}'.`);
|
|
847
|
+
}
|
|
848
|
+
var SPEECH_FORMATS = {
|
|
849
|
+
mp3: "audio/mpeg",
|
|
850
|
+
wav: "audio/wav",
|
|
851
|
+
ogg: "audio/ogg",
|
|
852
|
+
aac: "audio/aac",
|
|
853
|
+
flac: "audio/flac"
|
|
854
|
+
};
|
|
855
|
+
var SPEECH_FORMAT_NAMES = Object.keys(SPEECH_FORMATS);
|
|
856
|
+
var MEDIA_TYPES = {
|
|
857
|
+
".png": "image/png",
|
|
858
|
+
".jpg": "image/jpeg",
|
|
859
|
+
".jpeg": "image/jpeg",
|
|
860
|
+
".webp": "image/webp",
|
|
861
|
+
".gif": "image/gif",
|
|
862
|
+
".mp3": "audio/mpeg",
|
|
863
|
+
".wav": "audio/wav",
|
|
864
|
+
".ogg": "audio/ogg",
|
|
865
|
+
".aac": "audio/aac",
|
|
866
|
+
".flac": "audio/flac",
|
|
867
|
+
".opus": "audio/opus",
|
|
868
|
+
".m4a": "audio/mp4",
|
|
869
|
+
".webm": "audio/webm"
|
|
870
|
+
};
|
|
871
|
+
var EXTENSIONS = Object.fromEntries(Object.entries(MEDIA_TYPES).filter(([extension]) => extension !== ".jpeg").map(([extension, mediaType]) => [mediaType, extension]));
|
|
872
|
+
async function generateText(api, args) {
|
|
873
|
+
const { positionals, options } = parseCommandArgs(args, {
|
|
874
|
+
"--model": "string",
|
|
875
|
+
"--instructions": "string",
|
|
876
|
+
"--max-output-tokens": "string",
|
|
877
|
+
"--reasoning": "string",
|
|
878
|
+
"--file": "string"
|
|
879
|
+
}, "models text", [0, 1]);
|
|
880
|
+
const prompt = await readPrompt(positionals[0], options["--file"]);
|
|
881
|
+
const maxOutputTokens = options["--max-output-tokens"];
|
|
882
|
+
const result = await api.post("/api/models/language/generations", {
|
|
883
|
+
...await languageModelRef(api, options["--model"]),
|
|
884
|
+
prompt,
|
|
885
|
+
...options["--instructions"] === undefined ? {} : { instructions: options["--instructions"] },
|
|
886
|
+
...maxOutputTokens === undefined ? {} : { maxOutputTokens: integerInRange("--max-output-tokens", maxOutputTokens, 1, 1e6) },
|
|
887
|
+
...options["--reasoning"] === undefined ? {} : { reasoning: options["--reasoning"] }
|
|
888
|
+
});
|
|
889
|
+
writeJson({
|
|
890
|
+
callId: result.callId,
|
|
891
|
+
model: result.model.modelId,
|
|
892
|
+
text: result.text,
|
|
893
|
+
finishReason: result.finishReason,
|
|
894
|
+
cost: describeCost(result.cost)
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
async function generateImages(api, args) {
|
|
898
|
+
const { positionals, options } = parseCommandArgs(args, {
|
|
899
|
+
"--output": "string",
|
|
900
|
+
"--model": "string",
|
|
901
|
+
"--n": "string",
|
|
902
|
+
"--size": "string",
|
|
903
|
+
"--aspect-ratio": "string",
|
|
904
|
+
"--quality": "string",
|
|
905
|
+
"--style": "string",
|
|
906
|
+
"--seed": "string",
|
|
907
|
+
"--image": "strings"
|
|
908
|
+
}, "models image", 1);
|
|
909
|
+
const output = requireOutput(options["--output"], "models image");
|
|
910
|
+
const seed = options["--seed"];
|
|
911
|
+
if (seed !== undefined && !/^-?[0-9]+$/.test(seed))
|
|
912
|
+
fail("--seed must be an integer.");
|
|
913
|
+
const model = await modelRef(api, options["--model"], "image");
|
|
914
|
+
const images = [];
|
|
915
|
+
for (const path of options["--image"] ?? [])
|
|
916
|
+
images.push(await uploadInput(api, path, "image"));
|
|
917
|
+
const result = await api.post("/api/models/image/generations", {
|
|
918
|
+
...model,
|
|
919
|
+
prompt: positionals[0],
|
|
920
|
+
...options["--n"] === undefined ? {} : { n: integerInRange("--n", options["--n"], 1, 100) },
|
|
921
|
+
...optional("size", options["--size"]),
|
|
922
|
+
...optional("aspectRatio", options["--aspect-ratio"]),
|
|
923
|
+
...optional("quality", options["--quality"]),
|
|
924
|
+
...optional("style", options["--style"]),
|
|
925
|
+
...seed === undefined ? {} : { seed: Number(seed) },
|
|
926
|
+
...images.length > 0 ? { images } : {}
|
|
927
|
+
});
|
|
928
|
+
writeJson(await savedResult(api, result, output));
|
|
929
|
+
}
|
|
930
|
+
async function generateSpeech(api, args) {
|
|
931
|
+
const { positionals, options } = parseCommandArgs(args, {
|
|
932
|
+
"--output": "string",
|
|
933
|
+
"--model": "string",
|
|
934
|
+
"--voice": "string",
|
|
935
|
+
"--format": "string",
|
|
936
|
+
"--instructions": "string",
|
|
937
|
+
"--speed": "string",
|
|
938
|
+
"--language": "string"
|
|
939
|
+
}, "models speech", 1);
|
|
940
|
+
const output = requireOutput(options["--output"], "models speech");
|
|
941
|
+
const format = options["--format"];
|
|
942
|
+
const speed = options["--speed"];
|
|
943
|
+
if (speed !== undefined && !(Number(speed) > 0))
|
|
944
|
+
fail("--speed must be a positive number.");
|
|
945
|
+
const result = await api.post("/api/models/audio/speech", {
|
|
946
|
+
...await modelRef(api, options["--model"], "speech"),
|
|
947
|
+
text: positionals[0],
|
|
948
|
+
...optional("voice", options["--voice"]),
|
|
949
|
+
...format === undefined ? {} : { mediaType: SPEECH_FORMATS[enumValue("--format", format, SPEECH_FORMAT_NAMES)] },
|
|
950
|
+
...optional("instructions", options["--instructions"]),
|
|
951
|
+
...speed === undefined ? {} : { speed: Number(speed) },
|
|
952
|
+
...optional("language", options["--language"])
|
|
953
|
+
});
|
|
954
|
+
writeJson(await savedResult(api, result, output));
|
|
955
|
+
}
|
|
956
|
+
async function transcribe(api, args) {
|
|
957
|
+
const { positionals, options } = parseCommandArgs(args, { "--model": "string" }, "models transcribe", 1);
|
|
958
|
+
const model = await modelRef(api, options["--model"], "transcription");
|
|
959
|
+
const audio = await uploadInput(api, positionals[0] ?? "", "audio");
|
|
960
|
+
const result = await api.post("/api/models/audio/transcriptions", {
|
|
961
|
+
...model,
|
|
962
|
+
audio
|
|
963
|
+
});
|
|
964
|
+
writeJson({
|
|
965
|
+
callId: result.callId,
|
|
966
|
+
model: result.model.modelId,
|
|
967
|
+
text: result.text,
|
|
968
|
+
cost: describeCost(result.cost)
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
async function calls(api, args) {
|
|
972
|
+
const [sub, ...rest] = args;
|
|
973
|
+
if (sub !== "files")
|
|
974
|
+
fail(`Unknown models calls command '${sub ?? ""}'. Use 'models calls files'.`);
|
|
975
|
+
const { positionals, options } = parseCommandArgs(rest, { "--output": "string" }, "models calls files", 1);
|
|
976
|
+
const callId = positionals[0] ?? "";
|
|
977
|
+
const call = await api.get(`/api/ai/model-calls/${idSegment(callId)}/files`);
|
|
978
|
+
const output = options["--output"];
|
|
979
|
+
if (output === undefined)
|
|
980
|
+
return writeJson(call);
|
|
981
|
+
writeJson({ callId: call.callId, files: await saveFiles(api, call.callId, call.files, output) });
|
|
982
|
+
}
|
|
983
|
+
async function modelRef(api, reference, kind) {
|
|
984
|
+
if (reference === undefined)
|
|
985
|
+
return {};
|
|
986
|
+
const { entry } = findModel(catalogEntries(await api.get("/api/models")), reference, kind);
|
|
987
|
+
return { model: { provider: entry.provider, modelId: entry.modelId } };
|
|
988
|
+
}
|
|
989
|
+
var LANGUAGE_TIERS = ["light", "standard", "heavy"];
|
|
990
|
+
async function languageModelRef(api, reference) {
|
|
991
|
+
const tier = LANGUAGE_TIERS.find((candidate) => candidate === reference);
|
|
992
|
+
if (tier !== undefined)
|
|
993
|
+
return { model: tier };
|
|
994
|
+
return modelRef(api, reference, "language");
|
|
995
|
+
}
|
|
996
|
+
async function readPrompt(argument, source) {
|
|
997
|
+
if (argument !== undefined && source !== undefined) {
|
|
998
|
+
fail("models text takes its prompt as an argument or from --file, not both.");
|
|
999
|
+
}
|
|
1000
|
+
const prompt = source === undefined ? argument : await readText(source, "Prompt file");
|
|
1001
|
+
if (prompt === undefined) {
|
|
1002
|
+
fail('models text requires a prompt: "<prompt>", or --file <path|-> to read it.');
|
|
1003
|
+
}
|
|
1004
|
+
if (!prompt.trim())
|
|
1005
|
+
fail("The prompt is empty.");
|
|
1006
|
+
return prompt;
|
|
1007
|
+
}
|
|
1008
|
+
function requireOutput(output, command) {
|
|
1009
|
+
if (!output)
|
|
1010
|
+
fail(`${command} requires --output <path>: a file, or a directory ending in /.`);
|
|
1011
|
+
return output;
|
|
1012
|
+
}
|
|
1013
|
+
function optional(field, value) {
|
|
1014
|
+
return value === undefined ? {} : { [field]: value };
|
|
1015
|
+
}
|
|
1016
|
+
async function uploadInput(api, path, kind) {
|
|
1017
|
+
try {
|
|
1018
|
+
await access2(path);
|
|
1019
|
+
} catch {
|
|
1020
|
+
fail(`Input file '${path}' does not exist.`);
|
|
1021
|
+
}
|
|
1022
|
+
const mediaType = MEDIA_TYPES[extname(path).toLowerCase()];
|
|
1023
|
+
if (!mediaType?.startsWith(`${kind}/`)) {
|
|
1024
|
+
const accepted = Object.entries(MEDIA_TYPES).filter(([, type]) => type.startsWith(`${kind}/`)).map(([extension]) => extension);
|
|
1025
|
+
fail(`'${path}' is not an ${kind} file. Use ${accepted.join(", ")}.`);
|
|
1026
|
+
}
|
|
1027
|
+
return api.upload("/api/files", path, undefined, mediaType);
|
|
1028
|
+
}
|
|
1029
|
+
async function savedResult(api, result, output) {
|
|
1030
|
+
return {
|
|
1031
|
+
callId: result.callId,
|
|
1032
|
+
model: result.model.modelId,
|
|
1033
|
+
files: await saveFiles(api, result.callId, result.files ?? [], output),
|
|
1034
|
+
...result.warnings?.length ? { warnings: result.warnings } : {},
|
|
1035
|
+
cost: describeCost(result.cost)
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
async function saveFiles(api, callId, files, output) {
|
|
1039
|
+
const directory = output.endsWith("/") || (await stat(output).catch(() => null))?.isDirectory();
|
|
1040
|
+
const paths = files.map((file, index) => {
|
|
1041
|
+
const number = files.length > 1 ? `-${index + 1}` : "";
|
|
1042
|
+
const extension = (file.mediaType && EXTENSIONS[file.mediaType]) ?? "";
|
|
1043
|
+
if (directory)
|
|
1044
|
+
return join(output, `${callId}${number}${extension || ".bin"}`);
|
|
1045
|
+
const { dir, name, ext } = parse(output);
|
|
1046
|
+
return join(dir, `${name}${number}${extension || ext}`);
|
|
1047
|
+
});
|
|
1048
|
+
for (const [index, path] of paths.entries()) {
|
|
1049
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1050
|
+
await api.download(`/api/ai/model-calls/${idSegment(callId)}/files/content`, path, {
|
|
1051
|
+
path: `/files/${index}`
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
return paths;
|
|
1055
|
+
}
|
|
1056
|
+
function describeCost(cost) {
|
|
1057
|
+
if (!cost.money)
|
|
1058
|
+
return "unknown";
|
|
1059
|
+
const nanos = BigInt(cost.money.amountNanos);
|
|
1060
|
+
const whole = nanos / 1000000000n;
|
|
1061
|
+
const fraction = (nanos % 1000000000n).toString().padStart(9, "0").replace(/0+$/, "");
|
|
1062
|
+
return `$${whole}${fraction ? `.${fraction}` : ""}`;
|
|
1063
|
+
}
|
|
1064
|
+
function modelKind(value) {
|
|
1065
|
+
const kind = MODEL_KINDS.find((candidate) => candidate === value);
|
|
1066
|
+
if (!kind)
|
|
1067
|
+
fail(`Unknown model kind '${value}'. Use one of: ${MODEL_KINDS.join(", ")}.`);
|
|
1068
|
+
return kind;
|
|
1069
|
+
}
|
|
1070
|
+
function catalogEntries(catalog) {
|
|
1071
|
+
return [
|
|
1072
|
+
...catalog.language.map((entry) => ({ kind: "language", entry })),
|
|
1073
|
+
...catalog.image.map((entry) => ({ kind: "image", entry })),
|
|
1074
|
+
...catalog.audio.speech.map((entry) => ({ kind: "speech", entry })),
|
|
1075
|
+
...catalog.audio.transcription.map((entry) => ({ kind: "transcription", entry }))
|
|
1076
|
+
];
|
|
1077
|
+
}
|
|
1078
|
+
function findModel(entries, reference, kind) {
|
|
1079
|
+
const ofKind = entries.filter((candidate) => kind === undefined || candidate.kind === kind);
|
|
1080
|
+
let matches = ofKind.filter((candidate) => candidate.entry.modelId === reference);
|
|
1081
|
+
const separator = reference.indexOf(":");
|
|
1082
|
+
if (matches.length === 0 && separator > 0) {
|
|
1083
|
+
const provider = reference.slice(0, separator);
|
|
1084
|
+
const modelId = reference.slice(separator + 1);
|
|
1085
|
+
matches = ofKind.filter((candidate) => candidate.entry.provider === provider && candidate.entry.modelId === modelId);
|
|
1086
|
+
}
|
|
1087
|
+
const [match] = matches;
|
|
1088
|
+
if (!match) {
|
|
1089
|
+
fail(`No ${kind === undefined ? "" : `${kind} `}model '${reference}' is configured. Run 'sixb models list'.`);
|
|
1090
|
+
}
|
|
1091
|
+
if (matches.length > 1) {
|
|
1092
|
+
const kinds = new Set(matches.map((candidate) => candidate.kind));
|
|
1093
|
+
fail(kinds.size > 1 ? `'${reference}' is configured as ${[...kinds].join(" and ")}. Add --kind <kind>.` : `'${reference}' is configured by ${matches.map((candidate) => candidate.entry.provider).join(" and ")}. Use provider:${reference}.`);
|
|
1094
|
+
}
|
|
1095
|
+
return match;
|
|
1096
|
+
}
|
|
1097
|
+
function describePrice(card) {
|
|
1098
|
+
if (!card)
|
|
1099
|
+
return "price unknown";
|
|
1100
|
+
const parts = [
|
|
1101
|
+
variantPrice(card.images, "per image", "at"),
|
|
1102
|
+
variantPrice(card.characters, "per 1M characters", "for"),
|
|
1103
|
+
variantPrice(card.audioInputSeconds, "per second of audio", "for"),
|
|
1104
|
+
card.input !== undefined && card.output !== undefined ? `${dollars(tokenPrice(card.input))} input · ${dollars(tokenPrice(card.output))} output per 1M tokens` : undefined,
|
|
1105
|
+
card.audioOutput === undefined ? undefined : `${dollars(tokenPrice(card.audioOutput))} per 1M audio tokens`
|
|
1106
|
+
].filter((part) => part !== undefined);
|
|
1107
|
+
return parts.length === 0 ? "price unknown" : parts.join(" · ");
|
|
1108
|
+
}
|
|
1109
|
+
function variantPrice(price, unit, joiner) {
|
|
1110
|
+
if (price === undefined)
|
|
1111
|
+
return;
|
|
1112
|
+
if (typeof price === "string")
|
|
1113
|
+
return `${dollars(price)} ${unit}`;
|
|
1114
|
+
const only = price.default;
|
|
1115
|
+
if (only !== undefined && price.variants.every((variant) => variant.price === only)) {
|
|
1116
|
+
return `${dollars(only)} ${unit}`;
|
|
1117
|
+
}
|
|
1118
|
+
const variants = price.variants.map(({ price: amount, ...dimensions }) => {
|
|
1119
|
+
const label = Object.values(dimensions).join(" ");
|
|
1120
|
+
return `${dollars(amount)} ${joiner} ${label}`;
|
|
1121
|
+
});
|
|
1122
|
+
const otherwise = price.default === undefined ? [] : [`otherwise ${dollars(price.default)}`];
|
|
1123
|
+
return `${unit}: ${[...variants, ...otherwise].join(", ")}`;
|
|
1124
|
+
}
|
|
1125
|
+
function tokenPrice(price) {
|
|
1126
|
+
return typeof price === "string" ? price : price.default;
|
|
1127
|
+
}
|
|
1128
|
+
function dollars(amount) {
|
|
1129
|
+
return `$${amount}`;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// src/graph.ts
|
|
1133
|
+
async function inspectGraph(api, objectTypeId, primaryId, options) {
|
|
1134
|
+
if (options.depth === 0)
|
|
1135
|
+
return inspectRoot(api, objectTypeId, primaryId, options);
|
|
1136
|
+
let refs = [{ objectTypeId, primaryId, distance: 0 }];
|
|
1137
|
+
let frontier = refs;
|
|
1138
|
+
const objects = new Map;
|
|
1139
|
+
const links = new Map;
|
|
1140
|
+
let objectsTruncated = false;
|
|
1141
|
+
let linksTruncated = false;
|
|
1142
|
+
let pagesTruncated = false;
|
|
1143
|
+
let pagesRead = 0;
|
|
1144
|
+
let linksExamined = 0;
|
|
1145
|
+
for (let level = 0;level < options.depth && frontier.length > 0; level += 1) {
|
|
1146
|
+
const levelObjects = new Map;
|
|
1147
|
+
const levelLinks = new Map;
|
|
1148
|
+
const seenPageTokens = new Set;
|
|
1149
|
+
let pageToken;
|
|
1150
|
+
let continuePaging = true;
|
|
1151
|
+
while (continuePaging) {
|
|
1152
|
+
const remainingLinks = options.maxLinks - linksExamined;
|
|
1153
|
+
if (remainingLinks <= 0) {
|
|
1154
|
+
linksTruncated = true;
|
|
1155
|
+
break;
|
|
1156
|
+
}
|
|
1157
|
+
if (pagesRead >= CLI_LIMITS.inspect.maximumPages) {
|
|
1158
|
+
pagesTruncated = true;
|
|
1159
|
+
break;
|
|
1160
|
+
}
|
|
1161
|
+
const response = asLinksResponse(await api.post("/api/objects/query/links", {
|
|
1162
|
+
query: {
|
|
1163
|
+
kind: "refs",
|
|
1164
|
+
refs: frontier.map(({ objectTypeId: type, primaryId: id }) => ({
|
|
1165
|
+
objectTypeId: type,
|
|
1166
|
+
primaryId: id
|
|
1167
|
+
}))
|
|
1168
|
+
},
|
|
1169
|
+
direction: "both",
|
|
1170
|
+
includeObjects: true,
|
|
1171
|
+
pageSize: Math.min(CLI_LIMITS.linkPage.default, remainingLinks),
|
|
1172
|
+
...pageToken ? { pageToken } : {}
|
|
1173
|
+
}));
|
|
1174
|
+
pagesRead += 1;
|
|
1175
|
+
const pageLinks = response.links.slice(0, remainingLinks);
|
|
1176
|
+
linksExamined += pageLinks.length;
|
|
1177
|
+
linksTruncated ||= response.links.length > pageLinks.length;
|
|
1178
|
+
const relevantObjects = new Set(frontier.map(refKey));
|
|
1179
|
+
for (const link of pageLinks) {
|
|
1180
|
+
relevantObjects.add(refKey(link.source));
|
|
1181
|
+
relevantObjects.add(refKey(link.target));
|
|
1182
|
+
}
|
|
1183
|
+
for (const object of response.objects) {
|
|
1184
|
+
if (relevantObjects.has(refKey(object)))
|
|
1185
|
+
levelObjects.set(refKey(object), object);
|
|
1186
|
+
}
|
|
1187
|
+
for (const link of pageLinks) {
|
|
1188
|
+
const physical = {
|
|
1189
|
+
sourceTypeId: link.source.objectTypeId,
|
|
1190
|
+
sourceId: link.source.primaryId,
|
|
1191
|
+
linkId: link.linkId,
|
|
1192
|
+
targetTypeId: link.target.objectTypeId,
|
|
1193
|
+
targetId: link.target.primaryId,
|
|
1194
|
+
...Object.hasOwn(link, "properties") ? { properties: link.properties } : {}
|
|
1195
|
+
};
|
|
1196
|
+
levelLinks.set(linkKey(physical), physical);
|
|
1197
|
+
}
|
|
1198
|
+
if (!response.hasMore) {
|
|
1199
|
+
continuePaging = false;
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
if (linksExamined >= options.maxLinks) {
|
|
1203
|
+
linksTruncated = true;
|
|
1204
|
+
break;
|
|
1205
|
+
}
|
|
1206
|
+
if (pagesRead >= CLI_LIMITS.inspect.maximumPages) {
|
|
1207
|
+
pagesTruncated = true;
|
|
1208
|
+
break;
|
|
1209
|
+
}
|
|
1210
|
+
if (!response.nextPageToken) {
|
|
1211
|
+
invalidApiResponse("The object-links API reported another page without a nextPageToken.");
|
|
1212
|
+
}
|
|
1213
|
+
if (seenPageTokens.has(response.nextPageToken)) {
|
|
1214
|
+
invalidApiResponse("The object-links API repeated a nextPageToken while inspecting the graph.");
|
|
1215
|
+
}
|
|
1216
|
+
seenPageTokens.add(response.nextPageToken);
|
|
1217
|
+
pageToken = response.nextPageToken;
|
|
1218
|
+
}
|
|
1219
|
+
for (const [key, object] of levelObjects)
|
|
1220
|
+
objects.set(key, object);
|
|
1221
|
+
for (const [key, link] of levelLinks)
|
|
1222
|
+
links.set(key, link);
|
|
1223
|
+
const frontierKeys = new Set(frontier.map(refKey));
|
|
1224
|
+
const candidates = [];
|
|
1225
|
+
for (const link of levelLinks.values()) {
|
|
1226
|
+
const source = { objectTypeId: link.sourceTypeId, primaryId: link.sourceId };
|
|
1227
|
+
const target = { objectTypeId: link.targetTypeId, primaryId: link.targetId };
|
|
1228
|
+
if (frontierKeys.has(refKey(source)))
|
|
1229
|
+
candidates.push({ ...target, distance: level + 1 });
|
|
1230
|
+
else if (frontierKeys.has(refKey(target)))
|
|
1231
|
+
candidates.push({ ...source, distance: level + 1 });
|
|
1232
|
+
}
|
|
1233
|
+
const seen = new Set(refs.map(refKey));
|
|
1234
|
+
const all = dedupeRefs([...refs, ...candidates]);
|
|
1235
|
+
objectsTruncated ||= all.length > options.maxObjects;
|
|
1236
|
+
refs = all.slice(0, options.maxObjects);
|
|
1237
|
+
frontier = linksTruncated || pagesTruncated ? [] : refs.filter((ref) => !seen.has(refKey(ref)));
|
|
1238
|
+
}
|
|
1239
|
+
const kept = new Set(refs.map(refKey));
|
|
1240
|
+
const filteredLinks = [...links.values()].filter((link) => kept.has(refKey({ objectTypeId: link.sourceTypeId, primaryId: link.sourceId })) && kept.has(refKey({ objectTypeId: link.targetTypeId, primaryId: link.targetId }))).sort(compareLinks).map((link) => link.properties == null ? {
|
|
1241
|
+
sourceTypeId: link.sourceTypeId,
|
|
1242
|
+
sourceId: link.sourceId,
|
|
1243
|
+
linkId: link.linkId,
|
|
1244
|
+
targetTypeId: link.targetTypeId,
|
|
1245
|
+
targetId: link.targetId
|
|
1246
|
+
} : link);
|
|
1247
|
+
const root = objects.get(refKey({ objectTypeId, primaryId }));
|
|
1248
|
+
if (!root)
|
|
1249
|
+
objectNotFound(objectTypeId, primaryId);
|
|
1250
|
+
const relatedObjects = refs.filter((ref) => ref.distance > 0).flatMap((ref) => {
|
|
1251
|
+
const object = objects.get(refKey(ref));
|
|
1252
|
+
return object ? [{ ...object, distance: ref.distance }] : [];
|
|
1253
|
+
});
|
|
1254
|
+
const objectTypes = options.full ? await Promise.all([...new Set(refs.map((ref) => ref.objectTypeId))].sort((left, right) => left.localeCompare(right)).map((typeId) => api.get(`/api/object-types/${encodeURIComponent(typeId)}`))) : undefined;
|
|
1255
|
+
return {
|
|
1256
|
+
object: options.full ? root : compactObject(root),
|
|
1257
|
+
relatedObjects: options.full ? relatedObjects : relatedObjects.map(compactObject),
|
|
1258
|
+
links: filteredLinks,
|
|
1259
|
+
graph: {
|
|
1260
|
+
depth: options.depth,
|
|
1261
|
+
maxObjects: options.maxObjects,
|
|
1262
|
+
maxLinks: options.maxLinks,
|
|
1263
|
+
maxPages: CLI_LIMITS.inspect.maximumPages,
|
|
1264
|
+
objectCount: 1 + relatedObjects.length,
|
|
1265
|
+
linkCount: filteredLinks.length,
|
|
1266
|
+
pagesRead,
|
|
1267
|
+
linksExamined,
|
|
1268
|
+
truncated: objectsTruncated || linksTruncated || pagesTruncated,
|
|
1269
|
+
truncation: {
|
|
1270
|
+
objects: objectsTruncated,
|
|
1271
|
+
links: linksTruncated,
|
|
1272
|
+
pages: pagesTruncated
|
|
1273
|
+
}
|
|
1274
|
+
},
|
|
1275
|
+
...objectTypes ? { objectTypes } : {}
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
async function inspectRoot(api, objectTypeId, primaryId, options) {
|
|
1279
|
+
const response = await api.post("/api/objects/query", {
|
|
1280
|
+
query: { kind: "refs", refs: [{ objectTypeId, primaryId }] },
|
|
1281
|
+
includeTotal: false
|
|
1282
|
+
});
|
|
1283
|
+
if (typeof response !== "object" || response === null || Array.isArray(response)) {
|
|
1284
|
+
invalidApiResponse("The object query API returned an invalid response.");
|
|
1285
|
+
}
|
|
1286
|
+
const rows = response.objects;
|
|
1287
|
+
if (!Array.isArray(rows))
|
|
1288
|
+
invalidApiResponse("The object query API returned an invalid response.");
|
|
1289
|
+
const root = rows.find((value) => isMaterializedObject(value) && value.objectTypeId === objectTypeId && value.primaryId === primaryId);
|
|
1290
|
+
if (!root)
|
|
1291
|
+
objectNotFound(objectTypeId, primaryId);
|
|
1292
|
+
const objectTypes = options.full ? [await api.get(`/api/object-types/${encodeURIComponent(objectTypeId)}`)] : undefined;
|
|
1293
|
+
return {
|
|
1294
|
+
object: options.full ? root : compactObject(root),
|
|
1295
|
+
relatedObjects: [],
|
|
1296
|
+
links: [],
|
|
1297
|
+
graph: {
|
|
1298
|
+
depth: 0,
|
|
1299
|
+
maxObjects: options.maxObjects,
|
|
1300
|
+
maxLinks: options.maxLinks,
|
|
1301
|
+
maxPages: CLI_LIMITS.inspect.maximumPages,
|
|
1302
|
+
objectCount: 1,
|
|
1303
|
+
linkCount: 0,
|
|
1304
|
+
pagesRead: 0,
|
|
1305
|
+
linksExamined: 0,
|
|
1306
|
+
truncated: false,
|
|
1307
|
+
truncation: { objects: false, links: false, pages: false }
|
|
1308
|
+
},
|
|
1309
|
+
...objectTypes ? { objectTypes } : {}
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
function asLinksResponse(value) {
|
|
1313
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
1314
|
+
invalidLinksResponse();
|
|
1315
|
+
const record = value;
|
|
1316
|
+
if (!Array.isArray(record.objects) || !Array.isArray(record.links))
|
|
1317
|
+
invalidLinksResponse();
|
|
1318
|
+
if (typeof record.hasMore !== "boolean")
|
|
1319
|
+
invalidLinksResponse();
|
|
1320
|
+
if (record.nextPageToken !== undefined && typeof record.nextPageToken !== "string") {
|
|
1321
|
+
invalidLinksResponse();
|
|
1322
|
+
}
|
|
1323
|
+
if (!record.objects.every(isMaterializedObject) || !record.links.every(isPhysicalLinkResponse)) {
|
|
1324
|
+
invalidLinksResponse();
|
|
1325
|
+
}
|
|
1326
|
+
return record;
|
|
1327
|
+
}
|
|
1328
|
+
function invalidLinksResponse() {
|
|
1329
|
+
invalidApiResponse("The object-links API returned an invalid response.");
|
|
1330
|
+
}
|
|
1331
|
+
function invalidApiResponse(message) {
|
|
1332
|
+
throw new CliError({ code: "invalid_api_response", message }, EXIT_API);
|
|
1333
|
+
}
|
|
1334
|
+
function objectNotFound(objectTypeId, primaryId) {
|
|
1335
|
+
throw new CliError({ code: "not_found", message: `Object '${objectTypeId}/${primaryId}' was not found.` }, EXIT_API);
|
|
1336
|
+
}
|
|
1337
|
+
function isMaterializedObject(value) {
|
|
1338
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
1339
|
+
return false;
|
|
1340
|
+
const record = value;
|
|
1341
|
+
return typeof record.objectTypeId === "string" && typeof record.primaryId === "string";
|
|
1342
|
+
}
|
|
1343
|
+
function isPhysicalLinkResponse(value) {
|
|
1344
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
1345
|
+
return false;
|
|
1346
|
+
const record = value;
|
|
1347
|
+
return typeof record.linkId === "string" && isObjectRef(record.source) && isObjectRef(record.target);
|
|
1348
|
+
}
|
|
1349
|
+
function isObjectRef(value) {
|
|
1350
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
1351
|
+
return false;
|
|
1352
|
+
const record = value;
|
|
1353
|
+
return typeof record.objectTypeId === "string" && typeof record.primaryId === "string";
|
|
1354
|
+
}
|
|
1355
|
+
function compactObject(object) {
|
|
1356
|
+
const { createdAt: _createdAt, updatedAt: _updatedAt, ...compact } = object;
|
|
1357
|
+
return compact;
|
|
1358
|
+
}
|
|
1359
|
+
function dedupeRefs(values) {
|
|
1360
|
+
const unique = new Map;
|
|
1361
|
+
for (const value of values) {
|
|
1362
|
+
const key = refKey(value);
|
|
1363
|
+
const current = unique.get(key);
|
|
1364
|
+
if (!current || value.distance < current.distance)
|
|
1365
|
+
unique.set(key, value);
|
|
1366
|
+
}
|
|
1367
|
+
return [...unique.values()].sort((left, right) => left.distance - right.distance || left.objectTypeId.localeCompare(right.objectTypeId) || left.primaryId.localeCompare(right.primaryId));
|
|
1368
|
+
}
|
|
1369
|
+
function refKey(ref) {
|
|
1370
|
+
return JSON.stringify([ref.objectTypeId, ref.primaryId]);
|
|
1371
|
+
}
|
|
1372
|
+
function linkKey(link) {
|
|
1373
|
+
return JSON.stringify([
|
|
1374
|
+
link.sourceTypeId,
|
|
1375
|
+
link.sourceId,
|
|
1376
|
+
link.linkId,
|
|
1377
|
+
link.targetTypeId,
|
|
1378
|
+
link.targetId
|
|
1379
|
+
]);
|
|
1380
|
+
}
|
|
1381
|
+
function compareLinks(left, right) {
|
|
1382
|
+
return linkKey(left).localeCompare(linkKey(right));
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// src/commands/objects.ts
|
|
1386
|
+
async function objects(api, args) {
|
|
1387
|
+
const [sub, ...rest] = args;
|
|
1388
|
+
if (!sub || isHelp(sub))
|
|
1389
|
+
return writeText(OBJECTS_HELP);
|
|
1390
|
+
switch (sub) {
|
|
1391
|
+
case "inspect":
|
|
1392
|
+
return objectsInspect(api, rest);
|
|
1393
|
+
case "list":
|
|
1394
|
+
return objectsList(api, rest);
|
|
1395
|
+
case "get":
|
|
1396
|
+
return objectsGet(api, rest);
|
|
1397
|
+
case "get-by":
|
|
1398
|
+
return objectsGetBy(api, rest);
|
|
1399
|
+
case "search":
|
|
1400
|
+
return objectsSearch(api, rest);
|
|
1401
|
+
case "query":
|
|
1402
|
+
return objectsQuery(api, rest);
|
|
1403
|
+
case "count":
|
|
1404
|
+
case "exists":
|
|
1405
|
+
return objectsScalar(api, sub, rest);
|
|
1406
|
+
case "facets":
|
|
1407
|
+
return objectsFacets(api, rest);
|
|
1408
|
+
case "links":
|
|
1409
|
+
return objectsLinks(api, rest);
|
|
1410
|
+
default:
|
|
1411
|
+
fail(`Unknown objects command '${sub}'.`);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
async function objectsInspect(api, args) {
|
|
1415
|
+
if (requestsHelp(args))
|
|
1416
|
+
return writeText(OBJECTS_HELP);
|
|
1417
|
+
const { positionals, options } = parseCommandArgs(args, {
|
|
1418
|
+
"--full": "boolean",
|
|
1419
|
+
"--depth": "string",
|
|
1420
|
+
"--max-objects": "string",
|
|
1421
|
+
"--max-links": "string"
|
|
1422
|
+
}, "objects inspect", 2);
|
|
1423
|
+
const objectTypeId = positionals[0] ?? "";
|
|
1424
|
+
const primaryId = positionals[1] ?? "";
|
|
1425
|
+
const depth = integerInRange("--depth", options["--depth"] ?? String(CLI_LIMITS.inspect.depth.default), 0, CLI_LIMITS.inspect.depth.maximum);
|
|
1426
|
+
const maxObjects = integerInRange("--max-objects", options["--max-objects"] ?? String(CLI_LIMITS.inspect.objects.default), 1, CLI_LIMITS.inspect.objects.maximum);
|
|
1427
|
+
const maxLinks = integerInRange("--max-links", options["--max-links"] ?? String(CLI_LIMITS.inspect.links.default), 1, CLI_LIMITS.inspect.links.maximum);
|
|
1428
|
+
const full = options["--full"] ?? false;
|
|
1429
|
+
writeJson(await inspectGraph(api, objectTypeId, primaryId, {
|
|
1430
|
+
depth,
|
|
1431
|
+
maxObjects,
|
|
1432
|
+
maxLinks,
|
|
1433
|
+
full
|
|
1434
|
+
}));
|
|
1435
|
+
}
|
|
1436
|
+
async function objectsList(api, args) {
|
|
1437
|
+
if (requestsHelp(args))
|
|
1438
|
+
return writeText(OBJECTS_HELP);
|
|
1439
|
+
const optionNames = {
|
|
1440
|
+
"--type": "objectTypeId",
|
|
1441
|
+
"--limit": "limit",
|
|
1442
|
+
"--offset": "offset",
|
|
1443
|
+
"--order-by": "orderBy",
|
|
1444
|
+
"--order": "order",
|
|
1445
|
+
"--id-prefix": "idPrefix",
|
|
1446
|
+
"--id-suffix": "idSuffix",
|
|
1447
|
+
"--created-after": "createdAfter",
|
|
1448
|
+
"--created-before": "createdBefore",
|
|
1449
|
+
"--updated-after": "updatedAfter",
|
|
1450
|
+
"--updated-before": "updatedBefore"
|
|
1451
|
+
};
|
|
1452
|
+
const options = normalizeWindowOptions(parseQueryOptions(args, optionNames, "objects list"), {
|
|
1453
|
+
defaultLimit: CLI_LIMITS.list.default,
|
|
1454
|
+
maximumLimit: CLI_LIMITS.list.maximum,
|
|
1455
|
+
defaultOrder: DEFAULT_LIST_ORDER,
|
|
1456
|
+
offset: true
|
|
1457
|
+
});
|
|
1458
|
+
options.orderBy = enumValue("--order-by", options.orderBy ?? DEFAULT_OBJECT_ORDER_BY, [
|
|
1459
|
+
"createdAt",
|
|
1460
|
+
"updatedAt",
|
|
1461
|
+
"primaryId"
|
|
1462
|
+
]);
|
|
1463
|
+
for (const [name, flag] of [
|
|
1464
|
+
["createdAfter", "--created-after"],
|
|
1465
|
+
["createdBefore", "--created-before"],
|
|
1466
|
+
["updatedAfter", "--updated-after"],
|
|
1467
|
+
["updatedBefore", "--updated-before"]
|
|
1468
|
+
]) {
|
|
1469
|
+
if (options[name] !== undefined)
|
|
1470
|
+
options[name] = rfc3339Value(flag, options[name]);
|
|
1471
|
+
}
|
|
1472
|
+
requireOrderedRange("--created-after", options.createdAfter, "--created-before", options.createdBefore);
|
|
1473
|
+
requireOrderedRange("--updated-after", options.updatedAfter, "--updated-before", options.updatedBefore);
|
|
1474
|
+
writeJson(await api.get("/api/objects", options));
|
|
1475
|
+
}
|
|
1476
|
+
async function objectsGet(api, args) {
|
|
1477
|
+
if (requestsHelp(args))
|
|
1478
|
+
return writeText("Usage: sixb objects get <object-type> <primary-id>...");
|
|
1479
|
+
const { positionals } = parseCommandArgs(args, {}, "objects get", [2, Number.POSITIVE_INFINITY]);
|
|
1480
|
+
const objectTypeId = positionals[0] ?? "";
|
|
1481
|
+
writeJson(await api.post("/api/objects/query", {
|
|
1482
|
+
query: {
|
|
1483
|
+
kind: "refs",
|
|
1484
|
+
refs: positionals.slice(1).map((primaryId) => ({ objectTypeId, primaryId }))
|
|
1485
|
+
},
|
|
1486
|
+
includeTotal: false
|
|
1487
|
+
}));
|
|
1488
|
+
}
|
|
1489
|
+
async function objectsGetBy(api, args) {
|
|
1490
|
+
if (requestsHelp(args))
|
|
1491
|
+
return writeText(GET_BY_HELP);
|
|
1492
|
+
const { positionals, options } = parseCommandArgs(args, { "--json": "boolean" }, "objects get-by", 3);
|
|
1493
|
+
const [objectTypeId = "", propertyId = "", text = ""] = positionals;
|
|
1494
|
+
const value = options["--json"] ? parseJsonValue(text) : text;
|
|
1495
|
+
const path = `/api/objects/${encodeURIComponent(objectTypeId)}/by/${encodeURIComponent(propertyId)}`;
|
|
1496
|
+
try {
|
|
1497
|
+
writeJson(await api.post(path, { value }));
|
|
1498
|
+
} catch (error) {
|
|
1499
|
+
if (!isObjectNotFound(error))
|
|
1500
|
+
throw error;
|
|
1501
|
+
writeJson(null);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
function parseJsonValue(text) {
|
|
1505
|
+
try {
|
|
1506
|
+
return JSON.parse(text);
|
|
1507
|
+
} catch {
|
|
1508
|
+
fail(`--json needs the value as JSON, such as 42 or '{"type":"user","id":"usr_1"}'.`);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
function isObjectNotFound(error) {
|
|
1512
|
+
return error instanceof CliError && error.body.status === 404 && error.body.code === "object.not_found";
|
|
1513
|
+
}
|
|
1514
|
+
async function objectsSearch(api, args) {
|
|
1515
|
+
if (requestsHelp(args))
|
|
1516
|
+
return writeText(SEARCH_HELP);
|
|
1517
|
+
const { positionals, options } = parseCommandArgs(args, { "--limit": "string", "--type": "string", "--vector-profile": "string" }, "objects search", 1);
|
|
1518
|
+
const text = positionals[0] ?? "";
|
|
1519
|
+
const limit = integerInRange("--limit", options["--limit"] ?? String(CLI_LIMITS.search.default), 1, CLI_LIMITS.search.maximum);
|
|
1520
|
+
const objectTypeId = options["--type"];
|
|
1521
|
+
if (!objectTypeId) {
|
|
1522
|
+
if (options["--vector-profile"])
|
|
1523
|
+
fail("--vector-profile requires --type.");
|
|
1524
|
+
return writeJson(await api.get("/api/objects/search", { q: text, limit: String(limit) }));
|
|
1525
|
+
}
|
|
1526
|
+
if (!text.trim() || text.length > CLI_LIMITS.vectorSearchText.maximum) {
|
|
1527
|
+
fail(`Semantic search text must be nonempty and at most ${CLI_LIMITS.vectorSearchText.maximum} characters.`);
|
|
1528
|
+
}
|
|
1529
|
+
const objectType = asRecord(await api.get(`/api/object-types/${encodeURIComponent(objectTypeId)}`));
|
|
1530
|
+
const profile = selectVectorProfile(objectTypeId, objectType, options["--vector-profile"]);
|
|
1531
|
+
writeJson(await api.post("/api/objects/query", {
|
|
1532
|
+
query: {
|
|
1533
|
+
kind: "vector",
|
|
1534
|
+
input: { kind: "start", objectTypeId },
|
|
1535
|
+
vector: text,
|
|
1536
|
+
profile,
|
|
1537
|
+
k: limit
|
|
1538
|
+
},
|
|
1539
|
+
includeTotal: false
|
|
1540
|
+
}));
|
|
1541
|
+
}
|
|
1542
|
+
function selectVectorProfile(objectTypeId, objectType, requested) {
|
|
1543
|
+
const profiles = vectorProfileNames(objectType);
|
|
1544
|
+
if (profiles.length === 0) {
|
|
1545
|
+
fail(`${objectTypeId} has no vector search profile.`, "invalid_arguments", "Omit --type for identifier and full-text search.");
|
|
1546
|
+
}
|
|
1547
|
+
if (requested !== undefined) {
|
|
1548
|
+
if (!profiles.includes(requested)) {
|
|
1549
|
+
fail(`${objectTypeId} has no vector profile '${requested}'. Use ${profiles.join(", ")}.`);
|
|
1550
|
+
}
|
|
1551
|
+
return requested;
|
|
1552
|
+
}
|
|
1553
|
+
if (profiles.length > 1) {
|
|
1554
|
+
fail(`${objectTypeId} has vector profiles ${profiles.join(", ")}. Pass --vector-profile <name>.`);
|
|
1555
|
+
}
|
|
1556
|
+
return profiles[0] ?? "";
|
|
1557
|
+
}
|
|
1558
|
+
async function objectsQuery(api, args) {
|
|
1559
|
+
if (requestsHelp(args))
|
|
1560
|
+
return writeText(QUERY_HELP);
|
|
1561
|
+
const { options } = parseCommandArgs(args, {
|
|
1562
|
+
"--example": "string",
|
|
1563
|
+
"--file": "string",
|
|
1564
|
+
"--include-total": "boolean",
|
|
1565
|
+
"--no-total": "boolean"
|
|
1566
|
+
}, "objects query");
|
|
1567
|
+
if (options["--include-total"] && options["--no-total"])
|
|
1568
|
+
fail("--include-total and --no-total cannot be used together.");
|
|
1569
|
+
if (options["--example"]) {
|
|
1570
|
+
if (Object.keys(options).length !== 1)
|
|
1571
|
+
fail("--example cannot be combined with query options.");
|
|
1572
|
+
const name = options["--example"];
|
|
1573
|
+
if (name === "list")
|
|
1574
|
+
return writeText(Object.keys(QUERY_EXAMPLES).join(" "));
|
|
1575
|
+
const example = QUERY_EXAMPLES[name];
|
|
1576
|
+
if (!example)
|
|
1577
|
+
fail(`Unknown query example '${name}'. Run 'sixb objects query --example list'.`);
|
|
1578
|
+
return writeText(example);
|
|
1579
|
+
}
|
|
1580
|
+
const source = options["--file"];
|
|
1581
|
+
const includeTotal = options["--include-total"] ?? false;
|
|
1582
|
+
if (!source)
|
|
1583
|
+
fail("objects query requires --file <path|->.");
|
|
1584
|
+
const input = await readJson(source);
|
|
1585
|
+
const record = asRecord(input);
|
|
1586
|
+
const body = Object.hasOwn(record, "query") ? { ...record, ...Object.hasOwn(record, "includeTotal") ? {} : { includeTotal } } : { query: input, includeTotal };
|
|
1587
|
+
writeJson(await api.post("/api/objects/query", body));
|
|
1588
|
+
}
|
|
1589
|
+
async function objectsScalar(api, operation, args) {
|
|
1590
|
+
if (requestsHelp(args))
|
|
1591
|
+
return writeText(`Usage: sixb objects ${operation} --file <path|->`);
|
|
1592
|
+
const source = singleFileOption(args, `objects ${operation}`);
|
|
1593
|
+
const input = await readJson(source);
|
|
1594
|
+
const record = asRecord(input);
|
|
1595
|
+
writeJson(await api.post(`/api/objects/query/${operation}`, {
|
|
1596
|
+
query: Object.hasOwn(record, "query") ? record.query : input
|
|
1597
|
+
}));
|
|
1598
|
+
}
|
|
1599
|
+
async function objectsFacets(api, args) {
|
|
1600
|
+
if (requestsHelp(args)) {
|
|
1601
|
+
return writeText(`Usage: sixb objects facets --file <path|->
|
|
1602
|
+
sixb objects facets --example`);
|
|
1603
|
+
}
|
|
1604
|
+
const { options } = parseCommandArgs(args, { "--file": "string", "--example": "boolean" }, "objects facets");
|
|
1605
|
+
if (options["--example"]) {
|
|
1606
|
+
if (options["--file"])
|
|
1607
|
+
fail("--example cannot be combined with --file.");
|
|
1608
|
+
return writeText(FACETS_EXAMPLE);
|
|
1609
|
+
}
|
|
1610
|
+
if (!options["--file"])
|
|
1611
|
+
fail("objects facets requires --file <path|->.");
|
|
1612
|
+
const body = await readJson(options["--file"]);
|
|
1613
|
+
const record = asRecord(body);
|
|
1614
|
+
if (!Object.hasOwn(record, "query") || !Object.hasOwn(record, "facets")) {
|
|
1615
|
+
fail("objects facets input must contain query and facets.");
|
|
1616
|
+
}
|
|
1617
|
+
writeJson(await api.post("/api/objects/query/facets", body));
|
|
1618
|
+
}
|
|
1619
|
+
async function objectsLinks(api, args) {
|
|
1620
|
+
if (requestsHelp(args))
|
|
1621
|
+
return writeText(OBJECTS_HELP);
|
|
1622
|
+
const { positionals, options } = parseCommandArgs(args, {
|
|
1623
|
+
"--link": "string",
|
|
1624
|
+
"--direction": "string",
|
|
1625
|
+
"--page-size": "string",
|
|
1626
|
+
"--page-token": "string",
|
|
1627
|
+
"--include-objects": "boolean"
|
|
1628
|
+
}, "objects links", 2);
|
|
1629
|
+
const objectTypeId = positionals[0] ?? "";
|
|
1630
|
+
const primaryId = positionals[1] ?? "";
|
|
1631
|
+
const linkId = options["--link"];
|
|
1632
|
+
const direction = enumValue("--direction", options["--direction"] ?? "both", [
|
|
1633
|
+
"outgoing",
|
|
1634
|
+
"incoming",
|
|
1635
|
+
"both"
|
|
1636
|
+
]);
|
|
1637
|
+
const pageSize = integerInRange("--page-size", options["--page-size"] ?? String(CLI_LIMITS.linkPage.default), 1, CLI_LIMITS.linkPage.maximum);
|
|
1638
|
+
const pageToken = options["--page-token"];
|
|
1639
|
+
const includeObjects = options["--include-objects"] ?? false;
|
|
1640
|
+
writeJson(await api.post("/api/objects/query/links", {
|
|
1641
|
+
query: { kind: "refs", refs: [{ objectTypeId, primaryId }] },
|
|
1642
|
+
direction,
|
|
1643
|
+
includeObjects,
|
|
1644
|
+
pageSize,
|
|
1645
|
+
...linkId ? { linkId } : {},
|
|
1646
|
+
...pageToken ? { pageToken } : {}
|
|
1647
|
+
}));
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
// src/commands/ontology.ts
|
|
1651
|
+
async function ontology(api, args) {
|
|
1652
|
+
const [sub, ...rest] = args;
|
|
1653
|
+
if (!sub || isHelp(sub))
|
|
1654
|
+
return writeText(GROUP_HELP.ontology);
|
|
1655
|
+
if (sub === "list") {
|
|
1656
|
+
if (requestsHelp(rest))
|
|
1657
|
+
return writeText("Usage: sixb ontology list [--full]");
|
|
1658
|
+
const { options } = parseCommandArgs(rest, { "--full": "boolean" }, "ontology list");
|
|
1659
|
+
const full = options["--full"] ?? false;
|
|
1660
|
+
const value = await api.get("/api/object-types");
|
|
1661
|
+
if (full)
|
|
1662
|
+
return writeJson(value);
|
|
1663
|
+
if (!Array.isArray(value))
|
|
1664
|
+
fail("The ontology API returned an invalid response.");
|
|
1665
|
+
return writeJson(value.map((entry) => {
|
|
1666
|
+
const type = asRecord(entry);
|
|
1667
|
+
const properties = asRecords(type.properties);
|
|
1668
|
+
const vectorProfiles = vectorProfileNames(type);
|
|
1669
|
+
return {
|
|
1670
|
+
id: type.id,
|
|
1671
|
+
name: type.name,
|
|
1672
|
+
description: type.description,
|
|
1673
|
+
primaryPropertyId: properties.find((property) => property.primary === true)?.id,
|
|
1674
|
+
...vectorProfiles.length === 0 ? {} : { vectorProfiles },
|
|
1675
|
+
links: asRecords(type.links).map(({ id, name, description, targetObjectTypeId, cardinality }) => ({
|
|
1676
|
+
id,
|
|
1677
|
+
name,
|
|
1678
|
+
...description === undefined ? {} : { description },
|
|
1679
|
+
targetObjectTypeId,
|
|
1680
|
+
cardinality
|
|
1681
|
+
})),
|
|
1682
|
+
actions: asRecords(type.actions).map(({ id, name, description }) => ({
|
|
1683
|
+
id,
|
|
1684
|
+
name,
|
|
1685
|
+
...description === undefined ? {} : { description }
|
|
1686
|
+
}))
|
|
1687
|
+
};
|
|
1688
|
+
}));
|
|
1689
|
+
}
|
|
1690
|
+
if (sub === "get") {
|
|
1691
|
+
if (requestsHelp(rest))
|
|
1692
|
+
return writeText("Usage: sixb ontology get <object-type>");
|
|
1693
|
+
const { positionals } = parseCommandArgs(rest, {}, "ontology get", 1);
|
|
1694
|
+
return writeJson(await api.get(`/api/object-types/${idSegment(positionals[0])}`));
|
|
1695
|
+
}
|
|
1696
|
+
fail(`Unknown ontology command '${sub}'.`);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// src/commands/project.ts
|
|
1700
|
+
async function project(api, args) {
|
|
1701
|
+
if (!args[0] || isHelp(args[0]) || args[0] === "show" && requestsHelp(args.slice(1))) {
|
|
1702
|
+
return writeText(GROUP_HELP.project);
|
|
1703
|
+
}
|
|
1704
|
+
if (args[0] !== "show")
|
|
1705
|
+
fail(`Unknown project command '${args[0]}'.`);
|
|
1706
|
+
parseCommandArgs(args.slice(1), {}, "project show");
|
|
1707
|
+
writeJson(await api.get("/api/project"));
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
// src/commands/runs.ts
|
|
1711
|
+
var ACTION_RUN_STATUSES = ["succeeded", "failed"];
|
|
1712
|
+
var WORKFLOW_RUN_STATUSES = [
|
|
1713
|
+
"queued",
|
|
1714
|
+
"running",
|
|
1715
|
+
"waiting",
|
|
1716
|
+
"succeeded",
|
|
1717
|
+
"failed",
|
|
1718
|
+
"cancelled"
|
|
1719
|
+
];
|
|
1720
|
+
var DATA_RUN_STATUSES = ["queued", "running", "succeeded", "failed", "cancelled"];
|
|
1721
|
+
var STATUSES = {
|
|
1722
|
+
action: ACTION_RUN_STATUSES,
|
|
1723
|
+
workflow: WORKFLOW_RUN_STATUSES,
|
|
1724
|
+
sync: DATA_RUN_STATUSES,
|
|
1725
|
+
pipeline: DATA_RUN_STATUSES
|
|
1726
|
+
};
|
|
1727
|
+
var FILTERS = {
|
|
1728
|
+
action: { "--action": "actionId", "--type": "objectTypeId", "--id": "primaryId" },
|
|
1729
|
+
workflow: { "--workflow": "workflowId" },
|
|
1730
|
+
sync: { "--sync": "syncId", "--dataset": "datasetId" },
|
|
1731
|
+
pipeline: { "--pipeline": "pipelineId" }
|
|
1732
|
+
};
|
|
1733
|
+
async function runs(api, kind, args) {
|
|
1734
|
+
const [sub, ...rest] = args;
|
|
1735
|
+
const group = `${kind}-runs`;
|
|
1736
|
+
if (!sub || isHelp(sub) || requestsHelp(rest))
|
|
1737
|
+
return writeText(GROUP_HELP[group]);
|
|
1738
|
+
if (sub === "get" && kind !== "sync") {
|
|
1739
|
+
const { positionals } = parseCommandArgs(rest, {}, `${group} get`, 1);
|
|
1740
|
+
return writeJson(await api.get(`/api/${group}/${encodeURIComponent(positionals[0] ?? "")}`));
|
|
1741
|
+
}
|
|
1742
|
+
if (sub === "list") {
|
|
1743
|
+
const common = {
|
|
1744
|
+
"--status": "status",
|
|
1745
|
+
"--started-after": "startedAfter",
|
|
1746
|
+
"--started-before": "startedBefore",
|
|
1747
|
+
"--limit": "limit",
|
|
1748
|
+
"--offset": "offset",
|
|
1749
|
+
"--order": "order"
|
|
1750
|
+
};
|
|
1751
|
+
const options = normalizeWindowOptions(parseQueryOptions(rest, { ...common, ...FILTERS[kind] }, `${group} list`), {
|
|
1752
|
+
defaultLimit: CLI_LIMITS.list.default,
|
|
1753
|
+
maximumLimit: CLI_LIMITS.list.maximum,
|
|
1754
|
+
defaultOrder: DEFAULT_LIST_ORDER,
|
|
1755
|
+
offset: true
|
|
1756
|
+
});
|
|
1757
|
+
if (options.status !== undefined) {
|
|
1758
|
+
options.status = enumValue("--status", options.status, STATUSES[kind]);
|
|
1759
|
+
}
|
|
1760
|
+
for (const [name, flag] of [
|
|
1761
|
+
["startedAfter", "--started-after"],
|
|
1762
|
+
["startedBefore", "--started-before"]
|
|
1763
|
+
]) {
|
|
1764
|
+
if (options[name] !== undefined)
|
|
1765
|
+
options[name] = rfc3339Value(flag, options[name]);
|
|
1766
|
+
}
|
|
1767
|
+
requireOrderedRange("--started-after", options.startedAfter, "--started-before", options.startedBefore);
|
|
1768
|
+
return writeJson(await api.get(`/api/${group}`, options));
|
|
1769
|
+
}
|
|
1770
|
+
fail(`Unknown ${group} command '${sub}'.`);
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
// src/commands/telemetry.ts
|
|
1774
|
+
async function telemetry(api, args) {
|
|
1775
|
+
const [sub, ...rest] = args;
|
|
1776
|
+
if (!sub || isHelp(sub) || requestsHelp(rest))
|
|
1777
|
+
return writeText(GROUP_HELP.telemetry);
|
|
1778
|
+
if (sub === "latest") {
|
|
1779
|
+
const { positionals } = parseCommandArgs(rest, {}, "telemetry latest", 3);
|
|
1780
|
+
return writeJson(await api.get(telemetryPath(positionals, "latest")));
|
|
1781
|
+
}
|
|
1782
|
+
if (sub === "history") {
|
|
1783
|
+
const { positionals, options } = parseCommandArgs(rest, {
|
|
1784
|
+
"--from": "string",
|
|
1785
|
+
"--to": "string",
|
|
1786
|
+
"--limit": "string",
|
|
1787
|
+
"--order": "string"
|
|
1788
|
+
}, "telemetry history", 3);
|
|
1789
|
+
const query = normalizeWindowOptions({
|
|
1790
|
+
from: options["--from"],
|
|
1791
|
+
to: options["--to"],
|
|
1792
|
+
limit: options["--limit"],
|
|
1793
|
+
order: options["--order"]
|
|
1794
|
+
}, {
|
|
1795
|
+
defaultLimit: CLI_LIMITS.telemetryHistory.default,
|
|
1796
|
+
maximumLimit: CLI_LIMITS.telemetryHistory.maximum,
|
|
1797
|
+
defaultOrder: DEFAULT_TELEMETRY_ORDER
|
|
1798
|
+
});
|
|
1799
|
+
if (query.from !== undefined)
|
|
1800
|
+
query.from = rfc3339Value("--from", query.from);
|
|
1801
|
+
if (query.to !== undefined)
|
|
1802
|
+
query.to = rfc3339Value("--to", query.to);
|
|
1803
|
+
requireOrderedRange("--from", query.from, "--to", query.to);
|
|
1804
|
+
return writeJson(await api.get(telemetryPath(positionals, "history"), query));
|
|
1805
|
+
}
|
|
1806
|
+
if (sub === "query") {
|
|
1807
|
+
return writeJson(await api.post("/api/telemetry/history", normalizeTelemetryQueryInput(await readJson(singleFileOption(rest, "telemetry query")))));
|
|
1808
|
+
}
|
|
1809
|
+
fail(`Unknown telemetry command '${sub}'.`);
|
|
1810
|
+
}
|
|
1811
|
+
function telemetryPath(args, terminal) {
|
|
1812
|
+
return `/api/objects/${idSegment(args[0])}/${idSegment(args[1])}/telemetry/${idSegment(args[2])}/${terminal}`;
|
|
1813
|
+
}
|
|
1814
|
+
function normalizeTelemetryQueryInput(input) {
|
|
1815
|
+
if (Array.isArray(input) || typeof input !== "object" || input === null) {
|
|
1816
|
+
fail("Telemetry query input must be a JSON object.");
|
|
1817
|
+
}
|
|
1818
|
+
const body = { ...input };
|
|
1819
|
+
const rawLimit = body.limitPerSeries;
|
|
1820
|
+
if (rawLimit !== undefined && typeof rawLimit !== "number") {
|
|
1821
|
+
fail("limitPerSeries must be a number.");
|
|
1822
|
+
}
|
|
1823
|
+
body.limitPerSeries = integerInRange("limitPerSeries", String(rawLimit ?? CLI_LIMITS.telemetryHistory.default), 1, CLI_LIMITS.telemetryHistory.maximum);
|
|
1824
|
+
const rawOrder = body.order;
|
|
1825
|
+
if (rawOrder !== undefined && typeof rawOrder !== "string") {
|
|
1826
|
+
fail("order must be a string.");
|
|
1827
|
+
}
|
|
1828
|
+
body.order = enumValue("order", rawOrder ?? DEFAULT_TELEMETRY_ORDER, ["asc", "desc"]);
|
|
1829
|
+
for (const field of ["from", "to"]) {
|
|
1830
|
+
const value = body[field];
|
|
1831
|
+
if (value === undefined)
|
|
1832
|
+
continue;
|
|
1833
|
+
if (typeof value !== "string")
|
|
1834
|
+
fail(`${field} must be an RFC 3339 timestamp.`);
|
|
1835
|
+
body[field] = rfc3339Value(field, value);
|
|
1836
|
+
}
|
|
1837
|
+
requireOrderedRange("from", body.from, "to", body.to);
|
|
1838
|
+
return body;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
// src/commands/users.ts
|
|
1842
|
+
var USER_STATUSES = ["active", "suspended", "all"];
|
|
1843
|
+
var LIST_FLAGS = {
|
|
1844
|
+
"--status": "string",
|
|
1845
|
+
"--limit": "string",
|
|
1846
|
+
"--offset": "string"
|
|
1847
|
+
};
|
|
1848
|
+
async function users(api, args) {
|
|
1849
|
+
const [sub, ...rest] = args;
|
|
1850
|
+
if (!sub || isHelp(sub) || requestsHelp(rest))
|
|
1851
|
+
return writeText(GROUP_HELP.users);
|
|
1852
|
+
if (sub === "get") {
|
|
1853
|
+
const { positionals } = parseCommandArgs(rest, {}, "users get", 1);
|
|
1854
|
+
const user = positionals[0]?.trim();
|
|
1855
|
+
if (!user)
|
|
1856
|
+
fail("users get requires a user id or email.");
|
|
1857
|
+
if (!user.includes("@")) {
|
|
1858
|
+
return writeJson(await api.get(`/api/users/${idSegment(user)}`));
|
|
1859
|
+
}
|
|
1860
|
+
const page = await api.get("/api/users", { email: user });
|
|
1861
|
+
const [member] = isRecord(page) && Array.isArray(page.users) ? page.users : [];
|
|
1862
|
+
if (member === undefined) {
|
|
1863
|
+
throw new CliError({ code: "not_found", message: `No member has the email '${user}'.` }, EXIT_API);
|
|
1864
|
+
}
|
|
1865
|
+
return writeJson(member);
|
|
1866
|
+
}
|
|
1867
|
+
if (sub === "list") {
|
|
1868
|
+
const { options } = parseCommandArgs(rest, LIST_FLAGS, "users list");
|
|
1869
|
+
return writeJson(await api.get("/api/users", listQuery(options)));
|
|
1870
|
+
}
|
|
1871
|
+
if (sub === "search") {
|
|
1872
|
+
const { positionals, options } = parseCommandArgs(rest, LIST_FLAGS, "users search", 1);
|
|
1873
|
+
const search = positionals[0]?.trim();
|
|
1874
|
+
if (!search)
|
|
1875
|
+
fail("users search requires the text to find in a name or email.");
|
|
1876
|
+
return writeJson(await api.get("/api/users", { ...listQuery(options), search }));
|
|
1877
|
+
}
|
|
1878
|
+
fail(`Unknown users command '${sub}'.`);
|
|
1879
|
+
}
|
|
1880
|
+
function listQuery(options) {
|
|
1881
|
+
const limit = integerInRange("--limit", options["--limit"] ?? String(CLI_LIMITS.users.default), 1, CLI_LIMITS.users.maximum);
|
|
1882
|
+
return {
|
|
1883
|
+
status: enumValue("--status", options["--status"] ?? "active", USER_STATUSES),
|
|
1884
|
+
limit: String(limit),
|
|
1885
|
+
...options["--offset"] === undefined ? {} : { offset: String(nonNegativeInteger("--offset", options["--offset"])) }
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
function isRecord(value) {
|
|
1889
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
// src/commands/workflows.ts
|
|
1893
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1894
|
+
async function workflows(api, args) {
|
|
1895
|
+
const [sub, ...rest] = args;
|
|
1896
|
+
if (!sub || isHelp(sub) || requestsHelp(rest))
|
|
1897
|
+
return writeText(GROUP_HELP.workflows);
|
|
1898
|
+
if (sub === "list") {
|
|
1899
|
+
parseCommandArgs(rest, {}, "workflows list");
|
|
1900
|
+
return writeJson(await api.get("/api/workflows"));
|
|
1901
|
+
}
|
|
1902
|
+
if (sub === "get") {
|
|
1903
|
+
const {
|
|
1904
|
+
positionals: [workflowId]
|
|
1905
|
+
} = parseCommandArgs(rest, {}, "workflows get", 1);
|
|
1906
|
+
return writeJson(await api.get(`/api/workflows/${idSegment(workflowId)}`));
|
|
1907
|
+
}
|
|
1908
|
+
if (sub === "start") {
|
|
1909
|
+
const {
|
|
1910
|
+
positionals: [workflowId],
|
|
1911
|
+
options
|
|
1912
|
+
} = parseCommandArgs(rest, { "--file": "string", "--run-id": "string" }, "workflows start", 1);
|
|
1913
|
+
const input = options["--file"] ? await readJson(options["--file"]) : {};
|
|
1914
|
+
if (Array.isArray(input) || typeof input !== "object" || input === null) {
|
|
1915
|
+
fail("Workflow input must be a JSON object.");
|
|
1916
|
+
}
|
|
1917
|
+
const runId = options["--run-id"] ?? `run_${randomUUID2()}`;
|
|
1918
|
+
return writeRequestResult(await api.post(`/api/workflows/${idSegment(workflowId)}/runs`, {
|
|
1919
|
+
input,
|
|
1920
|
+
runId
|
|
1921
|
+
}));
|
|
1922
|
+
}
|
|
1923
|
+
fail(`Unknown workflows command '${sub}'.`);
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
// src/commands/index.ts
|
|
1927
|
+
var INSTANCE_COMMANDS = [
|
|
1928
|
+
"project",
|
|
1929
|
+
"ontology",
|
|
1930
|
+
"objects",
|
|
1931
|
+
"telemetry",
|
|
1932
|
+
"actions",
|
|
1933
|
+
"action-runs",
|
|
1934
|
+
"files",
|
|
1935
|
+
"workflows",
|
|
1936
|
+
"workflow-runs",
|
|
1937
|
+
"users",
|
|
1938
|
+
"models",
|
|
1939
|
+
"datasets",
|
|
1940
|
+
"syncs",
|
|
1941
|
+
"sync-runs",
|
|
1942
|
+
"pipelines",
|
|
1943
|
+
"pipeline-runs"
|
|
1944
|
+
];
|
|
1945
|
+
function isInstanceCommand(command) {
|
|
1946
|
+
return INSTANCE_COMMANDS.includes(command);
|
|
1947
|
+
}
|
|
1948
|
+
async function dispatch(api, command, args) {
|
|
1949
|
+
switch (command) {
|
|
1950
|
+
case "project":
|
|
1951
|
+
return project(api, args);
|
|
1952
|
+
case "ontology":
|
|
1953
|
+
return ontology(api, args);
|
|
1954
|
+
case "objects":
|
|
1955
|
+
return objects(api, args);
|
|
1956
|
+
case "telemetry":
|
|
1957
|
+
return telemetry(api, args);
|
|
1958
|
+
case "actions":
|
|
1959
|
+
return actions(api, args);
|
|
1960
|
+
case "action-runs":
|
|
1961
|
+
return runs(api, "action", args);
|
|
1962
|
+
case "files":
|
|
1963
|
+
return files(api, args);
|
|
1964
|
+
case "workflows":
|
|
1965
|
+
return workflows(api, args);
|
|
1966
|
+
case "workflow-runs":
|
|
1967
|
+
return runs(api, "workflow", args);
|
|
1968
|
+
case "users":
|
|
1969
|
+
return users(api, args);
|
|
1970
|
+
case "models":
|
|
1971
|
+
return models(api, args);
|
|
1972
|
+
case "datasets":
|
|
1973
|
+
return datasets(api, args);
|
|
1974
|
+
case "syncs":
|
|
1975
|
+
return runnables(api, "syncs", args);
|
|
1976
|
+
case "sync-runs":
|
|
1977
|
+
return runs(api, "sync", args);
|
|
1978
|
+
case "pipelines":
|
|
1979
|
+
return runnables(api, "pipelines", args);
|
|
1980
|
+
case "pipeline-runs":
|
|
1981
|
+
return runs(api, "pipeline", args);
|
|
1982
|
+
default:
|
|
1983
|
+
fail(`Unknown command '${command}'. Run 'sixb --help'.`);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
// src/commands/history.ts
|
|
1988
|
+
var PAGE_TEXT_CHARS = 4000;
|
|
1989
|
+
var PAGE_TOOL_CHARS = 1000;
|
|
1990
|
+
async function history(api, args) {
|
|
1991
|
+
if (isHelp(args[0]) || requestsHelp(args))
|
|
1992
|
+
return writeText(GROUP_HELP.history);
|
|
1993
|
+
const { positionals, options } = parseCommandArgs(args, { "--offset": "string", "--limit": "string" }, "history", [0, 1]);
|
|
1994
|
+
const threadId = process.env.SIXB_THREAD_ID;
|
|
1995
|
+
if (!threadId)
|
|
1996
|
+
fail("This run has no conversation to read: SIXB_THREAD_ID is not set.");
|
|
1997
|
+
const path = `/api/agent-threads/${idSegment(threadId)}/messages`;
|
|
1998
|
+
const seq = positionals[0];
|
|
1999
|
+
if (seq !== undefined) {
|
|
2000
|
+
const wanted = integerInRange("<seq>", seq, 1, Number.MAX_SAFE_INTEGER);
|
|
2001
|
+
const page = messagePage(await api.get(path, { order: "asc", offset: String(wanted - 1), limit: "1" }));
|
|
2002
|
+
const message = page.messages[0];
|
|
2003
|
+
if (message?.seq !== wanted) {
|
|
2004
|
+
throw new CliError({ code: "not_found", message: `The conversation has no message #${wanted}.` }, EXIT_API);
|
|
2005
|
+
}
|
|
2006
|
+
return writeText(renderMessage(message, page, { whole: true }));
|
|
2007
|
+
}
|
|
2008
|
+
const offset = nonNegativeInteger("--offset", options["--offset"] ?? "0");
|
|
2009
|
+
const limit = integerInRange("--limit", options["--limit"] ?? String(CLI_LIMITS.history.default), 1, CLI_LIMITS.history.maximum);
|
|
2010
|
+
const page = messagePage(await api.get(path, { order: "asc", offset: String(offset), limit: String(limit) }));
|
|
2011
|
+
const shown = page.messages.map((message) => renderMessage(message, page, { whole: false }));
|
|
2012
|
+
const end = offset + page.messages.length;
|
|
2013
|
+
const footer = page.messages.length === 0 ? `No messages from offset ${offset}: the conversation has ${page.total}.` : page.hasMore ? `Messages ${offset + 1}–${end} of ${page.total}. Next: sixb history --offset ${end}` : `Messages ${offset + 1}–${end} of ${page.total}: the end of the conversation.`;
|
|
2014
|
+
writeText([...shown, footer].join(`
|
|
2015
|
+
|
|
2016
|
+
`));
|
|
2017
|
+
}
|
|
2018
|
+
function renderMessage(message, page, options) {
|
|
2019
|
+
const author = message.authorPrincipal ? page.authors.find((person) => person.id === message.authorPrincipal?.id)?.displayName : undefined;
|
|
2020
|
+
const heading = [`#${message.seq} ${message.role}`, author, message.createdAt].filter(Boolean).join(" · ");
|
|
2021
|
+
const bounds = {
|
|
2022
|
+
seq: message.seq,
|
|
2023
|
+
textChars: options.whole ? Number.POSITIVE_INFINITY : PAGE_TEXT_CHARS,
|
|
2024
|
+
toolChars: options.whole ? Number.POSITIVE_INFINITY : PAGE_TOOL_CHARS
|
|
2025
|
+
};
|
|
2026
|
+
return [
|
|
2027
|
+
`── ${heading}`,
|
|
2028
|
+
...message.parts.flatMap((part, index) => [
|
|
2029
|
+
...renderPart(part, bounds),
|
|
2030
|
+
...renderOutputFile(part, message, index)
|
|
2031
|
+
])
|
|
2032
|
+
].join(`
|
|
2033
|
+
`);
|
|
2034
|
+
}
|
|
2035
|
+
function renderOutputFile(part, message, index) {
|
|
2036
|
+
const file = part.type === "tool-call" ? record(part.outputFile) : undefined;
|
|
2037
|
+
const fileRef = record(file?.fileRef);
|
|
2038
|
+
if (typeof file?.path !== "string" || typeof fileRef?.sizeBytes !== "number")
|
|
2039
|
+
return [];
|
|
2040
|
+
const contextDir = process.env.SIXB_CONTEXT_DIR;
|
|
2041
|
+
const path = contextDir ? `${contextDir.replace(/\/$/, "")}/${file.path}` : file.path;
|
|
2042
|
+
const apiBaseUrl = process.env.SIXB_API_BASE_URL;
|
|
2043
|
+
const threadId = process.env.SIXB_THREAD_ID;
|
|
2044
|
+
const download = apiBaseUrl && threadId && message.id ? `; else: sixb files download url ${apiBaseUrl.replace(/\/$/, "")}/api/agent-threads/${idSegment(threadId)}/messages/${idSegment(message.id)}/files/content?path=${encodeURIComponent(`/parts/${index}/outputFile/fileRef`)} --output <file>` : "";
|
|
2045
|
+
return [
|
|
2046
|
+
` [whole output, ${fileRef.sizeBytes.toLocaleString("en-US")} bytes: ${path}, written back to the sandbox at each run while the conversation shows this call${download}]`
|
|
2047
|
+
];
|
|
2048
|
+
}
|
|
2049
|
+
function renderPart(part, bounds) {
|
|
2050
|
+
const cut = (value, max) => value.length <= max ? value : `${value.slice(0, max)}… [${(value.length - max).toLocaleString("en-US")} more characters: sixb history ${bounds.seq}]`;
|
|
2051
|
+
switch (part.type) {
|
|
2052
|
+
case "text":
|
|
2053
|
+
return typeof part.text === "string" && part.text.trim() ? [cut(part.text, bounds.textChars)] : [];
|
|
2054
|
+
case "file": {
|
|
2055
|
+
const file = record(part.fileRef);
|
|
2056
|
+
const name = file?.fileName ?? file?.logicalPath ?? file?.blobId;
|
|
2057
|
+
return [`[file ${String(name)}${file?.mediaType ? `, ${String(file.mediaType)}` : ""}]`];
|
|
2058
|
+
}
|
|
2059
|
+
case "tool-call": {
|
|
2060
|
+
const input = cut(JSON.stringify(part.input ?? {}), bounds.toolChars);
|
|
2061
|
+
const outcome = part.state === "output-error" ? `error: ${cut(String(part.errorText ?? ""), bounds.toolChars)}` : part.state === "output-available" ? cut(typeof part.output === "string" ? part.output : JSON.stringify(part.output), bounds.toolChars) : String(part.state ?? "");
|
|
2062
|
+
return [`[${String(part.toolName)}] ${input}`, `→ ${outcome}`];
|
|
2063
|
+
}
|
|
2064
|
+
case "continuation":
|
|
2065
|
+
return ["[asked to continue]"];
|
|
2066
|
+
case "approval-outcome":
|
|
2067
|
+
return [`[approval ${String(part.status)}]`];
|
|
2068
|
+
default:
|
|
2069
|
+
return [];
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
function messagePage(value) {
|
|
2073
|
+
const page = record(value);
|
|
2074
|
+
if (!page || !Array.isArray(page.messages) || typeof page.total !== "number") {
|
|
2075
|
+
throw new CliError({ code: "invalid_api_response", message: "The Sixb API returned an invalid message page." }, EXIT_API);
|
|
2076
|
+
}
|
|
2077
|
+
return {
|
|
2078
|
+
messages: page.messages,
|
|
2079
|
+
authors: Array.isArray(page.authors) ? page.authors : [],
|
|
2080
|
+
hasMore: page.hasMore === true,
|
|
2081
|
+
total: page.total
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
function record(value) {
|
|
2085
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// src/commands/memory.ts
|
|
2089
|
+
import { lstat, mkdir as mkdir2, readdir, readFile as readFile3, rm as rm2, writeFile } from "node:fs/promises";
|
|
2090
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
2091
|
+
|
|
2092
|
+
// src/memory.ts
|
|
2093
|
+
import { createHash } from "node:crypto";
|
|
2094
|
+
var AGENT_MEMORY_MANIFEST = "context/memory.json";
|
|
2095
|
+
var AGENT_MEMORY_ENTRY = "MEMORY.md";
|
|
2096
|
+
var AGENT_MEMORY_STARTER = `# Memory
|
|
2097
|
+
|
|
2098
|
+
`;
|
|
2099
|
+
function agentMemoryFileHash(contents) {
|
|
2100
|
+
return createHash("sha256").update(contents).digest("hex");
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
// src/api-client.ts
|
|
2104
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2105
|
+
import { createWriteStream } from "node:fs";
|
|
2106
|
+
import { readFile as readFile2, rename, rm } from "node:fs/promises";
|
|
2107
|
+
import { request as httpRequest } from "node:http";
|
|
2108
|
+
import { basename, dirname as dirname2, join as join2 } from "node:path";
|
|
2109
|
+
import { Readable } from "node:stream";
|
|
2110
|
+
import { pipeline } from "node:stream/promises";
|
|
2111
|
+
var UNIX_BASE_URL_PREFIX = "unix:";
|
|
2112
|
+
|
|
2113
|
+
class ApiClient {
|
|
2114
|
+
baseUrl;
|
|
2115
|
+
socketPath;
|
|
2116
|
+
authorization;
|
|
2117
|
+
missingBaseUrlMessage;
|
|
2118
|
+
unavailableMessage;
|
|
2119
|
+
unavailableHint;
|
|
2120
|
+
constructor(options) {
|
|
2121
|
+
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
2122
|
+
this.socketPath = baseUrl.startsWith(UNIX_BASE_URL_PREFIX) ? baseUrl.slice(UNIX_BASE_URL_PREFIX.length) : undefined;
|
|
2123
|
+
this.baseUrl = this.socketPath === undefined ? baseUrl : "http://localhost";
|
|
2124
|
+
this.authorization = options.authorization;
|
|
2125
|
+
this.missingBaseUrlMessage = options.missingBaseUrlMessage;
|
|
2126
|
+
this.unavailableMessage = options.unavailableMessage;
|
|
2127
|
+
this.unavailableHint = options.unavailableHint;
|
|
2128
|
+
}
|
|
2129
|
+
async get(path, query) {
|
|
2130
|
+
const url = this.url(path, query);
|
|
2131
|
+
return this.json(url, { method: "GET" });
|
|
2132
|
+
}
|
|
2133
|
+
async post(path, body, options = {}) {
|
|
2134
|
+
return this.json(this.url(path), {
|
|
2135
|
+
method: "POST",
|
|
2136
|
+
headers: { "content-type": "application/json" },
|
|
2137
|
+
body: JSON.stringify(body),
|
|
2138
|
+
...options.signal ? { signal: options.signal } : {}
|
|
2139
|
+
});
|
|
2140
|
+
}
|
|
2141
|
+
async upload(path, source, logicalPath, mediaType) {
|
|
2142
|
+
const form = new FormData;
|
|
2143
|
+
const bytes = new Uint8Array(await readFile2(source));
|
|
2144
|
+
form.append("file", new Blob([bytes], mediaType ? { type: mediaType } : undefined), basename(source));
|
|
2145
|
+
if (logicalPath)
|
|
2146
|
+
form.append("logicalPath", logicalPath);
|
|
2147
|
+
return this.json(this.url(path), { method: "POST", body: form });
|
|
2148
|
+
}
|
|
2149
|
+
async download(path, output, query) {
|
|
2150
|
+
const response = await this.fetch(this.url(path, query), { method: "GET" });
|
|
2151
|
+
const temporary = join2(dirname2(output), `.${basename(output)}.sixb-${randomUUID3()}.tmp`);
|
|
2152
|
+
try {
|
|
2153
|
+
const body = response.body ? Readable.fromWeb(response.body) : Readable.from([]);
|
|
2154
|
+
await pipeline(body, createWriteStream(temporary, { flags: "wx" }));
|
|
2155
|
+
await rename(temporary, output);
|
|
2156
|
+
} catch (error) {
|
|
2157
|
+
await rm(temporary, { force: true }).catch(() => {});
|
|
2158
|
+
throw error;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
url(path, query) {
|
|
2162
|
+
if (!this.baseUrl)
|
|
2163
|
+
fail(this.missingBaseUrlMessage, "runtime_unavailable");
|
|
2164
|
+
validateApiPath(path);
|
|
2165
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
2166
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
2167
|
+
if (value !== undefined)
|
|
2168
|
+
url.searchParams.set(key, value);
|
|
2169
|
+
}
|
|
2170
|
+
return url;
|
|
2171
|
+
}
|
|
2172
|
+
async json(url, init) {
|
|
2173
|
+
const response = await this.fetch(url, init);
|
|
2174
|
+
const text = await response.text();
|
|
2175
|
+
if (!text)
|
|
2176
|
+
return null;
|
|
2177
|
+
try {
|
|
2178
|
+
return JSON.parse(text);
|
|
2179
|
+
} catch {
|
|
2180
|
+
throw new CliError({ code: "invalid_api_response", message: "The Sixb API returned invalid JSON." }, EXIT_API);
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
async fetch(url, init) {
|
|
2184
|
+
let response;
|
|
2185
|
+
try {
|
|
2186
|
+
const headers = new Headers(init.headers);
|
|
2187
|
+
if (this.authorization)
|
|
2188
|
+
headers.set("authorization", this.authorization);
|
|
2189
|
+
response = await send(url, { ...init, headers }, this.socketPath);
|
|
2190
|
+
} catch (error) {
|
|
2191
|
+
if (init.signal?.aborted)
|
|
2192
|
+
throw error;
|
|
2193
|
+
throw new CliError({
|
|
2194
|
+
code: "api_unreachable",
|
|
2195
|
+
message: this.unavailableMessage,
|
|
2196
|
+
hint: this.unavailableHint
|
|
2197
|
+
}, EXIT_API);
|
|
2198
|
+
}
|
|
2199
|
+
if (response.ok)
|
|
2200
|
+
return response;
|
|
2201
|
+
const body = await response.text();
|
|
2202
|
+
let parsed;
|
|
2203
|
+
try {
|
|
2204
|
+
parsed = JSON.parse(body);
|
|
2205
|
+
} catch {
|
|
2206
|
+
parsed = undefined;
|
|
2207
|
+
}
|
|
2208
|
+
const record = asRecord2(parsed);
|
|
2209
|
+
const error = record.error;
|
|
2210
|
+
const nested = asRecord2(error);
|
|
2211
|
+
const code = stringField(record, "code") ?? stringField(nested, "code") ?? "http_error";
|
|
2212
|
+
const message = stringField(record, "message") ?? stringField(nested, "message") ?? (typeof error === "string" ? error : `The Sixb API request failed with HTTP ${response.status}.`);
|
|
2213
|
+
const hint = stringField(record, "hint") ?? stringField(nested, "hint");
|
|
2214
|
+
const issues = Array.isArray(record.issues) ? record.issues : Array.isArray(nested.issues) ? nested.issues : undefined;
|
|
2215
|
+
throw new CliError({
|
|
2216
|
+
code,
|
|
2217
|
+
status: response.status,
|
|
2218
|
+
message,
|
|
2219
|
+
...hint ? { hint } : {},
|
|
2220
|
+
...issues ? { issues } : {}
|
|
2221
|
+
}, EXIT_API);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
function validateApiPath(path) {
|
|
2225
|
+
const invalid = () => fail("API paths must be relative and start with /api/.");
|
|
2226
|
+
if (path.includes("\\") || path.includes("#"))
|
|
2227
|
+
invalid();
|
|
2228
|
+
const normalized = (() => {
|
|
2229
|
+
try {
|
|
2230
|
+
return new URL(path, "http://sixb.invalid");
|
|
2231
|
+
} catch {
|
|
2232
|
+
return invalid();
|
|
2233
|
+
}
|
|
2234
|
+
})();
|
|
2235
|
+
if (normalized.origin !== "http://sixb.invalid" || normalized.pathname !== "/api" && !normalized.pathname.startsWith("/api/")) {
|
|
2236
|
+
invalid();
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
function asRecord2(value) {
|
|
2240
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
2241
|
+
}
|
|
2242
|
+
function stringField(record, key) {
|
|
2243
|
+
const value = record[key];
|
|
2244
|
+
return typeof value === "string" ? value : undefined;
|
|
2245
|
+
}
|
|
2246
|
+
function send(url, init, socketPath) {
|
|
2247
|
+
if (socketPath === undefined)
|
|
2248
|
+
return fetch(url, init);
|
|
2249
|
+
if (typeof Bun !== "undefined")
|
|
2250
|
+
return fetch(url, { ...init, unix: socketPath });
|
|
2251
|
+
return sendOverNodeSocket(url, init, socketPath);
|
|
2252
|
+
}
|
|
2253
|
+
async function sendOverNodeSocket(url, init, socketPath) {
|
|
2254
|
+
const outgoing = new Request(url, init);
|
|
2255
|
+
const body = outgoing.body ? Buffer.from(await outgoing.arrayBuffer()) : undefined;
|
|
2256
|
+
const headers = Object.fromEntries(outgoing.headers);
|
|
2257
|
+
if (body)
|
|
2258
|
+
headers["content-length"] = String(body.byteLength);
|
|
2259
|
+
return new Promise((resolve, reject) => {
|
|
2260
|
+
const request = httpRequest({
|
|
2261
|
+
socketPath,
|
|
2262
|
+
method: outgoing.method,
|
|
2263
|
+
path: `${url.pathname}${url.search}`,
|
|
2264
|
+
headers: { ...headers, host: "localhost" },
|
|
2265
|
+
...init.signal ? { signal: init.signal } : {}
|
|
2266
|
+
}, (response) => {
|
|
2267
|
+
const responseHeaders = new Headers;
|
|
2268
|
+
for (const [name, value] of Object.entries(response.headers)) {
|
|
2269
|
+
for (const each of Array.isArray(value) ? value : value === undefined ? [] : [value]) {
|
|
2270
|
+
responseHeaders.append(name, each);
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
const status = response.statusCode ?? 502;
|
|
2274
|
+
const empty = status === 204 || status === 304 || outgoing.method === "HEAD";
|
|
2275
|
+
resolve(new Response(empty ? null : nodeBody(response), {
|
|
2276
|
+
status,
|
|
2277
|
+
statusText: response.statusMessage ?? "",
|
|
2278
|
+
headers: responseHeaders
|
|
2279
|
+
}));
|
|
2280
|
+
});
|
|
2281
|
+
request.on("error", reject);
|
|
2282
|
+
request.end(body);
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
function nodeBody(response) {
|
|
2286
|
+
return Readable.toWeb(response);
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
// src/run.ts
|
|
2290
|
+
async function runInstanceCli(input) {
|
|
2291
|
+
const [command, ...args] = input.args;
|
|
2292
|
+
await dispatch(createInstanceApiClient(input.mode), command ?? "", args);
|
|
2293
|
+
}
|
|
2294
|
+
function createInstanceApiClient(mode) {
|
|
2295
|
+
return new ApiClient({
|
|
2296
|
+
baseUrl: mode.baseUrl,
|
|
2297
|
+
...mode.kind === "local" && mode.token ? { authorization: `Bearer ${mode.token}` } : {},
|
|
2298
|
+
missingBaseUrlMessage: mode.kind === "sandbox" ? "SIXB_API_BASE_URL is not set." : "The selected Sixb profile has no API URL.",
|
|
2299
|
+
unavailableMessage: mode.kind === "sandbox" ? "The Sixb API could not be reached from the sandbox." : "The Sixb API could not be reached.",
|
|
2300
|
+
unavailableHint: mode.kind === "sandbox" ? "Run 'sixb status' to verify the sandbox runtime and its API access." : "Run 'sixb status' to verify the current profile."
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
// src/commands/memory.ts
|
|
2305
|
+
var HELP = `Usage:
|
|
2306
|
+
sixb memory save <repo> -m <message> Save local edits as one revision
|
|
2307
|
+
sixb memory pull [repo] Bring the local copy up to the latest revision
|
|
2308
|
+
|
|
2309
|
+
<repo> is user or project. Files live under $SIXB_MEMORY_DIR/<repo>/.
|
|
2310
|
+
|
|
2311
|
+
A save is rejected for files another conversation changed since they were loaded. Their
|
|
2312
|
+
current contents are written next to yours as <file>.theirs: merge each into its file, delete
|
|
2313
|
+
it, then save again. Changes others made to other files are brought in by save and pull.`;
|
|
2314
|
+
var THEIRS = ".theirs";
|
|
2315
|
+
var CONFLICT_HINT = "Merge each .theirs file into its file, delete it, then save again.";
|
|
2316
|
+
async function memory(args, mode) {
|
|
2317
|
+
const [sub, ...rest] = args;
|
|
2318
|
+
if (!sub || isHelp(sub) || rest.some(isHelp))
|
|
2319
|
+
return writeText(HELP);
|
|
2320
|
+
const local = localMemory();
|
|
2321
|
+
if (sub === "save")
|
|
2322
|
+
return save(local, rest, mode);
|
|
2323
|
+
if (sub === "pull")
|
|
2324
|
+
return pull(local, rest, mode);
|
|
2325
|
+
fail(`Unknown memory command '${sub}'. Run 'sixb memory --help'.`);
|
|
2326
|
+
}
|
|
2327
|
+
async function save(local, args, mode) {
|
|
2328
|
+
const { repo, message } = parseSaveArgs(args);
|
|
2329
|
+
const manifest = await local.readManifest();
|
|
2330
|
+
const loaded = manifest.repos[repo];
|
|
2331
|
+
if (!loaded)
|
|
2332
|
+
fail(`Memory repo '${repo}' is not installed.`, "invalid_arguments", `Run 'sixb memory pull ${repo}'.`);
|
|
2333
|
+
const files = await local.readRepo(repo);
|
|
2334
|
+
const unresolved = [...files.keys()].filter((path) => path.endsWith(THEIRS));
|
|
2335
|
+
if (unresolved.length > 0) {
|
|
2336
|
+
fail(`Resolve ${unresolved.join(", ")} first.`, "unresolved_conflicts", CONFLICT_HINT);
|
|
2337
|
+
}
|
|
2338
|
+
const changes = [...new Set([...files.keys(), ...Object.keys(loaded.files)])].sort().flatMap((path) => {
|
|
2339
|
+
const contents = files.get(path);
|
|
2340
|
+
const changed = (contents === undefined ? undefined : agentMemoryFileHash(contents)) !== loaded.files[path];
|
|
2341
|
+
return changed ? [{ path, contents: contents ?? null }] : [];
|
|
2342
|
+
});
|
|
2343
|
+
if (changes.length === 0) {
|
|
2344
|
+
return writeJson({
|
|
2345
|
+
repo,
|
|
2346
|
+
saved: false,
|
|
2347
|
+
changed: [],
|
|
2348
|
+
hint: "No file changed since it was loaded."
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
const api = createInstanceApiClient(mode);
|
|
2352
|
+
let saved;
|
|
2353
|
+
try {
|
|
2354
|
+
saved = asRecord(await api.post(`/api/agent-memory/${repo}/revisions`, {
|
|
2355
|
+
baseRevision: loaded.revision,
|
|
2356
|
+
message,
|
|
2357
|
+
changes
|
|
2358
|
+
}));
|
|
2359
|
+
} catch (error) {
|
|
2360
|
+
if (!(error instanceof CliError) || error.body.status !== 409)
|
|
2361
|
+
throw error;
|
|
2362
|
+
const { revision, conflicts } = await update(local, repo, manifest, await fetchRepo(api, repo));
|
|
2363
|
+
process.exitCode = 1;
|
|
2364
|
+
return writeJson({ repo, saved: false, revision, conflicts, hint: CONFLICT_HINT });
|
|
2365
|
+
}
|
|
2366
|
+
const savedFiles = { ...loaded.files };
|
|
2367
|
+
for (const change of changes) {
|
|
2368
|
+
if (change.contents === null)
|
|
2369
|
+
delete savedFiles[change.path];
|
|
2370
|
+
else
|
|
2371
|
+
savedFiles[change.path] = agentMemoryFileHash(change.contents);
|
|
2372
|
+
}
|
|
2373
|
+
const after = {
|
|
2374
|
+
repos: { ...manifest.repos, [repo]: { ...loaded, files: savedFiles } }
|
|
2375
|
+
};
|
|
2376
|
+
const { revision, conflicts } = await update(local, repo, after, await fetchRepo(api, repo));
|
|
2377
|
+
const warnings = Array.isArray(saved.warnings) ? saved.warnings : [];
|
|
2378
|
+
writeJson({
|
|
2379
|
+
repo,
|
|
2380
|
+
saved: true,
|
|
2381
|
+
revision,
|
|
2382
|
+
changed: changes.map((change) => change.path),
|
|
2383
|
+
...warnings.length > 0 ? { warnings } : {},
|
|
2384
|
+
...conflicts.length > 0 ? { conflicts, hint: CONFLICT_HINT } : {}
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
async function pull(local, args, mode) {
|
|
2388
|
+
if (args.length > 1)
|
|
2389
|
+
fail("Invalid arguments for 'memory pull'.");
|
|
2390
|
+
const manifest = await local.readManifest();
|
|
2391
|
+
const repos = args[0] === undefined ? installedRepos(manifest) : [parseRepo(args[0])];
|
|
2392
|
+
if (repos.length === 0) {
|
|
2393
|
+
fail("No memory repo is installed.", "invalid_arguments", "Run 'sixb memory pull user'.");
|
|
2394
|
+
}
|
|
2395
|
+
const api = createInstanceApiClient(mode);
|
|
2396
|
+
let current = manifest;
|
|
2397
|
+
for (const repo of repos) {
|
|
2398
|
+
const result = await update(local, repo, current, await fetchRepo(api, repo));
|
|
2399
|
+
current = result.manifest;
|
|
2400
|
+
writeJson({
|
|
2401
|
+
repo,
|
|
2402
|
+
revision: result.revision,
|
|
2403
|
+
updated: result.updated,
|
|
2404
|
+
conflicts: result.conflicts,
|
|
2405
|
+
...result.conflicts.length > 0 ? { hint: CONFLICT_HINT } : {}
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
async function update(local, repo, manifest, remote) {
|
|
2410
|
+
const base = manifest.repos[repo]?.files ?? {};
|
|
2411
|
+
const files = await local.readRepo(repo);
|
|
2412
|
+
const updated = [];
|
|
2413
|
+
const conflicts = [];
|
|
2414
|
+
for (const path of [
|
|
2415
|
+
...new Set([...files.keys(), ...remote.files.keys(), ...Object.keys(base)])
|
|
2416
|
+
].sort()) {
|
|
2417
|
+
if (path.endsWith(THEIRS))
|
|
2418
|
+
continue;
|
|
2419
|
+
const mine = files.get(path);
|
|
2420
|
+
const theirs = remote.files.get(path);
|
|
2421
|
+
const mineHash = mine === undefined ? undefined : agentMemoryFileHash(mine);
|
|
2422
|
+
const theirsHash = theirs === undefined ? undefined : agentMemoryFileHash(theirs);
|
|
2423
|
+
if (theirsHash === base[path] || theirsHash === mineHash)
|
|
2424
|
+
continue;
|
|
2425
|
+
if (mineHash === base[path]) {
|
|
2426
|
+
await local.write(repo, path, theirs);
|
|
2427
|
+
updated.push(path);
|
|
2428
|
+
} else if (theirs === undefined) {
|
|
2429
|
+
conflicts.push({ path, theirs: null });
|
|
2430
|
+
} else {
|
|
2431
|
+
await local.write(repo, `${path}${THEIRS}`, theirs);
|
|
2432
|
+
conflicts.push({ path, theirs: `${path}${THEIRS}` });
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
if (!remote.files.has(AGENT_MEMORY_ENTRY) && !(await local.readRepo(repo)).has(AGENT_MEMORY_ENTRY)) {
|
|
2436
|
+
await local.write(repo, AGENT_MEMORY_ENTRY, AGENT_MEMORY_STARTER);
|
|
2437
|
+
}
|
|
2438
|
+
const next = {
|
|
2439
|
+
repos: {
|
|
2440
|
+
...manifest.repos,
|
|
2441
|
+
[repo]: {
|
|
2442
|
+
revision: remote.revision,
|
|
2443
|
+
files: Object.fromEntries([...remote.files].map(([path, contents]) => [path, agentMemoryFileHash(contents)]))
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
};
|
|
2447
|
+
await local.writeManifest(next);
|
|
2448
|
+
return { manifest: next, revision: remote.revision, updated, conflicts };
|
|
2449
|
+
}
|
|
2450
|
+
async function fetchRepo(api, repo) {
|
|
2451
|
+
const body = asRecord(await api.get(`/api/agent-memory/${repo}`));
|
|
2452
|
+
const files = Array.isArray(body.files) ? body.files.map(asRecord) : null;
|
|
2453
|
+
if (typeof body.revision !== "number" || !files?.every((file) => typeof file.path === "string" && typeof file.contents === "string")) {
|
|
2454
|
+
throw new CliError({ code: "invalid_api_response", message: "The Sixb API returned an invalid memory repo." }, EXIT_API);
|
|
2455
|
+
}
|
|
2456
|
+
return {
|
|
2457
|
+
revision: body.revision,
|
|
2458
|
+
files: new Map(files.map((file) => [String(file.path), String(file.contents)]))
|
|
2459
|
+
};
|
|
2460
|
+
}
|
|
2461
|
+
function localMemory() {
|
|
2462
|
+
const root = process.env.SIXB_MEMORY_DIR;
|
|
2463
|
+
const contextDir = process.env.SIXB_CONTEXT_DIR;
|
|
2464
|
+
if (!root || !contextDir) {
|
|
2465
|
+
fail("Agent memory is not available in this run.", "runtime_unavailable");
|
|
2466
|
+
}
|
|
2467
|
+
const manifestPath = join3(contextDir, ...AGENT_MEMORY_MANIFEST.split("/"));
|
|
2468
|
+
const fileOf = (repo, path) => join3(root, repo, ...path.split("/"));
|
|
2469
|
+
return {
|
|
2470
|
+
async readManifest() {
|
|
2471
|
+
let text;
|
|
2472
|
+
try {
|
|
2473
|
+
text = await readFile3(manifestPath, "utf8");
|
|
2474
|
+
} catch (error) {
|
|
2475
|
+
if (isFileError(error, "ENOENT"))
|
|
2476
|
+
return { repos: {} };
|
|
2477
|
+
throw error;
|
|
2478
|
+
}
|
|
2479
|
+
try {
|
|
2480
|
+
return parseManifest(JSON.parse(text));
|
|
2481
|
+
} catch {
|
|
2482
|
+
fail(`Memory manifest '${manifestPath}' is not valid.`, "invalid_json");
|
|
2483
|
+
}
|
|
2484
|
+
},
|
|
2485
|
+
async writeManifest(manifest) {
|
|
2486
|
+
await mkdir2(dirname3(manifestPath), { recursive: true });
|
|
2487
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
2488
|
+
`);
|
|
2489
|
+
},
|
|
2490
|
+
async readRepo(repo) {
|
|
2491
|
+
const files = new Map;
|
|
2492
|
+
const walk = async (directory, prefix) => {
|
|
2493
|
+
let entries;
|
|
2494
|
+
try {
|
|
2495
|
+
entries = await readdir(directory);
|
|
2496
|
+
} catch (error) {
|
|
2497
|
+
if (isFileError(error, "ENOENT"))
|
|
2498
|
+
return;
|
|
2499
|
+
throw error;
|
|
2500
|
+
}
|
|
2501
|
+
for (const name of entries) {
|
|
2502
|
+
const path = prefix ? `${prefix}/${name}` : name;
|
|
2503
|
+
const stats = await lstat(join3(directory, name));
|
|
2504
|
+
if (stats.isDirectory())
|
|
2505
|
+
await walk(join3(directory, name), path);
|
|
2506
|
+
else if (stats.isFile())
|
|
2507
|
+
files.set(path, await readFile3(join3(directory, name), "utf8"));
|
|
2508
|
+
else
|
|
2509
|
+
fail(`Memory file '${repo}/${path}' is not a regular file.`);
|
|
2510
|
+
}
|
|
2511
|
+
};
|
|
2512
|
+
await walk(join3(root, repo), "");
|
|
2513
|
+
return files;
|
|
2514
|
+
},
|
|
2515
|
+
async write(repo, path, contents) {
|
|
2516
|
+
const file = fileOf(repo, path);
|
|
2517
|
+
if (contents === undefined) {
|
|
2518
|
+
await rm2(file, { force: true });
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
await mkdir2(dirname3(file), { recursive: true });
|
|
2522
|
+
await writeFile(file, contents);
|
|
2523
|
+
}
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2526
|
+
function parseSaveArgs(args) {
|
|
2527
|
+
const positionals = [];
|
|
2528
|
+
let message;
|
|
2529
|
+
for (let index = 0;index < args.length; index++) {
|
|
2530
|
+
const argument = args[index] ?? "";
|
|
2531
|
+
if (argument === "-m" || argument === "--message") {
|
|
2532
|
+
if (message !== undefined)
|
|
2533
|
+
fail(`${argument} may only be provided once.`);
|
|
2534
|
+
message = args[++index];
|
|
2535
|
+
if (!message)
|
|
2536
|
+
fail(`${argument} requires a value.`);
|
|
2537
|
+
} else if (argument.startsWith("-")) {
|
|
2538
|
+
fail(`Unknown memory save option '${argument}'.`);
|
|
2539
|
+
} else {
|
|
2540
|
+
positionals.push(argument);
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
if (positionals.length !== 1)
|
|
2544
|
+
fail("Invalid arguments for 'memory save'.");
|
|
2545
|
+
if (message === undefined || message.trim().length === 0) {
|
|
2546
|
+
fail("memory save requires -m <what you learned>.");
|
|
2547
|
+
}
|
|
2548
|
+
return { repo: parseRepo(positionals[0] ?? ""), message };
|
|
2549
|
+
}
|
|
2550
|
+
function parseRepo(value) {
|
|
2551
|
+
if (value === "user" || value === "project")
|
|
2552
|
+
return value;
|
|
2553
|
+
fail(`Unknown memory repo '${value}'. Use 'user' or 'project'.`);
|
|
2554
|
+
}
|
|
2555
|
+
function installedRepos(manifest) {
|
|
2556
|
+
return ["user", "project"].filter((repo) => manifest.repos[repo] !== undefined);
|
|
2557
|
+
}
|
|
2558
|
+
function parseManifest(value) {
|
|
2559
|
+
const repos = asRecord(asRecord(value).repos);
|
|
2560
|
+
const parsed = {};
|
|
2561
|
+
for (const repo of ["user", "project"]) {
|
|
2562
|
+
if (repos[repo] === undefined)
|
|
2563
|
+
continue;
|
|
2564
|
+
const entry = asRecord(repos[repo]);
|
|
2565
|
+
const files = asRecord(entry.files);
|
|
2566
|
+
if (typeof entry.revision !== "number" || !Object.values(files).every((digest) => typeof digest === "string")) {
|
|
2567
|
+
throw new Error("invalid memory manifest");
|
|
2568
|
+
}
|
|
2569
|
+
parsed[repo] = {
|
|
2570
|
+
revision: entry.revision,
|
|
2571
|
+
files
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
return { repos: parsed };
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
// src/commands/system.ts
|
|
2578
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
2579
|
+
var AGENT_RUNTIME_PROFILE = "sixb-agent-runtime/v1";
|
|
2580
|
+
async function status(args, mode) {
|
|
2581
|
+
if (isHelp(args[0]))
|
|
2582
|
+
return writeText("Usage: sixb status");
|
|
2583
|
+
if (args.some((arg) => arg !== "--json"))
|
|
2584
|
+
fail("status accepts no arguments but --json.");
|
|
2585
|
+
const project = asRecord(await createInstanceApiClient(mode).get("/api/project"));
|
|
2586
|
+
if (typeof project.id !== "string" || project.id.length === 0) {
|
|
2587
|
+
throw new CliError({ code: "invalid_api_response", message: "The Sixb API returned an invalid project." }, EXIT_API);
|
|
2588
|
+
}
|
|
2589
|
+
const report = {
|
|
2590
|
+
ok: true,
|
|
2591
|
+
profile: AGENT_RUNTIME_PROFILE,
|
|
2592
|
+
cli: { version: INSTANCE_CLI_VERSION },
|
|
2593
|
+
javascript: javascriptRuntime(),
|
|
2594
|
+
project: { id: project.id }
|
|
2595
|
+
};
|
|
2596
|
+
writeJson(report);
|
|
2597
|
+
}
|
|
2598
|
+
async function context(args) {
|
|
2599
|
+
if (isHelp(args[0]))
|
|
2600
|
+
return writeText("Usage: sixb context");
|
|
2601
|
+
if (args.length !== 0)
|
|
2602
|
+
fail("context accepts no arguments.");
|
|
2603
|
+
const path = process.env.SIXB_RUN_CONTEXT;
|
|
2604
|
+
if (!path)
|
|
2605
|
+
fail("SIXB_RUN_CONTEXT is not set.");
|
|
2606
|
+
let text;
|
|
2607
|
+
try {
|
|
2608
|
+
text = await readFile4(path, "utf8");
|
|
2609
|
+
} catch (error) {
|
|
2610
|
+
if (isFileError(error, "ENOENT"))
|
|
2611
|
+
fail(`Run context '${path}' does not exist.`);
|
|
2612
|
+
throw error;
|
|
2613
|
+
}
|
|
2614
|
+
try {
|
|
2615
|
+
writeJson(JSON.parse(text));
|
|
2616
|
+
} catch {
|
|
2617
|
+
fail(`Run context '${path}' is not valid JSON.`, "invalid_json");
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
function javascriptRuntime() {
|
|
2621
|
+
if (typeof globalThis.Bun === "object") {
|
|
2622
|
+
return { name: "bun", version: globalThis.Bun.version };
|
|
2623
|
+
}
|
|
2624
|
+
return { name: "node", version: process.versions.node };
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
// src/main.ts
|
|
2628
|
+
async function main(args) {
|
|
2629
|
+
const [command, ...rest] = args;
|
|
2630
|
+
if (!command || isHelp(command))
|
|
2631
|
+
return writeText(renderInstanceHelp());
|
|
2632
|
+
if (command === "--version" || command === "version") {
|
|
2633
|
+
return writeText(`sixb CLI ${INSTANCE_CLI_VERSION}`);
|
|
2634
|
+
}
|
|
2635
|
+
if (command === "status")
|
|
2636
|
+
return status(rest, sandboxMode());
|
|
2637
|
+
if (command === "context")
|
|
2638
|
+
return context(rest);
|
|
2639
|
+
if (command === "memory")
|
|
2640
|
+
return memory(rest, sandboxMode());
|
|
2641
|
+
if (command === "history")
|
|
2642
|
+
return history(createInstanceApiClient(sandboxMode()), rest);
|
|
2643
|
+
if (!isInstanceCommand(command))
|
|
2644
|
+
fail(`Unknown command '${command}'. Run 'sixb --help'.`);
|
|
2645
|
+
await runInstanceCli({ args, mode: sandboxMode() });
|
|
2646
|
+
}
|
|
2647
|
+
function sandboxMode() {
|
|
2648
|
+
return {
|
|
2649
|
+
kind: "sandbox",
|
|
2650
|
+
baseUrl: process.env.SIXB_API_BASE_URL ?? "",
|
|
2651
|
+
runContextPath: process.env.SIXB_RUN_CONTEXT ?? ""
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
try {
|
|
2655
|
+
await main(process.argv.slice(2));
|
|
2656
|
+
} catch (error) {
|
|
2657
|
+
process.exitCode = reportError(error);
|
|
2658
|
+
}
|