opencode-bifrost 0.0.1
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 +21 -0
- package/README.md +121 -0
- package/assets/demo.gif +0 -0
- package/bun.lock +34 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/hooks.d.ts +2 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26655 -0
- package/dist/manager.d.ts +54 -0
- package/dist/manager.d.ts.map +1 -0
- package/dist/security.d.ts +8 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/tools/connect.d.ts +3 -0
- package/dist/tools/connect.d.ts.map +1 -0
- package/dist/tools/disconnect.d.ts +3 -0
- package/dist/tools/disconnect.d.ts.map +1 -0
- package/dist/tools/download.d.ts +3 -0
- package/dist/tools/download.d.ts.map +1 -0
- package/dist/tools/exec.d.ts +3 -0
- package/dist/tools/exec.d.ts.map +1 -0
- package/dist/tools/status.d.ts +3 -0
- package/dist/tools/status.d.ts.map +1 -0
- package/dist/tools/upload.d.ts +3 -0
- package/dist/tools/upload.d.ts.map +1 -0
- package/package.json +47 -0
- package/src/config.ts +151 -0
- package/src/hooks.ts +1 -0
- package/src/index.ts +64 -0
- package/src/manager.ts +471 -0
- package/src/security.ts +98 -0
- package/src/tools/connect.ts +35 -0
- package/src/tools/disconnect.ts +30 -0
- package/src/tools/download.ts +51 -0
- package/src/tools/exec.ts +68 -0
- package/src/tools/status.ts +49 -0
- package/src/tools/upload.ts +56 -0
- package/test/config.test.ts +233 -0
- package/test/integration.test.ts +199 -0
- package/test/manager.test.ts +209 -0
- package/test/security.test.ts +245 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { BifrostConfig } from "./config";
|
|
2
|
+
export type ConnectionState = "disconnected" | "connecting" | "connected" | "disconnecting";
|
|
3
|
+
export type BifrostErrorCode = "UNREACHABLE" | "AUTH_FAILED" | "SOCKET_DEAD" | "COMMAND_FAILED" | "INVALID_STATE" | "TIMEOUT";
|
|
4
|
+
export declare class BifrostError extends Error {
|
|
5
|
+
readonly code: BifrostErrorCode;
|
|
6
|
+
readonly name: "BifrostError";
|
|
7
|
+
constructor(message: string, code: BifrostErrorCode);
|
|
8
|
+
}
|
|
9
|
+
export interface ExecResult {
|
|
10
|
+
stdout: string;
|
|
11
|
+
stderr: string;
|
|
12
|
+
exitCode: number;
|
|
13
|
+
}
|
|
14
|
+
export interface ExecOptions {
|
|
15
|
+
timeout?: number;
|
|
16
|
+
maxOutputBytes?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare class BifrostManager implements AsyncDisposable {
|
|
19
|
+
private _state;
|
|
20
|
+
private _config;
|
|
21
|
+
private _controlPath;
|
|
22
|
+
private _mutex;
|
|
23
|
+
/**
|
|
24
|
+
* Explicit Resource Management (ES2024)
|
|
25
|
+
* Enables: `await using manager = new BifrostManager()`
|
|
26
|
+
* Auto-disconnects when scope exits
|
|
27
|
+
*/
|
|
28
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
29
|
+
get state(): ConnectionState;
|
|
30
|
+
get config(): BifrostConfig | null;
|
|
31
|
+
get controlPath(): string | null;
|
|
32
|
+
get socketDir(): string;
|
|
33
|
+
private withMutex;
|
|
34
|
+
loadConfig(configPath: string): void;
|
|
35
|
+
private ensureSocketDir;
|
|
36
|
+
private translateSSHError;
|
|
37
|
+
private getDestination;
|
|
38
|
+
connect(): Promise<void>;
|
|
39
|
+
disconnect(): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Health check via ssh -O check
|
|
42
|
+
* Returns true if connection is alive
|
|
43
|
+
*/
|
|
44
|
+
isConnected(): Promise<boolean>;
|
|
45
|
+
exec(command: string, options?: ExecOptions): Promise<ExecResult>;
|
|
46
|
+
private runSftp;
|
|
47
|
+
upload(localPath: string, remotePath: string): Promise<void>;
|
|
48
|
+
download(remotePath: string, localPath: string): Promise<void>;
|
|
49
|
+
ensureConnected(): Promise<void>;
|
|
50
|
+
private doConnect;
|
|
51
|
+
cleanup(): void;
|
|
52
|
+
}
|
|
53
|
+
export declare const bifrostManager: BifrostManager;
|
|
54
|
+
//# sourceMappingURL=manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../src/manager.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAG9C,MAAM,MAAM,eAAe,GACvB,cAAc,GACd,YAAY,GACZ,WAAW,GACX,eAAe,CAAC;AAEpB,MAAM,MAAM,gBAAgB,GACxB,aAAa,GACb,aAAa,GACb,aAAa,GACb,gBAAgB,GAChB,eAAe,GACf,SAAS,CAAC;AAEd,qBAAa,YAAa,SAAQ,KAAK;aAKnB,IAAI,EAAE,gBAAgB;IAJxC,SAAyB,IAAI,EAAG,cAAc,CAAU;gBAGtD,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,gBAAgB;CAIzC;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAqDD,qBAAa,cAAe,YAAW,eAAe;IACpD,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,OAAO,CAA8B;IAC7C,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,MAAM,CAAoC;IAElD;;;;OAIG;IACG,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5C,IAAI,KAAK,IAAI,eAAe,CAE3B;IAED,IAAI,MAAM,IAAI,aAAa,GAAG,IAAI,CAEjC;IAED,IAAI,WAAW,IAAI,MAAM,GAAG,IAAI,CAE/B;IAED,IAAI,SAAS,IAAI,MAAM,CAEtB;YAEa,SAAS;IAavB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;YAKtB,eAAe;IAkB7B,OAAO,CAAC,iBAAiB;IAiCzB,OAAO,CAAC,cAAc;IAOhB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA4BxB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAsCjC;;;OAGG;IACG,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAqB/B,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;YAoD7D,OAAO;IAsCf,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI5D,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9D,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;YAqCxB,SAAS;IAiCvB,OAAO,IAAI,IAAI;CAUhB;AAED,eAAO,MAAM,cAAc,gBAAuB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ValidationResult {
|
|
2
|
+
valid: boolean;
|
|
3
|
+
error?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function validatePath(path: string, fieldName: string): ValidationResult;
|
|
6
|
+
export declare function validateCommand(command: string): ValidationResult;
|
|
7
|
+
export declare function escapeShellArg(arg: string): string;
|
|
8
|
+
//# sourceMappingURL=security.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.d.ts","sourceRoot":"","sources":["../src/security.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA+BD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAmC9E;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,CAsBjE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAElD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../../src/tools/connect.ts"],"names":[],"mappings":"AACA,OAAO,EAAQ,KAAK,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAGrE,eAAO,MAAM,eAAe,EAAE,cA8B5B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"disconnect.d.ts","sourceRoot":"","sources":["../../src/tools/disconnect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAGrE,eAAO,MAAM,kBAAkB,EAAE,cA0B/B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../src/tools/download.ts"],"names":[],"mappings":"AACA,OAAO,EAAQ,KAAK,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAIrE,eAAO,MAAM,gBAAgB,EAAE,cA6C7B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../../src/tools/exec.ts"],"names":[],"mappings":"AACA,OAAO,EAAQ,KAAK,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAIrE,eAAO,MAAM,YAAY,EAAE,cA8DzB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/tools/status.ts"],"names":[],"mappings":"AACA,OAAO,EAAQ,KAAK,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAIrE,eAAO,MAAM,cAAc,EAAE,cA2C3B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../src/tools/upload.ts"],"names":[],"mappings":"AACA,OAAO,EAAQ,KAAK,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAIrE,eAAO,MAAM,cAAc,EAAE,cAkD3B,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-bifrost",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "OpenCode plugin for persistent SSH connections via ControlMaster multiplexing",
|
|
5
|
+
"author": "itsmylife44",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/itsmylife44/bifrost.git"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "dist/index.js",
|
|
16
|
+
"types": "dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "bun build src/index.ts --outdir dist --format esm --target node && tsc --emitDeclarationOnly",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"test": "bun test"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@opencode-ai/plugin": "^1.1.53",
|
|
30
|
+
"zod": "^4.3.6"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"bun-types": "latest",
|
|
34
|
+
"typescript": "^5.9.3"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"opencode",
|
|
38
|
+
"opencode-plugin",
|
|
39
|
+
"plugin",
|
|
40
|
+
"ssh",
|
|
41
|
+
"controlmaster",
|
|
42
|
+
"ocx"
|
|
43
|
+
],
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18.0.0"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { readFileSync, existsSync, statSync } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
|
|
5
|
+
const SAFE_HOST_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9.\-]*[a-zA-Z0-9]$|^[a-zA-Z0-9]$/;
|
|
6
|
+
const SAFE_USER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_\-]*$/;
|
|
7
|
+
|
|
8
|
+
export const BifrostConfigSchema = z.object({
|
|
9
|
+
host: z
|
|
10
|
+
.string()
|
|
11
|
+
.min(1)
|
|
12
|
+
.max(253)
|
|
13
|
+
.refine(
|
|
14
|
+
(h) => SAFE_HOST_PATTERN.test(h),
|
|
15
|
+
{ message: "Invalid host format. Use IP or hostname without special characters." }
|
|
16
|
+
)
|
|
17
|
+
.describe("IP address or hostname of the remote server"),
|
|
18
|
+
user: z
|
|
19
|
+
.string()
|
|
20
|
+
.default("root")
|
|
21
|
+
.refine(
|
|
22
|
+
(u) => SAFE_USER_PATTERN.test(u),
|
|
23
|
+
{ message: "Invalid username format" }
|
|
24
|
+
)
|
|
25
|
+
.describe("SSH username for authentication"),
|
|
26
|
+
keyPath: z
|
|
27
|
+
.string()
|
|
28
|
+
.describe("Path to SSH private key (supports ~ expansion)"),
|
|
29
|
+
port: z
|
|
30
|
+
.number()
|
|
31
|
+
.int()
|
|
32
|
+
.positive()
|
|
33
|
+
.default(22)
|
|
34
|
+
.describe("SSH port number"),
|
|
35
|
+
connectTimeout: z
|
|
36
|
+
.number()
|
|
37
|
+
.int()
|
|
38
|
+
.positive()
|
|
39
|
+
.default(10)
|
|
40
|
+
.describe("Connection timeout in seconds"),
|
|
41
|
+
controlPersist: z
|
|
42
|
+
.string()
|
|
43
|
+
.default("15m")
|
|
44
|
+
.describe("ControlPersist value for SSH multiplexing"),
|
|
45
|
+
serverAliveInterval: z
|
|
46
|
+
.number()
|
|
47
|
+
.int()
|
|
48
|
+
.positive()
|
|
49
|
+
.default(30)
|
|
50
|
+
.describe("Keepalive interval in seconds"),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export type BifrostConfig = z.infer<typeof BifrostConfigSchema>;
|
|
54
|
+
|
|
55
|
+
function expandTildePath(path: string): string {
|
|
56
|
+
if (path.startsWith("~")) {
|
|
57
|
+
return path.replace("~", homedir());
|
|
58
|
+
}
|
|
59
|
+
return path;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function validateKeyFile(keyPath: string): void {
|
|
63
|
+
const expandedPath = expandTildePath(keyPath);
|
|
64
|
+
|
|
65
|
+
if (!existsSync(expandedPath)) {
|
|
66
|
+
throw new Error(`Key file not found: ${expandedPath}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const stats = statSync(expandedPath);
|
|
71
|
+
const mode = stats.mode;
|
|
72
|
+
|
|
73
|
+
// Check if world-readable (0o004)
|
|
74
|
+
if (mode & 0o004) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Key file ${expandedPath} is world-readable. Fix with: chmod 600 ${expandedPath}`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check if group-readable (0o040)
|
|
81
|
+
if (mode & 0o040) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Key file ${expandedPath} is group-readable. Fix with: chmod 600 ${expandedPath}`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Must be a regular file
|
|
88
|
+
if (!stats.isFile()) {
|
|
89
|
+
throw new Error(`Key path ${expandedPath} is not a regular file`);
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
if (err instanceof Error && err.message.startsWith("Key")) {
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
throw new Error(`Failed to check key file permissions: ${err}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function parseConfig(configPath: string): BifrostConfig {
|
|
100
|
+
try {
|
|
101
|
+
if (!existsSync(configPath)) {
|
|
102
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const rawContent = readFileSync(configPath, "utf-8");
|
|
106
|
+
const rawJson = JSON.parse(rawContent);
|
|
107
|
+
|
|
108
|
+
// Expand tilde in keyPath before validation
|
|
109
|
+
if (rawJson.keyPath) {
|
|
110
|
+
rawJson.keyPath = expandTildePath(rawJson.keyPath);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const config = BifrostConfigSchema.parse(rawJson);
|
|
114
|
+
|
|
115
|
+
// Validate key file after schema parsing
|
|
116
|
+
validateKeyFile(config.keyPath);
|
|
117
|
+
|
|
118
|
+
return config;
|
|
119
|
+
} catch (err) {
|
|
120
|
+
if (err instanceof Error) {
|
|
121
|
+
if (err.name === "ZodError") {
|
|
122
|
+
const pathMatches = err.message.match(/"path":\s*\[\s*"([^"]+)"/g);
|
|
123
|
+
if (pathMatches) {
|
|
124
|
+
const fields = pathMatches
|
|
125
|
+
.map((match) => {
|
|
126
|
+
const fieldMatch = match.match(/"([^"]+)"$/);
|
|
127
|
+
return fieldMatch ? fieldMatch[1] : null;
|
|
128
|
+
})
|
|
129
|
+
.filter((f) => f !== null) as string[];
|
|
130
|
+
|
|
131
|
+
const uniqueFields = Array.from(new Set(fields));
|
|
132
|
+
if (uniqueFields.length > 0) {
|
|
133
|
+
throw new Error(`Missing required field(s): ${uniqueFields.join(", ")}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
throw new Error(`Invalid config: ${err.message}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (err.message.startsWith("Key file") || err.message.startsWith("Failed to check")) {
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (err instanceof SyntaxError) {
|
|
145
|
+
throw new Error(`JSON parse error: ${err.message}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
}
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const placeholder = true;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
+
import { bifrostManager } from "./manager";
|
|
3
|
+
import { bifrost_connect } from "./tools/connect";
|
|
4
|
+
import { bifrost_exec } from "./tools/exec";
|
|
5
|
+
import { bifrost_status } from "./tools/status";
|
|
6
|
+
import { bifrost_disconnect } from "./tools/disconnect";
|
|
7
|
+
import { bifrost_upload } from "./tools/upload";
|
|
8
|
+
import { bifrost_download } from "./tools/download";
|
|
9
|
+
|
|
10
|
+
const Bifrost: Plugin = async (ctx) => {
|
|
11
|
+
// Cleanup stale sockets on plugin init
|
|
12
|
+
bifrostManager.cleanup();
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
tool: {
|
|
16
|
+
bifrost_connect,
|
|
17
|
+
bifrost_exec,
|
|
18
|
+
bifrost_status,
|
|
19
|
+
bifrost_disconnect,
|
|
20
|
+
bifrost_upload,
|
|
21
|
+
bifrost_download,
|
|
22
|
+
},
|
|
23
|
+
event: async (input) => {
|
|
24
|
+
// On session end → auto-disconnect
|
|
25
|
+
if (input.event.type === "session.deleted") {
|
|
26
|
+
await bifrostManager.disconnect();
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
30
|
+
output.system.push(`## Bifrost SSH Plugin — Remote Server Access
|
|
31
|
+
|
|
32
|
+
You have access to a remote server via persistent SSH connection. The server is pre-configured in ~/.config/opencode/bifrost.json.
|
|
33
|
+
|
|
34
|
+
### Available Tools
|
|
35
|
+
- \`bifrost_connect\` — Establish persistent SSH connection (call once per session)
|
|
36
|
+
- \`bifrost_exec\` — Execute any command on the remote server
|
|
37
|
+
- \`bifrost_status\` — Check connection status
|
|
38
|
+
- \`bifrost_disconnect\` — Close the connection
|
|
39
|
+
- \`bifrost_upload\` — Upload a local file to the remote server
|
|
40
|
+
- \`bifrost_download\` — Download a file from the remote server
|
|
41
|
+
|
|
42
|
+
### When to Use
|
|
43
|
+
Automatically use these tools when the user:
|
|
44
|
+
- Mentions "server", "remote", "deploy", "production", "staging", "VPS", "Hetzner"
|
|
45
|
+
- Asks to test something "on the server" or "remotely"
|
|
46
|
+
- Wants to install, configure, or check something on the remote machine
|
|
47
|
+
- Asks about server status, logs, or running processes
|
|
48
|
+
- Wants to transfer files to/from the server
|
|
49
|
+
|
|
50
|
+
### Workflow
|
|
51
|
+
1. Call \`bifrost_connect\` to establish the connection (only needed once, it persists)
|
|
52
|
+
2. Use \`bifrost_exec\` to run commands (e.g., \`bifrost_exec({command: "docker ps"})\`)
|
|
53
|
+
3. Use \`bifrost_upload\`/\`bifrost_download\` for file transfers
|
|
54
|
+
4. The connection auto-disconnects when the session ends
|
|
55
|
+
|
|
56
|
+
### Important
|
|
57
|
+
- Do NOT use raw \`ssh\` commands — always use bifrost tools
|
|
58
|
+
- The connection is persistent — you don't need to reconnect for each command
|
|
59
|
+
- If you get a connection error, try \`bifrost_connect\` again (it auto-reconnects)`);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export default Bifrost;
|