@sixb/cli-core 0.1.8 → 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.
Files changed (71) hide show
  1. package/README.md +21 -16
  2. package/dist/api-client.d.ts +3 -1
  3. package/dist/api-client.d.ts.map +1 -1
  4. package/dist/arguments.d.ts +4 -2
  5. package/dist/arguments.d.ts.map +1 -1
  6. package/dist/bin/sixb +26 -0
  7. package/dist/commands/data.d.ts +5 -0
  8. package/dist/commands/data.d.ts.map +1 -0
  9. package/dist/commands/files.d.ts.map +1 -1
  10. package/dist/commands/index.d.ts +1 -1
  11. package/dist/commands/index.d.ts.map +1 -1
  12. package/dist/commands/memory.d.ts +3 -0
  13. package/dist/commands/memory.d.ts.map +1 -0
  14. package/dist/commands/metadata.d.ts +14 -5
  15. package/dist/commands/metadata.d.ts.map +1 -1
  16. package/dist/commands/models.d.ts +79 -0
  17. package/dist/commands/models.d.ts.map +1 -0
  18. package/dist/commands/objects.d.ts.map +1 -1
  19. package/dist/commands/runs.d.ts +8 -1
  20. package/dist/commands/runs.d.ts.map +1 -1
  21. package/dist/commands/shared.d.ts +4 -0
  22. package/dist/commands/shared.d.ts.map +1 -1
  23. package/dist/commands/system.d.ts +21 -0
  24. package/dist/commands/system.d.ts.map +1 -0
  25. package/dist/commands/users.d.ts +3 -0
  26. package/dist/commands/users.d.ts.map +1 -0
  27. package/dist/commands/workflows.d.ts.map +1 -1
  28. package/dist/generated/sixb.mjs +2547 -0
  29. package/dist/index.d.ts +8 -3
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +800 -62
  32. package/dist/index.js.map +24 -18
  33. package/dist/main.d.ts +3 -0
  34. package/dist/main.d.ts.map +1 -0
  35. package/dist/memory.d.ts +21 -0
  36. package/dist/memory.d.ts.map +1 -0
  37. package/dist/output.d.ts +12 -0
  38. package/dist/output.d.ts.map +1 -1
  39. package/dist/policies.d.ts +14 -0
  40. package/dist/policies.d.ts.map +1 -1
  41. package/dist/run.d.ts +1 -0
  42. package/dist/run.d.ts.map +1 -1
  43. package/dist/sandbox-cli.d.ts +8 -0
  44. package/dist/sandbox-cli.d.ts.map +1 -0
  45. package/package.json +15 -1
  46. package/src/api-client.ts +83 -4
  47. package/src/arguments.ts +17 -6
  48. package/src/bin/sixb +26 -0
  49. package/src/commands/actions.ts +5 -5
  50. package/src/commands/data.ts +90 -0
  51. package/src/commands/files.ts +27 -3
  52. package/src/commands/index.ts +24 -0
  53. package/src/commands/memory.ts +358 -0
  54. package/src/commands/metadata.ts +137 -26
  55. package/src/commands/models.ts +508 -0
  56. package/src/commands/objects.ts +47 -2
  57. package/src/commands/ontology.ts +2 -2
  58. package/src/commands/runs.ts +22 -16
  59. package/src/commands/shared.ts +17 -7
  60. package/src/commands/system.ts +67 -0
  61. package/src/commands/telemetry.ts +2 -2
  62. package/src/commands/users.ts +87 -0
  63. package/src/commands/workflows.ts +12 -6
  64. package/src/generated/sixb.mjs +2547 -0
  65. package/src/index.ts +12 -3
  66. package/src/main.ts +39 -0
  67. package/src/memory.ts +30 -0
  68. package/src/output.ts +21 -0
  69. package/src/policies.ts +5 -0
  70. package/src/run.ts +3 -2
  71. package/src/sandbox-cli.ts +18 -0
@@ -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(
@@ -55,13 +61,7 @@ export function singleFileOption(args: readonly string[], command: string): stri
55
61
  }
56
62
 
57
63
  export async function readJson(source: string): Promise<unknown> {
58
- let text: string
59
- try {
60
- text = source === "-" ? await readStdin() : await readFile(source, "utf8")
61
- } catch (error) {
62
- if (isFileError(error, "ENOENT")) fail(`JSON file '${source}' does not exist.`)
63
- throw error
64
- }
64
+ const text = await readText(source, "JSON file")
65
65
  try {
66
66
  return JSON.parse(text)
67
67
  } catch {
@@ -74,6 +74,16 @@ export async function readJson(source: string): Promise<unknown> {
74
74
  }
75
75
  }
76
76
 
77
+ /** A file's text, or standard input's when the source is `-`. */
78
+ export async function readText(source: string, noun: string): Promise<string> {
79
+ try {
80
+ return source === "-" ? await readStdin() : await readFile(source, "utf8")
81
+ } catch (error) {
82
+ if (isFileError(error, "ENOENT")) fail(`${noun} '${source}' does not exist.`)
83
+ throw error
84
+ }
85
+ }
86
+
77
87
  export function asRecord(value: unknown): Record<string, unknown> {
78
88
  return typeof value === "object" && value !== null && !Array.isArray(value)
79
89
  ? (value as Record<string, unknown>)
@@ -0,0 +1,67 @@
1
+ import { readFile } from "node:fs/promises"
2
+ import { isHelp } from "../arguments"
3
+ import { CliError, EXIT_API, fail, INSTANCE_CLI_VERSION, writeJson, writeText } from "../output"
4
+ import { createInstanceApiClient, type SandboxInstanceCliMode } from "../run"
5
+ import { asRecord, isFileError } from "./shared"
6
+
7
+ /** The sandbox runtime `sixb status` reports and the agent worker's preflight expects. */
8
+ export const AGENT_RUNTIME_PROFILE = "sixb-agent-runtime/v1" as const
9
+
10
+ /** Stable JSON emitted by `sixb status` in a sandbox and consumed by worker preflight. */
11
+ export interface SandboxStatusReport {
12
+ readonly ok: true
13
+ readonly profile: typeof AGENT_RUNTIME_PROFILE
14
+ readonly cli: { readonly version: string }
15
+ readonly javascript: {
16
+ readonly name: "bun" | "node"
17
+ readonly version: string
18
+ }
19
+ readonly project: { readonly id: string }
20
+ }
21
+
22
+ export async function status(args: readonly string[], mode: SandboxInstanceCliMode): Promise<void> {
23
+ if (isHelp(args[0])) return writeText("Usage: sixb status")
24
+ // Its output is JSON either way: --json, which people's `sixb status` takes, changes nothing.
25
+ if (args.some((arg) => arg !== "--json")) fail("status accepts no arguments but --json.")
26
+ const project = asRecord(await createInstanceApiClient(mode).get("/api/project"))
27
+ if (typeof project.id !== "string" || project.id.length === 0) {
28
+ throw new CliError(
29
+ { code: "invalid_api_response", message: "The Sixb API returned an invalid project." },
30
+ EXIT_API
31
+ )
32
+ }
33
+ const report: SandboxStatusReport = {
34
+ ok: true,
35
+ profile: AGENT_RUNTIME_PROFILE,
36
+ cli: { version: INSTANCE_CLI_VERSION },
37
+ javascript: javascriptRuntime(),
38
+ project: { id: project.id },
39
+ }
40
+ writeJson(report)
41
+ }
42
+
43
+ export async function context(args: readonly string[]): Promise<void> {
44
+ if (isHelp(args[0])) return writeText("Usage: sixb context")
45
+ if (args.length !== 0) fail("context accepts no arguments.")
46
+ const path = process.env.SIXB_RUN_CONTEXT
47
+ if (!path) fail("SIXB_RUN_CONTEXT is not set.")
48
+ let text: string
49
+ try {
50
+ text = await readFile(path, "utf8")
51
+ } catch (error) {
52
+ if (isFileError(error, "ENOENT")) fail(`Run context '${path}' does not exist.`)
53
+ throw error
54
+ }
55
+ try {
56
+ writeJson(JSON.parse(text))
57
+ } catch {
58
+ fail(`Run context '${path}' is not valid JSON.`, "invalid_json")
59
+ }
60
+ }
61
+
62
+ function javascriptRuntime(): SandboxStatusReport["javascript"] {
63
+ if (typeof globalThis.Bun === "object") {
64
+ return { name: "bun", version: globalThis.Bun.version }
65
+ }
66
+ return { name: "node", version: process.versions.node }
67
+ }
@@ -11,7 +11,7 @@ import {
11
11
  import { fail, writeJson, writeText } from "../output"
12
12
  import { CLI_LIMITS, DEFAULT_TELEMETRY_ORDER } from "../policies"
13
13
  import { GROUP_HELP } from "./metadata"
14
- import { normalizeWindowOptions, readJson, singleFileOption } from "./shared"
14
+ import { idSegment, normalizeWindowOptions, readJson, singleFileOption } from "./shared"
15
15
 
16
16
  export async function telemetry(api: ApiClient, args: readonly string[]): Promise<void> {
17
17
  const [sub, ...rest] = args
@@ -62,7 +62,7 @@ export async function telemetry(api: ApiClient, args: readonly string[]): Promis
62
62
  }
63
63
 
64
64
  function telemetryPath(args: readonly string[], terminal: "latest" | "history"): string {
65
- return `/api/objects/${encodeURIComponent(args[0] ?? "")}/${encodeURIComponent(args[1] ?? "")}/telemetry/${encodeURIComponent(args[2] ?? "")}/${terminal}`
65
+ return `/api/objects/${idSegment(args[0])}/${idSegment(args[1])}/telemetry/${idSegment(args[2])}/${terminal}`
66
66
  }
67
67
 
68
68
  function normalizeTelemetryQueryInput(input: unknown): Record<string, unknown> {
@@ -0,0 +1,87 @@
1
+ import type { ApiClient } from "../api-client"
2
+ import {
3
+ enumValue,
4
+ integerInRange,
5
+ isHelp,
6
+ nonNegativeInteger,
7
+ parseCommandArgs,
8
+ requestsHelp,
9
+ } from "../arguments"
10
+ import { CliError, EXIT_API, fail, writeJson, writeText } from "../output"
11
+ import { CLI_LIMITS } from "../policies"
12
+ import { GROUP_HELP } from "./metadata"
13
+ import type { QueryOptions } from "./shared"
14
+ import { idSegment } from "./shared"
15
+
16
+ const USER_STATUSES = ["active", "suspended", "all"] as const
17
+
18
+ const LIST_FLAGS = {
19
+ "--status": "string",
20
+ "--limit": "string",
21
+ "--offset": "string",
22
+ } as const
23
+
24
+ export async function users(api: ApiClient, args: readonly string[]): Promise<void> {
25
+ const [sub, ...rest] = args
26
+ if (!sub || isHelp(sub) || requestsHelp(rest)) return writeText(GROUP_HELP.users)
27
+
28
+ if (sub === "get") {
29
+ const { positionals } = parseCommandArgs(rest, {}, "users get", 1)
30
+ const user = positionals[0]?.trim()
31
+ if (!user) fail("users get requires a user id or email.")
32
+ // Sign-in mints ids as `usr_<uuid>`, so an `@` marks an email.
33
+ if (!user.includes("@")) {
34
+ return writeJson(await api.get(`/api/users/${idSegment(user)}`))
35
+ }
36
+
37
+ const page = await api.get("/api/users", { email: user })
38
+ const [member] = isRecord(page) && Array.isArray(page.users) ? page.users : []
39
+ if (member === undefined) {
40
+ throw new CliError(
41
+ { code: "not_found", message: `No member has the email '${user}'.` },
42
+ EXIT_API
43
+ )
44
+ }
45
+ return writeJson(member)
46
+ }
47
+
48
+ if (sub === "list") {
49
+ const { options } = parseCommandArgs(rest, LIST_FLAGS, "users list")
50
+ return writeJson(await api.get("/api/users", listQuery(options)))
51
+ }
52
+
53
+ if (sub === "search") {
54
+ const { positionals, options } = parseCommandArgs(rest, LIST_FLAGS, "users search", 1)
55
+ const search = positionals[0]?.trim()
56
+ if (!search) fail("users search requires the text to find in a name or email.")
57
+
58
+ return writeJson(await api.get("/api/users", { ...listQuery(options), search }))
59
+ }
60
+
61
+ fail(`Unknown users command '${sub}'.`)
62
+ }
63
+
64
+ function listQuery(options: {
65
+ readonly "--status"?: string
66
+ readonly "--limit"?: string
67
+ readonly "--offset"?: string
68
+ }): QueryOptions {
69
+ const limit = integerInRange(
70
+ "--limit",
71
+ options["--limit"] ?? String(CLI_LIMITS.users.default),
72
+ 1,
73
+ CLI_LIMITS.users.maximum
74
+ )
75
+
76
+ return {
77
+ status: enumValue("--status", options["--status"] ?? "active", USER_STATUSES),
78
+ limit: String(limit),
79
+ ...(options["--offset"] === undefined
80
+ ? {}
81
+ : { offset: String(nonNegativeInteger("--offset", options["--offset"])) }),
82
+ }
83
+ }
84
+
85
+ function isRecord(value: unknown): value is Record<string, unknown> {
86
+ return typeof value === "object" && value !== null && !Array.isArray(value)
87
+ }
@@ -1,8 +1,9 @@
1
+ import { randomUUID } from "node:crypto"
1
2
  import type { ApiClient } from "../api-client"
2
3
  import { isHelp, parseCommandArgs, requestsHelp } from "../arguments"
3
- import { fail, writeJson, writeText } from "../output"
4
+ import { fail, writeJson, writeRequestResult, writeText } from "../output"
4
5
  import { GROUP_HELP } from "./metadata"
5
- import { readJson } from "./shared"
6
+ import { idSegment, readJson } from "./shared"
6
7
 
7
8
  export async function workflows(api: ApiClient, args: readonly string[]): Promise<void> {
8
9
  const [sub, ...rest] = args
@@ -15,19 +16,24 @@ export async function workflows(api: ApiClient, args: readonly string[]): Promis
15
16
  const {
16
17
  positionals: [workflowId],
17
18
  } = parseCommandArgs(rest, {}, "workflows get", 1)
18
- return writeJson(await api.get(`/api/workflows/${encodeURIComponent(workflowId ?? "")}`))
19
+ return writeJson(await api.get(`/api/workflows/${idSegment(workflowId)}`))
19
20
  }
20
21
  if (sub === "start") {
21
22
  const {
22
23
  positionals: [workflowId],
23
24
  options,
24
- } = parseCommandArgs(rest, { "--file": "string" }, "workflows start", 1)
25
+ } = parseCommandArgs(rest, { "--file": "string", "--run-id": "string" }, "workflows start", 1)
25
26
  const input: unknown = options["--file"] ? await readJson(options["--file"]) : {}
26
27
  if (Array.isArray(input) || typeof input !== "object" || input === null) {
27
28
  fail("Workflow input must be a JSON object.")
28
29
  }
29
- return writeJson(
30
- await api.post(`/api/workflows/${encodeURIComponent(workflowId ?? "")}/runs`, { input })
30
+ // Every start names its run, so a repeated request returns the same run instead of another.
31
+ const runId = options["--run-id"] ?? `run_${randomUUID()}`
32
+ return writeRequestResult(
33
+ await api.post(`/api/workflows/${idSegment(workflowId)}/runs`, {
34
+ input,
35
+ runId,
36
+ })
31
37
  )
32
38
  }
33
39
  fail(`Unknown workflows command '${sub}'.`)