@sixb/cli-core 0.1.10 → 0.2.1

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.
@@ -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
+ }
@@ -204,6 +204,13 @@ optionally under filters; k bounds the ranked matches and results carry a score.
204
204
  export const GROUP_HELP = {
205
205
  status: "Usage: sixb status",
206
206
  context: "Usage: sixb context",
207
+ history: `Usage:
208
+ sixb history [--offset <n>] [--limit <1-${CLI_LIMITS.history.maximum}>]
209
+ sixb history <seq>
210
+
211
+ Prints this conversation's messages as text, oldest first, including those summarized when it
212
+ was compacted: what was said, the files sent, and each tool call with its result. A page holds
213
+ ${CLI_LIMITS.history.default} messages by default and cuts long text; \`sixb history <seq>\` prints one message whole.`,
207
214
  project: "Usage: sixb project show",
208
215
  ontology: `Usage:
209
216
  sixb ontology list [--full]
@@ -167,6 +167,7 @@ var CLI_LIMITS = {
167
167
  telemetryHistory: { default: 100, maximum: 1000 },
168
168
  linkPage: { default: 100, maximum: 1000 },
169
169
  users: { default: 20, maximum: 200 },
170
+ history: { default: 20, maximum: 100 },
170
171
  datasetVersions: { default: 20, maximum: 100 },
171
172
  datasetRows: { default: 100, maximum: 1000 },
172
173
  inspect: {
@@ -372,6 +373,13 @@ optionally under filters; k bounds the ranked matches and results carry a score.
372
373
  var GROUP_HELP = {
373
374
  status: "Usage: sixb status",
374
375
  context: "Usage: sixb context",
376
+ history: `Usage:
377
+ sixb history [--offset <n>] [--limit <1-${CLI_LIMITS.history.maximum}>]
378
+ sixb history <seq>
379
+
380
+ Prints this conversation's messages as text, oldest first, including those summarized when it
381
+ was compacted: what was said, the files sent, and each tool call with its result. A page holds
382
+ ${CLI_LIMITS.history.default} messages by default and cuts long text; \`sixb history <seq>\` prints one message whole.`,
375
383
  project: "Usage: sixb project show",
376
384
  ontology: `Usage:
377
385
  sixb ontology list [--full]
@@ -1976,6 +1984,107 @@ async function dispatch(api, command, args) {
1976
1984
  }
1977
1985
  }
1978
1986
 
1987
+ // src/commands/history.ts
1988
+ var PAGE_TEXT_CHARS = 4000;
1989
+ var PAGE_TOOL_CHARS = 1000;
1990
+ async function history(api, args) {
1991
+ if (isHelp(args[0]) || requestsHelp(args))
1992
+ return writeText(GROUP_HELP.history);
1993
+ const { positionals, options } = parseCommandArgs(args, { "--offset": "string", "--limit": "string" }, "history", [0, 1]);
1994
+ const threadId = process.env.SIXB_THREAD_ID;
1995
+ if (!threadId)
1996
+ fail("This run has no conversation to read: SIXB_THREAD_ID is not set.");
1997
+ const path = `/api/agent-threads/${idSegment(threadId)}/messages`;
1998
+ const seq = positionals[0];
1999
+ if (seq !== undefined) {
2000
+ const wanted = integerInRange("<seq>", seq, 1, Number.MAX_SAFE_INTEGER);
2001
+ const page = messagePage(await api.get(path, { order: "asc", offset: String(wanted - 1), limit: "1" }));
2002
+ const message = page.messages[0];
2003
+ if (message?.seq !== wanted) {
2004
+ throw new CliError({ code: "not_found", message: `The conversation has no message #${wanted}.` }, EXIT_API);
2005
+ }
2006
+ return writeText(renderMessage(message, page, { whole: true }));
2007
+ }
2008
+ const offset = nonNegativeInteger("--offset", options["--offset"] ?? "0");
2009
+ const limit = integerInRange("--limit", options["--limit"] ?? String(CLI_LIMITS.history.default), 1, CLI_LIMITS.history.maximum);
2010
+ const page = messagePage(await api.get(path, { order: "asc", offset: String(offset), limit: String(limit) }));
2011
+ const shown = page.messages.map((message) => renderMessage(message, page, { whole: false }));
2012
+ const end = offset + page.messages.length;
2013
+ const footer = page.messages.length === 0 ? `No messages from offset ${offset}: the conversation has ${page.total}.` : page.hasMore ? `Messages ${offset + 1}–${end} of ${page.total}. Next: sixb history --offset ${end}` : `Messages ${offset + 1}–${end} of ${page.total}: the end of the conversation.`;
2014
+ writeText([...shown, footer].join(`
2015
+
2016
+ `));
2017
+ }
2018
+ function renderMessage(message, page, options) {
2019
+ const author = message.authorPrincipal ? page.authors.find((person) => person.id === message.authorPrincipal?.id)?.displayName : undefined;
2020
+ const heading = [`#${message.seq} ${message.role}`, author, message.createdAt].filter(Boolean).join(" · ");
2021
+ const bounds = {
2022
+ seq: message.seq,
2023
+ textChars: options.whole ? Number.POSITIVE_INFINITY : PAGE_TEXT_CHARS,
2024
+ toolChars: options.whole ? Number.POSITIVE_INFINITY : PAGE_TOOL_CHARS
2025
+ };
2026
+ return [
2027
+ `── ${heading}`,
2028
+ ...message.parts.flatMap((part, index) => [
2029
+ ...renderPart(part, bounds),
2030
+ ...renderOutputFile(part, message, index)
2031
+ ])
2032
+ ].join(`
2033
+ `);
2034
+ }
2035
+ function renderOutputFile(part, message, index) {
2036
+ const file = part.type === "tool-call" ? record(part.outputFile) : undefined;
2037
+ const fileRef = record(file?.fileRef);
2038
+ if (typeof file?.path !== "string" || typeof fileRef?.sizeBytes !== "number")
2039
+ return [];
2040
+ const contextDir = process.env.SIXB_CONTEXT_DIR;
2041
+ const path = contextDir ? `${contextDir.replace(/\/$/, "")}/${file.path}` : file.path;
2042
+ const apiBaseUrl = process.env.SIXB_API_BASE_URL;
2043
+ const threadId = process.env.SIXB_THREAD_ID;
2044
+ const download = apiBaseUrl && threadId && message.id ? `; 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>` : "";
2045
+ return [
2046
+ ` [whole output, ${fileRef.sizeBytes.toLocaleString("en-US")} bytes: ${path}, written back to the sandbox at each run while the conversation shows this call${download}]`
2047
+ ];
2048
+ }
2049
+ function renderPart(part, bounds) {
2050
+ const cut = (value, max) => value.length <= max ? value : `${value.slice(0, max)}… [${(value.length - max).toLocaleString("en-US")} more characters: sixb history ${bounds.seq}]`;
2051
+ switch (part.type) {
2052
+ case "text":
2053
+ return typeof part.text === "string" && part.text.trim() ? [cut(part.text, bounds.textChars)] : [];
2054
+ case "file": {
2055
+ const file = record(part.fileRef);
2056
+ const name = file?.fileName ?? file?.logicalPath ?? file?.blobId;
2057
+ return [`[file ${String(name)}${file?.mediaType ? `, ${String(file.mediaType)}` : ""}]`];
2058
+ }
2059
+ case "tool-call": {
2060
+ const input = cut(JSON.stringify(part.input ?? {}), bounds.toolChars);
2061
+ const outcome = part.state === "output-error" ? `error: ${cut(String(part.errorText ?? ""), bounds.toolChars)}` : part.state === "output-available" ? cut(typeof part.output === "string" ? part.output : JSON.stringify(part.output), bounds.toolChars) : String(part.state ?? "");
2062
+ return [`[${String(part.toolName)}] ${input}`, `→ ${outcome}`];
2063
+ }
2064
+ case "continuation":
2065
+ return ["[asked to continue]"];
2066
+ case "approval-outcome":
2067
+ return [`[approval ${String(part.status)}]`];
2068
+ default:
2069
+ return [];
2070
+ }
2071
+ }
2072
+ function messagePage(value) {
2073
+ const page = record(value);
2074
+ if (!page || !Array.isArray(page.messages) || typeof page.total !== "number") {
2075
+ throw new CliError({ code: "invalid_api_response", message: "The Sixb API returned an invalid message page." }, EXIT_API);
2076
+ }
2077
+ return {
2078
+ messages: page.messages,
2079
+ authors: Array.isArray(page.authors) ? page.authors : [],
2080
+ hasMore: page.hasMore === true,
2081
+ total: page.total
2082
+ };
2083
+ }
2084
+ function record(value) {
2085
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
2086
+ }
2087
+
1979
2088
  // src/commands/memory.ts
1980
2089
  import { lstat, mkdir as mkdir2, readdir, readFile as readFile3, rm as rm2, writeFile } from "node:fs/promises";
1981
2090
  import { dirname as dirname3, join as join3 } from "node:path";
@@ -2529,6 +2638,8 @@ async function main(args) {
2529
2638
  return context(rest);
2530
2639
  if (command === "memory")
2531
2640
  return memory(rest, sandboxMode());
2641
+ if (command === "history")
2642
+ return history(createInstanceApiClient(sandboxMode()), rest);
2532
2643
  if (!isInstanceCommand(command))
2533
2644
  fail(`Unknown command '${command}'. Run 'sixb --help'.`);
2534
2645
  await runInstanceCli({ args, mode: sandboxMode() });
package/src/main.ts CHANGED
@@ -1,14 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // The `sixb` a sandbox runs: the API commands people run, plus the run's own (`status`,
4
- // `context`, `memory`). `bun run generate:sandbox-cli` bundles it into generated/sixb.mjs.
4
+ // `context`, `memory`, `history`). `bun run generate:sandbox-cli` bundles it into generated/sixb.mjs.
5
5
  import { isHelp } from "./arguments"
6
6
  import { isInstanceCommand } from "./commands"
7
+ import { history } from "./commands/history"
7
8
  import { memory } from "./commands/memory"
8
9
  import { renderInstanceHelp } from "./commands/metadata"
9
10
  import { context, status } from "./commands/system"
10
11
  import { fail, INSTANCE_CLI_VERSION, reportError, writeText } from "./output"
11
- import { runInstanceCli } from "./run"
12
+ import { createInstanceApiClient, runInstanceCli } from "./run"
12
13
 
13
14
  async function main(args: readonly string[]): Promise<void> {
14
15
  const [command, ...rest] = args
@@ -19,6 +20,7 @@ async function main(args: readonly string[]): Promise<void> {
19
20
  if (command === "status") return status(rest, sandboxMode())
20
21
  if (command === "context") return context(rest)
21
22
  if (command === "memory") return memory(rest, sandboxMode())
23
+ if (command === "history") return history(createInstanceApiClient(sandboxMode()), rest)
22
24
  if (!isInstanceCommand(command)) fail(`Unknown command '${command}'. Run 'sixb --help'.`)
23
25
 
24
26
  await runInstanceCli({ args, mode: sandboxMode() })
package/src/policies.ts CHANGED
@@ -8,6 +8,8 @@ export const CLI_LIMITS = {
8
8
  linkPage: { default: 100, maximum: 1_000 },
9
9
  /** The server lists at most 200 members per page. */
10
10
  users: { default: 20, maximum: 200 },
11
+ /** A page of a conversation's messages, as `sixb history` prints them. */
12
+ history: { default: 20, maximum: 100 },
11
13
  /** The server's dataset bounds: 100 versions and 1,000 rows per page. */
12
14
  datasetVersions: { default: 20, maximum: 100 },
13
15
  datasetRows: { default: 100, maximum: 1_000 },
@@ -0,0 +1,10 @@
1
+ // What `sandbox-cli.ts` imports as text. Bun's loader reads them; the type checker needs telling.
2
+ declare module "*/bin/sixb" {
3
+ const text: string
4
+ export default text
5
+ }
6
+
7
+ declare module "*/generated/sixb.mjs" {
8
+ const text: string
9
+ export default text
10
+ }
@@ -1,4 +1,6 @@
1
- import { readFile } from "node:fs/promises"
1
+ /// <reference path="./sandbox-cli-assets.d.ts" />
2
+ import launcher from "./bin/sixb" with { type: "text" }
3
+ import bundle from "./generated/sixb.mjs" with { type: "text" }
2
4
 
3
5
  /** The `sixb` a sandbox installs: a shell launcher and the bundle it runs on Bun or Node. */
4
6
  export interface SandboxCliAssets {
@@ -6,13 +8,9 @@ export interface SandboxCliAssets {
6
8
  readonly artifact: Uint8Array
7
9
  }
8
10
 
9
- let cachedAssets: Promise<SandboxCliAssets> | undefined
11
+ const assets: SandboxCliAssets = { launcher, artifact: new TextEncoder().encode(bundle) }
10
12
 
11
- /** Read from beside this module: `src/` in the workspace, `dist/` once built. */
12
- export function loadSandboxCliAssets(): Promise<SandboxCliAssets> {
13
- cachedAssets ??= Promise.all([
14
- readFile(new URL("./bin/sixb", import.meta.url), "utf8"),
15
- readFile(new URL("./generated/sixb.mjs", import.meta.url)),
16
- ]).then(([launcher, artifact]) => ({ launcher, artifact }))
17
- return cachedAssets
13
+ /** Part of this module, so that a program compiled to one file carries them too. */
14
+ export async function loadSandboxCliAssets(): Promise<SandboxCliAssets> {
15
+ return assets
18
16
  }
package/dist/bin/sixb DELETED
@@ -1,26 +0,0 @@
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