@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.
Files changed (62) hide show
  1. package/README.md +21 -16
  2. package/dist/api-client.d.ts +1 -0
  3. package/dist/api-client.d.ts.map +1 -1
  4. package/dist/bin/sixb +26 -0
  5. package/dist/commands/data.d.ts +5 -0
  6. package/dist/commands/data.d.ts.map +1 -0
  7. package/dist/commands/files.d.ts.map +1 -1
  8. package/dist/commands/history.d.ts +7 -0
  9. package/dist/commands/history.d.ts.map +1 -0
  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 +11 -4
  15. package/dist/commands/metadata.d.ts.map +1 -1
  16. package/dist/commands/runs.d.ts +8 -1
  17. package/dist/commands/runs.d.ts.map +1 -1
  18. package/dist/commands/shared.d.ts +2 -0
  19. package/dist/commands/shared.d.ts.map +1 -1
  20. package/dist/commands/system.d.ts +21 -0
  21. package/dist/commands/system.d.ts.map +1 -0
  22. package/dist/commands/users.d.ts.map +1 -1
  23. package/dist/generated/sixb.mjs +2658 -0
  24. package/dist/index.d.ts +8 -3
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +298 -58
  27. package/dist/index.js.map +21 -17
  28. package/dist/main.d.ts +3 -0
  29. package/dist/main.d.ts.map +1 -0
  30. package/dist/memory.d.ts +21 -0
  31. package/dist/memory.d.ts.map +1 -0
  32. package/dist/policies.d.ts +14 -0
  33. package/dist/policies.d.ts.map +1 -1
  34. package/dist/run.d.ts +1 -0
  35. package/dist/run.d.ts.map +1 -1
  36. package/dist/sandbox-cli.d.ts +8 -0
  37. package/dist/sandbox-cli.d.ts.map +1 -0
  38. package/package.json +15 -1
  39. package/src/api-client.ts +70 -2
  40. package/src/bin/sixb +26 -0
  41. package/src/commands/actions.ts +3 -3
  42. package/src/commands/data.ts +90 -0
  43. package/src/commands/files.ts +27 -3
  44. package/src/commands/history.ts +204 -0
  45. package/src/commands/index.ts +16 -0
  46. package/src/commands/memory.ts +358 -0
  47. package/src/commands/metadata.ts +76 -29
  48. package/src/commands/models.ts +3 -3
  49. package/src/commands/ontology.ts +2 -2
  50. package/src/commands/runs.ts +22 -16
  51. package/src/commands/shared.ts +6 -0
  52. package/src/commands/system.ts +67 -0
  53. package/src/commands/telemetry.ts +2 -2
  54. package/src/commands/users.ts +22 -2
  55. package/src/commands/workflows.ts +3 -3
  56. package/src/generated/sixb.mjs +2658 -0
  57. package/src/index.ts +12 -3
  58. package/src/main.ts +41 -0
  59. package/src/memory.ts +30 -0
  60. package/src/policies.ts +5 -0
  61. package/src/run.ts +3 -2
  62. package/src/sandbox-cli.ts +18 -0
package/dist/main.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=main.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":""}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The Agent's memory as installed in its sandbox: the worker writes each repo under
3
+ * `$SIXB_MEMORY_DIR/<repo>/` with a manifest of what it loaded, and `sixb memory` saves the files
4
+ * that differ from it.
5
+ */
6
+ export type AgentMemoryRepo = "user" | "project";
7
+ /** Where the manifest is, relative to `$SIXB_CONTEXT_DIR`. */
8
+ export declare const AGENT_MEMORY_MANIFEST = "context/memory.json";
9
+ export declare const AGENT_MEMORY_ENTRY = "MEMORY.md";
10
+ /** What a repo starts from before its first save; it is saved with the first save. */
11
+ export declare const AGENT_MEMORY_STARTER = "# Memory\n\n";
12
+ /** What the local copy was loaded from: the revision and each file's SHA-256, by path. */
13
+ export interface AgentMemoryManifest {
14
+ readonly repos: Partial<Record<AgentMemoryRepo, AgentMemoryManifestRepo>>;
15
+ }
16
+ export interface AgentMemoryManifestRepo {
17
+ readonly revision: number;
18
+ readonly files: Readonly<Record<string, string>>;
19
+ }
20
+ export declare function agentMemoryFileHash(contents: string): string;
21
+ //# sourceMappingURL=memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,SAAS,CAAA;AAEhD,8DAA8D;AAC9D,eAAO,MAAM,qBAAqB,wBAAwB,CAAA;AAE1D,eAAO,MAAM,kBAAkB,cAAc,CAAA;AAE7C,sFAAsF;AACtF,eAAO,MAAM,oBAAoB,iBAAiB,CAAA;AAElD,0FAA0F;AAC1F,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,uBAAuB,CAAC,CAAC,CAAA;CAC1E;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACjD;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE5D"}
@@ -25,6 +25,20 @@ export declare const CLI_LIMITS: {
25
25
  readonly default: 20;
26
26
  readonly maximum: 200;
27
27
  };
28
+ /** A page of a conversation's messages, as `sixb history` prints them. */
29
+ readonly history: {
30
+ readonly default: 20;
31
+ readonly maximum: 100;
32
+ };
33
+ /** The server's dataset bounds: 100 versions and 1,000 rows per page. */
34
+ readonly datasetVersions: {
35
+ readonly default: 20;
36
+ readonly maximum: 100;
37
+ };
38
+ readonly datasetRows: {
39
+ readonly default: 100;
40
+ readonly maximum: 1000;
41
+ };
28
42
  readonly inspect: {
29
43
  readonly depth: {
30
44
  readonly default: 2;
@@ -1 +1 @@
1
- {"version":3,"file":"policies.d.ts","sourceRoot":"","sources":["../src/policies.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,eAAO,MAAM,UAAU;;;;;;;;;IAGrB,+FAA+F;;;;;;;;;;;;IAI/F,qDAAqD;;;;;;;;;;;;;;;;;;;;CAQ7C,CAAA;AAEV,eAAO,MAAM,kBAAkB,EAAG,MAAe,CAAA;AACjD,eAAO,MAAM,uBAAuB,EAAG,WAAoB,CAAA;AAC3D,eAAO,MAAM,uBAAuB,EAAG,MAAe,CAAA"}
1
+ {"version":3,"file":"policies.d.ts","sourceRoot":"","sources":["../src/policies.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,eAAO,MAAM,UAAU;;;;;;;;;IAGrB,+FAA+F;;;;;;;;;;;;IAI/F,qDAAqD;;;;;IAErD,0EAA0E;;;;;IAE1E,yEAAyE;;;;;;;;;;;;;;;;;;;;;;;;CASjE,CAAA;AAEV,eAAO,MAAM,kBAAkB,EAAG,MAAe,CAAA;AACjD,eAAO,MAAM,uBAAuB,EAAG,WAAoB,CAAA;AAC3D,eAAO,MAAM,uBAAuB,EAAG,MAAe,CAAA"}
package/dist/run.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { ApiClient } from "./api-client";
2
2
  export interface SandboxInstanceCliMode {
3
3
  readonly kind: "sandbox";
4
+ /** The sandbox's credentialed address for the API: the run's token is added on the way out. */
4
5
  readonly baseUrl: string;
5
6
  readonly runContextPath: string;
6
7
  }
package/dist/run.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAGxC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;CAChC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,MAAM,eAAe,GAAG,sBAAsB,GAAG,oBAAoB,CAAA;AAE3E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAA;CAC/B;AAED,wBAAsB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAG9E;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,eAAe,GAAG,SAAS,CAiBxE"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAGxC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;IACxB,+FAA+F;IAC/F,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;CAChC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,MAAM,eAAe,GAAG,sBAAsB,GAAG,oBAAoB,CAAA;AAE3E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAA;CAC/B;AAED,wBAAsB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAG9E;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,eAAe,GAAG,SAAS,CAiBxE"}
@@ -0,0 +1,8 @@
1
+ /** The `sixb` a sandbox installs: a shell launcher and the bundle it runs on Bun or Node. */
2
+ export interface SandboxCliAssets {
3
+ readonly launcher: string;
4
+ readonly artifact: Uint8Array;
5
+ }
6
+ /** Read from beside this module: `src/` in the workspace, `dist/` once built. */
7
+ export declare function loadSandboxCliAssets(): Promise<SandboxCliAssets>;
8
+ //# sourceMappingURL=sandbox-cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox-cli.d.ts","sourceRoot":"","sources":["../src/sandbox-cli.ts"],"names":[],"mappings":"AAEA,6FAA6F;AAC7F,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAA;CAC9B;AAID,iFAAiF;AACjF,wBAAgB,oBAAoB,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAMhE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sixb/cli-core",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
4
4
  "description": "Shared instance command runtime for Sixb CLIs",
5
5
  "keywords": [
6
6
  "sixb",
@@ -29,10 +29,24 @@
29
29
  },
30
30
  "scripts": {
31
31
  "build": "bun ../../scripts/build-package.ts",
32
+ "generate:sandbox-cli": "bun ./scripts/generate-sandbox-cli.ts",
33
+ "check:sandbox-cli": "bun ./scripts/generate-sandbox-cli.ts --check",
32
34
  "typecheck": "tsc --noEmit",
33
35
  "test": "bun test",
34
36
  "prepack": "bun run build"
35
37
  },
38
+ "sixbBuild": {
39
+ "assets": [
40
+ {
41
+ "from": "./src/bin",
42
+ "to": "./dist/bin"
43
+ },
44
+ {
45
+ "from": "./src/generated",
46
+ "to": "./dist/generated"
47
+ }
48
+ ]
49
+ },
36
50
  "devDependencies": {
37
51
  "@types/bun": "latest",
38
52
  "typescript": "^5.7.0"
package/src/api-client.ts CHANGED
@@ -1,21 +1,33 @@
1
1
  import { randomUUID } from "node:crypto"
2
2
  import { createWriteStream } from "node:fs"
3
3
  import { readFile, rename, rm } from "node:fs/promises"
4
+ import { request as httpRequest } from "node:http"
4
5
  import { basename, dirname, join } from "node:path"
5
6
  import { Readable } from "node:stream"
6
7
  import { pipeline } from "node:stream/promises"
7
8
  import type { ReadableStream as NodeReadableStream } from "node:stream/web"
8
9
  import { CliError, EXIT_API, fail } from "./output"
9
10
 
11
+ /**
12
+ * A base URL of `unix:<socket path>` reaches the API through a local socket, such as the one a
13
+ * sandbox's credential forwarder listens on: requests go to `http://localhost` over that socket.
14
+ */
15
+ const UNIX_BASE_URL_PREFIX = "unix:"
16
+
10
17
  export class ApiClient {
11
18
  readonly baseUrl: string
19
+ private readonly socketPath: string | undefined
12
20
  private readonly authorization: string | undefined
13
21
  private readonly missingBaseUrlMessage: string
14
22
  private readonly unavailableMessage: string
15
23
  private readonly unavailableHint: string
16
24
 
17
25
  constructor(options: ApiClientOptions) {
18
- this.baseUrl = options.baseUrl.replace(/\/+$/, "")
26
+ const baseUrl = options.baseUrl.replace(/\/+$/, "")
27
+ this.socketPath = baseUrl.startsWith(UNIX_BASE_URL_PREFIX)
28
+ ? baseUrl.slice(UNIX_BASE_URL_PREFIX.length)
29
+ : undefined
30
+ this.baseUrl = this.socketPath === undefined ? baseUrl : "http://localhost"
19
31
  this.authorization = options.authorization
20
32
  this.missingBaseUrlMessage = options.missingBaseUrlMessage
21
33
  this.unavailableMessage = options.unavailableMessage
@@ -107,7 +119,7 @@ export class ApiClient {
107
119
  try {
108
120
  const headers = new Headers(init.headers)
109
121
  if (this.authorization) headers.set("authorization", this.authorization)
110
- response = await fetch(url, { ...init, headers })
122
+ response = await send(url, { ...init, headers }, this.socketPath)
111
123
  } catch (error) {
112
124
  if (init.signal?.aborted) throw error
113
125
  throw new CliError(
@@ -193,3 +205,59 @@ function stringField(record: Readonly<Record<string, unknown>>, key: string): st
193
205
  const value = record[key]
194
206
  return typeof value === "string" ? value : undefined
195
207
  }
208
+
209
+ /** One request, over a Unix socket when the API is behind one. */
210
+ function send(url: URL, init: RequestInit, socketPath: string | undefined): Promise<Response> {
211
+ if (socketPath === undefined) return fetch(url, init)
212
+ // Bun's fetch speaks over a Unix socket itself; Node's does not, so it goes through node:http.
213
+ if (typeof Bun !== "undefined") return fetch(url, { ...init, unix: socketPath })
214
+ return sendOverNodeSocket(url, init, socketPath)
215
+ }
216
+
217
+ async function sendOverNodeSocket(
218
+ url: URL,
219
+ init: RequestInit,
220
+ socketPath: string
221
+ ): Promise<Response> {
222
+ // A Request settles the body and its content type (a form's boundary included) as fetch would.
223
+ const outgoing = new Request(url, init)
224
+ const body = outgoing.body ? Buffer.from(await outgoing.arrayBuffer()) : undefined
225
+ const headers: Record<string, string> = Object.fromEntries(outgoing.headers)
226
+ if (body) headers["content-length"] = String(body.byteLength)
227
+
228
+ return new Promise((resolve, reject) => {
229
+ const request = httpRequest(
230
+ {
231
+ socketPath,
232
+ method: outgoing.method,
233
+ path: `${url.pathname}${url.search}`,
234
+ headers: { ...headers, host: "localhost" },
235
+ ...(init.signal ? { signal: init.signal } : {}),
236
+ },
237
+ (response) => {
238
+ const responseHeaders = new Headers()
239
+ for (const [name, value] of Object.entries(response.headers)) {
240
+ for (const each of Array.isArray(value) ? value : value === undefined ? [] : [value]) {
241
+ responseHeaders.append(name, each)
242
+ }
243
+ }
244
+ const status = response.statusCode ?? 502
245
+ const empty = status === 204 || status === 304 || outgoing.method === "HEAD"
246
+ resolve(
247
+ new Response(empty ? null : nodeBody(response), {
248
+ status,
249
+ statusText: response.statusMessage ?? "",
250
+ headers: responseHeaders,
251
+ })
252
+ )
253
+ }
254
+ )
255
+ request.on("error", reject)
256
+ request.end(body)
257
+ })
258
+ }
259
+
260
+ /** The response body as a web stream: Node's and the global stream types differ only in typing. */
261
+ function nodeBody(response: Readable): ReadableStream<Uint8Array> {
262
+ return Readable.toWeb(response) as unknown as ReadableStream<Uint8Array>
263
+ }
package/src/bin/sixb ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env sh
2
+
3
+ set -eu
4
+
5
+ script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
6
+ artifact="$script_dir/../lib/sixb.mjs"
7
+ if [ ! -f "$artifact" ]; then
8
+ artifact="$script_dir/../generated/sixb.mjs"
9
+ fi
10
+ if [ ! -f "$artifact" ]; then
11
+ printf '%s\n' '{"error":{"code":"runtime_unavailable","message":"The Sixb CLI artifact is missing."}}' >&2
12
+ exit 2
13
+ fi
14
+
15
+ bun_path=$(command -v bun || true)
16
+ if [ -n "$bun_path" ]; then
17
+ exec "$bun_path" "$artifact" "$@"
18
+ fi
19
+
20
+ node_path=$(command -v node || true)
21
+ if [ -n "$node_path" ]; then
22
+ exec "$node_path" "$artifact" "$@"
23
+ fi
24
+
25
+ printf '%s\n' '{"error":{"code":"runtime_unavailable","message":"The Sixb CLI requires Bun or Node.js."}}' >&2
26
+ exit 2
@@ -3,7 +3,7 @@ import type { ApiClient } from "../api-client"
3
3
  import { isHelp, parseCommandArgs, requestsHelp } from "../arguments"
4
4
  import { CliError, EXIT_API, fail, writeJson, writeRequestResult, writeText } from "../output"
5
5
  import { GROUP_HELP } from "./metadata"
6
- import { asRecord, parseQueryOptions, readJson } from "./shared"
6
+ import { asRecord, idSegment, parseQueryOptions, readJson } from "./shared"
7
7
 
8
8
  /**
9
9
  * How long `actions request` waits for the run it requested. An agent's command is killed after 30
@@ -22,7 +22,7 @@ export async function actions(
22
22
  if (!sub || isHelp(sub) || requestsHelp(rest)) return writeText(GROUP_HELP.actions)
23
23
  if (sub === "get") {
24
24
  const { positionals } = parseCommandArgs(rest, {}, "actions get", 1)
25
- return writeJson(await api.get(`/api/actions/${encodeURIComponent(positionals[0] ?? "")}`))
25
+ return writeJson(await api.get(`/api/actions/${idSegment(positionals[0])}`))
26
26
  }
27
27
  if (sub === "list") {
28
28
  const options = parseQueryOptions(rest, { "--type": "objectTypeId" }, "actions list")
@@ -64,7 +64,7 @@ export async function actions(
64
64
  try {
65
65
  return writeRequestResult(
66
66
  await api.post(
67
- `/api/actions/${encodeURIComponent(actionId)}`,
67
+ `/api/actions/${idSegment(actionId)}`,
68
68
  {
69
69
  params,
70
70
  ...(subjectType && subjectId
@@ -0,0 +1,90 @@
1
+ import type { ApiClient } from "../api-client"
2
+ import {
3
+ integerInRange,
4
+ isHelp,
5
+ nonNegativeInteger,
6
+ parseCommandArgs,
7
+ requestsHelp,
8
+ } from "../arguments"
9
+ import { fail, writeJson, writeText } from "../output"
10
+ import { CLI_LIMITS } from "../policies"
11
+ import { GROUP_HELP } from "./metadata"
12
+ import { idSegment, type QueryOptions } from "./shared"
13
+
14
+ export async function datasets(api: ApiClient, args: readonly string[]): Promise<void> {
15
+ const [sub, ...rest] = args
16
+ if (!sub || isHelp(sub) || requestsHelp(rest)) return writeText(GROUP_HELP.datasets)
17
+
18
+ if (sub === "list") {
19
+ parseCommandArgs(rest, {}, "datasets list")
20
+ return writeJson(await api.get("/api/datasets"))
21
+ }
22
+
23
+ if (sub === "get") {
24
+ const { positionals } = parseCommandArgs(rest, {}, "datasets get", 1)
25
+ return writeJson(await api.get(`/api/datasets/${idSegment(positionals[0])}`))
26
+ }
27
+
28
+ if (sub === "versions") {
29
+ const { positionals, options } = parseCommandArgs(
30
+ rest,
31
+ { "--limit": "string" },
32
+ "datasets versions",
33
+ 1
34
+ )
35
+ const { default: fallback, maximum } = CLI_LIMITS.datasetVersions
36
+ const limit = integerInRange("--limit", options["--limit"] ?? String(fallback), 1, maximum)
37
+ return writeJson(
38
+ await api.get(`/api/datasets/${idSegment(positionals[0])}/versions`, { limit: String(limit) })
39
+ )
40
+ }
41
+
42
+ if (sub === "rows") {
43
+ const { positionals, options } = parseCommandArgs(
44
+ rest,
45
+ { "--version": "string", "--columns": "string", "--limit": "string", "--offset": "string" },
46
+ "datasets rows",
47
+ 1
48
+ )
49
+ const { default: fallback, maximum } = CLI_LIMITS.datasetRows
50
+ const query: QueryOptions = {
51
+ limit: String(integerInRange("--limit", options["--limit"] ?? String(fallback), 1, maximum)),
52
+ ...(options["--version"] === undefined ? {} : { versionId: options["--version"] }),
53
+ ...(options["--columns"] === undefined ? {} : { columns: columnList(options["--columns"]) }),
54
+ ...(options["--offset"] === undefined
55
+ ? {}
56
+ : { offset: String(nonNegativeInteger("--offset", options["--offset"])) }),
57
+ }
58
+ return writeJson(await api.get(`/api/datasets/${idSegment(positionals[0])}/rows`, query))
59
+ }
60
+
61
+ fail(`Unknown datasets command '${sub}'.`)
62
+ }
63
+
64
+ /** `syncs` and `pipelines`: the definitions the caller may run, with each one's latest run. */
65
+ export async function runnables(
66
+ api: ApiClient,
67
+ group: "syncs" | "pipelines",
68
+ args: readonly string[]
69
+ ): Promise<void> {
70
+ const [sub, ...rest] = args
71
+ if (!sub || isHelp(sub) || requestsHelp(rest)) return writeText(GROUP_HELP[group])
72
+
73
+ if (sub === "list") {
74
+ parseCommandArgs(rest, {}, `${group} list`)
75
+ return writeJson(await api.get(`/api/${group}`))
76
+ }
77
+
78
+ if (sub === "get") {
79
+ const { positionals } = parseCommandArgs(rest, {}, `${group} get`, 1)
80
+ return writeJson(await api.get(`/api/${group}/${idSegment(positionals[0])}`))
81
+ }
82
+
83
+ fail(`Unknown ${group} command '${sub}'.`)
84
+ }
85
+
86
+ function columnList(value: string): string {
87
+ const columns = value.split(",").map((column) => column.trim())
88
+ if (columns.some((column) => !column)) fail("--columns takes column names separated by commas.")
89
+ return columns.join(",")
90
+ }
@@ -3,6 +3,7 @@ import type { ApiClient } from "../api-client"
3
3
  import { isHelp, parseCommandArgs, requestsHelp, requireExact } from "../arguments"
4
4
  import { fail, writeJson, writeText } from "../output"
5
5
  import { GROUP_HELP } from "./metadata"
6
+ import { idSegment } from "./shared"
6
7
 
7
8
  export async function files(api: ApiClient, args: readonly string[]): Promise<void> {
8
9
  const [sub, ...rest] = args
@@ -30,19 +31,28 @@ export async function files(api: ApiClient, args: readonly string[]): Promise<vo
30
31
  [2, 3]
31
32
  )
32
33
  const context = positionals[0]
34
+ const output = options["--output"]
35
+ if (context === "url") {
36
+ // A content URL a manifest lists, such as an attachment's: fetched the way every other
37
+ // command reaches the API, so its credential is added wherever the CLI's address adds it.
38
+ requireExact(positionals, 2, "files download url requires exactly one content URL.")
39
+ const url = parseContentUrl(positionals[1] ?? "")
40
+ if (!output) fail("files download requires --output <local-path>.")
41
+ await api.download(url.pathname, output, Object.fromEntries(url.searchParams))
42
+ return writeJson({ downloaded: true, output })
43
+ }
33
44
  let route: string
34
45
  if (context === "object") {
35
46
  requireExact(positionals, 3, "files download object requires object type and primary id.")
36
47
  const type = positionals[1] ?? ""
37
48
  const id = positionals[2] ?? ""
38
- route = `/api/objects/${encodeURIComponent(type)}/${encodeURIComponent(id)}/files/content`
49
+ route = `/api/objects/${idSegment(type)}/${idSegment(id)}/files/content`
39
50
  } else if (context === "action-run" || context === "workflow-run") {
40
51
  requireExact(positionals, 2, `files download ${context} requires exactly one run id.`)
41
52
  const id = positionals[1] ?? ""
42
- route = `/api/${context}s/${encodeURIComponent(id)}/files/content`
53
+ route = `/api/${context}s/${idSegment(id)}/files/content`
43
54
  } else fail(`Unknown file download context '${context}'.`)
44
55
  const path = options["--path"]
45
- const output = options["--output"]
46
56
  if (!path) fail("files download requires --path <json-pointer>.")
47
57
  if (!output) fail("files download requires --output <local-path>.")
48
58
  await api.download(route, output, { path })
@@ -50,3 +60,17 @@ export async function files(api: ApiClient, args: readonly string[]): Promise<vo
50
60
  }
51
61
  fail(`Unknown files command '${sub}'.`)
52
62
  }
63
+
64
+ /** A Sixb API file content URL: only its path and query are used, never its origin. */
65
+ function parseContentUrl(value: string): URL {
66
+ let url: URL
67
+ try {
68
+ url = new URL(value)
69
+ } catch {
70
+ fail(`'${value}' is not a URL.`)
71
+ }
72
+ if (!url.pathname.startsWith("/api/") || !url.pathname.endsWith("/files/content")) {
73
+ fail(`'${value}' is not a Sixb file content URL.`)
74
+ }
75
+ return url
76
+ }
@@ -0,0 +1,204 @@
1
+ import type { ApiClient } from "../api-client"
2
+ import {
3
+ integerInRange,
4
+ isHelp,
5
+ nonNegativeInteger,
6
+ parseCommandArgs,
7
+ requestsHelp,
8
+ } from "../arguments"
9
+ import { CliError, EXIT_API, fail, writeText } from "../output"
10
+ import { CLI_LIMITS } from "../policies"
11
+ import { GROUP_HELP } from "./metadata"
12
+ import { idSegment } from "./shared"
13
+
14
+ /** In a page, longer text is cut: `sixb history <seq>` prints a message whole. */
15
+ const PAGE_TEXT_CHARS = 4_000
16
+ const PAGE_TOOL_CHARS = 1_000
17
+
18
+ interface StoredMessage {
19
+ readonly id?: string
20
+ readonly seq: number
21
+ readonly role: string
22
+ readonly createdAt?: string
23
+ readonly authorPrincipal?: { readonly type: string; readonly id: string }
24
+ readonly parts: readonly Record<string, unknown>[]
25
+ }
26
+
27
+ interface MessagePage {
28
+ readonly messages: readonly StoredMessage[]
29
+ readonly authors: readonly { readonly id: string; readonly displayName?: string }[]
30
+ readonly hasMore: boolean
31
+ readonly total: number
32
+ }
33
+
34
+ /**
35
+ * Read the run's conversation as text, oldest first, compacted turns included: what was said,
36
+ * the files sent, and each tool call with its result. Reasoning stays out.
37
+ */
38
+ export async function history(api: ApiClient, args: readonly string[]): Promise<void> {
39
+ if (isHelp(args[0]) || requestsHelp(args)) return writeText(GROUP_HELP.history)
40
+
41
+ const { positionals, options } = parseCommandArgs(
42
+ args,
43
+ { "--offset": "string", "--limit": "string" },
44
+ "history",
45
+ [0, 1]
46
+ )
47
+ const threadId = process.env.SIXB_THREAD_ID
48
+ if (!threadId) fail("This run has no conversation to read: SIXB_THREAD_ID is not set.")
49
+ const path = `/api/agent-threads/${idSegment(threadId)}/messages`
50
+
51
+ const seq = positionals[0]
52
+ if (seq !== undefined) {
53
+ const wanted = integerInRange("<seq>", seq, 1, Number.MAX_SAFE_INTEGER)
54
+ const page = messagePage(
55
+ await api.get(path, { order: "asc", offset: String(wanted - 1), limit: "1" })
56
+ )
57
+ const message = page.messages[0]
58
+ if (message?.seq !== wanted) {
59
+ throw new CliError(
60
+ { code: "not_found", message: `The conversation has no message #${wanted}.` },
61
+ EXIT_API
62
+ )
63
+ }
64
+ return writeText(renderMessage(message, page, { whole: true }))
65
+ }
66
+
67
+ const offset = nonNegativeInteger("--offset", options["--offset"] ?? "0")
68
+ const limit = integerInRange(
69
+ "--limit",
70
+ options["--limit"] ?? String(CLI_LIMITS.history.default),
71
+ 1,
72
+ CLI_LIMITS.history.maximum
73
+ )
74
+ const page = messagePage(
75
+ await api.get(path, { order: "asc", offset: String(offset), limit: String(limit) })
76
+ )
77
+
78
+ const shown = page.messages.map((message) => renderMessage(message, page, { whole: false }))
79
+ const end = offset + page.messages.length
80
+ const footer =
81
+ page.messages.length === 0
82
+ ? `No messages from offset ${offset}: the conversation has ${page.total}.`
83
+ : page.hasMore
84
+ ? `Messages ${offset + 1}–${end} of ${page.total}. Next: sixb history --offset ${end}`
85
+ : `Messages ${offset + 1}–${end} of ${page.total}: the end of the conversation.`
86
+ writeText([...shown, footer].join("\n\n"))
87
+ }
88
+
89
+ function renderMessage(
90
+ message: StoredMessage,
91
+ page: MessagePage,
92
+ options: { readonly whole: boolean }
93
+ ): string {
94
+ const author = message.authorPrincipal
95
+ ? page.authors.find((person) => person.id === message.authorPrincipal?.id)?.displayName
96
+ : undefined
97
+ const heading = [`#${message.seq} ${message.role}`, author, message.createdAt]
98
+ .filter(Boolean)
99
+ .join(" · ")
100
+ const bounds = {
101
+ seq: message.seq,
102
+ textChars: options.whole ? Number.POSITIVE_INFINITY : PAGE_TEXT_CHARS,
103
+ toolChars: options.whole ? Number.POSITIVE_INFINITY : PAGE_TOOL_CHARS,
104
+ }
105
+
106
+ return [
107
+ `── ${heading}`,
108
+ ...message.parts.flatMap((part, index) => [
109
+ ...renderPart(part, bounds),
110
+ ...renderOutputFile(part, message, index),
111
+ ]),
112
+ ].join("\n")
113
+ }
114
+
115
+ /**
116
+ * Where a call's whole output is, when it was too large for the context and its part keeps a
117
+ * bounded copy: in the sandbox while the conversation still shows the call, which every run
118
+ * writes back, and through the message's file route whatever happened since.
119
+ */
120
+ function renderOutputFile(
121
+ part: Record<string, unknown>,
122
+ message: StoredMessage,
123
+ index: number
124
+ ): string[] {
125
+ const file = part.type === "tool-call" ? record(part.outputFile) : undefined
126
+ const fileRef = record(file?.fileRef)
127
+ if (typeof file?.path !== "string" || typeof fileRef?.sizeBytes !== "number") return []
128
+
129
+ const contextDir = process.env.SIXB_CONTEXT_DIR
130
+ const path = contextDir ? `${contextDir.replace(/\/$/, "")}/${file.path}` : file.path
131
+ const apiBaseUrl = process.env.SIXB_API_BASE_URL
132
+ const threadId = process.env.SIXB_THREAD_ID
133
+ const download =
134
+ apiBaseUrl && threadId && message.id
135
+ ? `; 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>`
136
+ : ""
137
+ return [
138
+ ` [whole output, ${fileRef.sizeBytes.toLocaleString("en-US")} bytes: ${path}, written back to the sandbox at each run while the conversation shows this call${download}]`,
139
+ ]
140
+ }
141
+
142
+ function renderPart(
143
+ part: Record<string, unknown>,
144
+ bounds: { readonly seq: number; readonly textChars: number; readonly toolChars: number }
145
+ ): string[] {
146
+ const cut = (value: string, max: number) =>
147
+ value.length <= max
148
+ ? value
149
+ : `${value.slice(0, max)}… [${(value.length - max).toLocaleString("en-US")} more characters: sixb history ${bounds.seq}]`
150
+
151
+ switch (part.type) {
152
+ case "text":
153
+ return typeof part.text === "string" && part.text.trim()
154
+ ? [cut(part.text, bounds.textChars)]
155
+ : []
156
+ case "file": {
157
+ const file = record(part.fileRef)
158
+ const name = file?.fileName ?? file?.logicalPath ?? file?.blobId
159
+ return [`[file ${String(name)}${file?.mediaType ? `, ${String(file.mediaType)}` : ""}]`]
160
+ }
161
+ case "tool-call": {
162
+ const input = cut(JSON.stringify(part.input ?? {}), bounds.toolChars)
163
+ const outcome =
164
+ part.state === "output-error"
165
+ ? `error: ${cut(String(part.errorText ?? ""), bounds.toolChars)}`
166
+ : part.state === "output-available"
167
+ ? cut(
168
+ typeof part.output === "string" ? part.output : JSON.stringify(part.output),
169
+ bounds.toolChars
170
+ )
171
+ : String(part.state ?? "")
172
+ return [`[${String(part.toolName)}] ${input}`, `→ ${outcome}`]
173
+ }
174
+ case "continuation":
175
+ return ["[asked to continue]"]
176
+ case "approval-outcome":
177
+ return [`[approval ${String(part.status)}]`]
178
+ // Reasoning, provider state and step marks are not part of what was said.
179
+ default:
180
+ return []
181
+ }
182
+ }
183
+
184
+ function messagePage(value: unknown): MessagePage {
185
+ const page = record(value)
186
+ if (!page || !Array.isArray(page.messages) || typeof page.total !== "number") {
187
+ throw new CliError(
188
+ { code: "invalid_api_response", message: "The Sixb API returned an invalid message page." },
189
+ EXIT_API
190
+ )
191
+ }
192
+ return {
193
+ messages: page.messages as StoredMessage[],
194
+ authors: Array.isArray(page.authors) ? (page.authors as MessagePage["authors"]) : [],
195
+ hasMore: page.hasMore === true,
196
+ total: page.total,
197
+ }
198
+ }
199
+
200
+ function record(value: unknown): Record<string, unknown> | undefined {
201
+ return typeof value === "object" && value !== null && !Array.isArray(value)
202
+ ? (value as Record<string, unknown>)
203
+ : undefined
204
+ }
@@ -1,6 +1,7 @@
1
1
  import type { ApiClient } from "../api-client"
2
2
  import { fail } from "../output"
3
3
  import { actions } from "./actions"
4
+ import { datasets, runnables } from "./data"
4
5
  import { files } from "./files"
5
6
  import { models } from "./models"
6
7
  import { objects } from "./objects"
@@ -23,6 +24,11 @@ export const INSTANCE_COMMANDS = [
23
24
  "workflow-runs",
24
25
  "users",
25
26
  "models",
27
+ "datasets",
28
+ "syncs",
29
+ "sync-runs",
30
+ "pipelines",
31
+ "pipeline-runs",
26
32
  ] as const
27
33
 
28
34
  export type InstanceCommand = (typeof INSTANCE_COMMANDS)[number]
@@ -59,6 +65,16 @@ export async function dispatch(
59
65
  return users(api, args)
60
66
  case "models":
61
67
  return models(api, args)
68
+ case "datasets":
69
+ return datasets(api, args)
70
+ case "syncs":
71
+ return runnables(api, "syncs", args)
72
+ case "sync-runs":
73
+ return runs(api, "sync", args)
74
+ case "pipelines":
75
+ return runnables(api, "pipelines", args)
76
+ case "pipeline-runs":
77
+ return runs(api, "pipeline", args)
62
78
  default:
63
79
  fail(`Unknown command '${command}'. Run 'sixb --help'.`)
64
80
  }