chad-bridge 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/dist/cli.js ADDED
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const commander_1 = require("commander");
38
+ const path = __importStar(require("path"));
39
+ const bridge_1 = require("./bridge");
40
+ const VERSION = "0.1.0";
41
+ const program = new commander_1.Command();
42
+ program
43
+ .name("chad-bridge")
44
+ .description("Connect your local machine to Chad AI on disclosedcapitol.com")
45
+ .version(VERSION)
46
+ .requiredOption("-k, --key <key>", "API key from disclosedcapitol.com/account")
47
+ .option("-d, --dir <directory>", "Working directory (default: current directory)", process.cwd())
48
+ .option("--auto-approve", "Auto-approve all commands (dangerous!)", false)
49
+ .option("-v, --verbose", "Verbose logging", false)
50
+ .action(async (opts) => {
51
+ const workingDir = path.resolve(opts.dir);
52
+ console.log("");
53
+ console.log(" ╔══════════════════════════════════════════╗");
54
+ console.log(` ║ Chad Bridge v${VERSION} ║`);
55
+ console.log(" ║ disclosedcapitol.com ║");
56
+ console.log(" ╠══════════════════════════════════════════╣");
57
+ console.log(` ║ Project: ${workingDir.slice(0, 30).padEnd(30)} ║`);
58
+ console.log(" ╚══════════════════════════════════════════╝");
59
+ console.log("");
60
+ if (opts.autoApprove) {
61
+ console.log(" \x1b[31m⚠ AUTO-APPROVE MODE: All commands will execute without confirmation\x1b[0m");
62
+ console.log("");
63
+ }
64
+ const bridge = new bridge_1.ChadBridge({
65
+ apiKey: opts.key,
66
+ workingDir,
67
+ autoApproveAll: opts.autoApprove,
68
+ verbose: opts.verbose,
69
+ });
70
+ // Graceful shutdown
71
+ process.on("SIGINT", () => {
72
+ console.log("\n Disconnecting...");
73
+ bridge.disconnect();
74
+ process.exit(0);
75
+ });
76
+ process.on("SIGTERM", () => {
77
+ bridge.disconnect();
78
+ process.exit(0);
79
+ });
80
+ try {
81
+ await bridge.connect();
82
+ }
83
+ catch (err) {
84
+ const e = err;
85
+ console.error(` \x1b[31m✗ Failed to connect: ${e.message}\x1b[0m`);
86
+ console.error(" Check your API key and internet connection.");
87
+ console.error(" Get your API key at: https://disclosedcapitol.com/account?tab=api-keys");
88
+ process.exit(1);
89
+ }
90
+ });
91
+ program.parse();
92
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEA,yCAAoC;AACpC,2CAA6B;AAC7B,qCAAsC;AAEtC,MAAM,OAAO,GAAG,OAAO,CAAC;AAExB,MAAM,OAAO,GAAG,IAAI,mBAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,aAAa,CAAC;KACnB,WAAW,CAAC,+DAA+D,CAAC;KAC5E,OAAO,CAAC,OAAO,CAAC;KAChB,cAAc,CAAC,iBAAiB,EAAE,2CAA2C,CAAC;KAC9E,MAAM,CAAC,uBAAuB,EAAE,gDAAgD,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC;KAChG,MAAM,CAAC,gBAAgB,EAAE,wCAAwC,EAAE,KAAK,CAAC;KACzE,MAAM,CAAC,eAAe,EAAE,iBAAiB,EAAE,KAAK,CAAC;KACjD,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;IACrB,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAE1C,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAChB,OAAO,CAAC,GAAG,CAAC,gDAAgD,CAAC,CAAC;IAC9D,OAAO,CAAC,GAAG,CAAC,qBAAqB,OAAO,yBAAyB,CAAC,CAAC;IACnE,OAAO,CAAC,GAAG,CAAC,gDAAgD,CAAC,CAAC;IAC9D,OAAO,CAAC,GAAG,CAAC,gDAAgD,CAAC,CAAC;IAC9D,OAAO,CAAC,GAAG,CAAC,iBAAiB,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;IACrE,OAAO,CAAC,GAAG,CAAC,gDAAgD,CAAC,CAAC;IAC9D,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAEhB,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;QACrB,OAAO,CAAC,GAAG,CAAC,uFAAuF,CAAC,CAAC;QACrG,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,mBAAU,CAAC;QAC5B,MAAM,EAAE,IAAI,CAAC,GAAG;QAChB,UAAU;QACV,cAAc,EAAE,IAAI,CAAC,WAAW;QAChC,OAAO,EAAE,IAAI,CAAC,OAAO;KACtB,CAAC,CAAC;IAEH,oBAAoB;IACpB,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;QACxB,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;QACpC,MAAM,CAAC,UAAU,EAAE,CAAC;QACpB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;QACzB,MAAM,CAAC,UAAU,EAAE,CAAC;QACpB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,OAAO,EAAE,CAAC;IACzB,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACtB,MAAM,CAAC,GAAG,GAA2B,CAAC;QACtC,OAAO,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC,OAAO,SAAS,CAAC,CAAC;QACpE,OAAO,CAAC,KAAK,CAAC,+CAA+C,CAAC,CAAC;QAC/D,OAAO,CAAC,KAAK,CAAC,0EAA0E,CAAC,CAAC;QAC1F,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,KAAK,EAAE,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { ChadBridge } from "./bridge";
2
+ export { executeAction, AUTO_APPROVE_ACTIONS } from "./actions";
3
+ export type { CommandParams, CommandResult } from "./actions";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAChE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AUTO_APPROVE_ACTIONS = exports.executeAction = exports.ChadBridge = void 0;
4
+ var bridge_1 = require("./bridge");
5
+ Object.defineProperty(exports, "ChadBridge", { enumerable: true, get: function () { return bridge_1.ChadBridge; } });
6
+ var actions_1 = require("./actions");
7
+ Object.defineProperty(exports, "executeAction", { enumerable: true, get: function () { return actions_1.executeAction; } });
8
+ Object.defineProperty(exports, "AUTO_APPROVE_ACTIONS", { enumerable: true, get: function () { return actions_1.AUTO_APPROVE_ACTIONS; } });
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,mCAAsC;AAA7B,oGAAA,UAAU,OAAA;AACnB,qCAAgE;AAAvD,wGAAA,aAAa,OAAA;AAAE,+GAAA,oBAAoB,OAAA"}
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "chad-bridge",
3
+ "version": "0.1.0",
4
+ "description": "Connect your local machine to Chad AI on disclosedcapitol.com",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "chad-bridge": "dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "ts-node src/cli.ts",
12
+ "start": "node dist/cli.js"
13
+ },
14
+ "keywords": ["chad", "ai", "coding-assistant", "disclosedcapitol", "bridge"],
15
+ "author": "Disclosed Capitol",
16
+ "license": "MIT",
17
+ "dependencies": {
18
+ "ws": "^8.18.0",
19
+ "chalk": "^5.3.0",
20
+ "commander": "^12.1.0",
21
+ "chokidar": "^4.0.0"
22
+ },
23
+ "devDependencies": {
24
+ "typescript": "^5.5.0",
25
+ "@types/node": "^22.0.0",
26
+ "@types/ws": "^8.5.0",
27
+ "ts-node": "^10.9.0"
28
+ }
29
+ }
package/src/actions.ts ADDED
@@ -0,0 +1,235 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { execSync, spawn } from "child_process";
4
+
5
+ export interface CommandParams {
6
+ path?: string;
7
+ content?: string;
8
+ command?: string;
9
+ cwd?: string;
10
+ pattern?: string;
11
+ glob?: string;
12
+ message?: string;
13
+ branch?: string;
14
+ [key: string]: unknown;
15
+ }
16
+
17
+ export interface CommandResult {
18
+ status: "success" | "error" | "stream";
19
+ data?: Record<string, unknown>;
20
+ error?: string;
21
+ }
22
+
23
+ // Actions that don't need user approval
24
+ export const AUTO_APPROVE_ACTIONS = new Set([
25
+ "read_file",
26
+ "list_dir",
27
+ "file_info",
28
+ "search_files",
29
+ "git_status",
30
+ "git_diff",
31
+ "git_log",
32
+ ]);
33
+
34
+ function resolvePath(filePath: string, workingDir: string): string {
35
+ if (path.isAbsolute(filePath)) return filePath;
36
+ return path.resolve(workingDir, filePath);
37
+ }
38
+
39
+ export function executeAction(
40
+ action: string,
41
+ params: CommandParams,
42
+ workingDir: string
43
+ ): CommandResult {
44
+ try {
45
+ switch (action) {
46
+ case "read_file": {
47
+ const p = resolvePath(params.path || "", workingDir);
48
+ if (!fs.existsSync(p)) return { status: "error", error: `File not found: ${p}` };
49
+ const content = fs.readFileSync(p, "utf-8");
50
+ const stat = fs.statSync(p);
51
+ return {
52
+ status: "success",
53
+ data: {
54
+ content,
55
+ size: stat.size,
56
+ modified: stat.mtime.toISOString(),
57
+ },
58
+ };
59
+ }
60
+
61
+ case "write_file": {
62
+ const p = resolvePath(params.path || "", workingDir);
63
+ const dir = path.dirname(p);
64
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
65
+ fs.writeFileSync(p, params.content || "", "utf-8");
66
+ return {
67
+ status: "success",
68
+ data: { path: p, size: Buffer.byteLength(params.content || "", "utf-8") },
69
+ };
70
+ }
71
+
72
+ case "edit_file": {
73
+ const p = resolvePath(params.path || "", workingDir);
74
+ if (!fs.existsSync(p)) return { status: "error", error: `File not found: ${p}` };
75
+ // For now, full replacement. Diff-based patching can come later.
76
+ fs.writeFileSync(p, params.content || "", "utf-8");
77
+ return { status: "success", data: { path: p } };
78
+ }
79
+
80
+ case "delete_file": {
81
+ const p = resolvePath(params.path || "", workingDir);
82
+ if (!fs.existsSync(p)) return { status: "error", error: `File not found: ${p}` };
83
+ fs.unlinkSync(p);
84
+ return { status: "success", data: { deleted: p } };
85
+ }
86
+
87
+ case "list_dir": {
88
+ const p = resolvePath(params.path || ".", workingDir);
89
+ if (!fs.existsSync(p)) return { status: "error", error: `Directory not found: ${p}` };
90
+ const entries = fs.readdirSync(p, { withFileTypes: true });
91
+ const items = entries.map((e) => ({
92
+ name: e.name,
93
+ type: e.isDirectory() ? "directory" : "file",
94
+ size: e.isFile() ? fs.statSync(path.join(p, e.name)).size : undefined,
95
+ }));
96
+ return { status: "success", data: { path: p, entries: items } };
97
+ }
98
+
99
+ case "search_files": {
100
+ const searchPath = resolvePath(params.path || ".", workingDir);
101
+ const pattern = params.pattern || "";
102
+ try {
103
+ const result = execSync(
104
+ `grep -rn --include="*" -l "${pattern.replace(/"/g, '\\"')}" "${searchPath}"`,
105
+ { timeout: 10000, maxBuffer: 1024 * 1024 }
106
+ ).toString().trim();
107
+ const files = result ? result.split("\n").slice(0, 50) : [];
108
+ return { status: "success", data: { pattern, files, count: files.length } };
109
+ } catch {
110
+ return { status: "success", data: { pattern, files: [], count: 0 } };
111
+ }
112
+ }
113
+
114
+ case "file_info": {
115
+ const p = resolvePath(params.path || "", workingDir);
116
+ if (!fs.existsSync(p)) return { status: "error", error: `File not found: ${p}` };
117
+ const stat = fs.statSync(p);
118
+ return {
119
+ status: "success",
120
+ data: {
121
+ path: p,
122
+ size: stat.size,
123
+ isDirectory: stat.isDirectory(),
124
+ modified: stat.mtime.toISOString(),
125
+ created: stat.birthtime.toISOString(),
126
+ },
127
+ };
128
+ }
129
+
130
+ case "run_command": {
131
+ const cwd = params.cwd ? resolvePath(params.cwd, workingDir) : workingDir;
132
+ const cmd = params.command || "";
133
+ try {
134
+ const output = execSync(cmd, {
135
+ cwd,
136
+ timeout: 60000,
137
+ maxBuffer: 5 * 1024 * 1024,
138
+ encoding: "utf-8",
139
+ });
140
+ return { status: "success", data: { output, exitCode: 0 } };
141
+ } catch (err: unknown) {
142
+ const e = err as { status?: number; stdout?: string; stderr?: string; message?: string };
143
+ return {
144
+ status: "success",
145
+ data: {
146
+ output: (e.stdout || "") + (e.stderr || ""),
147
+ exitCode: e.status || 1,
148
+ error: e.message,
149
+ },
150
+ };
151
+ }
152
+ }
153
+
154
+ case "run_script": {
155
+ const p = resolvePath(params.path || "", workingDir);
156
+ if (!fs.existsSync(p)) return { status: "error", error: `Script not found: ${p}` };
157
+ const cwd = params.cwd ? resolvePath(params.cwd, workingDir) : workingDir;
158
+ try {
159
+ const output = execSync(`python3 "${p}"`, {
160
+ cwd,
161
+ timeout: 120000,
162
+ maxBuffer: 5 * 1024 * 1024,
163
+ encoding: "utf-8",
164
+ });
165
+ return { status: "success", data: { output, exitCode: 0 } };
166
+ } catch (err: unknown) {
167
+ const e = err as { status?: number; stdout?: string; stderr?: string };
168
+ return {
169
+ status: "success",
170
+ data: { output: (e.stdout || "") + (e.stderr || ""), exitCode: e.status || 1 },
171
+ };
172
+ }
173
+ }
174
+
175
+ case "git_status": {
176
+ const cwd = params.cwd ? resolvePath(params.cwd, workingDir) : workingDir;
177
+ const output = execSync("git status --porcelain", { cwd, encoding: "utf-8" });
178
+ const branch = execSync("git branch --show-current", { cwd, encoding: "utf-8" }).trim();
179
+ return { status: "success", data: { branch, status: output.trim(), clean: !output.trim() } };
180
+ }
181
+
182
+ case "git_diff": {
183
+ const cwd = params.cwd ? resolvePath(params.cwd, workingDir) : workingDir;
184
+ const output = execSync("git diff", { cwd, encoding: "utf-8", maxBuffer: 5 * 1024 * 1024 });
185
+ return { status: "success", data: { diff: output } };
186
+ }
187
+
188
+ case "git_log": {
189
+ const cwd = params.cwd ? resolvePath(params.cwd, workingDir) : workingDir;
190
+ const output = execSync('git log --oneline -20', { cwd, encoding: "utf-8" });
191
+ return { status: "success", data: { log: output.trim() } };
192
+ }
193
+
194
+ case "git_commit": {
195
+ const cwd = params.cwd ? resolvePath(params.cwd, workingDir) : workingDir;
196
+ const msg = params.message || "Update via Chad Bridge";
197
+ execSync("git add -A", { cwd });
198
+ const output = execSync(`git commit -m "${msg.replace(/"/g, '\\"')}"`, { cwd, encoding: "utf-8" });
199
+ return { status: "success", data: { output: output.trim() } };
200
+ }
201
+
202
+ case "git_push": {
203
+ const cwd = params.cwd ? resolvePath(params.cwd, workingDir) : workingDir;
204
+ const output = execSync("git push", { cwd, encoding: "utf-8", timeout: 30000 });
205
+ return { status: "success", data: { output: output.trim() } };
206
+ }
207
+
208
+ case "git_pull": {
209
+ const cwd = params.cwd ? resolvePath(params.cwd, workingDir) : workingDir;
210
+ const output = execSync("git pull", { cwd, encoding: "utf-8", timeout: 30000 });
211
+ return { status: "success", data: { output: output.trim() } };
212
+ }
213
+
214
+ case "git_branch": {
215
+ const cwd = params.cwd ? resolvePath(params.cwd, workingDir) : workingDir;
216
+ const branch = params.branch || "main";
217
+ const output = execSync(`git checkout -b "${branch}"`, { cwd, encoding: "utf-8" });
218
+ return { status: "success", data: { output: output.trim(), branch } };
219
+ }
220
+
221
+ case "install_deps": {
222
+ const cwd = params.cwd ? resolvePath(params.cwd, workingDir) : workingDir;
223
+ const cmd = params.command || "pip install -r requirements.txt";
224
+ const output = execSync(cmd, { cwd, encoding: "utf-8", timeout: 120000, maxBuffer: 10 * 1024 * 1024 });
225
+ return { status: "success", data: { output: output.trim() } };
226
+ }
227
+
228
+ default:
229
+ return { status: "error", error: `Unknown action: ${action}` };
230
+ }
231
+ } catch (err: unknown) {
232
+ const e = err as { message?: string };
233
+ return { status: "error", error: e.message || String(err) };
234
+ }
235
+ }
package/src/bridge.ts ADDED
@@ -0,0 +1,219 @@
1
+ import WebSocket from "ws";
2
+ import * as readline from "readline";
3
+ import { executeAction, AUTO_APPROVE_ACTIONS, CommandParams, CommandResult } from "./actions";
4
+
5
+ const API_URL = process.env.CHAD_API_URL || "wss://api.disclosedcapitol.com";
6
+
7
+ interface BridgeOptions {
8
+ apiKey: string;
9
+ workingDir: string;
10
+ autoApproveAll?: boolean;
11
+ verbose?: boolean;
12
+ }
13
+
14
+ interface IncomingCommand {
15
+ type: "command";
16
+ id: string;
17
+ action: string;
18
+ params: CommandParams;
19
+ }
20
+
21
+ interface ApprovalResponse {
22
+ type: "approval_response";
23
+ id: string;
24
+ approved: boolean;
25
+ }
26
+
27
+ export class ChadBridge {
28
+ private ws: WebSocket | null = null;
29
+ private options: BridgeOptions;
30
+ private rl: readline.Interface;
31
+ private reconnectAttempts = 0;
32
+ // Infinite reconnect — a Fly deploy can take 30-60s to come back, and a
33
+ // capped retry loop leaves the bridge silently stranded. Better to keep
34
+ // trying forever with an exponential backoff that caps at 30s.
35
+ private maxReconnects = Number.POSITIVE_INFINITY;
36
+ private pendingApprovals: Map<string, (approved: boolean) => void> = new Map();
37
+ private log: (msg: string) => void;
38
+
39
+ constructor(options: BridgeOptions) {
40
+ this.options = options;
41
+ this.rl = readline.createInterface({ input: process.stdin, output: process.stdout });
42
+ this.log = options.verbose
43
+ ? (msg: string) => console.log(` [${new Date().toLocaleTimeString()}] ${msg}`)
44
+ : () => {};
45
+ }
46
+
47
+ async connect(): Promise<void> {
48
+ const url = `${API_URL}/bridge/ws?key=${this.options.apiKey}&dir=${encodeURIComponent(this.options.workingDir)}`;
49
+
50
+ return new Promise((resolve, reject) => {
51
+ this.ws = new WebSocket(url);
52
+
53
+ this.ws.on("open", () => {
54
+ this.reconnectAttempts = 0;
55
+ console.log(" Status: \x1b[32m● Online\x1b[0m");
56
+ console.log("");
57
+ console.log(" Waiting for commands from Chad...");
58
+ console.log(" Auto-approve: file reads, directory listings, git status/diff/log");
59
+ console.log(" Require approval: file writes, terminal commands, git push/commit");
60
+ console.log("");
61
+ resolve();
62
+ });
63
+
64
+ this.ws.on("message", (data) => {
65
+ try {
66
+ const msg = JSON.parse(data.toString());
67
+ this.handleMessage(msg);
68
+ } catch (err) {
69
+ this.log(`Parse error: ${err}`);
70
+ }
71
+ });
72
+
73
+ this.ws.on("close", (code) => {
74
+ console.log(` \x1b[33m● Disconnected\x1b[0m (code: ${code})`);
75
+ if (this.reconnectAttempts < this.maxReconnects) {
76
+ const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
77
+ this.reconnectAttempts++;
78
+ console.log(` Reconnecting in ${delay / 1000}s... (attempt ${this.reconnectAttempts}/${this.maxReconnects})`);
79
+ setTimeout(() => this.connect(), delay);
80
+ }
81
+ });
82
+
83
+ this.ws.on("error", (err) => {
84
+ if (this.reconnectAttempts === 0) {
85
+ reject(err);
86
+ }
87
+ this.log(`WebSocket error: ${err.message}`);
88
+ });
89
+ });
90
+ }
91
+
92
+ private async handleMessage(msg: IncomingCommand | ApprovalResponse | { type: string; message?: string; user_id?: number; working_dir?: string }) {
93
+ // Surface server-side errors (auth, etc.) so the user sees WHY the
94
+ // connection dropped instead of just "Disconnected (code: 1000)".
95
+ if (msg.type === "error") {
96
+ const m = (msg as { message?: string }).message || "(no message)";
97
+ console.log(` \x1b[31m✗ Server error: ${m}\x1b[0m`);
98
+ return;
99
+ }
100
+ if (msg.type === "connected") {
101
+ // Confirms the server accepted our auth. Server already sent this on
102
+ // every successful connect; we just log it for visibility.
103
+ return;
104
+ }
105
+ if (msg.type === "heartbeat_ack") {
106
+ return;
107
+ }
108
+ if (msg.type === "approval_response") {
109
+ // Web-side approval came through
110
+ const resolver = this.pendingApprovals.get((msg as ApprovalResponse).id);
111
+ if (resolver) {
112
+ resolver((msg as ApprovalResponse).approved);
113
+ this.pendingApprovals.delete((msg as ApprovalResponse).id);
114
+ }
115
+ return;
116
+ }
117
+
118
+ if (msg.type !== "command") return;
119
+
120
+ const cmd = msg as IncomingCommand;
121
+ const { id, action, params } = cmd;
122
+ const needsApproval = !AUTO_APPROVE_ACTIONS.has(action) && !this.options.autoApproveAll;
123
+
124
+ if (needsApproval) {
125
+ const approved = await this.requestApproval(id, action, params);
126
+ if (!approved) {
127
+ this.send({ type: "result", id, status: "rejected", error: "User rejected the command" });
128
+ console.log(` \x1b[31m✗\x1b[0m ${action}: rejected by user`);
129
+ return;
130
+ }
131
+ }
132
+
133
+ // Execute the action
134
+ const result = executeAction(action, params, this.options.workingDir);
135
+
136
+ if (result.status === "success") {
137
+ console.log(` \x1b[32m✓\x1b[0m ${action}${params.path ? `: ${params.path}` : ""}${params.command ? `: ${params.command}` : ""}`);
138
+ } else {
139
+ console.log(` \x1b[31m✗\x1b[0m ${action}: ${result.error}`);
140
+ }
141
+
142
+ this.send({ type: "result", id, ...result });
143
+ }
144
+
145
+ private async requestApproval(id: string, action: string, params: CommandParams): Promise<boolean> {
146
+ // Send approval request to server (web UI will show popup)
147
+ this.send({
148
+ type: "approval_required",
149
+ id,
150
+ action,
151
+ params,
152
+ description: this.describeAction(action, params),
153
+ });
154
+
155
+ // Also prompt locally as fallback
156
+ const desc = this.describeAction(action, params);
157
+ console.log(` \x1b[33m⚠\x1b[0m APPROVAL NEEDED: ${desc}`);
158
+
159
+ return new Promise((resolve) => {
160
+ // Store resolver for web-side approval
161
+ this.pendingApprovals.set(id, resolve);
162
+
163
+ // CLI fallback with 30s timeout
164
+ const timeout = setTimeout(() => {
165
+ if (this.pendingApprovals.has(id)) {
166
+ this.pendingApprovals.delete(id);
167
+ resolve(false);
168
+ console.log(" Timed out — rejected");
169
+ }
170
+ }, 30000);
171
+
172
+ this.rl.question(" Allow? [y/n]: ", (answer) => {
173
+ clearTimeout(timeout);
174
+ if (this.pendingApprovals.has(id)) {
175
+ this.pendingApprovals.delete(id);
176
+ const approved = answer.toLowerCase().startsWith("y");
177
+ resolve(approved);
178
+ }
179
+ });
180
+ });
181
+ }
182
+
183
+ private describeAction(action: string, params: CommandParams): string {
184
+ switch (action) {
185
+ case "write_file":
186
+ case "edit_file":
187
+ return `${action} "${params.path}"`;
188
+ case "delete_file":
189
+ return `delete "${params.path}"`;
190
+ case "run_command":
191
+ return `run: ${params.command}`;
192
+ case "run_script":
193
+ return `run script: ${params.path}`;
194
+ case "git_commit":
195
+ return `git commit -m "${params.message}"`;
196
+ case "git_push":
197
+ return "git push";
198
+ case "git_pull":
199
+ return "git pull";
200
+ case "git_branch":
201
+ return `git checkout -b ${params.branch}`;
202
+ case "install_deps":
203
+ return `install: ${params.command}`;
204
+ default:
205
+ return `${action} ${JSON.stringify(params)}`;
206
+ }
207
+ }
208
+
209
+ private send(data: Record<string, unknown>) {
210
+ if (this.ws?.readyState === WebSocket.OPEN) {
211
+ this.ws.send(JSON.stringify(data));
212
+ }
213
+ }
214
+
215
+ disconnect() {
216
+ this.ws?.close();
217
+ this.rl.close();
218
+ }
219
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from "commander";
4
+ import * as path from "path";
5
+ import { ChadBridge } from "./bridge";
6
+
7
+ const VERSION = "0.1.0";
8
+
9
+ const program = new Command();
10
+
11
+ program
12
+ .name("chad-bridge")
13
+ .description("Connect your local machine to Chad AI on disclosedcapitol.com")
14
+ .version(VERSION)
15
+ .requiredOption("-k, --key <key>", "API key from disclosedcapitol.com/account")
16
+ .option("-d, --dir <directory>", "Working directory (default: current directory)", process.cwd())
17
+ .option("--auto-approve", "Auto-approve all commands (dangerous!)", false)
18
+ .option("-v, --verbose", "Verbose logging", false)
19
+ .action(async (opts) => {
20
+ const workingDir = path.resolve(opts.dir);
21
+
22
+ console.log("");
23
+ console.log(" ╔══════════════════════════════════════════╗");
24
+ console.log(` ║ Chad Bridge v${VERSION} ║`);
25
+ console.log(" ║ disclosedcapitol.com ║");
26
+ console.log(" ╠══════════════════════════════════════════╣");
27
+ console.log(` ║ Project: ${workingDir.slice(0, 30).padEnd(30)} ║`);
28
+ console.log(" ╚══════════════════════════════════════════╝");
29
+ console.log("");
30
+
31
+ if (opts.autoApprove) {
32
+ console.log(" \x1b[31m⚠ AUTO-APPROVE MODE: All commands will execute without confirmation\x1b[0m");
33
+ console.log("");
34
+ }
35
+
36
+ const bridge = new ChadBridge({
37
+ apiKey: opts.key,
38
+ workingDir,
39
+ autoApproveAll: opts.autoApprove,
40
+ verbose: opts.verbose,
41
+ });
42
+
43
+ // Graceful shutdown
44
+ process.on("SIGINT", () => {
45
+ console.log("\n Disconnecting...");
46
+ bridge.disconnect();
47
+ process.exit(0);
48
+ });
49
+
50
+ process.on("SIGTERM", () => {
51
+ bridge.disconnect();
52
+ process.exit(0);
53
+ });
54
+
55
+ try {
56
+ await bridge.connect();
57
+ } catch (err: unknown) {
58
+ const e = err as { message?: string };
59
+ console.error(` \x1b[31m✗ Failed to connect: ${e.message}\x1b[0m`);
60
+ console.error(" Check your API key and internet connection.");
61
+ console.error(" Get your API key at: https://disclosedcapitol.com/account?tab=api-keys");
62
+ process.exit(1);
63
+ }
64
+ });
65
+
66
+ program.parse();
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { ChadBridge } from "./bridge";
2
+ export { executeAction, AUTO_APPROVE_ACTIONS } from "./actions";
3
+ export type { CommandParams, CommandResult } from "./actions";