@sixb/cli-core 0.1.9 → 0.1.10
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/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 +10 -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 +2547 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +290 -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 +9 -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/index.ts +16 -0
- package/src/commands/memory.ts +358 -0
- package/src/commands/metadata.ts +69 -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 +2547 -0
- package/src/index.ts +12 -3
- package/src/main.ts +39 -0
- package/src/memory.ts +30 -0
- package/src/policies.ts +3 -0
- package/src/run.ts +3 -2
- package/src/sandbox-cli.ts +18 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { lstat, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises"
|
|
2
|
+
import { dirname, join } from "node:path"
|
|
3
|
+
import { isHelp } from "../arguments"
|
|
4
|
+
import {
|
|
5
|
+
AGENT_MEMORY_ENTRY,
|
|
6
|
+
AGENT_MEMORY_MANIFEST,
|
|
7
|
+
AGENT_MEMORY_STARTER,
|
|
8
|
+
type AgentMemoryManifest,
|
|
9
|
+
type AgentMemoryManifestRepo,
|
|
10
|
+
type AgentMemoryRepo,
|
|
11
|
+
agentMemoryFileHash,
|
|
12
|
+
} from "../memory"
|
|
13
|
+
import { CliError, EXIT_API, fail, writeJson, writeText } from "../output"
|
|
14
|
+
import { createInstanceApiClient, type SandboxInstanceCliMode } from "../run"
|
|
15
|
+
import { asRecord, isFileError } from "./shared"
|
|
16
|
+
|
|
17
|
+
const HELP = `Usage:
|
|
18
|
+
sixb memory save <repo> -m <message> Save local edits as one revision
|
|
19
|
+
sixb memory pull [repo] Bring the local copy up to the latest revision
|
|
20
|
+
|
|
21
|
+
<repo> is user or project. Files live under $SIXB_MEMORY_DIR/<repo>/.
|
|
22
|
+
|
|
23
|
+
A save is rejected for files another conversation changed since they were loaded. Their
|
|
24
|
+
current contents are written next to yours as <file>.theirs: merge each into its file, delete
|
|
25
|
+
it, then save again. Changes others made to other files are brought in by save and pull.`
|
|
26
|
+
|
|
27
|
+
interface RemoteRepo {
|
|
28
|
+
readonly revision: number
|
|
29
|
+
readonly files: ReadonlyMap<string, string>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface MemoryConflict {
|
|
33
|
+
readonly path: string
|
|
34
|
+
/** Where their version was written, or `null` when another save deleted the file. */
|
|
35
|
+
readonly theirs: string | null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const THEIRS = ".theirs"
|
|
39
|
+
const CONFLICT_HINT = "Merge each .theirs file into its file, delete it, then save again."
|
|
40
|
+
|
|
41
|
+
export async function memory(args: readonly string[], mode: SandboxInstanceCliMode): Promise<void> {
|
|
42
|
+
const [sub, ...rest] = args
|
|
43
|
+
if (!sub || isHelp(sub) || rest.some(isHelp)) return writeText(HELP)
|
|
44
|
+
const local = localMemory()
|
|
45
|
+
if (sub === "save") return save(local, rest, mode)
|
|
46
|
+
if (sub === "pull") return pull(local, rest, mode)
|
|
47
|
+
fail(`Unknown memory command '${sub}'. Run 'sixb memory --help'.`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function save(
|
|
51
|
+
local: LocalMemory,
|
|
52
|
+
args: readonly string[],
|
|
53
|
+
mode: SandboxInstanceCliMode
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
const { repo, message } = parseSaveArgs(args)
|
|
56
|
+
const manifest = await local.readManifest()
|
|
57
|
+
const loaded = manifest.repos[repo]
|
|
58
|
+
if (!loaded)
|
|
59
|
+
fail(
|
|
60
|
+
`Memory repo '${repo}' is not installed.`,
|
|
61
|
+
"invalid_arguments",
|
|
62
|
+
`Run 'sixb memory pull ${repo}'.`
|
|
63
|
+
)
|
|
64
|
+
const files = await local.readRepo(repo)
|
|
65
|
+
const unresolved = [...files.keys()].filter((path) => path.endsWith(THEIRS))
|
|
66
|
+
if (unresolved.length > 0) {
|
|
67
|
+
fail(`Resolve ${unresolved.join(", ")} first.`, "unresolved_conflicts", CONFLICT_HINT)
|
|
68
|
+
}
|
|
69
|
+
const changes = [...new Set([...files.keys(), ...Object.keys(loaded.files)])]
|
|
70
|
+
.sort()
|
|
71
|
+
.flatMap((path) => {
|
|
72
|
+
const contents = files.get(path)
|
|
73
|
+
const changed =
|
|
74
|
+
(contents === undefined ? undefined : agentMemoryFileHash(contents)) !== loaded.files[path]
|
|
75
|
+
return changed ? [{ path, contents: contents ?? null }] : []
|
|
76
|
+
})
|
|
77
|
+
if (changes.length === 0) {
|
|
78
|
+
return writeJson({
|
|
79
|
+
repo,
|
|
80
|
+
saved: false,
|
|
81
|
+
changed: [],
|
|
82
|
+
hint: "No file changed since it was loaded.",
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const api = createInstanceApiClient(mode)
|
|
87
|
+
let saved: Record<string, unknown>
|
|
88
|
+
try {
|
|
89
|
+
saved = asRecord(
|
|
90
|
+
await api.post(`/api/agent-memory/${repo}/revisions`, {
|
|
91
|
+
baseRevision: loaded.revision,
|
|
92
|
+
message,
|
|
93
|
+
changes,
|
|
94
|
+
})
|
|
95
|
+
)
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (!(error instanceof CliError) || error.body.status !== 409) throw error
|
|
98
|
+
const { revision, conflicts } = await update(local, repo, manifest, await fetchRepo(api, repo))
|
|
99
|
+
process.exitCode = 1
|
|
100
|
+
return writeJson({ repo, saved: false, revision, conflicts, hint: CONFLICT_HINT })
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// The saved files are now what the repo holds, so updating does not report them as conflicts.
|
|
104
|
+
const savedFiles = { ...loaded.files }
|
|
105
|
+
for (const change of changes) {
|
|
106
|
+
if (change.contents === null) delete savedFiles[change.path]
|
|
107
|
+
else savedFiles[change.path] = agentMemoryFileHash(change.contents)
|
|
108
|
+
}
|
|
109
|
+
const after: AgentMemoryManifest = {
|
|
110
|
+
repos: { ...manifest.repos, [repo]: { ...loaded, files: savedFiles } },
|
|
111
|
+
}
|
|
112
|
+
const { revision, conflicts } = await update(local, repo, after, await fetchRepo(api, repo))
|
|
113
|
+
const warnings = Array.isArray(saved.warnings) ? saved.warnings : []
|
|
114
|
+
writeJson({
|
|
115
|
+
repo,
|
|
116
|
+
saved: true,
|
|
117
|
+
revision,
|
|
118
|
+
changed: changes.map((change) => change.path),
|
|
119
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
120
|
+
...(conflicts.length > 0 ? { conflicts, hint: CONFLICT_HINT } : {}),
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function pull(
|
|
125
|
+
local: LocalMemory,
|
|
126
|
+
args: readonly string[],
|
|
127
|
+
mode: SandboxInstanceCliMode
|
|
128
|
+
): Promise<void> {
|
|
129
|
+
if (args.length > 1) fail("Invalid arguments for 'memory pull'.")
|
|
130
|
+
const manifest = await local.readManifest()
|
|
131
|
+
const repos = args[0] === undefined ? installedRepos(manifest) : [parseRepo(args[0])]
|
|
132
|
+
if (repos.length === 0) {
|
|
133
|
+
fail("No memory repo is installed.", "invalid_arguments", "Run 'sixb memory pull user'.")
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const api = createInstanceApiClient(mode)
|
|
137
|
+
let current = manifest
|
|
138
|
+
for (const repo of repos) {
|
|
139
|
+
const result = await update(local, repo, current, await fetchRepo(api, repo))
|
|
140
|
+
current = result.manifest
|
|
141
|
+
writeJson({
|
|
142
|
+
repo,
|
|
143
|
+
revision: result.revision,
|
|
144
|
+
updated: result.updated,
|
|
145
|
+
conflicts: result.conflicts,
|
|
146
|
+
...(result.conflicts.length > 0 ? { hint: CONFLICT_HINT } : {}),
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Bring the local copy of `repo` to `remote`, three ways against the manifest: a file only the
|
|
153
|
+
* repo changed is updated, a file only this copy changed is kept, and a file both changed gets
|
|
154
|
+
* the repo's version next to it as `.theirs`. The manifest then records `remote`, so the next save
|
|
155
|
+
* sends what this copy holds against the latest revision.
|
|
156
|
+
*/
|
|
157
|
+
async function update(
|
|
158
|
+
local: LocalMemory,
|
|
159
|
+
repo: AgentMemoryRepo,
|
|
160
|
+
manifest: AgentMemoryManifest,
|
|
161
|
+
remote: RemoteRepo
|
|
162
|
+
): Promise<{
|
|
163
|
+
readonly manifest: AgentMemoryManifest
|
|
164
|
+
readonly revision: number
|
|
165
|
+
readonly updated: readonly string[]
|
|
166
|
+
readonly conflicts: readonly MemoryConflict[]
|
|
167
|
+
}> {
|
|
168
|
+
const base = manifest.repos[repo]?.files ?? {}
|
|
169
|
+
const files = await local.readRepo(repo)
|
|
170
|
+
const updated: string[] = []
|
|
171
|
+
const conflicts: MemoryConflict[] = []
|
|
172
|
+
|
|
173
|
+
for (const path of [
|
|
174
|
+
...new Set([...files.keys(), ...remote.files.keys(), ...Object.keys(base)]),
|
|
175
|
+
].sort()) {
|
|
176
|
+
if (path.endsWith(THEIRS)) continue
|
|
177
|
+
const mine = files.get(path)
|
|
178
|
+
const theirs = remote.files.get(path)
|
|
179
|
+
const mineHash = mine === undefined ? undefined : agentMemoryFileHash(mine)
|
|
180
|
+
const theirsHash = theirs === undefined ? undefined : agentMemoryFileHash(theirs)
|
|
181
|
+
if (theirsHash === base[path] || theirsHash === mineHash) continue
|
|
182
|
+
if (mineHash === base[path]) {
|
|
183
|
+
await local.write(repo, path, theirs)
|
|
184
|
+
updated.push(path)
|
|
185
|
+
} else if (theirs === undefined) {
|
|
186
|
+
conflicts.push({ path, theirs: null })
|
|
187
|
+
} else {
|
|
188
|
+
await local.write(repo, `${path}${THEIRS}`, theirs)
|
|
189
|
+
conflicts.push({ path, theirs: `${path}${THEIRS}` })
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (
|
|
193
|
+
!remote.files.has(AGENT_MEMORY_ENTRY) &&
|
|
194
|
+
!(await local.readRepo(repo)).has(AGENT_MEMORY_ENTRY)
|
|
195
|
+
) {
|
|
196
|
+
await local.write(repo, AGENT_MEMORY_ENTRY, AGENT_MEMORY_STARTER)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const next: AgentMemoryManifest = {
|
|
200
|
+
repos: {
|
|
201
|
+
...manifest.repos,
|
|
202
|
+
[repo]: {
|
|
203
|
+
revision: remote.revision,
|
|
204
|
+
files: Object.fromEntries(
|
|
205
|
+
[...remote.files].map(([path, contents]) => [path, agentMemoryFileHash(contents)])
|
|
206
|
+
),
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
}
|
|
210
|
+
await local.writeManifest(next)
|
|
211
|
+
return { manifest: next, revision: remote.revision, updated, conflicts }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function fetchRepo(
|
|
215
|
+
api: ReturnType<typeof createInstanceApiClient>,
|
|
216
|
+
repo: AgentMemoryRepo
|
|
217
|
+
): Promise<RemoteRepo> {
|
|
218
|
+
const body = asRecord(await api.get(`/api/agent-memory/${repo}`))
|
|
219
|
+
const files = Array.isArray(body.files) ? body.files.map(asRecord) : null
|
|
220
|
+
if (
|
|
221
|
+
typeof body.revision !== "number" ||
|
|
222
|
+
!files?.every((file) => typeof file.path === "string" && typeof file.contents === "string")
|
|
223
|
+
) {
|
|
224
|
+
throw new CliError(
|
|
225
|
+
{ code: "invalid_api_response", message: "The Sixb API returned an invalid memory repo." },
|
|
226
|
+
EXIT_API
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
revision: body.revision,
|
|
231
|
+
files: new Map(files.map((file) => [String(file.path), String(file.contents)])),
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
interface LocalMemory {
|
|
236
|
+
readManifest(): Promise<AgentMemoryManifest>
|
|
237
|
+
writeManifest(manifest: AgentMemoryManifest): Promise<void>
|
|
238
|
+
/** Every file of the local copy, by path from the repo root. */
|
|
239
|
+
readRepo(repo: AgentMemoryRepo): Promise<Map<string, string>>
|
|
240
|
+
/** `undefined` deletes the file. */
|
|
241
|
+
write(repo: AgentMemoryRepo, path: string, contents: string | undefined): Promise<void>
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function localMemory(): LocalMemory {
|
|
245
|
+
const root = process.env.SIXB_MEMORY_DIR
|
|
246
|
+
const contextDir = process.env.SIXB_CONTEXT_DIR
|
|
247
|
+
if (!root || !contextDir) {
|
|
248
|
+
fail("Agent memory is not available in this run.", "runtime_unavailable")
|
|
249
|
+
}
|
|
250
|
+
const manifestPath = join(contextDir, ...AGENT_MEMORY_MANIFEST.split("/"))
|
|
251
|
+
const fileOf = (repo: AgentMemoryRepo, path: string) => join(root, repo, ...path.split("/"))
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
async readManifest() {
|
|
255
|
+
let text: string
|
|
256
|
+
try {
|
|
257
|
+
text = await readFile(manifestPath, "utf8")
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (isFileError(error, "ENOENT")) return { repos: {} }
|
|
260
|
+
throw error
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
return parseManifest(JSON.parse(text))
|
|
264
|
+
} catch {
|
|
265
|
+
fail(`Memory manifest '${manifestPath}' is not valid.`, "invalid_json")
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
async writeManifest(manifest) {
|
|
270
|
+
await mkdir(dirname(manifestPath), { recursive: true })
|
|
271
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
async readRepo(repo) {
|
|
275
|
+
const files = new Map<string, string>()
|
|
276
|
+
const walk = async (directory: string, prefix: string): Promise<void> => {
|
|
277
|
+
let entries: string[]
|
|
278
|
+
try {
|
|
279
|
+
entries = await readdir(directory)
|
|
280
|
+
} catch (error) {
|
|
281
|
+
if (isFileError(error, "ENOENT")) return
|
|
282
|
+
throw error
|
|
283
|
+
}
|
|
284
|
+
for (const name of entries) {
|
|
285
|
+
const path = prefix ? `${prefix}/${name}` : name
|
|
286
|
+
const stats = await lstat(join(directory, name))
|
|
287
|
+
if (stats.isDirectory()) await walk(join(directory, name), path)
|
|
288
|
+
else if (stats.isFile()) files.set(path, await readFile(join(directory, name), "utf8"))
|
|
289
|
+
else fail(`Memory file '${repo}/${path}' is not a regular file.`)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
await walk(join(root, repo), "")
|
|
293
|
+
return files
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
async write(repo, path, contents) {
|
|
297
|
+
const file = fileOf(repo, path)
|
|
298
|
+
if (contents === undefined) {
|
|
299
|
+
await rm(file, { force: true })
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
await mkdir(dirname(file), { recursive: true })
|
|
303
|
+
await writeFile(file, contents)
|
|
304
|
+
},
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function parseSaveArgs(args: readonly string[]): { repo: AgentMemoryRepo; message: string } {
|
|
309
|
+
const positionals: string[] = []
|
|
310
|
+
let message: string | undefined
|
|
311
|
+
for (let index = 0; index < args.length; index++) {
|
|
312
|
+
const argument = args[index] ?? ""
|
|
313
|
+
if (argument === "-m" || argument === "--message") {
|
|
314
|
+
if (message !== undefined) fail(`${argument} may only be provided once.`)
|
|
315
|
+
message = args[++index]
|
|
316
|
+
if (!message) fail(`${argument} requires a value.`)
|
|
317
|
+
} else if (argument.startsWith("-")) {
|
|
318
|
+
fail(`Unknown memory save option '${argument}'.`)
|
|
319
|
+
} else {
|
|
320
|
+
positionals.push(argument)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (positionals.length !== 1) fail("Invalid arguments for 'memory save'.")
|
|
324
|
+
if (message === undefined || message.trim().length === 0) {
|
|
325
|
+
fail("memory save requires -m <what you learned>.")
|
|
326
|
+
}
|
|
327
|
+
return { repo: parseRepo(positionals[0] ?? ""), message }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function parseRepo(value: string): AgentMemoryRepo {
|
|
331
|
+
if (value === "user" || value === "project") return value
|
|
332
|
+
fail(`Unknown memory repo '${value}'. Use 'user' or 'project'.`)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function installedRepos(manifest: AgentMemoryManifest): AgentMemoryRepo[] {
|
|
336
|
+
return (["user", "project"] as const).filter((repo) => manifest.repos[repo] !== undefined)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function parseManifest(value: unknown): AgentMemoryManifest {
|
|
340
|
+
const repos = asRecord(asRecord(value).repos)
|
|
341
|
+
const parsed: Partial<Record<AgentMemoryRepo, AgentMemoryManifestRepo>> = {}
|
|
342
|
+
for (const repo of ["user", "project"] as const) {
|
|
343
|
+
if (repos[repo] === undefined) continue
|
|
344
|
+
const entry = asRecord(repos[repo])
|
|
345
|
+
const files = asRecord(entry.files)
|
|
346
|
+
if (
|
|
347
|
+
typeof entry.revision !== "number" ||
|
|
348
|
+
!Object.values(files).every((digest) => typeof digest === "string")
|
|
349
|
+
) {
|
|
350
|
+
throw new Error("invalid memory manifest")
|
|
351
|
+
}
|
|
352
|
+
parsed[repo] = {
|
|
353
|
+
revision: entry.revision,
|
|
354
|
+
files: files as Record<string, string>,
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return { repos: parsed }
|
|
358
|
+
}
|
package/src/commands/metadata.ts
CHANGED
|
@@ -5,17 +5,17 @@ import {
|
|
|
5
5
|
DEFAULT_TELEMETRY_ORDER,
|
|
6
6
|
} from "../policies"
|
|
7
7
|
|
|
8
|
-
const SANDBOX_MAIN_HELP = `Sixb
|
|
8
|
+
const SANDBOX_MAIN_HELP = `Sixb CLI
|
|
9
9
|
|
|
10
10
|
Usage:
|
|
11
11
|
sixb <command> [options]
|
|
12
12
|
|
|
13
|
-
The CLI uses the run
|
|
13
|
+
The CLI uses the run's SIXB_API_BASE_URL, which adds its credentials. Do not configure authentication or another origin.
|
|
14
14
|
API commands emit JSON. Help, version, and examples emit text. Downloads write to --output and
|
|
15
15
|
emit a JSON receipt.
|
|
16
16
|
|
|
17
17
|
Discovery:
|
|
18
|
-
sixb
|
|
18
|
+
sixb status Check the sandbox and its API access
|
|
19
19
|
sixb context Print the current run context
|
|
20
20
|
sixb project show Show project metadata
|
|
21
21
|
sixb ontology list Compact visible type and relationship catalog
|
|
@@ -50,6 +50,13 @@ Actions and workflows:
|
|
|
50
50
|
sixb workflows list|get|start ...
|
|
51
51
|
sixb workflow-runs list|get ...
|
|
52
52
|
|
|
53
|
+
Data:
|
|
54
|
+
sixb datasets list|get|versions|rows ...
|
|
55
|
+
sixb syncs list|get ... Syncs you may run, each with its latest run
|
|
56
|
+
sixb sync-runs list ...
|
|
57
|
+
sixb pipelines list|get ... Pipelines you may run, each with its latest run
|
|
58
|
+
sixb pipeline-runs list|get ...
|
|
59
|
+
|
|
53
60
|
Files:
|
|
54
61
|
sixb files upload|download ...
|
|
55
62
|
|
|
@@ -63,28 +70,9 @@ A user parameter or property is {"type":"user","id":"<user-id>"}. Find the id wi
|
|
|
63
70
|
Run \`sixb <group> --help\` or \`sixb <group> <command> --help\` for exact arguments.
|
|
64
71
|
For query IR, run \`sixb objects query --help\` and \`sixb objects query --example list\`.`
|
|
65
72
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
"The CLI uses the selected local profile and its API credentials."
|
|
70
|
-
)
|
|
71
|
-
.replace(
|
|
72
|
-
" sixb doctor Check the sandbox and API gateway\n sixb context Print the current run context\n",
|
|
73
|
-
""
|
|
74
|
-
)
|
|
75
|
-
.replace(
|
|
76
|
-
"\nWhen you do not know a type's properties or links, read its file under $SIXB_ONTOLOGY_DIR. Use\n`sixb ontology get` for a type that has no file there.\n",
|
|
77
|
-
""
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
// The agent reaches models through its run's gateway once the model routes allow it.
|
|
81
|
-
.replace(
|
|
82
|
-
"Files:\n sixb files upload|download ...\n",
|
|
83
|
-
"Files:\n sixb files upload|download ...\n\nModels:\n sixb models list [--kind <kind>] Configured models of every kind, with prices\n sixb models get <model> One model's options and rate card\n sixb models text|image|speech|transcribe ... Call a model; see sixb models --help\n"
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
export function renderInstanceHelp(mode: "local" | "sandbox"): string {
|
|
87
|
-
return mode === "sandbox" ? SANDBOX_MAIN_HELP : LOCAL_MAIN_HELP
|
|
73
|
+
/** The sandbox CLI's help, which the agent's prompt includes. People's comes from @sixb/cli. */
|
|
74
|
+
export function renderInstanceHelp(): string {
|
|
75
|
+
return SANDBOX_MAIN_HELP
|
|
88
76
|
}
|
|
89
77
|
|
|
90
78
|
const SEARCH_DETAILS = `Without --type, search matches primary-id prefixes and full-text fields across visible types.
|
|
@@ -214,7 +202,7 @@ Vector profile names are the keys of the type's search.vectors. Vector input is
|
|
|
214
202
|
optionally under filters; k bounds the ranked matches and results carry a score.`
|
|
215
203
|
|
|
216
204
|
export const GROUP_HELP = {
|
|
217
|
-
|
|
205
|
+
status: "Usage: sixb status",
|
|
218
206
|
context: "Usage: sixb context",
|
|
219
207
|
project: "Usage: sixb project show",
|
|
220
208
|
ontology: `Usage:
|
|
@@ -256,7 +244,8 @@ List options:
|
|
|
256
244
|
files: `Usage:
|
|
257
245
|
sixb files upload <local-path> [--logical-path <path>]
|
|
258
246
|
sixb files download object <type> <id> --path <json-pointer> --output <local-path>
|
|
259
|
-
sixb files download action-run|workflow-run <run-id> --path <json-pointer> --output <local-path
|
|
247
|
+
sixb files download action-run|workflow-run <run-id> --path <json-pointer> --output <local-path>
|
|
248
|
+
sixb files download url <content-url> --output <local-path>`,
|
|
260
249
|
workflows: `Usage:
|
|
261
250
|
sixb workflows list
|
|
262
251
|
sixb workflows get <workflow-id>
|
|
@@ -267,14 +256,15 @@ for the user's approval prints \`"status": "awaiting_approval"\` and exits with
|
|
|
267
256
|
users: `Usage:
|
|
268
257
|
sixb users list [options]
|
|
269
258
|
sixb users search <text> [options]
|
|
270
|
-
sixb users get <user-id>
|
|
259
|
+
sixb users get <user-id|email>
|
|
271
260
|
|
|
272
261
|
Options:
|
|
273
262
|
--status active|suspended|all Defaults to active
|
|
274
263
|
--limit <1-${CLI_LIMITS.users.maximum}> Defaults to ${CLI_LIMITS.users.default}
|
|
275
264
|
--offset <n> Non-negative; defaults to 0
|
|
276
265
|
|
|
277
|
-
\`users search\` matches part of a display name or email, ignoring case
|
|
266
|
+
\`users search\` matches part of a display name or email, ignoring case; \`users get\` takes an
|
|
267
|
+
exact email, ignoring case, and finds suspended members too. Each member is
|
|
278
268
|
{"id","email","displayName","avatarUrl","status"}; a user parameter takes
|
|
279
269
|
{"type":"user","id":"<id>"}. Reading members requires can.view(project.users).`,
|
|
280
270
|
models: `Usage:
|
|
@@ -313,6 +303,56 @@ List options:
|
|
|
313
303
|
--limit <1-${CLI_LIMITS.list.maximum}> Defaults to ${CLI_LIMITS.list.default}
|
|
314
304
|
--offset <n> Non-negative; defaults to 0
|
|
315
305
|
--order <asc|desc> Started-time order; defaults to ${DEFAULT_LIST_ORDER}`,
|
|
306
|
+
datasets: `Usage:
|
|
307
|
+
sixb datasets list
|
|
308
|
+
sixb datasets get <dataset-id>
|
|
309
|
+
sixb datasets versions <dataset-id> [--limit <1-${CLI_LIMITS.datasetVersions.maximum}>]
|
|
310
|
+
sixb datasets rows <dataset-id> [options]
|
|
311
|
+
|
|
312
|
+
Rows options:
|
|
313
|
+
--version <version-id> Defaults to the latest version
|
|
314
|
+
--columns <a,b,...> Defaults to every column
|
|
315
|
+
--limit <1-${CLI_LIMITS.datasetRows.maximum}> Defaults to ${CLI_LIMITS.datasetRows.default}
|
|
316
|
+
--offset <n> Non-negative; defaults to 0
|
|
317
|
+
|
|
318
|
+
\`list\` and \`get\` show a dataset's schema, its latest version, and the syncs and pipelines you
|
|
319
|
+
may run that read or write it. \`versions\` lists the newest first. \`rows\` leaves out the
|
|
320
|
+
columns your clearances do not cover and names them under redactions.`,
|
|
321
|
+
syncs: `Usage:
|
|
322
|
+
sixb syncs list
|
|
323
|
+
sixb syncs get <sync-id>
|
|
324
|
+
|
|
325
|
+
Lists the syncs you may run, each with its latest run. \`sixb sync-runs list\` shows the history.`,
|
|
326
|
+
"sync-runs": `Usage:
|
|
327
|
+
sixb sync-runs list [options]
|
|
328
|
+
|
|
329
|
+
List options:
|
|
330
|
+
--sync <sync-id>
|
|
331
|
+
--dataset <dataset-id>
|
|
332
|
+
--status queued|running|succeeded|failed|cancelled
|
|
333
|
+
--started-after|--started-before <RFC3339>
|
|
334
|
+
--limit <1-${CLI_LIMITS.list.maximum}> Defaults to ${CLI_LIMITS.list.default}
|
|
335
|
+
--offset <n> Non-negative; defaults to 0
|
|
336
|
+
--order <asc|desc> Started-time order; defaults to ${DEFAULT_LIST_ORDER}`,
|
|
337
|
+
pipelines: `Usage:
|
|
338
|
+
sixb pipelines list
|
|
339
|
+
sixb pipelines get <pipeline-id>
|
|
340
|
+
|
|
341
|
+
Lists the pipelines you may run, each with its latest run. \`sixb pipeline-runs\` shows the
|
|
342
|
+
history and each run's steps.`,
|
|
343
|
+
"pipeline-runs": `Usage:
|
|
344
|
+
sixb pipeline-runs list [options]
|
|
345
|
+
sixb pipeline-runs get <run-id>
|
|
346
|
+
|
|
347
|
+
List options:
|
|
348
|
+
--pipeline <pipeline-id>
|
|
349
|
+
--status queued|running|succeeded|failed|cancelled
|
|
350
|
+
--started-after|--started-before <RFC3339>
|
|
351
|
+
--limit <1-${CLI_LIMITS.list.maximum}> Defaults to ${CLI_LIMITS.list.default}
|
|
352
|
+
--offset <n> Non-negative; defaults to 0
|
|
353
|
+
--order <asc|desc> Started-time order; defaults to ${DEFAULT_LIST_ORDER}
|
|
354
|
+
|
|
355
|
+
\`get\` includes the run's steps.`,
|
|
316
356
|
} as const
|
|
317
357
|
|
|
318
358
|
export const QUERY_EXAMPLES: Readonly<Record<string, string>> = {
|
package/src/commands/models.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { ApiClient } from "../api-client"
|
|
|
4
4
|
import { enumValue, integerInRange, isHelp, parseCommandArgs, requestsHelp } from "../arguments"
|
|
5
5
|
import { fail, writeJson, writeText } from "../output"
|
|
6
6
|
import { GROUP_HELP } from "./metadata"
|
|
7
|
-
import { readText } from "./shared"
|
|
7
|
+
import { idSegment, readText } from "./shared"
|
|
8
8
|
|
|
9
9
|
export const MODEL_KINDS = ["language", "image", "speech", "transcription"] as const
|
|
10
10
|
export type ModelKind = (typeof MODEL_KINDS)[number]
|
|
@@ -277,7 +277,7 @@ async function calls(api: ApiClient, args: readonly string[]): Promise<void> {
|
|
|
277
277
|
1
|
|
278
278
|
)
|
|
279
279
|
const callId = positionals[0] ?? ""
|
|
280
|
-
const call = (await api.get(`/api/ai/model-calls/${
|
|
280
|
+
const call = (await api.get(`/api/ai/model-calls/${idSegment(callId)}/files`)) as {
|
|
281
281
|
readonly callId: string
|
|
282
282
|
readonly files: readonly FileRef[]
|
|
283
283
|
}
|
|
@@ -390,7 +390,7 @@ async function saveFiles(
|
|
|
390
390
|
})
|
|
391
391
|
for (const [index, path] of paths.entries()) {
|
|
392
392
|
await mkdir(dirname(path), { recursive: true })
|
|
393
|
-
await api.download(`/api/ai/model-calls/${
|
|
393
|
+
await api.download(`/api/ai/model-calls/${idSegment(callId)}/files/content`, path, {
|
|
394
394
|
path: `/files/${index}`,
|
|
395
395
|
})
|
|
396
396
|
}
|
package/src/commands/ontology.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ApiClient } from "../api-client"
|
|
|
2
2
|
import { isHelp, parseCommandArgs, requestsHelp } from "../arguments"
|
|
3
3
|
import { fail, writeJson, writeText } from "../output"
|
|
4
4
|
import { GROUP_HELP } from "./metadata"
|
|
5
|
-
import { asRecord, asRecords, vectorProfileNames } from "./shared"
|
|
5
|
+
import { asRecord, asRecords, idSegment, vectorProfileNames } from "./shared"
|
|
6
6
|
|
|
7
7
|
export async function ontology(api: ApiClient, args: readonly string[]): Promise<void> {
|
|
8
8
|
const [sub, ...rest] = args
|
|
@@ -46,7 +46,7 @@ export async function ontology(api: ApiClient, args: readonly string[]): Promise
|
|
|
46
46
|
if (sub === "get") {
|
|
47
47
|
if (requestsHelp(rest)) return writeText("Usage: sixb ontology get <object-type>")
|
|
48
48
|
const { positionals } = parseCommandArgs(rest, {}, "ontology get", 1)
|
|
49
|
-
return writeJson(await api.get(`/api/object-types/${
|
|
49
|
+
return writeJson(await api.get(`/api/object-types/${idSegment(positionals[0])}`))
|
|
50
50
|
}
|
|
51
51
|
fail(`Unknown ontology command '${sub}'.`)
|
|
52
52
|
}
|
package/src/commands/runs.ts
CHANGED
|
@@ -21,16 +21,33 @@ const WORKFLOW_RUN_STATUSES = [
|
|
|
21
21
|
"failed",
|
|
22
22
|
"cancelled",
|
|
23
23
|
] as const
|
|
24
|
+
const DATA_RUN_STATUSES = ["queued", "running", "succeeded", "failed", "cancelled"] as const
|
|
25
|
+
|
|
26
|
+
const STATUSES = {
|
|
27
|
+
action: ACTION_RUN_STATUSES,
|
|
28
|
+
workflow: WORKFLOW_RUN_STATUSES,
|
|
29
|
+
sync: DATA_RUN_STATUSES,
|
|
30
|
+
pipeline: DATA_RUN_STATUSES,
|
|
31
|
+
} as const
|
|
32
|
+
|
|
33
|
+
/** The list filters each kind adds to the common window, by flag and query parameter. */
|
|
34
|
+
const FILTERS = {
|
|
35
|
+
action: { "--action": "actionId", "--type": "objectTypeId", "--id": "primaryId" },
|
|
36
|
+
workflow: { "--workflow": "workflowId" },
|
|
37
|
+
sync: { "--sync": "syncId", "--dataset": "datasetId" },
|
|
38
|
+
pipeline: { "--pipeline": "pipelineId" },
|
|
39
|
+
} as const
|
|
24
40
|
|
|
25
41
|
export async function runs(
|
|
26
42
|
api: ApiClient,
|
|
27
|
-
kind:
|
|
43
|
+
kind: keyof typeof STATUSES,
|
|
28
44
|
args: readonly string[]
|
|
29
45
|
): Promise<void> {
|
|
30
46
|
const [sub, ...rest] = args
|
|
31
|
-
const group = `${kind}-runs` as
|
|
47
|
+
const group = `${kind}-runs` as const
|
|
32
48
|
if (!sub || isHelp(sub) || requestsHelp(rest)) return writeText(GROUP_HELP[group])
|
|
33
|
-
|
|
49
|
+
// The API lists sync runs but has no route for one.
|
|
50
|
+
if (sub === "get" && kind !== "sync") {
|
|
34
51
|
const { positionals } = parseCommandArgs(rest, {}, `${group} get`, 1)
|
|
35
52
|
return writeJson(await api.get(`/api/${group}/${encodeURIComponent(positionals[0] ?? "")}`))
|
|
36
53
|
}
|
|
@@ -43,15 +60,8 @@ export async function runs(
|
|
|
43
60
|
"--offset": "offset",
|
|
44
61
|
"--order": "order",
|
|
45
62
|
}
|
|
46
|
-
const action = {
|
|
47
|
-
...common,
|
|
48
|
-
"--action": "actionId",
|
|
49
|
-
"--type": "objectTypeId",
|
|
50
|
-
"--id": "primaryId",
|
|
51
|
-
}
|
|
52
|
-
const workflow = { ...common, "--workflow": "workflowId" }
|
|
53
63
|
const options = normalizeWindowOptions(
|
|
54
|
-
parseQueryOptions(rest,
|
|
64
|
+
parseQueryOptions(rest, { ...common, ...FILTERS[kind] }, `${group} list`),
|
|
55
65
|
{
|
|
56
66
|
defaultLimit: CLI_LIMITS.list.default,
|
|
57
67
|
maximumLimit: CLI_LIMITS.list.maximum,
|
|
@@ -60,11 +70,7 @@ export async function runs(
|
|
|
60
70
|
}
|
|
61
71
|
)
|
|
62
72
|
if (options.status !== undefined) {
|
|
63
|
-
options.status = enumValue(
|
|
64
|
-
"--status",
|
|
65
|
-
options.status,
|
|
66
|
-
kind === "action" ? ACTION_RUN_STATUSES : WORKFLOW_RUN_STATUSES
|
|
67
|
-
)
|
|
73
|
+
options.status = enumValue("--status", options.status, STATUSES[kind])
|
|
68
74
|
}
|
|
69
75
|
for (const [name, flag] of [
|
|
70
76
|
["startedAfter", "--started-after"],
|
package/src/commands/shared.ts
CHANGED
|
@@ -4,6 +4,12 @@ import { fail } from "../output"
|
|
|
4
4
|
|
|
5
5
|
export type QueryOptions = Record<string, string | undefined>
|
|
6
6
|
|
|
7
|
+
/** An id as one segment of an API path: `.` or `..` would name another route. */
|
|
8
|
+
export function idSegment(value: string | undefined): string {
|
|
9
|
+
if (!value || value === "." || value === "..") fail(`'${value ?? ""}' is not an id.`)
|
|
10
|
+
return encodeURIComponent(value)
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
/** Parse flag/value pairs into API query parameters. */
|
|
8
14
|
|
|
9
15
|
export function parseQueryOptions(
|