@chatbridge/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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 7milch
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @chatbridge/cli
2
+
3
+ The CLI for [chatbridge](https://github.com/7milch/chatbridge-cli): drive browser-only web chat AI services from the command line.
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { createCli } from "./create-cli.js";
3
+ // Set exitCode instead of calling process.exit(): on a pipe, stdout writes are
4
+ // asynchronous and process.exit() would drop pending output.
5
+ process.exitCode = await createCli({ name: "chatbridge" }).run(process.argv);
@@ -0,0 +1,14 @@
1
+ export interface CliConfig {
2
+ /** Provider spec used when --provider is absent. Relative paths are
3
+ * already resolved against the config file's directory. */
4
+ defaultProvider?: string;
5
+ }
6
+ export interface ConfigLocation {
7
+ /** Directory name under the base dir, e.g. "chatbridge". */
8
+ configDir: string;
9
+ /** Base directory; defaults to ~/.config. Overridable for tests. */
10
+ baseDir?: string;
11
+ }
12
+ export declare function configPath(loc: ConfigLocation): string;
13
+ /** Reads <base>/<configDir>/config.json. Missing file → {}. */
14
+ export declare function loadConfig(loc: ConfigLocation): Promise<CliConfig>;
package/dist/config.js ADDED
@@ -0,0 +1,48 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { ChatBridgeError } from "@chatbridge/core";
5
+ export function configPath(loc) {
6
+ const base = loc.baseDir ?? join(homedir(), ".config");
7
+ return join(base, loc.configDir, "config.json");
8
+ }
9
+ function invalid(file, why, cause) {
10
+ return new ChatBridgeError("INVALID_CONFIG", `Invalid config ${file}: ${why}`, {
11
+ cause,
12
+ });
13
+ }
14
+ /** Reads <base>/<configDir>/config.json. Missing file → {}. */
15
+ export async function loadConfig(loc) {
16
+ const file = configPath(loc);
17
+ let raw;
18
+ try {
19
+ raw = await readFile(file, "utf8");
20
+ }
21
+ catch (err) {
22
+ if (err.code === "ENOENT")
23
+ return {};
24
+ throw invalid(file, "could not read the file", err);
25
+ }
26
+ let doc;
27
+ try {
28
+ doc = JSON.parse(raw);
29
+ }
30
+ catch (err) {
31
+ throw invalid(file, "not valid JSON", err);
32
+ }
33
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
34
+ throw invalid(file, "top level must be a JSON object");
35
+ }
36
+ const { defaultProvider } = doc;
37
+ const cfg = {};
38
+ if (defaultProvider !== undefined) {
39
+ if (typeof defaultProvider !== "string") {
40
+ throw invalid(file, '"defaultProvider" must be a string');
41
+ }
42
+ const isRelative = defaultProvider.startsWith("./") || defaultProvider.startsWith("../");
43
+ cfg.defaultProvider = isRelative
44
+ ? resolve(dirname(file), defaultProvider)
45
+ : defaultProvider;
46
+ }
47
+ return cfg;
48
+ }
@@ -0,0 +1,14 @@
1
+ import { type Provider } from "@chatbridge/core";
2
+ export interface CreateCliOptions {
3
+ /** CLI name shown in help and errors, e.g. "chatbridge" or "company-ai-cli". */
4
+ name: string;
5
+ /** Pinned provider. When set, --provider is rejected and config is not read. */
6
+ provider?: Provider;
7
+ /** Config directory name under ~/.config; defaults to `name`. */
8
+ configDir?: string;
9
+ /** Test-only: overrides the config/auth-store base directory. */
10
+ baseDir?: string;
11
+ }
12
+ export declare function createCli(opts: CreateCliOptions): {
13
+ run: (argv: string[]) => Promise<number>;
14
+ };
@@ -0,0 +1,160 @@
1
+ import { parseArgs } from "node:util";
2
+ import { ChatBridgeError, ProviderLoadError, createAuthStore, runLogin, runOneShot, } from "@chatbridge/core";
3
+ import { configPath, loadConfig } from "./config.js";
4
+ import { resolveProvider } from "./resolve-provider.js";
5
+ /** Single source of truth for `ChatBridgeError.code` → process exit code. */
6
+ const EXIT_CODES = {
7
+ INVALID_ARGUMENT: 1,
8
+ INVALID_CONFIG: 1,
9
+ AUTH_REQUIRED: 2,
10
+ AUTH_EXPIRED: 3,
11
+ RESPONSE_TIMEOUT: 4,
12
+ PROVIDER_LOAD: 5,
13
+ INVALID_PROVIDER: 5,
14
+ };
15
+ const DEFAULT_TIMEOUT_SEC = 120;
16
+ /** Validates --timeout before any browser is launched. */
17
+ function parseTimeoutMs(raw) {
18
+ if (raw === undefined)
19
+ return DEFAULT_TIMEOUT_SEC * 1000;
20
+ const seconds = Number(raw);
21
+ if (!Number.isFinite(seconds) || seconds <= 0) {
22
+ throw new ChatBridgeError("INVALID_ARGUMENT", "--timeout must be a positive number of seconds");
23
+ }
24
+ return seconds * 1000;
25
+ }
26
+ function isParseArgsError(err) {
27
+ const code = err?.code;
28
+ return (err instanceof Error &&
29
+ typeof code === "string" &&
30
+ code.startsWith("ERR_PARSE_ARGS_"));
31
+ }
32
+ export function createCli(opts) {
33
+ const configDir = opts.configDir ?? opts.name;
34
+ const location = { configDir, baseDir: opts.baseDir };
35
+ function help() {
36
+ const providerFlag = opts.provider ? "" : " [--provider <name|path>]";
37
+ return [
38
+ "Usage:",
39
+ ` ${opts.name} -p <prompt>${providerFlag} [--headful] [--timeout <sec>]`,
40
+ ` ${opts.name} auth login${providerFlag}`,
41
+ ` ${opts.name} auth logout${providerFlag}`,
42
+ ` ${opts.name} auth status${providerFlag}`,
43
+ "",
44
+ "One-shot mode prints the AI response to stdout.",
45
+ ...(opts.provider
46
+ ? []
47
+ : [
48
+ "",
49
+ `Without --provider, "defaultProvider" from ${configPath(location)} is used.`,
50
+ ]),
51
+ ].join("\n");
52
+ }
53
+ // Progress goes to stderr, and only when stderr is a TTY (stdout stays
54
+ // pipe-safe: response body only).
55
+ function progress(message) {
56
+ if (process.stderr.isTTY)
57
+ process.stderr.write(`${message}\n`);
58
+ }
59
+ /** Resolution order: pinned provider → --provider → config defaultProvider. */
60
+ async function getProvider(flag) {
61
+ if (opts.provider) {
62
+ if (flag !== undefined) {
63
+ throw new ChatBridgeError("INVALID_ARGUMENT", `${opts.name} has a fixed provider; --provider is not accepted`);
64
+ }
65
+ return opts.provider;
66
+ }
67
+ const spec = flag ?? (await loadConfig(location)).defaultProvider;
68
+ if (!spec) {
69
+ throw new ProviderLoadError(`No provider specified. Pass --provider <npm-package|./path> or set "defaultProvider" in ${configPath(location)}.`);
70
+ }
71
+ return resolveProvider(spec);
72
+ }
73
+ function reportError(err) {
74
+ if (isParseArgsError(err)) {
75
+ process.stderr.write(`${opts.name}: ${err.message}\n\n${help()}\n`);
76
+ return 1;
77
+ }
78
+ if (err instanceof ChatBridgeError) {
79
+ process.stderr.write(`${opts.name}: ${err.message}\n`);
80
+ if (process.env.CHATBRIDGE_DEBUG === "1" && err.cause !== undefined) {
81
+ const cause = err.cause;
82
+ const detail = cause instanceof Error
83
+ ? (cause.stack ?? cause.message)
84
+ : String(cause);
85
+ process.stderr.write(`Caused by: ${detail}\n`);
86
+ }
87
+ return EXIT_CODES[err.code] ?? 1;
88
+ }
89
+ process.stderr.write(`${opts.name}: unexpected error: ${err instanceof Error ? err.message : String(err)}\n`);
90
+ return 1;
91
+ }
92
+ async function run(argv) {
93
+ try {
94
+ const { values, positionals } = parseArgs({
95
+ args: argv.slice(2),
96
+ options: {
97
+ prompt: { type: "string", short: "p" },
98
+ provider: { type: "string" },
99
+ headful: { type: "boolean", default: false },
100
+ timeout: { type: "string" },
101
+ help: { type: "boolean", short: "h", default: false },
102
+ },
103
+ allowPositionals: true,
104
+ });
105
+ if (values.help) {
106
+ console.log(help());
107
+ return 0;
108
+ }
109
+ const [cmd, sub] = positionals;
110
+ if (cmd === "auth" &&
111
+ (sub === "login" || sub === "logout" || sub === "status")) {
112
+ const provider = await getProvider(values.provider);
113
+ const authStore = createAuthStore({
114
+ configDir,
115
+ providerName: provider.name,
116
+ baseDir: opts.baseDir,
117
+ });
118
+ if (sub === "login") {
119
+ await runLogin({ provider, authStore, onProgress: progress });
120
+ return 0;
121
+ }
122
+ if (sub === "logout") {
123
+ await authStore.clear();
124
+ progress("✓ Auth state deleted");
125
+ return 0;
126
+ }
127
+ console.log(authStore.has()
128
+ ? `Auth state present for "${provider.name}" (${authStore.path()})`
129
+ : `No auth state for "${provider.name}"`);
130
+ return 0;
131
+ }
132
+ if (typeof values.prompt === "string") {
133
+ const timeoutMs = parseTimeoutMs(values.timeout);
134
+ const provider = await getProvider(values.provider);
135
+ const authStore = createAuthStore({
136
+ configDir,
137
+ providerName: provider.name,
138
+ baseDir: opts.baseDir,
139
+ });
140
+ const reply = await runOneShot({
141
+ provider,
142
+ authStore,
143
+ prompt: values.prompt,
144
+ headless: !values.headful,
145
+ timeoutMs,
146
+ onProgress: progress,
147
+ });
148
+ // stdout: response body only.
149
+ process.stdout.write(`${reply}\n`);
150
+ return 0;
151
+ }
152
+ console.log(help());
153
+ return cmd === undefined ? 0 : 1;
154
+ }
155
+ catch (err) {
156
+ return reportError(err);
157
+ }
158
+ }
159
+ return { run };
160
+ }
@@ -0,0 +1,3 @@
1
+ export { createCli, type CreateCliOptions } from "./create-cli.js";
2
+ export { resolveProvider } from "./resolve-provider.js";
3
+ export { type CliConfig, configPath, loadConfig } from "./config.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { createCli } from "./create-cli.js";
2
+ export { resolveProvider } from "./resolve-provider.js";
3
+ export { configPath, loadConfig } from "./config.js";
@@ -0,0 +1,4 @@
1
+ import { type Provider } from "@chatbridge/core";
2
+ /** Loads a Provider from a local file path or an npm package name.
3
+ * The module's default export must implement the Provider interface. */
4
+ export declare function resolveProvider(spec: string): Promise<Provider>;
@@ -0,0 +1,34 @@
1
+ import { resolve } from "node:path";
2
+ import { ProviderLoadError } from "@chatbridge/core";
3
+ const REQUIRED_METHODS = [
4
+ "navigateToLogin",
5
+ "isLoggedIn",
6
+ "startNewChat",
7
+ "sendMessage",
8
+ "waitForResponse",
9
+ ];
10
+ function isProvider(value) {
11
+ if (typeof value !== "object" || value === null)
12
+ return false;
13
+ const v = value;
14
+ return (typeof v.name === "string" &&
15
+ typeof v.chatUrl === "string" &&
16
+ REQUIRED_METHODS.every((m) => typeof v[m] === "function"));
17
+ }
18
+ /** Loads a Provider from a local file path or an npm package name.
19
+ * The module's default export must implement the Provider interface. */
20
+ export async function resolveProvider(spec) {
21
+ const isPath = spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/");
22
+ const target = isPath ? resolve(process.cwd(), spec) : spec;
23
+ let mod;
24
+ try {
25
+ mod = await import(target);
26
+ }
27
+ catch (err) {
28
+ throw new ProviderLoadError(`Could not load provider "${spec}": ${err instanceof Error ? err.message : String(err)}`, { cause: err });
29
+ }
30
+ if (!isProvider(mod.default)) {
31
+ throw new ProviderLoadError(`Module "${spec}" does not default-export a Provider (name, chatUrl, and the five methods are required).`);
32
+ }
33
+ return mod.default;
34
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@chatbridge/cli",
3
+ "version": "0.1.0",
4
+ "description": "Drive browser-only web chat AI services from the command line",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/7milch/chatbridge-cli.git",
9
+ "directory": "packages/cli"
10
+ },
11
+ "keywords": ["chatbridge", "cli", "playwright", "chat", "ai"],
12
+ "type": "module",
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "bin": {
23
+ "chatbridge": "./dist/bin.js"
24
+ },
25
+ "files": ["dist", "README.md", "LICENSE"],
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "dependencies": {
30
+ "@chatbridge/core": "0.1.0"
31
+ },
32
+ "devDependencies": {
33
+ "@chatbridge/example-dummy-chat": "0.0.0"
34
+ }
35
+ }