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/README.md +64 -0
- package/dist/actions.d.ts +19 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +240 -0
- package/dist/actions.js.map +1 -0
- package/dist/bridge.d.ts +24 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +219 -0
- package/dist/bridge.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +92 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/package.json +29 -0
- package/src/actions.ts +235 -0
- package/src/bridge.ts +219 -0
- package/src/cli.ts +66 -0
- package/src/index.ts +3 -0
- package/tsconfig.json +19 -0
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
|
package/dist/cli.js.map
ADDED
|
@@ -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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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