ohmyvibe 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/.env.example +2 -0
- package/README.md +140 -0
- package/dist/acp/index.js +92 -0
- package/dist/cli.js +111 -0
- package/dist/daemon/codexAppServerClient.js +101 -0
- package/dist/daemon/httpServer.js +117 -0
- package/dist/daemon/index.js +15 -0
- package/dist/daemon/jsonRpc.js +136 -0
- package/dist/daemon/managementBridge.js +156 -0
- package/dist/daemon/sessionManager.js +1192 -0
- package/dist/daemon/sessionStore.js +35 -0
- package/dist/shared/types.js +1 -0
- package/package.json +46 -0
package/.env.example
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# OhMyVibe
|
|
2
|
+
|
|
3
|
+
这是一个 `VibeCoding` 控制台:
|
|
4
|
+
|
|
5
|
+
- `daemon -> control server <- browser` 架构(daemon 主动连接管理端)
|
|
6
|
+
- 每个会话独立启动一个 `codex app-server` 子进程
|
|
7
|
+
- daemon 统一管理多会话、消息发送、中断、状态同步
|
|
8
|
+
- 额外提供一个标准 `ACP` bridge 入口,方便后续给编辑器或其他 ACP client 接入
|
|
9
|
+
- 应用侧 session 本地持久化到 `data/sessions.json`
|
|
10
|
+
- 支持从 Codex 历史 `~/.codex/sessions` 恢复会话,并绑定到原始 Codex thread
|
|
11
|
+
- Web 控制台为独立 React + shadcn 风格项目,浏览器只连接管理端
|
|
12
|
+
|
|
13
|
+
## 为什么这样做
|
|
14
|
+
|
|
15
|
+
当前官方能力里,`Codex CLI` 暴露的是 `app-server` 自动化接口,而不是原生 `ACP agent`。因此这个 MVP 采用两层桥接:
|
|
16
|
+
|
|
17
|
+
1. 南向:daemon 通过 `codex app-server --listen stdio://` 控制 Codex
|
|
18
|
+
2. 北向:daemon 自己暴露 `ACP` 兼容 agent,供外部 ACP client 使用
|
|
19
|
+
|
|
20
|
+
这能保证现在就能正确接入 Codex,同时不把上层协议绑死在 Codex 私有接口上。
|
|
21
|
+
|
|
22
|
+
## 运行
|
|
23
|
+
|
|
24
|
+
要求:
|
|
25
|
+
|
|
26
|
+
- Node.js 22+
|
|
27
|
+
- 本机已安装并可运行 `codex`
|
|
28
|
+
- `codex` 已完成登录
|
|
29
|
+
|
|
30
|
+
1. 启动 Web 管理端(API + 页面):
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install
|
|
34
|
+
npm --prefix web install
|
|
35
|
+
npm --prefix web run build
|
|
36
|
+
npm run web:server
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
默认监听 `http://localhost:3310`
|
|
40
|
+
默认读取 `web/.env`
|
|
41
|
+
|
|
42
|
+
2. 在被控机器启动 daemon,并主动连接管理端:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm run daemon
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
daemon 不再暴露本地 HTTP API,浏览器也不应直接连接 daemon。
|
|
49
|
+
默认读取根目录 `.env`
|
|
50
|
+
|
|
51
|
+
可选环境变量:
|
|
52
|
+
|
|
53
|
+
- `DAEMON_ID`:固定 daemon 标识
|
|
54
|
+
- `DAEMON_NAME`:展示名称
|
|
55
|
+
|
|
56
|
+
3. 浏览器访问管理端页面:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
http://your-control-host:3310
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
开发模式(前端热更新):
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm run web:dev
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
启动 ACP bridge:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm run acp
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 全局安装 daemon
|
|
75
|
+
|
|
76
|
+
如果你要把 daemon 作为全局命令安装,当前包已经支持:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm install -g ohmyvibe
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
然后直接启动:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
ohmyvibe --management-server-url http://localhost:3310
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
也可以显式指定 daemon 名称或 id:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
ohmyvibe daemon \
|
|
92
|
+
--management-server-url http://localhost:3310 \
|
|
93
|
+
--daemon-name ohmyvibe-local \
|
|
94
|
+
--daemon-id local-1
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
如果仍然想走环境变量,也支持:
|
|
98
|
+
|
|
99
|
+
- `MANAGEMENT_SERVER_URL`
|
|
100
|
+
- `DAEMON_ID`
|
|
101
|
+
- `DAEMON_NAME`
|
|
102
|
+
|
|
103
|
+
发布前可先验证打包内容:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm run build:daemon
|
|
107
|
+
npm run pack:dry-run
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
正式发布:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
npm publish --access public
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## 现在支持的能力
|
|
117
|
+
|
|
118
|
+
- 创建多个独立 Codex 会话
|
|
119
|
+
- daemon 重启后恢复应用内 session 列表与 transcript
|
|
120
|
+
- 给指定会话发送消息
|
|
121
|
+
- 流式接收 assistant 文本增量
|
|
122
|
+
- 用 `item/*` 与 `turn/*` 事件维护实时 transcript
|
|
123
|
+
- 中断运行中的 turn
|
|
124
|
+
- 关闭会话
|
|
125
|
+
- 从 Codex 历史会话列表恢复,并继续在同一 `threadId` 上对话
|
|
126
|
+
- 使用独立前端从其他设备远程管理 daemon
|
|
127
|
+
- daemon 主动连接管理端,浏览器不需要直连 daemon
|
|
128
|
+
|
|
129
|
+
## 后续建议
|
|
130
|
+
|
|
131
|
+
- 把 `turn/interrupt`、审批、文件 diff、命令执行输出做成更细粒度 UI
|
|
132
|
+
- 将 ACP session 和 web session 统一到同一后端存储
|
|
133
|
+
- 为 `codex app-server` 请求/通知补完整类型约束
|
|
134
|
+
|
|
135
|
+
## 参考文档
|
|
136
|
+
|
|
137
|
+
- OpenAI Codex App Server: https://developers.openai.com/codex/app-server
|
|
138
|
+
- OpenAI Codex CLI repo: https://github.com/openai/codex
|
|
139
|
+
- ACP 协议主页: https://agentclientprotocol.com
|
|
140
|
+
- ACP TypeScript SDK: https://www.npmjs.com/package/@agentclientprotocol/sdk
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Readable, Writable } from "node:stream";
|
|
2
|
+
import { AgentSideConnection, PROTOCOL_VERSION, ndJsonStream, } from "@agentclientprotocol/sdk";
|
|
3
|
+
import { SessionManager } from "../daemon/sessionManager.js";
|
|
4
|
+
class OhMyVibeAcpAgent {
|
|
5
|
+
connection;
|
|
6
|
+
sessions;
|
|
7
|
+
acpToManaged;
|
|
8
|
+
constructor(connection, sessions, acpToManaged = new Map()) {
|
|
9
|
+
this.connection = connection;
|
|
10
|
+
this.sessions = sessions;
|
|
11
|
+
this.acpToManaged = acpToManaged;
|
|
12
|
+
}
|
|
13
|
+
async initialize() {
|
|
14
|
+
return {
|
|
15
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
16
|
+
agentCapabilities: {
|
|
17
|
+
loadSession: false,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
async newSession(params) {
|
|
22
|
+
const session = await this.sessions.create({
|
|
23
|
+
cwd: params.cwd,
|
|
24
|
+
approvalPolicy: "never",
|
|
25
|
+
sandbox: "workspace-write",
|
|
26
|
+
});
|
|
27
|
+
this.acpToManaged.set(session.id, session.id);
|
|
28
|
+
return { sessionId: session.id };
|
|
29
|
+
}
|
|
30
|
+
async authenticate() {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
async prompt(params) {
|
|
34
|
+
const managedId = this.resolveSession(params.sessionId);
|
|
35
|
+
const text = (params.prompt ?? [])
|
|
36
|
+
.filter((block) => block?.type === "text")
|
|
37
|
+
.map((block) => block.text)
|
|
38
|
+
.join("\n");
|
|
39
|
+
await this.sessions.sendMessage(managedId, text);
|
|
40
|
+
const listener = async (event) => {
|
|
41
|
+
if (event.type !== "session-entry" || event.sessionId !== managedId) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (event.entry.kind === "assistant") {
|
|
45
|
+
await this.connection.sessionUpdate({
|
|
46
|
+
sessionId: params.sessionId,
|
|
47
|
+
update: {
|
|
48
|
+
sessionUpdate: "agent_message_chunk",
|
|
49
|
+
content: {
|
|
50
|
+
type: "text",
|
|
51
|
+
text: event.entry.text,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
this.sessions.on("event", listener);
|
|
58
|
+
try {
|
|
59
|
+
while (true) {
|
|
60
|
+
const session = this.sessions.get(managedId);
|
|
61
|
+
const status = session?.status;
|
|
62
|
+
if (!session ||
|
|
63
|
+
(status !== undefined && ["completed", "interrupted", "failed", "idle"].includes(status))) {
|
|
64
|
+
return {
|
|
65
|
+
stopReason: status === "interrupted" ? "cancelled" : "end_turn",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
this.sessions.off("event", listener);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async cancel(params) {
|
|
76
|
+
const managedId = this.resolveSession(params.sessionId);
|
|
77
|
+
await this.sessions.interrupt(managedId);
|
|
78
|
+
}
|
|
79
|
+
resolveSession(sessionId) {
|
|
80
|
+
const managedId = this.acpToManaged.get(sessionId) ?? sessionId;
|
|
81
|
+
const session = this.sessions.get(managedId);
|
|
82
|
+
if (!session) {
|
|
83
|
+
throw new Error(`ACP session not found: ${sessionId}`);
|
|
84
|
+
}
|
|
85
|
+
return managedId;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const sessionManager = new SessionManager();
|
|
89
|
+
const input = Writable.toWeb(process.stdout);
|
|
90
|
+
const output = Readable.toWeb(process.stdin);
|
|
91
|
+
const stream = ndJsonStream(input, output);
|
|
92
|
+
new AgentSideConnection((connection) => new OhMyVibeAcpAgent(connection, sessionManager), stream);
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
function printHelp() {
|
|
4
|
+
console.log(`OhMyVibe CLI
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
ohmyvibe [command] [options]
|
|
8
|
+
|
|
9
|
+
Commands:
|
|
10
|
+
daemon Start the managed daemon (default)
|
|
11
|
+
acp Start the ACP bridge
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
-u, --management-server-url <url> Control server URL
|
|
15
|
+
--daemon-id <id> Override daemon id
|
|
16
|
+
-n, --daemon-name <name> Override daemon display name
|
|
17
|
+
-h, --help Show help
|
|
18
|
+
-v, --version Show version
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
ohmyvibe --management-server-url http://localhost:3310
|
|
22
|
+
ohmyvibe daemon -u http://localhost:3310 -n my-daemon
|
|
23
|
+
ohmyvibe acp
|
|
24
|
+
`);
|
|
25
|
+
}
|
|
26
|
+
function printVersion() {
|
|
27
|
+
console.log("0.1.0");
|
|
28
|
+
}
|
|
29
|
+
function readOptionValue(args, index, flag) {
|
|
30
|
+
const value = args[index + 1];
|
|
31
|
+
if (!value || value.startsWith("-")) {
|
|
32
|
+
throw new Error(`Missing value for ${flag}`);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
function parseCliArgs(argv) {
|
|
37
|
+
let command = "daemon";
|
|
38
|
+
let startIndex = 0;
|
|
39
|
+
const first = argv[0];
|
|
40
|
+
if (first === "daemon" || first === "acp") {
|
|
41
|
+
command = first;
|
|
42
|
+
startIndex = 1;
|
|
43
|
+
}
|
|
44
|
+
const options = { command };
|
|
45
|
+
for (let index = startIndex; index < argv.length; index += 1) {
|
|
46
|
+
const arg = argv[index];
|
|
47
|
+
switch (arg) {
|
|
48
|
+
case "-u":
|
|
49
|
+
case "--management-server-url":
|
|
50
|
+
options.managementServerUrl = readOptionValue(argv, index, arg);
|
|
51
|
+
index += 1;
|
|
52
|
+
break;
|
|
53
|
+
case "--daemon-id":
|
|
54
|
+
options.daemonId = readOptionValue(argv, index, arg);
|
|
55
|
+
index += 1;
|
|
56
|
+
break;
|
|
57
|
+
case "-n":
|
|
58
|
+
case "--daemon-name":
|
|
59
|
+
options.daemonName = readOptionValue(argv, index, arg);
|
|
60
|
+
index += 1;
|
|
61
|
+
break;
|
|
62
|
+
case "-h":
|
|
63
|
+
case "--help":
|
|
64
|
+
options.help = true;
|
|
65
|
+
break;
|
|
66
|
+
case "-v":
|
|
67
|
+
case "--version":
|
|
68
|
+
options.version = true;
|
|
69
|
+
break;
|
|
70
|
+
default:
|
|
71
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return options;
|
|
75
|
+
}
|
|
76
|
+
function applyEnvOverrides(options) {
|
|
77
|
+
if (options.managementServerUrl) {
|
|
78
|
+
process.env.MANAGEMENT_SERVER_URL = options.managementServerUrl;
|
|
79
|
+
}
|
|
80
|
+
if (options.daemonId) {
|
|
81
|
+
process.env.DAEMON_ID = options.daemonId;
|
|
82
|
+
}
|
|
83
|
+
if (options.daemonName) {
|
|
84
|
+
process.env.DAEMON_NAME = options.daemonName;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function run() {
|
|
88
|
+
const options = parseCliArgs(process.argv.slice(2));
|
|
89
|
+
if (options.version) {
|
|
90
|
+
printVersion();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (options.help) {
|
|
94
|
+
printHelp();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
applyEnvOverrides(options);
|
|
98
|
+
if (options.command === "acp") {
|
|
99
|
+
await import("./acp/index.js");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
await import("./daemon/index.js");
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
await run();
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
109
|
+
console.error(`OhMyVibe CLI error: ${message}`);
|
|
110
|
+
process.exitCode = 1;
|
|
111
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { JsonRpcProcess } from "./jsonRpc.js";
|
|
2
|
+
export class CodexAppServerClient {
|
|
3
|
+
rpc;
|
|
4
|
+
constructor(options) {
|
|
5
|
+
const command = process.platform === "win32" ? process.env.ComSpec ?? "cmd.exe" : "codex";
|
|
6
|
+
const args = process.platform === "win32"
|
|
7
|
+
? ["/d", "/s", "/c", "codex.cmd app-server --listen stdio://"]
|
|
8
|
+
: ["app-server", "--listen", "stdio://"];
|
|
9
|
+
this.rpc = new JsonRpcProcess(command, args, options.cwd);
|
|
10
|
+
}
|
|
11
|
+
onNotification(listener) {
|
|
12
|
+
this.rpc.on("notification", listener);
|
|
13
|
+
}
|
|
14
|
+
onRequest(listener) {
|
|
15
|
+
this.rpc.on("request", listener);
|
|
16
|
+
}
|
|
17
|
+
onStderr(listener) {
|
|
18
|
+
this.rpc.on("stderr", listener);
|
|
19
|
+
}
|
|
20
|
+
onExit(listener) {
|
|
21
|
+
this.rpc.on("exit", listener);
|
|
22
|
+
}
|
|
23
|
+
initialize() {
|
|
24
|
+
return this.rpc.request("initialize", {
|
|
25
|
+
clientInfo: {
|
|
26
|
+
name: "ohmyvibe-daemon",
|
|
27
|
+
title: "OhMyVibe Daemon",
|
|
28
|
+
version: "0.1.0",
|
|
29
|
+
},
|
|
30
|
+
capabilities: {
|
|
31
|
+
experimentalApi: true,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
threadStart(params) {
|
|
36
|
+
return this.rpc.request("thread/start", {
|
|
37
|
+
cwd: params.cwd,
|
|
38
|
+
model: params.model ?? null,
|
|
39
|
+
sandbox: params.sandbox ?? "workspace-write",
|
|
40
|
+
approvalPolicy: params.approvalPolicy ?? "never",
|
|
41
|
+
personality: "pragmatic",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
threadResume(params) {
|
|
45
|
+
return this.rpc.request("thread/resume", {
|
|
46
|
+
threadId: params.threadId,
|
|
47
|
+
cwd: params.cwd ?? null,
|
|
48
|
+
model: params.model ?? null,
|
|
49
|
+
sandbox: params.sandbox ?? "workspace-write",
|
|
50
|
+
approvalPolicy: params.approvalPolicy ?? "never",
|
|
51
|
+
personality: "pragmatic",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
turnStart(params) {
|
|
55
|
+
return this.rpc.request("turn/start", {
|
|
56
|
+
threadId: params.threadId,
|
|
57
|
+
effort: params.effort ?? null,
|
|
58
|
+
model: params.model ?? null,
|
|
59
|
+
approvalPolicy: params.approvalPolicy ?? null,
|
|
60
|
+
summary: params.summary ?? "detailed",
|
|
61
|
+
input: [
|
|
62
|
+
{
|
|
63
|
+
type: "text",
|
|
64
|
+
text: params.text,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
threadRead(threadId) {
|
|
70
|
+
return this.rpc.request("thread/read", { threadId });
|
|
71
|
+
}
|
|
72
|
+
threadList(params) {
|
|
73
|
+
return this.rpc.request("thread/list", {
|
|
74
|
+
limit: params?.limit ?? 100,
|
|
75
|
+
cursor: params?.cursor ?? null,
|
|
76
|
+
cwd: params?.cwd ?? null,
|
|
77
|
+
searchTerm: params?.searchTerm ?? null,
|
|
78
|
+
sortKey: params?.sortKey ?? "updated_at",
|
|
79
|
+
sourceKinds: params?.sourceKinds ?? ["cli", "vscode", "appServer", "unknown"],
|
|
80
|
+
archived: params?.archived ?? false,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
turnInterrupt(threadId, turnId) {
|
|
84
|
+
return this.rpc.request("turn/interrupt", { threadId, turnId });
|
|
85
|
+
}
|
|
86
|
+
modelList() {
|
|
87
|
+
return this.rpc.request("model/list", {
|
|
88
|
+
includeHidden: false,
|
|
89
|
+
limit: 50,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
respond(requestId, result) {
|
|
93
|
+
this.rpc.respond(requestId, result);
|
|
94
|
+
}
|
|
95
|
+
respondError(requestId, message, code = -32601) {
|
|
96
|
+
this.rpc.respondError(requestId, code, message);
|
|
97
|
+
}
|
|
98
|
+
close() {
|
|
99
|
+
return this.rpc.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import { WebSocketServer } from "ws";
|
|
4
|
+
export function startHttpServer(sessionManager, port) {
|
|
5
|
+
const app = express();
|
|
6
|
+
const allowOrigin = process.env.ALLOW_ORIGIN ?? "*";
|
|
7
|
+
app.use((req, res, next) => {
|
|
8
|
+
res.setHeader("Access-Control-Allow-Origin", allowOrigin);
|
|
9
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
10
|
+
res.setHeader("Access-Control-Allow-Methods", "GET,POST,DELETE,OPTIONS");
|
|
11
|
+
if (req.method === "OPTIONS") {
|
|
12
|
+
res.status(204).end();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
next();
|
|
16
|
+
});
|
|
17
|
+
app.use(express.json());
|
|
18
|
+
app.get("/api/sessions", (_req, res) => {
|
|
19
|
+
res.json(sessionManager.list());
|
|
20
|
+
});
|
|
21
|
+
app.get("/api/config", async (_req, res) => {
|
|
22
|
+
try {
|
|
23
|
+
res.json(await sessionManager.getConfig());
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
app.get("/api/history", async (_req, res) => {
|
|
30
|
+
try {
|
|
31
|
+
res.json(await sessionManager.listHistory());
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
app.post("/api/sessions", async (req, res) => {
|
|
38
|
+
try {
|
|
39
|
+
const body = req.body;
|
|
40
|
+
const session = await sessionManager.create(body);
|
|
41
|
+
res.status(201).json(session);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
app.post("/api/history/:threadId/restore", async (req, res) => {
|
|
48
|
+
try {
|
|
49
|
+
const body = (req.body ?? {});
|
|
50
|
+
const session = await sessionManager.restore({
|
|
51
|
+
threadId: req.params.threadId,
|
|
52
|
+
cwd: body.cwd,
|
|
53
|
+
model: body.model,
|
|
54
|
+
reasoningEffort: body.reasoningEffort,
|
|
55
|
+
sandbox: body.sandbox,
|
|
56
|
+
approvalPolicy: body.approvalPolicy,
|
|
57
|
+
});
|
|
58
|
+
res.status(201).json(session);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
app.get("/api/sessions/:sessionId", (req, res) => {
|
|
65
|
+
const session = sessionManager.get(req.params.sessionId);
|
|
66
|
+
if (!session) {
|
|
67
|
+
res.status(404).json({ error: "Session not found" });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
res.json(session);
|
|
71
|
+
});
|
|
72
|
+
app.post("/api/sessions/:sessionId/messages", async (req, res) => {
|
|
73
|
+
try {
|
|
74
|
+
const body = req.body;
|
|
75
|
+
await sessionManager.sendMessage(req.params.sessionId, body.text);
|
|
76
|
+
res.status(202).json({ ok: true });
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
app.post("/api/sessions/:sessionId/interrupt", async (req, res) => {
|
|
83
|
+
try {
|
|
84
|
+
await sessionManager.interrupt(req.params.sessionId);
|
|
85
|
+
res.status(202).json({ ok: true });
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
app.delete("/api/sessions/:sessionId", async (req, res) => {
|
|
92
|
+
try {
|
|
93
|
+
await sessionManager.close(req.params.sessionId);
|
|
94
|
+
res.status(204).end();
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
app.get("/", (_req, res) => {
|
|
101
|
+
res.json({ name: "ohmyvibe-daemon", ok: true });
|
|
102
|
+
});
|
|
103
|
+
const server = http.createServer(app);
|
|
104
|
+
const wss = new WebSocketServer({ server, path: "/ws" });
|
|
105
|
+
wss.on("connection", (socket) => {
|
|
106
|
+
socket.send(JSON.stringify({ type: "hello", sessions: sessionManager.list() }));
|
|
107
|
+
const listener = (event) => {
|
|
108
|
+
socket.send(JSON.stringify(event));
|
|
109
|
+
};
|
|
110
|
+
sessionManager.on("event", listener);
|
|
111
|
+
socket.on("close", () => {
|
|
112
|
+
sessionManager.off("event", listener);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
server.listen(port);
|
|
116
|
+
return server;
|
|
117
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { ManagementBridge } from "./managementBridge.js";
|
|
3
|
+
import { SessionManager } from "./sessionManager.js";
|
|
4
|
+
const sessionManager = new SessionManager();
|
|
5
|
+
const managementServerUrl = process.env.MANAGEMENT_SERVER_URL;
|
|
6
|
+
if (!managementServerUrl) {
|
|
7
|
+
throw new Error("MANAGEMENT_SERVER_URL is required");
|
|
8
|
+
}
|
|
9
|
+
const bridge = new ManagementBridge(sessionManager, {
|
|
10
|
+
serverUrl: managementServerUrl,
|
|
11
|
+
daemonId: process.env.DAEMON_ID,
|
|
12
|
+
daemonName: process.env.DAEMON_NAME,
|
|
13
|
+
});
|
|
14
|
+
bridge.start();
|
|
15
|
+
console.log(`OhMyVibe daemon connecting to ${managementServerUrl}`);
|