@tonbo/cli 0.0.3 → 0.0.5
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 +24 -17
- package/dist/src/app.js +11 -1
- package/dist/src/commands.d.ts +6 -0
- package/dist/src/commands.js +31 -1
- package/dist/src/contracts.js +1 -1
- package/dist/src/declaration.d.ts +8 -2
- package/dist/src/declaration.js +71 -5
- package/dist/src/generated/contracts.d.ts +1 -0
- package/dist/src/generated/contracts.js +6 -1
- package/dist/src/prompt.d.ts +1 -0
- package/dist/src/prompt.js +10 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -7,25 +7,32 @@ npm install --global @tonbo/cli
|
|
|
7
7
|
tonbo --version
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
The V1 CLI deploys a PI-based Agent directly from a local directory without requiring Git or Tonbo calls in the Agent source.
|
|
11
|
-
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
10
|
+
The V1 CLI deploys a PI-based Agent directly from a local directory without requiring Git or Tonbo calls in the Agent source. Initialize the current directory interactively:
|
|
11
|
+
|
|
12
|
+
```console
|
|
13
|
+
tonbo init
|
|
14
|
+
? Inference model [claude-sonnet-4-5]:
|
|
15
|
+
Created .tonbo.
|
|
16
|
+
Next: tonbo project create <slug>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The generated `.tonbo` is TOML:
|
|
20
|
+
|
|
21
|
+
```toml
|
|
22
|
+
version = 1
|
|
23
|
+
|
|
24
|
+
[execution]
|
|
25
|
+
mode = "managed"
|
|
26
|
+
runtime = "pi"
|
|
27
|
+
|
|
28
|
+
[inference]
|
|
29
|
+
model = "claude-sonnet-4-5"
|
|
30
|
+
|
|
31
|
+
[session_capture]
|
|
32
|
+
adapter = "pi-jsonl-v3"
|
|
26
33
|
```
|
|
27
34
|
|
|
28
|
-
Then authenticate, bind the directory and deploy its current contents:
|
|
35
|
+
For automation, use `tonbo init --model <model>`; an existing declaration requires interactive confirmation or `--force`. Then authenticate, bind the directory and deploy its current contents:
|
|
29
36
|
|
|
30
37
|
```console
|
|
31
38
|
tonbo login
|
package/dist/src/app.js
CHANGED
|
@@ -2,10 +2,12 @@ import { Command } from "commander";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { TonboApi } from "./api.js";
|
|
4
4
|
import { AuthClient } from "./auth.js";
|
|
5
|
-
import { deployCommand, runCommand, loginCommand, projectCreateCommand, projectUseCommand, sshCommand, sshKeyAddCommand, sshKeyRemoveCommand, secretListCommand, secretRemoveCommand, secretSetCommand, } from "./commands.js";
|
|
5
|
+
import { deployCommand, initCommand, runCommand, loginCommand, projectCreateCommand, projectUseCommand, sshCommand, sshKeyAddCommand, sshKeyRemoveCommand, secretListCommand, secretRemoveCommand, secretSetCommand, } from "./commands.js";
|
|
6
6
|
import { FileConfigStore } from "./config.js";
|
|
7
7
|
import { FileCredentialStore } from "./credentials.js";
|
|
8
|
+
import { DECLARATION_FILENAME } from "./declaration.js";
|
|
8
9
|
import { silentProgress, TerminalProgress } from "./progress.js";
|
|
10
|
+
import { terminalPrompt } from "./prompt.js";
|
|
9
11
|
import { readDefaultSshPublicKeys } from "./ssh-key.js";
|
|
10
12
|
const packageVersion = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
|
11
13
|
export function createDependencies(json = false) {
|
|
@@ -17,6 +19,7 @@ export function createDependencies(json = false) {
|
|
|
17
19
|
config: new FileConfigStore(),
|
|
18
20
|
cwd: () => process.cwd(),
|
|
19
21
|
defaultSshPublicKeys: readDefaultSshPublicKeys,
|
|
22
|
+
interactive: () => !json && process.stdin.isTTY === true && process.stderr.isTTY === true,
|
|
20
23
|
output: (value) => {
|
|
21
24
|
if (json)
|
|
22
25
|
console.log(JSON.stringify(value));
|
|
@@ -24,6 +27,7 @@ export function createDependencies(json = false) {
|
|
|
24
27
|
console.log(value.message ?? value);
|
|
25
28
|
},
|
|
26
29
|
progress: json ? silentProgress : new TerminalProgress(process.stderr),
|
|
30
|
+
prompt: terminalPrompt,
|
|
27
31
|
secretValue: async (name, fromEnvironment) => {
|
|
28
32
|
const environmentName = fromEnvironment ?? name;
|
|
29
33
|
const value = process.env[environmentName];
|
|
@@ -39,6 +43,12 @@ export function createProgram(dependencies = createDependencies) {
|
|
|
39
43
|
.description("Deploy a persistent Project Agent to Tonbo.")
|
|
40
44
|
.version(packageVersion.version)
|
|
41
45
|
.option("--json", "print machine-readable JSON");
|
|
46
|
+
program
|
|
47
|
+
.command("init")
|
|
48
|
+
.description("interactively create a Tonbo Agent declaration in this directory")
|
|
49
|
+
.option("--model <model>", "inference model")
|
|
50
|
+
.option("--force", `replace an existing ${DECLARATION_FILENAME}`)
|
|
51
|
+
.action(async (options) => initCommand(dependencies(program.opts().json), options));
|
|
42
52
|
program
|
|
43
53
|
.command("login")
|
|
44
54
|
.description("sign in through the browser and store the session in the user config")
|
package/dist/src/commands.d.ts
CHANGED
|
@@ -10,8 +10,10 @@ export interface CommandDependencies {
|
|
|
10
10
|
config: ConfigStore;
|
|
11
11
|
cwd: () => string;
|
|
12
12
|
defaultSshPublicKeys: typeof readDefaultSshPublicKeys;
|
|
13
|
+
interactive: () => boolean;
|
|
13
14
|
output: (value: unknown) => void;
|
|
14
15
|
progress: ProgressReporter;
|
|
16
|
+
prompt: (question: string) => Promise<string>;
|
|
15
17
|
executable?: () => string;
|
|
16
18
|
secretValue: (name: string, fromEnvironment?: string) => Promise<string>;
|
|
17
19
|
}
|
|
@@ -20,6 +22,10 @@ export declare function resolveProject(deps: CommandDependencies, selector?: str
|
|
|
20
22
|
project: ProjectSummary;
|
|
21
23
|
}>;
|
|
22
24
|
export declare function selectProject(projects: ProjectSummary[], selector: string): ProjectSummary;
|
|
25
|
+
export declare function initCommand(deps: CommandDependencies, options: {
|
|
26
|
+
force?: boolean;
|
|
27
|
+
model?: string;
|
|
28
|
+
}): Promise<void>;
|
|
23
29
|
export declare function loginCommand(deps: CommandDependencies): Promise<void>;
|
|
24
30
|
export declare function sshKeyAddCommand(deps: CommandDependencies, path: string): Promise<void>;
|
|
25
31
|
export declare function sshKeyRemoveCommand(deps: CommandDependencies, fingerprint: string): Promise<void>;
|
package/dist/src/commands.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { buildRevision, createDeclaration, declarationExists, DECLARATION_FILENAME, DEFAULT_INFERENCE_MODEL, loadDeclaration, saveDeclaration, } from "./declaration.js";
|
|
2
3
|
import { buildSourceBundle, findDeclarationRoot } from "./source.js";
|
|
3
4
|
import { readSshPublicKey } from "./ssh-key.js";
|
|
4
5
|
import { launchProjectSsh } from "./ssh.js";
|
|
@@ -36,6 +37,35 @@ export function selectProject(projects, selector) {
|
|
|
36
37
|
throw new Error(`Project ${selector} is not active.`);
|
|
37
38
|
return matches[0];
|
|
38
39
|
}
|
|
40
|
+
export async function initCommand(deps, options) {
|
|
41
|
+
const root = deps.cwd();
|
|
42
|
+
const exists = await declarationExists(root);
|
|
43
|
+
let overwrite = options.force === true;
|
|
44
|
+
if (exists && !overwrite) {
|
|
45
|
+
if (!deps.interactive())
|
|
46
|
+
throw new Error(`${DECLARATION_FILENAME} already exists. Pass --force to replace it.`);
|
|
47
|
+
const answer = (await deps.prompt(`Replace existing ${DECLARATION_FILENAME}? [y/N] `))
|
|
48
|
+
.trim()
|
|
49
|
+
.toLowerCase();
|
|
50
|
+
if (answer !== "y" && answer !== "yes") {
|
|
51
|
+
deps.output({ message: `Kept existing ${DECLARATION_FILENAME}.` });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
overwrite = true;
|
|
55
|
+
}
|
|
56
|
+
let model = options.model?.trim();
|
|
57
|
+
if (!model && deps.interactive()) {
|
|
58
|
+
model = (await deps.prompt(`Inference model [${DEFAULT_INFERENCE_MODEL}]: `)).trim();
|
|
59
|
+
}
|
|
60
|
+
model ||= DEFAULT_INFERENCE_MODEL;
|
|
61
|
+
const declaration = createDeclaration(model);
|
|
62
|
+
await saveDeclaration(root, declaration, overwrite);
|
|
63
|
+
deps.output({
|
|
64
|
+
message: `${exists ? "Updated" : "Created"} ${DECLARATION_FILENAME}.\nNext: tonbo project create <slug>`,
|
|
65
|
+
declaration,
|
|
66
|
+
path: path.join(root, DECLARATION_FILENAME),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
39
69
|
export async function loginCommand(deps) {
|
|
40
70
|
try {
|
|
41
71
|
const tokens = await deps.auth.login((event) => reportLoginProgress(deps.progress, event));
|
package/dist/src/contracts.js
CHANGED
|
@@ -13,7 +13,7 @@ function validationMessage(label, errors) {
|
|
|
13
13
|
export function parseDeclaration(value) {
|
|
14
14
|
const candidate = structuredClone(value);
|
|
15
15
|
if (!validateDeclaration(candidate)) {
|
|
16
|
-
throw new Error(validationMessage(".tonbo", validateDeclaration.errors));
|
|
16
|
+
throw new Error(validationMessage(".tonbo TOML", validateDeclaration.errors));
|
|
17
17
|
}
|
|
18
18
|
return candidate;
|
|
19
19
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { parseDeclaration } from "./contracts.js";
|
|
2
|
-
import type { ManagedRevisionSpec, SourceBundle } from "./types.js";
|
|
3
|
-
export declare
|
|
2
|
+
import type { ManagedRevisionSpec, SourceBundle, TonboDeclaration } from "./types.js";
|
|
3
|
+
export declare const DECLARATION_FILENAME = ".tonbo";
|
|
4
|
+
export declare const DEFAULT_INFERENCE_MODEL = "claude-sonnet-4-5";
|
|
5
|
+
export declare function createDeclaration(model?: string): TonboDeclaration;
|
|
6
|
+
export declare function renderDeclaration(declaration: TonboDeclaration): string;
|
|
7
|
+
export declare function declarationExists(root: string): Promise<boolean>;
|
|
8
|
+
export declare function saveDeclaration(root: string, declaration: TonboDeclaration, overwrite: boolean): Promise<void>;
|
|
9
|
+
export declare function loadDeclaration(declarationRoot: string): Promise<TonboDeclaration>;
|
|
4
10
|
export declare function buildRevision(declaration: ReturnType<typeof parseDeclaration>, source: SourceBundle): ManagedRevisionSpec;
|
package/dist/src/declaration.js
CHANGED
|
@@ -1,16 +1,82 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { lstat, open, readFile, rename, rm } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
4
|
+
import { parse, stringify } from "smol-toml";
|
|
3
5
|
import { assertManagedRevision, parseDeclaration } from "./contracts.js";
|
|
6
|
+
export const DECLARATION_FILENAME = ".tonbo";
|
|
7
|
+
export const DEFAULT_INFERENCE_MODEL = "claude-sonnet-4-5";
|
|
8
|
+
export function createDeclaration(model = DEFAULT_INFERENCE_MODEL) {
|
|
9
|
+
return parseDeclaration({
|
|
10
|
+
version: 1,
|
|
11
|
+
execution: { mode: "managed", runtime: "pi" },
|
|
12
|
+
inference: { model: model.trim() },
|
|
13
|
+
session_capture: { adapter: "pi-jsonl-v3" },
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export function renderDeclaration(declaration) {
|
|
17
|
+
return `# Tonbo Project Agent configuration.\n${stringify(declaration)}`;
|
|
18
|
+
}
|
|
19
|
+
export async function declarationExists(root) {
|
|
20
|
+
const filename = path.join(root, DECLARATION_FILENAME);
|
|
21
|
+
try {
|
|
22
|
+
const metadata = await lstat(filename);
|
|
23
|
+
if (metadata.isSymbolicLink() || !metadata.isFile())
|
|
24
|
+
throw new Error(`${filename} must be a regular file.`);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error.code === "ENOENT")
|
|
29
|
+
return false;
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export async function saveDeclaration(root, declaration, overwrite) {
|
|
34
|
+
const filename = path.join(root, DECLARATION_FILENAME);
|
|
35
|
+
const exists = await declarationExists(root);
|
|
36
|
+
if (exists && !overwrite)
|
|
37
|
+
throw new Error(`${DECLARATION_FILENAME} already exists. Pass --force to replace it.`);
|
|
38
|
+
const contents = renderDeclaration(declaration);
|
|
39
|
+
if (!overwrite) {
|
|
40
|
+
const handle = await open(filename, "wx", 0o644).catch((error) => {
|
|
41
|
+
if (error.code === "EEXIST")
|
|
42
|
+
throw new Error(`${DECLARATION_FILENAME} already exists. Pass --force to replace it.`);
|
|
43
|
+
throw error;
|
|
44
|
+
});
|
|
45
|
+
try {
|
|
46
|
+
await handle.writeFile(contents, "utf8");
|
|
47
|
+
await handle.sync();
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
await handle.close();
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const temporary = path.join(root, `.${DECLARATION_FILENAME}.${process.pid}.${randomUUID()}.tmp`);
|
|
55
|
+
let handle;
|
|
56
|
+
try {
|
|
57
|
+
handle = await open(temporary, "wx", 0o644);
|
|
58
|
+
await handle.writeFile(contents, "utf8");
|
|
59
|
+
await handle.sync();
|
|
60
|
+
await handle.close();
|
|
61
|
+
handle = undefined;
|
|
62
|
+
await rename(temporary, filename);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
await handle?.close().catch(() => undefined);
|
|
66
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
4
70
|
export async function loadDeclaration(declarationRoot) {
|
|
5
|
-
const filename = path.join(declarationRoot,
|
|
71
|
+
const filename = path.join(declarationRoot, DECLARATION_FILENAME);
|
|
6
72
|
let parsed;
|
|
7
73
|
try {
|
|
8
|
-
parsed =
|
|
74
|
+
parsed = parse(await readFile(filename, "utf8"));
|
|
9
75
|
}
|
|
10
76
|
catch (error) {
|
|
11
77
|
if (error.code === "ENOENT")
|
|
12
|
-
throw new Error(`No
|
|
13
|
-
throw new Error(`Could not read ${filename} as
|
|
78
|
+
throw new Error(`No ${DECLARATION_FILENAME} declaration found at ${filename}.`);
|
|
79
|
+
throw new Error(`Could not read ${filename} as TOML.`, { cause: error });
|
|
14
80
|
}
|
|
15
81
|
return parseDeclaration(parsed);
|
|
16
82
|
}
|
|
@@ -184,4 +184,5 @@ export declare const piSessionContract: {
|
|
|
184
184
|
readonly durable_completion_timeout_seconds: 30;
|
|
185
185
|
readonly session_directory: "/sessions";
|
|
186
186
|
readonly path_template: "/sessions/{session_id}.jsonl";
|
|
187
|
+
readonly preflight_command: readonly ["/usr/local/bin/artifacts", "runtime", "session-preflight"];
|
|
187
188
|
};
|
|
@@ -217,5 +217,10 @@ export const piSessionContract = {
|
|
|
217
217
|
"format_version": 3,
|
|
218
218
|
"durable_completion_timeout_seconds": 30,
|
|
219
219
|
"session_directory": "/sessions",
|
|
220
|
-
"path_template": "/sessions/{session_id}.jsonl"
|
|
220
|
+
"path_template": "/sessions/{session_id}.jsonl",
|
|
221
|
+
"preflight_command": [
|
|
222
|
+
"/usr/local/bin/artifacts",
|
|
223
|
+
"runtime",
|
|
224
|
+
"session-preflight"
|
|
225
|
+
]
|
|
221
226
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function terminalPrompt(question: string): Promise<string>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
export async function terminalPrompt(question) {
|
|
3
|
+
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
4
|
+
try {
|
|
5
|
+
return await prompt.question(`? ${question}`);
|
|
6
|
+
}
|
|
7
|
+
finally {
|
|
8
|
+
prompt.close();
|
|
9
|
+
}
|
|
10
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tonbo/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Deploy one persistent Agent per Project from the command line.",
|
|
5
5
|
"homepage": "https://tonbo.dev",
|
|
6
6
|
"bugs": {
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"ajv": "8.20.0",
|
|
34
34
|
"commander": "^14.0.3",
|
|
35
35
|
"ignore": "^7.0.5",
|
|
36
|
+
"smol-toml": "1.6.1",
|
|
36
37
|
"tar-stream": "^3.1.7"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|