mini-opti-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 +16 -0
- package/cli.js +2 -0
- package/dist/cli.js +232 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# mini-opti-bridge
|
|
2
|
+
|
|
3
|
+
One-command bridge bootstrap for the Mini Opti Codex dashboard.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx mini-opti-bridge@latest https://mini-opti.vercel.app <registrationKey>
|
|
9
|
+
pnpm dlx mini-opti-bridge@latest https://mini-opti.vercel.app <registrationKey>
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Optional named node:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx mini-opti-bridge@latest https://mini-opti.vercel.app <registrationKey> my-node local
|
|
16
|
+
```
|
package/cli.js
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/run.ts
|
|
4
|
+
import os from "os";
|
|
5
|
+
import process2 from "process";
|
|
6
|
+
import { readFile, writeFile } from "fs/promises";
|
|
7
|
+
|
|
8
|
+
// ../connectors/src/bridge/bootstrap.ts
|
|
9
|
+
var normalizePlatform = (platform) => {
|
|
10
|
+
if (platform === "win32") return "windows";
|
|
11
|
+
if (platform === "darwin") return "macos";
|
|
12
|
+
return platform;
|
|
13
|
+
};
|
|
14
|
+
var defaultNodeLabel = (input) => `${input.hostname}-${normalizePlatform(input.platform)}`;
|
|
15
|
+
var readPollMs = (value) => {
|
|
16
|
+
const parsed = Number(value ?? "4000");
|
|
17
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 4e3;
|
|
18
|
+
};
|
|
19
|
+
var parseBridgeBootstrap = (input) => {
|
|
20
|
+
const env = input.env ?? {};
|
|
21
|
+
return {
|
|
22
|
+
serverUrl: input.argv[3] ?? env.MINI_OPTI_SERVER_URL ?? "",
|
|
23
|
+
key: input.argv[4] ?? env.MINI_OPTI_REGISTRATION_KEY ?? "",
|
|
24
|
+
label: input.argv[5] ?? env.MINI_OPTI_NODE_LABEL ?? defaultNodeLabel({ hostname: input.hostname, platform: input.platform }),
|
|
25
|
+
kind: input.argv[6] ?? env.MINI_OPTI_NODE_KIND ?? "local",
|
|
26
|
+
pollMs: readPollMs(env.MINI_OPTI_POLL_MS)
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ../connectors/src/codex/app-server-client.ts
|
|
31
|
+
import { spawn } from "child_process";
|
|
32
|
+
import readline from "readline";
|
|
33
|
+
var CodexAppServerClient = class {
|
|
34
|
+
proc = null;
|
|
35
|
+
counter = 0;
|
|
36
|
+
pending = /* @__PURE__ */ new Map();
|
|
37
|
+
async start() {
|
|
38
|
+
if (this.proc) return;
|
|
39
|
+
this.proc = spawn("codex", ["app-server"], { stdio: ["pipe", "pipe", "inherit"] });
|
|
40
|
+
const rl = readline.createInterface({ input: this.proc.stdout });
|
|
41
|
+
rl.on("line", (line) => this.onMessage(JSON.parse(line)));
|
|
42
|
+
await this.request("initialize", {
|
|
43
|
+
clientInfo: { name: "mini-opti-bridge", title: "Mini Opti Bridge", version: "0.1.0" }
|
|
44
|
+
});
|
|
45
|
+
this.notify("initialized", {});
|
|
46
|
+
}
|
|
47
|
+
async listThreads() {
|
|
48
|
+
const data = [];
|
|
49
|
+
let cursor = null;
|
|
50
|
+
do {
|
|
51
|
+
const result = await this.request("thread/list", {
|
|
52
|
+
cursor,
|
|
53
|
+
limit: 100,
|
|
54
|
+
sortKey: "updated_at"
|
|
55
|
+
});
|
|
56
|
+
data.push(...result.data ?? []);
|
|
57
|
+
cursor = result.nextCursor ?? null;
|
|
58
|
+
} while (cursor);
|
|
59
|
+
return data;
|
|
60
|
+
}
|
|
61
|
+
async readThread(threadId) {
|
|
62
|
+
const result = await this.request("thread/read", { threadId, includeTurns: true });
|
|
63
|
+
return result.thread;
|
|
64
|
+
}
|
|
65
|
+
async interrupt(threadId, turnId) {
|
|
66
|
+
return this.request("turn/interrupt", { threadId, turnId });
|
|
67
|
+
}
|
|
68
|
+
onMessage(message) {
|
|
69
|
+
if (typeof message.id === "number") {
|
|
70
|
+
this.pending.get(message.id)?.(message);
|
|
71
|
+
this.pending.delete(message.id);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
notify(method, params) {
|
|
75
|
+
this.proc?.stdin.write(`${JSON.stringify({ method, params })}
|
|
76
|
+
`);
|
|
77
|
+
}
|
|
78
|
+
request(method, params) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const id = ++this.counter;
|
|
81
|
+
this.pending.set(id, (message) => {
|
|
82
|
+
if (message.error) reject(new Error(message.error.message));
|
|
83
|
+
else resolve(message.result);
|
|
84
|
+
});
|
|
85
|
+
this.proc?.stdin.write(`${JSON.stringify({ method, id, params })}
|
|
86
|
+
`);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// ../connectors/src/codex/normalize.ts
|
|
92
|
+
var mapContent = (content = []) => content.map((item) => item.text ?? "").join("\n").trim();
|
|
93
|
+
var normalizeLog = (thread) => (thread.turns ?? []).flatMap(
|
|
94
|
+
(turn) => (turn.items ?? []).flatMap((item) => {
|
|
95
|
+
if (item.type === "userMessage") {
|
|
96
|
+
return [{ role: "user", text: mapContent(item.content), turnId: turn.id, timestamp: Date.now() }];
|
|
97
|
+
}
|
|
98
|
+
if (item.type === "agentMessage") {
|
|
99
|
+
return [{ role: "assistant", text: item.text ?? "", turnId: turn.id, timestamp: Date.now() }];
|
|
100
|
+
}
|
|
101
|
+
return [];
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
var normalizeSession = (thread, log = []) => ({
|
|
105
|
+
id: thread.id,
|
|
106
|
+
name: thread.name ?? "Untitled session",
|
|
107
|
+
preview: thread.preview ?? "",
|
|
108
|
+
status: thread.status?.type ?? "notLoaded",
|
|
109
|
+
createdAt: thread.createdAt ?? 0,
|
|
110
|
+
updatedAt: thread.updatedAt ?? thread.createdAt ?? 0,
|
|
111
|
+
modelProvider: thread.modelProvider ?? "openai",
|
|
112
|
+
log
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ../connectors/src/codex/sdk-client.ts
|
|
116
|
+
var sendMessageToThread = async (threadId, prompt) => {
|
|
117
|
+
const { Codex } = await import("@openai/codex-sdk");
|
|
118
|
+
const codex = new Codex();
|
|
119
|
+
const thread = codex.resumeThread(threadId);
|
|
120
|
+
return thread.run(prompt);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// src/args.ts
|
|
124
|
+
var normalizeBridgeArgv = (argv) => {
|
|
125
|
+
const command = argv[2];
|
|
126
|
+
if (command === "register" || command === "serve" || command === "up") return argv;
|
|
127
|
+
return [argv[0] ?? "node", argv[1] ?? "mini-opti-bridge", "up", ...argv.slice(2)];
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// src/run.ts
|
|
131
|
+
var configPath = ".codex-bridge.json";
|
|
132
|
+
var readConfig = async () => JSON.parse(await readFile(configPath, "utf8"));
|
|
133
|
+
var saveConfig = async (next) => {
|
|
134
|
+
await writeFile(configPath, JSON.stringify(next, null, 2), "utf8");
|
|
135
|
+
};
|
|
136
|
+
var postJson = async (serverUrl, pathname, body) => {
|
|
137
|
+
const response = await fetch(new URL(pathname, serverUrl), {
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: { "content-type": "application/json" },
|
|
140
|
+
body: JSON.stringify(body)
|
|
141
|
+
});
|
|
142
|
+
const text = await response.text();
|
|
143
|
+
if (!response.ok) throw new Error(text || `${response.status} ${response.statusText}`);
|
|
144
|
+
return text ? JSON.parse(text) : {};
|
|
145
|
+
};
|
|
146
|
+
var readBootstrap = (argv) => parseBridgeBootstrap({
|
|
147
|
+
argv: normalizeBridgeArgv(argv),
|
|
148
|
+
env: process2.env,
|
|
149
|
+
hostname: os.hostname(),
|
|
150
|
+
platform: process2.platform
|
|
151
|
+
});
|
|
152
|
+
var register = async (bootstrap) => {
|
|
153
|
+
const node = await postJson(bootstrap.serverUrl, "/api/bridges/register", {
|
|
154
|
+
key: bootstrap.key,
|
|
155
|
+
label: bootstrap.label,
|
|
156
|
+
kind: bootstrap.kind,
|
|
157
|
+
cwd: process2.cwd()
|
|
158
|
+
});
|
|
159
|
+
const next = { ...node, serverUrl: bootstrap.serverUrl, pollMs: bootstrap.pollMs };
|
|
160
|
+
await saveConfig(next);
|
|
161
|
+
console.log(`registered ${next.nodeId}`);
|
|
162
|
+
return next;
|
|
163
|
+
};
|
|
164
|
+
var loadOrRegister = async (argv) => {
|
|
165
|
+
try {
|
|
166
|
+
return await readConfig();
|
|
167
|
+
} catch {
|
|
168
|
+
const bootstrap = readBootstrap(argv);
|
|
169
|
+
if (!bootstrap.serverUrl || !bootstrap.key) {
|
|
170
|
+
throw new Error("Usage: mini-opti-bridge [up] <serverUrl> <registrationKey> [label] [local|remote]");
|
|
171
|
+
}
|
|
172
|
+
return register(bootstrap);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
var completeCommand = async (config, client, command) => {
|
|
176
|
+
if (!command.sessionId) return;
|
|
177
|
+
if (command.type === "send-message") {
|
|
178
|
+
await sendMessageToThread(command.sessionId, command.payload?.text ?? "");
|
|
179
|
+
}
|
|
180
|
+
if (command.type === "send-message" || command.type === "refresh-log") {
|
|
181
|
+
const fullThread = await client.readThread(command.sessionId);
|
|
182
|
+
const log = normalizeLog(fullThread).map((item) => `${item.role}: ${item.text}`).join("\n\n");
|
|
183
|
+
await postJson(config.serverUrl, "/api/bridges/commands/complete", {
|
|
184
|
+
commandId: command.id,
|
|
185
|
+
status: "completed",
|
|
186
|
+
result: command.type === "send-message" ? "message delivered" : "log refreshed",
|
|
187
|
+
log
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
var syncOnce = async (config, client) => {
|
|
192
|
+
const threads = await client.listThreads();
|
|
193
|
+
const sessions = threads.map((thread) => normalizeSession(thread));
|
|
194
|
+
const beat = await postJson(config.serverUrl, "/api/bridges/heartbeat", {
|
|
195
|
+
nodeId: config.nodeId,
|
|
196
|
+
bridgeToken: config.bridgeToken,
|
|
197
|
+
sessions
|
|
198
|
+
});
|
|
199
|
+
for (const command of beat.commands ?? []) await completeCommand(config, client, command);
|
|
200
|
+
};
|
|
201
|
+
var serve = async (argv) => {
|
|
202
|
+
const config = await loadOrRegister(argv);
|
|
203
|
+
const client = new CodexAppServerClient();
|
|
204
|
+
await client.start();
|
|
205
|
+
await syncOnce(config, client);
|
|
206
|
+
setInterval(() => {
|
|
207
|
+
syncOnce(config, client).catch((error) => console.error(error));
|
|
208
|
+
}, config.pollMs);
|
|
209
|
+
};
|
|
210
|
+
var runBridgeCli = async (argv = process2.argv) => {
|
|
211
|
+
const normalizedArgv = normalizeBridgeArgv(argv);
|
|
212
|
+
const command = normalizedArgv[2];
|
|
213
|
+
if (command === "register") {
|
|
214
|
+
const bootstrap = readBootstrap(normalizedArgv);
|
|
215
|
+
if (!bootstrap.serverUrl || !bootstrap.key) {
|
|
216
|
+
throw new Error("Usage: mini-opti-bridge register <serverUrl> <registrationKey> [label] [local|remote]");
|
|
217
|
+
}
|
|
218
|
+
await register(bootstrap);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (command === "serve" || command === "up") {
|
|
222
|
+
await serve(normalizedArgv);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
throw new Error("Usage: mini-opti-bridge [up] <serverUrl> <registrationKey> [label] [local|remote]");
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// src/cli.ts
|
|
229
|
+
runBridgeCli().catch((error) => {
|
|
230
|
+
console.error(error instanceof Error ? error.message : error);
|
|
231
|
+
process.exitCode = 1;
|
|
232
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mini-opti-bridge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "One-command Codex bridge bootstrap for the Mini Opti dashboard",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": "cli.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"cli.js",
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/dimkk/mini-opti.git",
|
|
17
|
+
"directory": "packages/bridge-cli"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/dimkk/mini-opti/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/dimkk/mini-opti/tree/main/packages/bridge-cli",
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsup --config tsup.config.ts",
|
|
25
|
+
"lint": "tsc -p tsconfig.json --noEmit",
|
|
26
|
+
"test": "vitest run --config ../../vitest.config.ts --passWithNoTests",
|
|
27
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@openai/codex-sdk": "latest"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@mini-opti/core-schemas": "workspace:*"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"codex",
|
|
37
|
+
"openai",
|
|
38
|
+
"telegram",
|
|
39
|
+
"vercel",
|
|
40
|
+
"bridge"
|
|
41
|
+
],
|
|
42
|
+
"license": "MIT"
|
|
43
|
+
}
|