@chvor/cli 0.1.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/bin/chvor.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ await import('../dist/cli.js');
package/dist/cli.js ADDED
@@ -0,0 +1,226 @@
1
+ import { Command } from "commander";
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf8"));
7
+ const program = new Command()
8
+ .name("chvor")
9
+ .description("Your own AI — install and run chvor.")
10
+ .version(pkg.version);
11
+ program
12
+ .action(async () => {
13
+ const { isOnboarded } = await import("./lib/config.js");
14
+ if (!isOnboarded()) {
15
+ const { onboard } = await import("./commands/onboard.js");
16
+ await onboard();
17
+ }
18
+ else {
19
+ const { start } = await import("./commands/start.js");
20
+ await start({});
21
+ }
22
+ });
23
+ program
24
+ .command("start")
25
+ .description("Start the chvor server")
26
+ .option("-p, --port <port>", "Server port")
27
+ .option("--foreground", "Run in foreground (no detach)")
28
+ .option("-i, --instance <name>", "Named instance to start")
29
+ .action(async (opts) => {
30
+ if (opts.instance) {
31
+ const { setInstance } = await import("./lib/paths.js");
32
+ setInstance(opts.instance);
33
+ }
34
+ if (opts.port) {
35
+ const { validatePort } = await import("./lib/validate.js");
36
+ validatePort(opts.port);
37
+ }
38
+ const { start } = await import("./commands/start.js");
39
+ await start(opts);
40
+ });
41
+ program
42
+ .command("stop")
43
+ .description("Stop the running chvor server")
44
+ .option("-i, --instance <name>", "Named instance to stop")
45
+ .action(async (opts) => {
46
+ if (opts.instance) {
47
+ const { setInstance } = await import("./lib/paths.js");
48
+ setInstance(opts.instance);
49
+ }
50
+ const { stop } = await import("./commands/stop.js");
51
+ await stop();
52
+ });
53
+ program
54
+ .command("init")
55
+ .description("Set up a new agent from a template")
56
+ .option("-t, --template <name>", "Template name or ID")
57
+ .option("-n, --name <instance>", "Instance name for multi-instance support")
58
+ .option("--from <path>", "Local template directory path")
59
+ .action(async (opts) => {
60
+ const { init } = await import("./commands/init.js");
61
+ await init(opts);
62
+ });
63
+ const instancesCmd = program
64
+ .command("instances")
65
+ .description("Manage chvor instances");
66
+ instancesCmd
67
+ .action(async () => {
68
+ const { listInstances } = await import("./commands/instances.js");
69
+ await listInstances();
70
+ });
71
+ instancesCmd
72
+ .command("start <name>")
73
+ .description("Start a named instance")
74
+ .action(async (name) => {
75
+ const { startInstance } = await import("./commands/instances.js");
76
+ await startInstance(name);
77
+ });
78
+ instancesCmd
79
+ .command("stop <name>")
80
+ .description("Stop a named instance")
81
+ .action(async (name) => {
82
+ const { stopInstance } = await import("./commands/instances.js");
83
+ await stopInstance(name);
84
+ });
85
+ program
86
+ .command("onboard")
87
+ .description("Interactive first-time setup wizard")
88
+ .action(async () => {
89
+ const { onboard } = await import("./commands/onboard.js");
90
+ await onboard();
91
+ });
92
+ program
93
+ .command("update")
94
+ .description("Update to the latest chvor release")
95
+ .action(async () => {
96
+ const { update } = await import("./commands/update.js");
97
+ await update();
98
+ });
99
+ program
100
+ .command("docker")
101
+ .description("Pull and run chvor as a Docker container")
102
+ .option("-p, --port <port>", "Host port", "3001")
103
+ .action(async (opts) => {
104
+ const { docker } = await import("./commands/docker.js");
105
+ await docker(opts);
106
+ });
107
+ const skillCmd = program
108
+ .command("skill")
109
+ .description("Manage skills from the community registry");
110
+ skillCmd
111
+ .command("search <query>")
112
+ .description("Search the skill registry")
113
+ .action(async (query) => {
114
+ const { skillSearch } = await import("./commands/skill.js");
115
+ await skillSearch(query);
116
+ });
117
+ skillCmd
118
+ .command("install <name>")
119
+ .description("Install a skill from the registry")
120
+ .action(async (name) => {
121
+ const { skillInstall } = await import("./commands/skill.js");
122
+ await skillInstall(name);
123
+ });
124
+ skillCmd
125
+ .command("uninstall <name>")
126
+ .description("Uninstall a registry skill")
127
+ .action(async (name) => {
128
+ const { skillUninstall } = await import("./commands/skill.js");
129
+ await skillUninstall(name);
130
+ });
131
+ skillCmd
132
+ .command("update [name]")
133
+ .description("Update one or all registry skills")
134
+ .action(async (name) => {
135
+ const { skillUpdate } = await import("./commands/skill.js");
136
+ await skillUpdate(name);
137
+ });
138
+ skillCmd
139
+ .command("list")
140
+ .description("List all installed skills")
141
+ .action(async () => {
142
+ const { skillList } = await import("./commands/skill.js");
143
+ await skillList();
144
+ });
145
+ skillCmd
146
+ .command("info <name>")
147
+ .description("Show details for a registry skill")
148
+ .action(async (name) => {
149
+ const { skillInfo } = await import("./commands/skill.js");
150
+ await skillInfo(name);
151
+ });
152
+ skillCmd
153
+ .command("publish <path>")
154
+ .description("Validate a skill file for publishing")
155
+ .action(async (path) => {
156
+ const { skillPublish } = await import("./commands/skill.js");
157
+ await skillPublish(path);
158
+ });
159
+ const toolCmd = program
160
+ .command("tool")
161
+ .description("Manage tools from the community registry");
162
+ toolCmd
163
+ .command("search <query>")
164
+ .description("Search the tool registry")
165
+ .action(async (query) => {
166
+ const { toolSearch } = await import("./commands/skill.js");
167
+ await toolSearch(query);
168
+ });
169
+ toolCmd
170
+ .command("install <name>")
171
+ .description("Install a tool from the registry")
172
+ .action(async (name) => {
173
+ const { toolInstall } = await import("./commands/skill.js");
174
+ await toolInstall(name);
175
+ });
176
+ toolCmd
177
+ .command("uninstall <name>")
178
+ .description("Uninstall a registry tool")
179
+ .action(async (name) => {
180
+ const { toolUninstall } = await import("./commands/skill.js");
181
+ await toolUninstall(name);
182
+ });
183
+ toolCmd
184
+ .command("update [name]")
185
+ .description("Update one or all registry tools")
186
+ .action(async (name) => {
187
+ const { toolUpdate } = await import("./commands/skill.js");
188
+ await toolUpdate(name);
189
+ });
190
+ toolCmd
191
+ .command("list")
192
+ .description("List all installed tools")
193
+ .action(async () => {
194
+ const { toolList } = await import("./commands/skill.js");
195
+ await toolList();
196
+ });
197
+ toolCmd
198
+ .command("info <name>")
199
+ .description("Show details for a registry tool")
200
+ .action(async (name) => {
201
+ const { toolInfo } = await import("./commands/skill.js");
202
+ await toolInfo(name);
203
+ });
204
+ toolCmd
205
+ .command("publish <path>")
206
+ .description("Validate and publish a tool file")
207
+ .action(async (path) => {
208
+ const { toolPublish } = await import("./commands/skill.js");
209
+ await toolPublish(path);
210
+ });
211
+ const authCmd = program
212
+ .command("auth")
213
+ .description("Manage authentication");
214
+ authCmd
215
+ .command("reset")
216
+ .description("Reset authentication credentials (requires access to data directory)")
217
+ .option("-i, --instance <name>", "Named instance to reset")
218
+ .action(async (opts) => {
219
+ if (opts.instance) {
220
+ const { setInstance } = await import("./lib/paths.js");
221
+ setInstance(opts.instance);
222
+ }
223
+ const { authReset } = await import("./commands/auth.js");
224
+ await authReset();
225
+ });
226
+ await program.parseAsync(process.argv);
@@ -0,0 +1,50 @@
1
+ import { join } from "node:path";
2
+ import { existsSync } from "node:fs";
3
+ import { confirm } from "@inquirer/prompts";
4
+ import { getDataDir } from "../lib/paths.js";
5
+ export async function authReset() {
6
+ const dataDir = getDataDir();
7
+ const dbPath = join(dataDir, "chvor.db");
8
+ if (!existsSync(dbPath)) {
9
+ console.error("No database found at", dbPath);
10
+ console.error("Nothing to reset.");
11
+ process.exit(1);
12
+ }
13
+ const confirmed = await confirm({
14
+ message: "This will remove all authentication data (login credentials, sessions, API keys).\nYou will need to set up new credentials on next login.\n\nContinue?",
15
+ default: false,
16
+ });
17
+ if (!confirmed) {
18
+ console.log("Cancelled.");
19
+ return;
20
+ }
21
+ // Dynamic import — better-sqlite3 is an optional peer dep for the CLI
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ let Database;
24
+ try {
25
+ const mod = await import("better-sqlite3");
26
+ Database = mod.default;
27
+ }
28
+ catch {
29
+ console.error("better-sqlite3 is not installed. Install it to use auth reset:\n npm install better-sqlite3");
30
+ process.exit(1);
31
+ }
32
+ const db = new Database(dbPath);
33
+ try {
34
+ // Check if auth tables exist
35
+ const hasAuthConfig = db
36
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='auth_config'")
37
+ .get();
38
+ if (!hasAuthConfig) {
39
+ console.log("Auth tables not found — nothing to reset.");
40
+ return;
41
+ }
42
+ db.exec("DELETE FROM auth_config");
43
+ db.exec("DELETE FROM auth_sessions");
44
+ db.exec("DELETE FROM api_keys");
45
+ console.log("\nAuthentication reset successfully.\nYou will be prompted to set up new credentials on next login.");
46
+ }
47
+ finally {
48
+ db.close();
49
+ }
50
+ }
@@ -0,0 +1,17 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { homedir } from "node:os";
3
+ import { validatePort } from "../lib/validate.js";
4
+ export async function docker(opts) {
5
+ const port = validatePort(opts.port ?? "3001");
6
+ const image = "ghcr.io/luka-zivkovic/chvor:latest";
7
+ console.log("Pulling latest chvor Docker image...");
8
+ execFileSync("docker", ["pull", image], { stdio: "inherit" });
9
+ console.log("Starting chvor container...");
10
+ execFileSync("docker", [
11
+ "run", "-d", "--name", "chvor",
12
+ "-p", `${port}:3001`,
13
+ "-v", `${homedir()}/.chvor:/home/node/.chvor`,
14
+ image,
15
+ ], { stdio: "inherit" });
16
+ console.log(`chvor is running at http://localhost:${port}`);
17
+ }
@@ -0,0 +1,221 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { existsSync, readFileSync, rmSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { input, select, password, confirm } from "@inquirer/prompts";
6
+ import { writeConfig } from "../lib/config.js";
7
+ import { validatePort } from "../lib/validate.js";
8
+ import { getChvorHome, getDataDir, setInstance, ensureDir, } from "../lib/paths.js";
9
+ import { downloadRelease, isInstalled } from "../lib/download.js";
10
+ import { spawnServer, pollHealth } from "../lib/process.js";
11
+ import { resolveTemplate, resolveRegistryTemplate, listBundledTemplates, } from "../lib/template-loader.js";
12
+ import { provision } from "../lib/template-provisioner.js";
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ const pkg = JSON.parse(readFileSync(join(__dirname, "../../package.json"), "utf8"));
15
+ async function pickTemplate() {
16
+ const bundled = listBundledTemplates();
17
+ if (bundled.length === 0) {
18
+ throw new Error("No bundled templates found. Use --template <path> or --from <path> to specify a template directory.");
19
+ }
20
+ const choice = await select({
21
+ message: "Choose a template:",
22
+ choices: bundled.map((t) => ({
23
+ name: `${t.name} — ${t.description}`,
24
+ value: t.id,
25
+ })),
26
+ });
27
+ return resolveTemplate(choice);
28
+ }
29
+ async function collectCredentials(credentialDefs, port, token) {
30
+ if (!credentialDefs.length)
31
+ return;
32
+ console.log("\n This template requires the following credentials:\n");
33
+ for (const cred of credentialDefs) {
34
+ console.log(` ${cred.name}: ${cred.description}`);
35
+ const data = {};
36
+ for (const field of cred.fields) {
37
+ if (field.secret) {
38
+ data[field.name] = await password({ message: ` ${field.label}:` });
39
+ }
40
+ else {
41
+ data[field.name] = await input({ message: ` ${field.label}:` });
42
+ }
43
+ }
44
+ const res = await fetch(`http://localhost:${port}/api/credentials`, {
45
+ method: "POST",
46
+ headers: {
47
+ "Content-Type": "application/json",
48
+ Authorization: `Bearer ${token}`,
49
+ },
50
+ body: JSON.stringify({
51
+ name: cred.name,
52
+ type: cred.type,
53
+ data,
54
+ }),
55
+ });
56
+ if (!res.ok) {
57
+ console.warn(` Warning: failed to save ${cred.name} credential (${res.status}).`);
58
+ }
59
+ else {
60
+ console.log(` ${cred.name} credential saved.`);
61
+ }
62
+ }
63
+ }
64
+ export async function init(opts) {
65
+ console.log("\n chvor init — set up a new agent from a template.\n");
66
+ // 1. Resolve template
67
+ let templatePath;
68
+ let manifest;
69
+ let isRegistryTemplate = false;
70
+ const source = opts.from || opts.template;
71
+ if (source?.startsWith("registry:")) {
72
+ const id = source.slice("registry:".length);
73
+ console.log(` Fetching template "${id}" from registry...`);
74
+ const result = await resolveRegistryTemplate(id);
75
+ templatePath = result.path;
76
+ manifest = result.manifest;
77
+ isRegistryTemplate = true;
78
+ }
79
+ else if (source) {
80
+ const result = resolveTemplate(source);
81
+ templatePath = result.path;
82
+ manifest = result.manifest;
83
+ }
84
+ else {
85
+ const result = await pickTemplate();
86
+ templatePath = result.path;
87
+ manifest = result.manifest;
88
+ }
89
+ console.log(` Template: ${manifest.name} (v${manifest.version})`);
90
+ if (manifest.description) {
91
+ console.log(` ${manifest.description}\n`);
92
+ }
93
+ // 2. Instance name
94
+ const instanceName = opts.name ?? await input({
95
+ message: "Instance name (leave blank for default):",
96
+ default: "",
97
+ });
98
+ if (instanceName) {
99
+ // Validate instance name: alphanumeric, hyphens, underscores
100
+ if (!/^[a-zA-Z0-9_-]+$/.test(instanceName)) {
101
+ throw new Error("Instance name must contain only letters, numbers, hyphens, and underscores.");
102
+ }
103
+ setInstance(instanceName);
104
+ }
105
+ const home = getChvorHome();
106
+ if (existsSync(join(home, "config.json"))) {
107
+ const overwrite = await confirm({
108
+ message: `Instance directory ${home} already exists. Overwrite?`,
109
+ default: false,
110
+ });
111
+ if (!overwrite) {
112
+ console.log(" Aborted.");
113
+ return;
114
+ }
115
+ }
116
+ // 3. Basic onboarding prompts
117
+ const userName = await input({ message: "What's your name?" });
118
+ const detectedTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
119
+ const timezone = await input({
120
+ message: "Your timezone?",
121
+ default: detectedTimezone,
122
+ });
123
+ const provider = await select({
124
+ message: "LLM provider?",
125
+ choices: [
126
+ { name: "Anthropic (Claude)", value: "anthropic" },
127
+ { name: "OpenAI (GPT)", value: "openai" },
128
+ { name: "Google (Gemini)", value: "google-ai" },
129
+ ],
130
+ });
131
+ const apiKey = await password({ message: "API key:" });
132
+ const defaultPort = instanceName ? "3002" : "3001";
133
+ const port = validatePort(await input({ message: "Port?", default: defaultPort }));
134
+ const token = randomBytes(32).toString("hex");
135
+ // 4. Write config
136
+ writeConfig({
137
+ port,
138
+ token,
139
+ onboarded: true,
140
+ llmProvider: provider,
141
+ instanceName: instanceName || undefined,
142
+ templateName: manifest.name,
143
+ });
144
+ ensureDir(getDataDir());
145
+ // 5. Download release if needed
146
+ const version = pkg.version;
147
+ if (!isInstalled(version)) {
148
+ await downloadRelease(version);
149
+ }
150
+ // 6. Start server
151
+ await spawnServer({ port });
152
+ const healthy = await pollHealth(port, token);
153
+ if (!healthy) {
154
+ console.warn(" Server started but health check did not pass within timeout.");
155
+ return;
156
+ }
157
+ // 7. Save LLM provider credential
158
+ const providerNames = {
159
+ anthropic: "Anthropic",
160
+ openai: "OpenAI",
161
+ "google-ai": "Google AI",
162
+ };
163
+ await fetch(`http://localhost:${port}/api/credentials`, {
164
+ method: "POST",
165
+ headers: {
166
+ "Content-Type": "application/json",
167
+ Authorization: `Bearer ${token}`,
168
+ },
169
+ body: JSON.stringify({
170
+ name: providerNames[provider],
171
+ type: provider,
172
+ data: { apiKey },
173
+ }),
174
+ });
175
+ // 8. Save persona (user name + timezone + template persona)
176
+ await fetch(`http://localhost:${port}/api/persona`, {
177
+ method: "PATCH",
178
+ headers: {
179
+ "Content-Type": "application/json",
180
+ Authorization: `Bearer ${token}`,
181
+ },
182
+ body: JSON.stringify({
183
+ name: userName,
184
+ timezone,
185
+ onboarded: true,
186
+ }),
187
+ });
188
+ // 9. Collect template-specific credentials
189
+ const templateCredentials = (manifest.credentials ?? []).filter((c) => c.type !== provider // Skip if already collected as LLM provider
190
+ );
191
+ await collectCredentials(templateCredentials, port, token);
192
+ // 10. Provision template
193
+ await provision({
194
+ port,
195
+ token,
196
+ templatePath,
197
+ manifest,
198
+ });
199
+ // Clean up temp directory from registry template
200
+ if (isRegistryTemplate) {
201
+ try {
202
+ rmSync(templatePath, { recursive: true, force: true });
203
+ }
204
+ catch { /* non-critical */ }
205
+ }
206
+ // 11. Done
207
+ const label = instanceName ? ` (instance: ${instanceName})` : "";
208
+ console.log(`\n chvor is running at http://localhost:${port}${label}`);
209
+ console.log(" Open this URL in your browser to get started.\n");
210
+ console.log(" Useful commands:");
211
+ if (instanceName) {
212
+ console.log(` chvor stop --instance ${instanceName} Stop this instance`);
213
+ console.log(` chvor start --instance ${instanceName} Start this instance`);
214
+ console.log(" chvor instances List all instances");
215
+ }
216
+ else {
217
+ console.log(" chvor stop Stop the server");
218
+ console.log(" chvor start Start the server");
219
+ }
220
+ console.log(" chvor update Update to latest version\n");
221
+ }
@@ -0,0 +1,126 @@
1
+ import { readdirSync, existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { setInstance } from "../lib/paths.js";
5
+ import { readConfig } from "../lib/config.js";
6
+ import { stopServer } from "../lib/process.js";
7
+ function discoverInstances() {
8
+ const home = homedir();
9
+ const instances = [];
10
+ // Check default instance
11
+ const defaultConfig = join(home, ".chvor", "config.json");
12
+ if (existsSync(defaultConfig)) {
13
+ try {
14
+ const config = JSON.parse(readFileSync(defaultConfig, "utf-8"));
15
+ // Check if running
16
+ const pidPath = join(home, ".chvor", "chvor.pid");
17
+ let running = false;
18
+ let pid;
19
+ if (existsSync(pidPath)) {
20
+ const rawPid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
21
+ if (!isNaN(rawPid)) {
22
+ try {
23
+ process.kill(rawPid, 0);
24
+ running = true;
25
+ pid = rawPid;
26
+ }
27
+ catch {
28
+ // not running
29
+ }
30
+ }
31
+ }
32
+ instances.push({
33
+ name: "(default)",
34
+ port: config.port || "3001",
35
+ running,
36
+ pid,
37
+ template: config.templateName,
38
+ });
39
+ }
40
+ catch (err) {
41
+ console.warn(` Warning: could not read default instance config: ${err instanceof Error ? err.message : err}`);
42
+ }
43
+ }
44
+ // Scan for named instances (~/.chvor-*/config.json)
45
+ const entries = readdirSync(home, { withFileTypes: true });
46
+ for (const entry of entries) {
47
+ if (!entry.isDirectory())
48
+ continue;
49
+ const match = entry.name.match(/^\.chvor-(.+)$/);
50
+ if (!match)
51
+ continue;
52
+ const instanceName = match[1];
53
+ const configPath = join(home, entry.name, "config.json");
54
+ if (!existsSync(configPath))
55
+ continue;
56
+ try {
57
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
58
+ const pidPath = join(home, entry.name, "chvor.pid");
59
+ let running = false;
60
+ let pid;
61
+ if (existsSync(pidPath)) {
62
+ const rawPid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
63
+ if (!isNaN(rawPid)) {
64
+ try {
65
+ process.kill(rawPid, 0);
66
+ running = true;
67
+ pid = rawPid;
68
+ }
69
+ catch {
70
+ // not running
71
+ }
72
+ }
73
+ }
74
+ instances.push({
75
+ name: instanceName,
76
+ port: config.port || "?",
77
+ running,
78
+ pid,
79
+ template: config.templateName,
80
+ });
81
+ }
82
+ catch (err) {
83
+ console.warn(` Warning: could not read instance "${instanceName}" config: ${err instanceof Error ? err.message : err}`);
84
+ }
85
+ }
86
+ return instances;
87
+ }
88
+ export async function listInstances() {
89
+ const instances = discoverInstances();
90
+ if (instances.length === 0) {
91
+ console.log(" No chvor instances found. Run 'chvor init' to create one.");
92
+ return;
93
+ }
94
+ console.log("\n Chvor instances:\n");
95
+ console.log(" " +
96
+ "NAME".padEnd(20) +
97
+ "PORT".padEnd(8) +
98
+ "STATUS".padEnd(12) +
99
+ "TEMPLATE");
100
+ console.log(" " + "-".repeat(60));
101
+ for (const inst of instances) {
102
+ const status = inst.running
103
+ ? `running (${inst.pid})`
104
+ : "stopped";
105
+ console.log(" " +
106
+ inst.name.padEnd(20) +
107
+ inst.port.padEnd(8) +
108
+ status.padEnd(12) +
109
+ (inst.template || "-"));
110
+ }
111
+ console.log();
112
+ }
113
+ export async function startInstance(name) {
114
+ setInstance(name);
115
+ const config = readConfig();
116
+ if (!config.onboarded) {
117
+ console.log(`Instance "${name}" has not been set up. Run 'chvor init --name ${name}' first.`);
118
+ return;
119
+ }
120
+ const { start } = await import("./start.js");
121
+ await start({});
122
+ }
123
+ export async function stopInstance(name) {
124
+ setInstance(name);
125
+ await stopServer();
126
+ }