@nowcrew/daemon 0.6.34 → 0.6.35
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/package.json +1 -1
- package/dist/remote/claude-bridge.js +0 -558
- package/dist/remote/claude-channel.js +0 -164
- package/dist/remote/codex-client.js +0 -451
- package/dist/remote/codex-runtime.js +0 -77
- package/dist/remote/config.js +0 -135
- package/dist/remote/gateway.js +0 -879
- package/dist/remote/identity.js +0 -39
- package/dist/remote/owner.js +0 -77
- package/dist/remote/protocol.js +0 -211
- package/dist/remote/remote-cli.js +0 -254
- package/dist/remote/runtime-probe.js +0 -182
- package/dist/remote/session-discovery.js +0 -249
- package/dist/remote/wrapper.js +0 -40
- package/dist/remote-web/assets/index-B_6VM_tw.js +0 -94
- package/dist/remote-web/assets/index-L6EiQbJn.css +0 -1
- package/dist/remote-web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/remote-web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/remote-web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/remote-web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/remote-web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/remote-web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/remote-web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/remote-web/icons/nowwork-192.png +0 -0
- package/dist/remote-web/icons/nowwork-512.png +0 -0
- package/dist/remote-web/icons/nowwork.svg +0 -7
- package/dist/remote-web/index.html +0 -20
- package/dist/remote-web/manifest.webmanifest +0 -13
- package/dist/remote-web/sw.js +0 -12
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, NotificationSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
-
import { WebSocket } from "ws";
|
|
6
|
-
import { z } from "zod";
|
|
7
|
-
import { z as z4 } from "zod/v4";
|
|
8
|
-
import { ChannelToGatewaySchema, GatewayToChannelSchema, REMOTE_PROTOCOL_VERSION, } from "./protocol.js";
|
|
9
|
-
const PermissionRequestSchema = NotificationSchema.extend({
|
|
10
|
-
method: z4.literal("notifications/claude/channel/permission_request"),
|
|
11
|
-
params: z4.object({
|
|
12
|
-
request_id: z4.string().regex(/^[a-km-z]{5}$/),
|
|
13
|
-
tool_name: z4.string(),
|
|
14
|
-
description: z4.string(),
|
|
15
|
-
input_preview: z4.string(),
|
|
16
|
-
}),
|
|
17
|
-
});
|
|
18
|
-
const ChannelEnvironmentSchema = z.object({
|
|
19
|
-
NOWCREW_REMOTE_URL: z.string().url(),
|
|
20
|
-
NOWCREW_REMOTE_TOKEN_FILE: z.string().min(1),
|
|
21
|
-
NOWCREW_REMOTE_SESSION_ID: z.string().uuid(),
|
|
22
|
-
NOWCREW_REMOTE_GENERATION: z.string().uuid(),
|
|
23
|
-
NOWCREW_REMOTE_CWD: z.string().min(1),
|
|
24
|
-
}).passthrough();
|
|
25
|
-
export function createClaudeChannelServer(sendToGateway) {
|
|
26
|
-
const mcp = new Server({ name: "nowcrew", version: "1.0.0" }, {
|
|
27
|
-
capabilities: {
|
|
28
|
-
experimental: {
|
|
29
|
-
"claude/channel": {},
|
|
30
|
-
"claude/channel/permission": {},
|
|
31
|
-
},
|
|
32
|
-
tools: {},
|
|
33
|
-
},
|
|
34
|
-
instructions: "Messages from the user's authenticated NowCrew phone arrive as <channel source=\"nowcrew\" request_id=\"...\">. "
|
|
35
|
-
+ "Handle them as direct user requests in this session. Render the complete user-facing answer normally in the terminal, "
|
|
36
|
-
+ "then call the reply tool exactly once with the same complete answer and request_id so the phone stays synchronized.",
|
|
37
|
-
});
|
|
38
|
-
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
39
|
-
tools: [{
|
|
40
|
-
name: "reply",
|
|
41
|
-
description: "Send the complete user-facing answer back to the authenticated NowCrew phone",
|
|
42
|
-
inputSchema: {
|
|
43
|
-
type: "object",
|
|
44
|
-
properties: {
|
|
45
|
-
request_id: { type: "string", format: "uuid" },
|
|
46
|
-
text: { type: "string", minLength: 1 },
|
|
47
|
-
},
|
|
48
|
-
required: ["request_id", "text"],
|
|
49
|
-
additionalProperties: false,
|
|
50
|
-
},
|
|
51
|
-
}],
|
|
52
|
-
}));
|
|
53
|
-
mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
54
|
-
if (request.params.name !== "reply")
|
|
55
|
-
throw new Error(`unknown tool: ${request.params.name}`);
|
|
56
|
-
const args = z.object({ request_id: z.string().uuid(), text: z.string().min(1).max(200_000) })
|
|
57
|
-
.parse(request.params.arguments);
|
|
58
|
-
sendToGateway({ type: "reply", ...args });
|
|
59
|
-
return { content: [{ type: "text", text: "sent" }] };
|
|
60
|
-
});
|
|
61
|
-
mcp.setNotificationHandler(PermissionRequestSchema, ({ params }) => {
|
|
62
|
-
sendToGateway({
|
|
63
|
-
type: "permission",
|
|
64
|
-
requestId: params.request_id,
|
|
65
|
-
tool: params.tool_name,
|
|
66
|
-
description: params.description,
|
|
67
|
-
preview: params.input_preview,
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
return mcp;
|
|
71
|
-
}
|
|
72
|
-
export async function runClaudeChannel(env = process.env) {
|
|
73
|
-
const parsed = ChannelEnvironmentSchema.parse(env);
|
|
74
|
-
const token = (await readFile(parsed.NOWCREW_REMOTE_TOKEN_FILE, "utf8")).trim();
|
|
75
|
-
const url = new URL("/ws/channel", parsed.NOWCREW_REMOTE_URL);
|
|
76
|
-
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
77
|
-
let socket = null;
|
|
78
|
-
const queued = [];
|
|
79
|
-
const send = (value) => {
|
|
80
|
-
if (socket?.readyState === WebSocket.OPEN)
|
|
81
|
-
socket.send(JSON.stringify(value));
|
|
82
|
-
else
|
|
83
|
-
queued.push(value);
|
|
84
|
-
};
|
|
85
|
-
const mcp = createClaudeChannelServer((value) => {
|
|
86
|
-
const record = value;
|
|
87
|
-
if (record.type === "reply") {
|
|
88
|
-
send(ChannelToGatewaySchema.parse({
|
|
89
|
-
type: "channel.reply",
|
|
90
|
-
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
91
|
-
sessionId: parsed.NOWCREW_REMOTE_SESSION_ID,
|
|
92
|
-
requestId: record.request_id,
|
|
93
|
-
text: record.text,
|
|
94
|
-
}));
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
send(ChannelToGatewaySchema.parse({
|
|
98
|
-
type: "channel.permission",
|
|
99
|
-
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
100
|
-
sessionId: parsed.NOWCREW_REMOTE_SESSION_ID,
|
|
101
|
-
requestId: record.requestId,
|
|
102
|
-
tool: record.tool,
|
|
103
|
-
description: record.description,
|
|
104
|
-
preview: record.preview,
|
|
105
|
-
}));
|
|
106
|
-
});
|
|
107
|
-
await mcp.connect(new StdioServerTransport());
|
|
108
|
-
let stopped = false;
|
|
109
|
-
const connect = () => {
|
|
110
|
-
if (stopped)
|
|
111
|
-
return;
|
|
112
|
-
const next = new WebSocket(url, { headers: { authorization: `Bearer ${token}` } });
|
|
113
|
-
socket = next;
|
|
114
|
-
next.on("open", () => {
|
|
115
|
-
next.send(JSON.stringify(ChannelToGatewaySchema.parse({
|
|
116
|
-
type: "channel.register",
|
|
117
|
-
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
118
|
-
sessionId: parsed.NOWCREW_REMOTE_SESSION_ID,
|
|
119
|
-
cwd: parsed.NOWCREW_REMOTE_CWD,
|
|
120
|
-
pid: process.ppid,
|
|
121
|
-
generation: parsed.NOWCREW_REMOTE_GENERATION,
|
|
122
|
-
})));
|
|
123
|
-
for (const value of queued.splice(0))
|
|
124
|
-
next.send(JSON.stringify(value));
|
|
125
|
-
});
|
|
126
|
-
next.on("message", (raw) => {
|
|
127
|
-
const frame = GatewayToChannelSchema.safeParse(JSON.parse(raw.toString()));
|
|
128
|
-
if (!frame.success)
|
|
129
|
-
return;
|
|
130
|
-
if (frame.data.type === "channel.prompt") {
|
|
131
|
-
void mcp.notification({
|
|
132
|
-
method: "notifications/claude/channel",
|
|
133
|
-
params: {
|
|
134
|
-
content: frame.data.text,
|
|
135
|
-
meta: { request_id: frame.data.requestId },
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
else if (frame.data.type === "channel.permission_response") {
|
|
140
|
-
void mcp.notification({
|
|
141
|
-
method: "notifications/claude/channel/permission",
|
|
142
|
-
params: {
|
|
143
|
-
request_id: frame.data.requestId,
|
|
144
|
-
behavior: frame.data.decision,
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
next.on("close", () => {
|
|
150
|
-
if (socket === next)
|
|
151
|
-
socket = null;
|
|
152
|
-
if (!stopped)
|
|
153
|
-
setTimeout(connect, 1_000).unref();
|
|
154
|
-
});
|
|
155
|
-
next.on("error", () => { });
|
|
156
|
-
};
|
|
157
|
-
connect();
|
|
158
|
-
const stop = () => {
|
|
159
|
-
stopped = true;
|
|
160
|
-
socket?.close();
|
|
161
|
-
};
|
|
162
|
-
process.once("SIGTERM", stop);
|
|
163
|
-
process.once("SIGINT", stop);
|
|
164
|
-
}
|
|
@@ -1,451 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { WebSocket } from "ws";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { MAX_REMOTE_WEBSOCKET_BUFFER_BYTES, } from "./protocol.js";
|
|
5
|
-
export const MAX_CODEX_PENDING_RPCS = 128;
|
|
6
|
-
export const MAX_CODEX_PENDING_INTERACTIONS = 128;
|
|
7
|
-
const ThreadStatusSchema = z.discriminatedUnion("type", [
|
|
8
|
-
z.object({ type: z.literal("notLoaded") }).passthrough(),
|
|
9
|
-
z.object({ type: z.literal("idle") }).passthrough(),
|
|
10
|
-
z.object({ type: z.literal("systemError") }).passthrough(),
|
|
11
|
-
z.object({ type: z.literal("active"), activeFlags: z.array(z.unknown()).optional() }).passthrough(),
|
|
12
|
-
]);
|
|
13
|
-
const ThreadItemSchema = z.object({
|
|
14
|
-
type: z.string(),
|
|
15
|
-
id: z.string().optional(),
|
|
16
|
-
}).passthrough();
|
|
17
|
-
const TurnSchema = z.object({
|
|
18
|
-
id: z.string(),
|
|
19
|
-
status: z.string(),
|
|
20
|
-
items: z.array(ThreadItemSchema),
|
|
21
|
-
startedAt: z.number().nullable().optional(),
|
|
22
|
-
completedAt: z.number().nullable().optional(),
|
|
23
|
-
}).passthrough();
|
|
24
|
-
const ThreadSchema = z.object({
|
|
25
|
-
id: z.string().uuid(),
|
|
26
|
-
preview: z.string().default(""),
|
|
27
|
-
createdAt: z.number(),
|
|
28
|
-
updatedAt: z.number(),
|
|
29
|
-
recencyAt: z.number().nullable().optional(),
|
|
30
|
-
status: ThreadStatusSchema,
|
|
31
|
-
cwd: z.string(),
|
|
32
|
-
name: z.string().nullable().optional(),
|
|
33
|
-
turns: z.array(TurnSchema).default([]),
|
|
34
|
-
}).passthrough();
|
|
35
|
-
const ThreadListResponseSchema = z.object({
|
|
36
|
-
data: z.array(ThreadSchema),
|
|
37
|
-
nextCursor: z.string().nullable(),
|
|
38
|
-
}).passthrough();
|
|
39
|
-
const ThreadReadResponseSchema = z.object({ thread: ThreadSchema }).passthrough();
|
|
40
|
-
const ThreadResumeResponseSchema = z.object({ thread: ThreadSchema }).passthrough();
|
|
41
|
-
const ApprovalParamsSchema = z.object({
|
|
42
|
-
threadId: z.string().uuid(),
|
|
43
|
-
command: z.string().nullable().optional(),
|
|
44
|
-
reason: z.string().nullable().optional(),
|
|
45
|
-
grantRoot: z.string().nullable().optional(),
|
|
46
|
-
}).passthrough();
|
|
47
|
-
const QuestionParamsSchema = z.object({
|
|
48
|
-
threadId: z.string().uuid(),
|
|
49
|
-
questions: z.array(z.object({
|
|
50
|
-
id: z.string().min(1),
|
|
51
|
-
header: z.string(),
|
|
52
|
-
question: z.string(),
|
|
53
|
-
isSecret: z.boolean(),
|
|
54
|
-
options: z.array(z.object({
|
|
55
|
-
label: z.string(),
|
|
56
|
-
description: z.string(),
|
|
57
|
-
}).passthrough()).nullable(),
|
|
58
|
-
}).passthrough()).min(1).max(3),
|
|
59
|
-
}).passthrough();
|
|
60
|
-
function isoFromSeconds(value) {
|
|
61
|
-
return new Date((value ?? Date.now() / 1_000) * 1_000).toISOString();
|
|
62
|
-
}
|
|
63
|
-
function textInputs(content) {
|
|
64
|
-
if (!Array.isArray(content))
|
|
65
|
-
return "";
|
|
66
|
-
return content.flatMap((entry) => {
|
|
67
|
-
const value = entry;
|
|
68
|
-
if (value.type === "text" && typeof value.text === "string")
|
|
69
|
-
return [value.text];
|
|
70
|
-
if (value.type === "localImage" && typeof value.path === "string")
|
|
71
|
-
return [`[Image: ${value.path}]`];
|
|
72
|
-
return [];
|
|
73
|
-
}).join("\n").trim();
|
|
74
|
-
}
|
|
75
|
-
function itemToTimeline(raw, createdAt) {
|
|
76
|
-
const item = raw;
|
|
77
|
-
const id = typeof item.id === "string" ? `codex:${item.id}` : `codex:${randomUUID()}`;
|
|
78
|
-
if (item.type === "userMessage") {
|
|
79
|
-
const text = textInputs(item.content);
|
|
80
|
-
return text ? { id, kind: "message", role: "user", text, createdAt } : null;
|
|
81
|
-
}
|
|
82
|
-
if (item.type === "agentMessage" && typeof item.text === "string" && item.text.trim()) {
|
|
83
|
-
return { id, kind: "message", role: "assistant", text: item.text, createdAt };
|
|
84
|
-
}
|
|
85
|
-
if (item.type === "plan" && typeof item.text === "string") {
|
|
86
|
-
return { id, kind: "tool", name: "Plan", summary: item.text.slice(0, 12_000), status: "updated", createdAt };
|
|
87
|
-
}
|
|
88
|
-
if (item.type === "commandExecution" && typeof item.command === "string") {
|
|
89
|
-
const output = typeof item.aggregatedOutput === "string" ? `\n\n${item.aggregatedOutput}` : "";
|
|
90
|
-
return {
|
|
91
|
-
id,
|
|
92
|
-
kind: "tool",
|
|
93
|
-
name: "Command",
|
|
94
|
-
summary: `${item.command}${output}`.slice(0, 12_000),
|
|
95
|
-
status: typeof item.status === "string" ? item.status : "completed",
|
|
96
|
-
createdAt,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
if (item.type === "fileChange") {
|
|
100
|
-
return {
|
|
101
|
-
id,
|
|
102
|
-
kind: "tool",
|
|
103
|
-
name: "File change",
|
|
104
|
-
summary: JSON.stringify(item.changes ?? []).slice(0, 12_000),
|
|
105
|
-
status: typeof item.status === "string" ? item.status : "completed",
|
|
106
|
-
createdAt,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
if (item.type === "mcpToolCall" || item.type === "dynamicToolCall") {
|
|
110
|
-
const server = typeof item.server === "string" ? `${item.server}.` : "";
|
|
111
|
-
const tool = typeof item.tool === "string" ? item.tool : "tool";
|
|
112
|
-
return {
|
|
113
|
-
id,
|
|
114
|
-
kind: "tool",
|
|
115
|
-
name: `${server}${tool}`,
|
|
116
|
-
summary: JSON.stringify(item.arguments ?? {}).slice(0, 12_000),
|
|
117
|
-
status: typeof item.status === "string" ? item.status : "completed",
|
|
118
|
-
createdAt,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
function threadSummary(thread) {
|
|
124
|
-
return {
|
|
125
|
-
id: thread.id,
|
|
126
|
-
runtime: "codex",
|
|
127
|
-
title: thread.name?.trim() || thread.preview.trim().slice(0, 240) || "Codex session",
|
|
128
|
-
cwd: thread.cwd,
|
|
129
|
-
updatedAt: isoFromSeconds(thread.recencyAt ?? thread.updatedAt),
|
|
130
|
-
controlState: "live",
|
|
131
|
-
busy: thread.status.type === "active",
|
|
132
|
-
source: "app_server",
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
function threadTimeline(thread) {
|
|
136
|
-
const items = [];
|
|
137
|
-
for (const turn of thread.turns) {
|
|
138
|
-
const createdAt = isoFromSeconds(turn.startedAt ?? turn.completedAt ?? thread.updatedAt);
|
|
139
|
-
for (const raw of turn.items) {
|
|
140
|
-
const mapped = itemToTimeline(raw, createdAt);
|
|
141
|
-
if (mapped)
|
|
142
|
-
items.push(mapped);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return items.length > 400 ? items.slice(items.length - 400) : items;
|
|
146
|
-
}
|
|
147
|
-
function ipcWebSocketUrl(socketPath) {
|
|
148
|
-
if (!socketPath.startsWith("/"))
|
|
149
|
-
throw new Error("Codex app-server socket path must be absolute");
|
|
150
|
-
if (socketPath.includes(":"))
|
|
151
|
-
throw new Error("Codex app-server socket path cannot contain ':'");
|
|
152
|
-
return `ws+unix://${socketPath}:/`;
|
|
153
|
-
}
|
|
154
|
-
export class CodexAppServerClient {
|
|
155
|
-
socket;
|
|
156
|
-
nextId = 1;
|
|
157
|
-
pending = new Map();
|
|
158
|
-
interactions = new Map();
|
|
159
|
-
listeners = new Set();
|
|
160
|
-
submissionTails = new Map();
|
|
161
|
-
closedError = null;
|
|
162
|
-
constructor(socket) {
|
|
163
|
-
this.socket = socket;
|
|
164
|
-
socket.on("message", (raw) => this.receive(raw.toString()));
|
|
165
|
-
socket.once("close", () => this.fail(new Error("Codex app-server connection closed")));
|
|
166
|
-
socket.once("error", (error) => this.fail(error));
|
|
167
|
-
}
|
|
168
|
-
static async connect(socketPath, timeoutMs = 5_000) {
|
|
169
|
-
const socket = new WebSocket(ipcWebSocketUrl(socketPath), {
|
|
170
|
-
perMessageDeflate: false,
|
|
171
|
-
maxPayload: MAX_REMOTE_WEBSOCKET_BUFFER_BYTES,
|
|
172
|
-
});
|
|
173
|
-
await new Promise((resolve, reject) => {
|
|
174
|
-
const timer = setTimeout(() => {
|
|
175
|
-
socket.terminate();
|
|
176
|
-
reject(new Error(`Timed out connecting to Codex app-server at ${socketPath}`));
|
|
177
|
-
}, timeoutMs);
|
|
178
|
-
socket.once("open", () => {
|
|
179
|
-
clearTimeout(timer);
|
|
180
|
-
resolve();
|
|
181
|
-
});
|
|
182
|
-
socket.once("error", (error) => {
|
|
183
|
-
clearTimeout(timer);
|
|
184
|
-
reject(error);
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
const client = new CodexAppServerClient(socket);
|
|
188
|
-
await client.request("initialize", {
|
|
189
|
-
clientInfo: { name: "nowcrew_remote", title: "NowCrew Remote", version: "1.0.0" },
|
|
190
|
-
capabilities: { experimentalApi: true, requestAttestation: false },
|
|
191
|
-
});
|
|
192
|
-
client.notify("initialized");
|
|
193
|
-
return client;
|
|
194
|
-
}
|
|
195
|
-
async listSessions() {
|
|
196
|
-
const threads = [];
|
|
197
|
-
let cursor = null;
|
|
198
|
-
for (let page = 0; page < 2; page += 1) {
|
|
199
|
-
const response = ThreadListResponseSchema.parse(await this.request("thread/list", {
|
|
200
|
-
limit: 100,
|
|
201
|
-
...(cursor === null ? {} : { cursor }),
|
|
202
|
-
}));
|
|
203
|
-
threads.push(...response.data);
|
|
204
|
-
cursor = response.nextCursor;
|
|
205
|
-
if (!cursor)
|
|
206
|
-
break;
|
|
207
|
-
}
|
|
208
|
-
return threads.map(threadSummary).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
209
|
-
}
|
|
210
|
-
async timeline(sessionId) {
|
|
211
|
-
try {
|
|
212
|
-
const response = ThreadReadResponseSchema.parse(await this.request("thread/read", {
|
|
213
|
-
threadId: sessionId,
|
|
214
|
-
includeTurns: true,
|
|
215
|
-
}));
|
|
216
|
-
return threadTimeline(response.thread);
|
|
217
|
-
}
|
|
218
|
-
catch (error) {
|
|
219
|
-
if (error instanceof Error && /not found|does not exist/i.test(error.message))
|
|
220
|
-
return null;
|
|
221
|
-
throw error;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
async submit(sessionId, input) {
|
|
225
|
-
const previous = this.submissionTails.get(sessionId) ?? Promise.resolve();
|
|
226
|
-
const execution = previous.catch(() => { }).then(() => this.submitNow(sessionId, input));
|
|
227
|
-
const tail = execution.then(() => { }, () => { });
|
|
228
|
-
this.submissionTails.set(sessionId, tail);
|
|
229
|
-
try {
|
|
230
|
-
return await execution;
|
|
231
|
-
}
|
|
232
|
-
finally {
|
|
233
|
-
if (this.submissionTails.get(sessionId) === tail)
|
|
234
|
-
this.submissionTails.delete(sessionId);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
async submitNow(sessionId, input) {
|
|
238
|
-
const resumed = ThreadResumeResponseSchema.parse(await this.request("thread/resume", { threadId: sessionId }));
|
|
239
|
-
const active = [...resumed.thread.turns].reverse().find((turn) => turn.status === "inProgress");
|
|
240
|
-
const params = {
|
|
241
|
-
threadId: sessionId,
|
|
242
|
-
clientUserMessageId: input.idempotencyKey,
|
|
243
|
-
input: [{ type: "text", text: input.text, text_elements: [] }],
|
|
244
|
-
};
|
|
245
|
-
if (active) {
|
|
246
|
-
await this.request("turn/steer", { ...params, expectedTurnId: active.id });
|
|
247
|
-
}
|
|
248
|
-
else {
|
|
249
|
-
await this.request("turn/start", params);
|
|
250
|
-
}
|
|
251
|
-
return { requestId: input.idempotencyKey };
|
|
252
|
-
}
|
|
253
|
-
resolveApproval(sessionId, requestId, decision) {
|
|
254
|
-
const pending = this.takeInteraction(sessionId, requestId, "approval");
|
|
255
|
-
if (!pending)
|
|
256
|
-
return false;
|
|
257
|
-
this.respond(pending.rpcId, { decision: decision === "allow" ? "accept" : "decline" });
|
|
258
|
-
return true;
|
|
259
|
-
}
|
|
260
|
-
resolveQuestion(sessionId, requestId, input) {
|
|
261
|
-
const pending = this.takeInteraction(sessionId, requestId, "question");
|
|
262
|
-
if (!pending)
|
|
263
|
-
return false;
|
|
264
|
-
this.respond(pending.rpcId, {
|
|
265
|
-
answers: Object.fromEntries(Object.entries(input.answers).map(([id, answers]) => [id, { answers }])),
|
|
266
|
-
});
|
|
267
|
-
return true;
|
|
268
|
-
}
|
|
269
|
-
onEvent(listener) {
|
|
270
|
-
this.listeners.add(listener);
|
|
271
|
-
return () => this.listeners.delete(listener);
|
|
272
|
-
}
|
|
273
|
-
async close() {
|
|
274
|
-
if (this.socket.readyState === WebSocket.CLOSED)
|
|
275
|
-
return;
|
|
276
|
-
await new Promise((resolve) => {
|
|
277
|
-
const timer = setTimeout(() => {
|
|
278
|
-
this.socket.terminate();
|
|
279
|
-
resolve();
|
|
280
|
-
}, 1_000);
|
|
281
|
-
this.socket.once("close", () => {
|
|
282
|
-
clearTimeout(timer);
|
|
283
|
-
resolve();
|
|
284
|
-
});
|
|
285
|
-
this.socket.close(1000, "NowCrew gateway stopping");
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
request(method, params, timeoutMs = 30_000) {
|
|
289
|
-
if (this.closedError)
|
|
290
|
-
return Promise.reject(this.closedError);
|
|
291
|
-
if (this.pending.size >= MAX_CODEX_PENDING_RPCS) {
|
|
292
|
-
return Promise.reject(new Error("Codex app-server RPC queue is full"));
|
|
293
|
-
}
|
|
294
|
-
const id = this.nextId++;
|
|
295
|
-
return new Promise((resolve, reject) => {
|
|
296
|
-
const timer = setTimeout(() => {
|
|
297
|
-
this.pending.delete(id);
|
|
298
|
-
reject(new Error(`Codex app-server ${method} timed out`));
|
|
299
|
-
}, timeoutMs);
|
|
300
|
-
this.pending.set(id, { resolve, reject, timer });
|
|
301
|
-
try {
|
|
302
|
-
this.send({ id, method, params });
|
|
303
|
-
}
|
|
304
|
-
catch (error) {
|
|
305
|
-
clearTimeout(timer);
|
|
306
|
-
this.pending.delete(id);
|
|
307
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
308
|
-
}
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
notify(method, params) {
|
|
312
|
-
this.send({ method, ...(params === undefined ? {} : { params }) });
|
|
313
|
-
}
|
|
314
|
-
respond(id, result) {
|
|
315
|
-
this.send({ id, result });
|
|
316
|
-
}
|
|
317
|
-
send(value) {
|
|
318
|
-
if (this.socket.readyState !== WebSocket.OPEN)
|
|
319
|
-
throw this.closedError ?? new Error("Codex app-server is not open");
|
|
320
|
-
const serialized = JSON.stringify(value);
|
|
321
|
-
if (this.socket.bufferedAmount + Buffer.byteLength(serialized, "utf8") > MAX_REMOTE_WEBSOCKET_BUFFER_BYTES) {
|
|
322
|
-
const error = new Error("Codex app-server outbound buffer limit exceeded");
|
|
323
|
-
this.fail(error);
|
|
324
|
-
this.socket.close(1013, "outbound buffer limit");
|
|
325
|
-
throw error;
|
|
326
|
-
}
|
|
327
|
-
this.socket.send(serialized);
|
|
328
|
-
}
|
|
329
|
-
receive(raw) {
|
|
330
|
-
let message;
|
|
331
|
-
try {
|
|
332
|
-
message = JSON.parse(raw);
|
|
333
|
-
}
|
|
334
|
-
catch {
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
if (message.id !== undefined && message.method !== undefined) {
|
|
338
|
-
this.receiveServerRequest(message.id, message.method, message.params);
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
if (message.id !== undefined) {
|
|
342
|
-
const pending = this.pending.get(message.id);
|
|
343
|
-
if (!pending)
|
|
344
|
-
return;
|
|
345
|
-
this.pending.delete(message.id);
|
|
346
|
-
clearTimeout(pending.timer);
|
|
347
|
-
if (message.error)
|
|
348
|
-
pending.reject(new Error(`Codex app-server RPC failed: ${message.error.message ?? "unknown error"}`));
|
|
349
|
-
else
|
|
350
|
-
pending.resolve(message.result);
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
if (message.method)
|
|
354
|
-
this.receiveNotification(message.method, message.params);
|
|
355
|
-
}
|
|
356
|
-
receiveServerRequest(id, method, params) {
|
|
357
|
-
if (method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval") {
|
|
358
|
-
const parsed = ApprovalParamsSchema.safeParse(params);
|
|
359
|
-
if (!parsed.success) {
|
|
360
|
-
this.respond(id, { decision: "decline" });
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
if (this.interactions.size >= MAX_CODEX_PENDING_INTERACTIONS) {
|
|
364
|
-
this.respond(id, { decision: "decline" });
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
const requestId = randomUUID();
|
|
368
|
-
this.interactions.set(requestId, { rpcId: id, sessionId: parsed.data.threadId, type: "approval" });
|
|
369
|
-
const tool = method.includes("commandExecution") ? "Command" : "File change";
|
|
370
|
-
const preview = parsed.data.command ?? parsed.data.grantRoot ?? parsed.data.reason ?? tool;
|
|
371
|
-
this.emit({
|
|
372
|
-
type: "timeline.appended",
|
|
373
|
-
sessionId: parsed.data.threadId,
|
|
374
|
-
item: { id: `codex-approval:${requestId}`, kind: "approval", requestId, tool, preview, createdAt: new Date().toISOString() },
|
|
375
|
-
});
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
if (method === "item/tool/requestUserInput") {
|
|
379
|
-
const parsed = QuestionParamsSchema.safeParse(params);
|
|
380
|
-
if (!parsed.success) {
|
|
381
|
-
this.respond(id, { answers: {} });
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
if (this.interactions.size >= MAX_CODEX_PENDING_INTERACTIONS) {
|
|
385
|
-
this.respond(id, { answers: {} });
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
const requestId = randomUUID();
|
|
389
|
-
this.interactions.set(requestId, { rpcId: id, sessionId: parsed.data.threadId, type: "question" });
|
|
390
|
-
this.emit({
|
|
391
|
-
type: "timeline.appended",
|
|
392
|
-
sessionId: parsed.data.threadId,
|
|
393
|
-
item: {
|
|
394
|
-
id: `codex-question:${requestId}`,
|
|
395
|
-
kind: "question",
|
|
396
|
-
requestId,
|
|
397
|
-
questions: parsed.data.questions.map((question) => ({
|
|
398
|
-
id: question.id,
|
|
399
|
-
header: question.header,
|
|
400
|
-
question: question.question,
|
|
401
|
-
isSecret: question.isSecret,
|
|
402
|
-
options: question.options,
|
|
403
|
-
})),
|
|
404
|
-
createdAt: new Date().toISOString(),
|
|
405
|
-
},
|
|
406
|
-
});
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
this.send({ id, error: { code: -32001, message: `NowCrew does not support ${method}` } });
|
|
410
|
-
}
|
|
411
|
-
receiveNotification(method, params) {
|
|
412
|
-
if (["thread/started", "thread/status/changed", "thread/name/updated", "turn/started", "turn/completed"].includes(method)) {
|
|
413
|
-
this.emit({ type: "sessions.changed" });
|
|
414
|
-
}
|
|
415
|
-
if (method !== "item/completed")
|
|
416
|
-
return;
|
|
417
|
-
const parsed = z.object({
|
|
418
|
-
threadId: z.string().uuid(),
|
|
419
|
-
item: ThreadItemSchema,
|
|
420
|
-
completedAtMs: z.number().optional(),
|
|
421
|
-
}).passthrough().safeParse(params);
|
|
422
|
-
if (!parsed.success)
|
|
423
|
-
return;
|
|
424
|
-
const item = itemToTimeline(parsed.data.item, new Date(parsed.data.completedAtMs ?? Date.now()).toISOString());
|
|
425
|
-
if (item)
|
|
426
|
-
this.emit({ type: "timeline.appended", sessionId: parsed.data.threadId, item });
|
|
427
|
-
}
|
|
428
|
-
takeInteraction(sessionId, requestId, type) {
|
|
429
|
-
const pending = this.interactions.get(requestId);
|
|
430
|
-
if (!pending || pending.sessionId !== sessionId || pending.type !== type)
|
|
431
|
-
return null;
|
|
432
|
-
this.interactions.delete(requestId);
|
|
433
|
-
return pending;
|
|
434
|
-
}
|
|
435
|
-
emit(event) {
|
|
436
|
-
for (const listener of this.listeners)
|
|
437
|
-
listener(event);
|
|
438
|
-
}
|
|
439
|
-
fail(error) {
|
|
440
|
-
if (this.closedError)
|
|
441
|
-
return;
|
|
442
|
-
this.closedError = error;
|
|
443
|
-
for (const pending of this.pending.values()) {
|
|
444
|
-
clearTimeout(pending.timer);
|
|
445
|
-
pending.reject(error);
|
|
446
|
-
}
|
|
447
|
-
this.pending.clear();
|
|
448
|
-
this.interactions.clear();
|
|
449
|
-
this.emit({ type: "sessions.changed" });
|
|
450
|
-
}
|
|
451
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { once } from "node:events";
|
|
2
|
-
import { lstat, mkdir, unlink } from "node:fs/promises";
|
|
3
|
-
import { dirname } from "node:path";
|
|
4
|
-
import spawn from "cross-spawn";
|
|
5
|
-
import { CodexAppServerClient } from "./codex-client.js";
|
|
6
|
-
async function connect(socketPath, timeoutMs) {
|
|
7
|
-
try {
|
|
8
|
-
return await CodexAppServerClient.connect(socketPath, timeoutMs);
|
|
9
|
-
}
|
|
10
|
-
catch {
|
|
11
|
-
return null;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
async function removeStaleSocket(socketPath) {
|
|
15
|
-
try {
|
|
16
|
-
const info = await lstat(socketPath);
|
|
17
|
-
if (!info.isSocket())
|
|
18
|
-
throw new Error(`Refusing to replace non-socket path: ${socketPath}`);
|
|
19
|
-
await unlink(socketPath);
|
|
20
|
-
}
|
|
21
|
-
catch (error) {
|
|
22
|
-
if (error.code !== "ENOENT")
|
|
23
|
-
throw error;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
export async function startCodexRuntime(socketPath, bin = process.env.NOWCREW_CODEX_BIN ?? "codex") {
|
|
27
|
-
const existing = await connect(socketPath, 500);
|
|
28
|
-
if (existing) {
|
|
29
|
-
return { adapter: existing, socketPath, close: () => existing.close() };
|
|
30
|
-
}
|
|
31
|
-
await mkdir(dirname(socketPath), { recursive: true, mode: 0o700 });
|
|
32
|
-
await removeStaleSocket(socketPath);
|
|
33
|
-
const child = spawn(bin, ["app-server", "--listen", `unix://${socketPath}`], {
|
|
34
|
-
env: process.env,
|
|
35
|
-
stdio: ["ignore", "ignore", "pipe"],
|
|
36
|
-
});
|
|
37
|
-
let stderr = "";
|
|
38
|
-
child.stderr?.on("data", (chunk) => {
|
|
39
|
-
stderr = `${stderr}${String(chunk)}`.slice(-4_000);
|
|
40
|
-
});
|
|
41
|
-
let adapter = null;
|
|
42
|
-
for (let attempt = 0; attempt < 50; attempt += 1) {
|
|
43
|
-
if (child.exitCode !== null || child.signalCode !== null)
|
|
44
|
-
break;
|
|
45
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
46
|
-
adapter = await connect(socketPath, 500);
|
|
47
|
-
if (adapter)
|
|
48
|
-
break;
|
|
49
|
-
}
|
|
50
|
-
if (!adapter) {
|
|
51
|
-
if (child.exitCode === null && child.signalCode === null)
|
|
52
|
-
child.kill("SIGTERM");
|
|
53
|
-
throw new Error(`Codex app-server did not start at ${socketPath}${stderr ? `: ${stderr.trim()}` : ""}`);
|
|
54
|
-
}
|
|
55
|
-
return {
|
|
56
|
-
adapter,
|
|
57
|
-
socketPath,
|
|
58
|
-
async close() {
|
|
59
|
-
await adapter.close();
|
|
60
|
-
if (child.exitCode !== null || child.signalCode !== null)
|
|
61
|
-
return;
|
|
62
|
-
const closed = once(child, "close").then(() => undefined);
|
|
63
|
-
child.kill("SIGTERM");
|
|
64
|
-
let timer;
|
|
65
|
-
const stopped = await Promise.race([
|
|
66
|
-
closed.then(() => true),
|
|
67
|
-
new Promise((resolve) => { timer = setTimeout(() => resolve(false), 1_000); }),
|
|
68
|
-
]);
|
|
69
|
-
if (timer)
|
|
70
|
-
clearTimeout(timer);
|
|
71
|
-
if (!stopped && child.exitCode === null && child.signalCode === null) {
|
|
72
|
-
child.kill("SIGKILL");
|
|
73
|
-
await closed;
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
}
|