@sixb/cli-core 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -16
- package/dist/api-client.d.ts +1 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/bin/sixb +26 -0
- package/dist/commands/data.d.ts +5 -0
- package/dist/commands/data.d.ts.map +1 -0
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/history.d.ts +7 -0
- package/dist/commands/history.d.ts.map +1 -0
- package/dist/commands/index.d.ts +1 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/memory.d.ts +3 -0
- package/dist/commands/memory.d.ts.map +1 -0
- package/dist/commands/metadata.d.ts +11 -4
- package/dist/commands/metadata.d.ts.map +1 -1
- package/dist/commands/runs.d.ts +8 -1
- package/dist/commands/runs.d.ts.map +1 -1
- package/dist/commands/shared.d.ts +2 -0
- package/dist/commands/shared.d.ts.map +1 -1
- package/dist/commands/system.d.ts +21 -0
- package/dist/commands/system.d.ts.map +1 -0
- package/dist/commands/users.d.ts.map +1 -1
- package/dist/generated/sixb.mjs +2658 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +298 -58
- package/dist/index.js.map +21 -17
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/memory.d.ts +21 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/policies.d.ts +14 -0
- package/dist/policies.d.ts.map +1 -1
- package/dist/run.d.ts +1 -0
- package/dist/run.d.ts.map +1 -1
- package/dist/sandbox-cli.d.ts +8 -0
- package/dist/sandbox-cli.d.ts.map +1 -0
- package/package.json +15 -1
- package/src/api-client.ts +70 -2
- package/src/bin/sixb +26 -0
- package/src/commands/actions.ts +3 -3
- package/src/commands/data.ts +90 -0
- package/src/commands/files.ts +27 -3
- package/src/commands/history.ts +204 -0
- package/src/commands/index.ts +16 -0
- package/src/commands/memory.ts +358 -0
- package/src/commands/metadata.ts +76 -29
- package/src/commands/models.ts +3 -3
- package/src/commands/ontology.ts +2 -2
- package/src/commands/runs.ts +22 -16
- package/src/commands/shared.ts +6 -0
- package/src/commands/system.ts +67 -0
- package/src/commands/telemetry.ts +2 -2
- package/src/commands/users.ts +22 -2
- package/src/commands/workflows.ts +3 -3
- package/src/generated/sixb.mjs +2658 -0
- package/src/index.ts +12 -3
- package/src/main.ts +41 -0
- package/src/memory.ts +30 -0
- package/src/policies.ts +5 -0
- package/src/run.ts +3 -2
- package/src/sandbox-cli.ts +18 -0
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":""}
|
package/dist/memory.d.ts
ADDED
|
@@ -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/policies.d.ts
CHANGED
|
@@ -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;
|
package/dist/policies.d.ts.map
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
package/src/commands/actions.ts
CHANGED
|
@@ -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/${
|
|
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/${
|
|
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
|
+
}
|
package/src/commands/files.ts
CHANGED
|
@@ -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/${
|
|
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/${
|
|
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
|
+
}
|
package/src/commands/index.ts
CHANGED
|
@@ -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
|
}
|