@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
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"}
package/dist/output.d.ts CHANGED
@@ -2,6 +2,8 @@
2
2
  export declare const INSTANCE_CLI_VERSION = "1";
3
3
  export declare const EXIT_USAGE = 2;
4
4
  export declare const EXIT_API = 3;
5
+ /** The request waits for its user's approval: it did not run. */
6
+ export declare const EXIT_AWAITING_APPROVAL = 4;
5
7
  export interface CliErrorBody {
6
8
  readonly code: string;
7
9
  readonly status?: number;
@@ -16,6 +18,16 @@ export declare class CliError extends Error {
16
18
  }
17
19
  export declare function fail(message: string, code?: string, hint?: string): never;
18
20
  export declare function writeJson(value: unknown): void;
21
+ /** Ends a command that already wrote its output, with an exit code that is not a success. */
22
+ export declare class CliExit extends Error {
23
+ readonly exitCode: number;
24
+ constructor(exitCode: number);
25
+ }
26
+ /**
27
+ * Print what a request that changes state answered. A request held for the user's approval prints
28
+ * as the API answered it, and exits with {@link EXIT_AWAITING_APPROVAL}: `a && b` does not run `b`.
29
+ */
30
+ export declare function writeRequestResult(value: unknown): void;
19
31
  export declare function writeText(value: string): void;
20
32
  export declare function reportError(error: unknown): number;
21
33
  //# sourceMappingURL=output.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,eAAO,MAAM,oBAAoB,MAAM,CAAA;AACvC,eAAO,MAAM,UAAU,IAAI,CAAA;AAC3B,eAAO,MAAM,QAAQ,IAAI,CAAA;AAEzB,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,CAAA;CACrC;AAED,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;gBAEb,IAAI,EAAE,YAAY,EAAE,QAAQ,SAAa;CAMtD;AAED,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,SAAsB,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,CAEtF;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAE9C;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAE7C;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAalD"}
1
+ {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,eAAO,MAAM,oBAAoB,MAAM,CAAA;AACvC,eAAO,MAAM,UAAU,IAAI,CAAA;AAC3B,eAAO,MAAM,QAAQ,IAAI,CAAA;AACzB,iEAAiE;AACjE,eAAO,MAAM,sBAAsB,IAAI,CAAA;AAEvC,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,CAAA;CACrC;AAED,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;gBAEb,IAAI,EAAE,YAAY,EAAE,QAAQ,SAAa;CAMtD;AAED,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,SAAsB,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,CAEtF;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAE9C;AAED,6FAA6F;AAC7F,qBAAa,OAAQ,SAAQ,KAAK;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM;gBAAhB,QAAQ,EAAE,MAAM;CAItC;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAIvD;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAE7C;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAclD"}
@@ -20,6 +20,20 @@ export declare const CLI_LIMITS: {
20
20
  readonly default: 100;
21
21
  readonly maximum: 1000;
22
22
  };
23
+ /** The server lists at most 200 members per page. */
24
+ readonly users: {
25
+ readonly default: 20;
26
+ readonly maximum: 200;
27
+ };
28
+ /** The server's dataset bounds: 100 versions and 1,000 rows per page. */
29
+ readonly datasetVersions: {
30
+ readonly default: 20;
31
+ readonly maximum: 100;
32
+ };
33
+ readonly datasetRows: {
34
+ readonly default: 100;
35
+ readonly maximum: 1000;
36
+ };
23
37
  readonly inspect: {
24
38
  readonly depth: {
25
39
  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;;;;;;;;;;;;;;;;;;;;;;;;;;;CAUvF,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,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.8",
3
+ "version": "0.1.10",
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
@@ -41,9 +53,20 @@ export class ApiClient {
41
53
  })
42
54
  }
43
55
 
44
- async upload(path: string, source: string, logicalPath?: string): Promise<unknown> {
56
+ /** `mediaType` labels the part; without it, the server reads the type from the file name. */
57
+ async upload(
58
+ path: string,
59
+ source: string,
60
+ logicalPath?: string,
61
+ mediaType?: string
62
+ ): Promise<unknown> {
45
63
  const form = new FormData()
46
- form.append("file", new Blob([new Uint8Array(await readFile(source))]), basename(source))
64
+ const bytes = new Uint8Array(await readFile(source))
65
+ form.append(
66
+ "file",
67
+ new Blob([bytes], mediaType ? { type: mediaType } : undefined),
68
+ basename(source)
69
+ )
47
70
  if (logicalPath) form.append("logicalPath", logicalPath)
48
71
  return this.json(this.url(path), { method: "POST", body: form })
49
72
  }
@@ -96,7 +119,7 @@ export class ApiClient {
96
119
  try {
97
120
  const headers = new Headers(init.headers)
98
121
  if (this.authorization) headers.set("authorization", this.authorization)
99
- response = await fetch(url, { ...init, headers })
122
+ response = await send(url, { ...init, headers }, this.socketPath)
100
123
  } catch (error) {
101
124
  if (init.signal?.aborted) throw error
102
125
  throw new CliError(
@@ -182,3 +205,59 @@ function stringField(record: Readonly<Record<string, unknown>>, key: string): st
182
205
  const value = record[key]
183
206
  return typeof value === "string" ? value : undefined
184
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/arguments.ts CHANGED
@@ -1,6 +1,13 @@
1
1
  import { fail } from "./output"
2
2
 
3
- type OptionKinds = Readonly<Record<string, "string" | "boolean">>
3
+ /** `strings` may repeat, and collects every value in order. */
4
+ type OptionKinds = Readonly<Record<string, "string" | "strings" | "boolean">>
5
+
6
+ type OptionValue<TKind> = TKind extends "boolean"
7
+ ? boolean
8
+ : TKind extends "strings"
9
+ ? string[]
10
+ : string
4
11
 
5
12
  /** Parse a command's options once, before reading files or calling the API. */
6
13
  export function parseCommandArgs<const T extends OptionKinds>(
@@ -10,10 +17,10 @@ export function parseCommandArgs<const T extends OptionKinds>(
10
17
  positionalCount: number | readonly [number, number] = 0
11
18
  ): {
12
19
  positionals: string[]
13
- options: { [K in keyof T]?: T[K] extends "boolean" ? boolean : string }
20
+ options: { [K in keyof T]?: OptionValue<T[K]> }
14
21
  } {
15
22
  const positionals: string[] = []
16
- const options: Record<string, string | boolean> = {}
23
+ const options: Record<string, string | string[] | boolean> = {}
17
24
  let positionalOnly = false
18
25
  for (let index = 0; index < args.length; index++) {
19
26
  const argument = args[index] ?? ""
@@ -28,7 +35,9 @@ export function parseCommandArgs<const T extends OptionKinds>(
28
35
  const equals = argument.indexOf("=")
29
36
  const flag = equals < 0 ? argument : argument.slice(0, equals)
30
37
  if (!Object.hasOwn(kinds, flag)) fail(`Unknown ${command} option '${flag}'.`)
31
- if (Object.hasOwn(options, flag)) fail(`${flag} may only be provided once.`)
38
+ if (Object.hasOwn(options, flag) && kinds[flag] !== "strings") {
39
+ fail(`${flag} may only be provided once.`)
40
+ }
32
41
  if (kinds[flag] === "boolean") {
33
42
  if (equals >= 0) fail(`${flag} does not accept a value.`)
34
43
  options[flag] = true
@@ -37,7 +46,9 @@ export function parseCommandArgs<const T extends OptionKinds>(
37
46
  if (!value || (equals < 0 && value.startsWith("--"))) {
38
47
  fail(`${flag} requires a value.`)
39
48
  }
40
- options[flag] = value
49
+ const previous = options[flag]
50
+ options[flag] =
51
+ kinds[flag] === "strings" ? [...(Array.isArray(previous) ? previous : []), value] : value
41
52
  }
42
53
  }
43
54
  const [minimum, maximum] =
@@ -48,7 +59,7 @@ export function parseCommandArgs<const T extends OptionKinds>(
48
59
  return {
49
60
  positionals,
50
61
  // Every assigned option was checked against its declared kind above.
51
- options: options as { [K in keyof T]?: T[K] extends "boolean" ? boolean : string },
62
+ options: options as { [K in keyof T]?: OptionValue<T[K]> },
52
63
  }
53
64
  }
54
65
 
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
@@ -1,9 +1,9 @@
1
1
  import { randomUUID } from "node:crypto"
2
2
  import type { ApiClient } from "../api-client"
3
3
  import { isHelp, parseCommandArgs, requestsHelp } from "../arguments"
4
- import { CliError, EXIT_API, fail, writeJson, writeText } from "../output"
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")
@@ -62,9 +62,9 @@ export async function actions(
62
62
  const requestedRunId = runId ?? `act_${randomUUID()}`
63
63
  const waiting = AbortSignal.timeout(requestWaitMs)
64
64
  try {
65
- return writeJson(
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
+ }
@@ -1,12 +1,15 @@
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"
6
+ import { models } from "./models"
5
7
  import { objects } from "./objects"
6
8
  import { ontology } from "./ontology"
7
9
  import { project } from "./project"
8
10
  import { runs } from "./runs"
9
11
  import { telemetry } from "./telemetry"
12
+ import { users } from "./users"
10
13
  import { workflows } from "./workflows"
11
14
 
12
15
  export const INSTANCE_COMMANDS = [
@@ -19,6 +22,13 @@ export const INSTANCE_COMMANDS = [
19
22
  "files",
20
23
  "workflows",
21
24
  "workflow-runs",
25
+ "users",
26
+ "models",
27
+ "datasets",
28
+ "syncs",
29
+ "sync-runs",
30
+ "pipelines",
31
+ "pipeline-runs",
22
32
  ] as const
23
33
 
24
34
  export type InstanceCommand = (typeof INSTANCE_COMMANDS)[number]
@@ -51,6 +61,20 @@ export async function dispatch(
51
61
  return workflows(api, args)
52
62
  case "workflow-runs":
53
63
  return runs(api, "workflow", args)
64
+ case "users":
65
+ return users(api, args)
66
+ case "models":
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)
54
78
  default:
55
79
  fail(`Unknown command '${command}'. Run 'sixb --help'.`)
56
80
  }