@nowcrew/daemon 0.6.11 → 0.6.13
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 +6 -3
- package/dist/agent-ability/materializer.js +4 -3
- package/dist/agent-ability/resolver.js +13 -3
- package/dist/agent-ability/types.js +11 -7
- package/dist/config.js +4 -2
- package/dist/execution-runner.js +4 -1
- package/dist/host-execution-coordinator.js +3 -3
- package/dist/main.js +0 -0
- package/dist/memory-prune-diagnostics.js +6 -0
- package/dist/remote/claude-bridge.js +558 -0
- package/dist/remote/claude-channel.js +164 -0
- package/dist/remote/codex-client.js +451 -0
- package/dist/remote/codex-runtime.js +77 -0
- package/dist/remote/config.js +135 -0
- package/dist/remote/gateway.js +879 -0
- package/dist/remote/identity.js +39 -0
- package/dist/remote/owner.js +77 -0
- package/dist/remote/protocol.js +211 -0
- package/dist/remote/remote-cli.js +254 -0
- package/dist/remote/runtime-probe.js +182 -0
- package/dist/remote/session-discovery.js +249 -0
- package/dist/remote/wrapper.js +40 -0
- package/dist/remote-web/assets/index-B_6VM_tw.js +94 -0
- package/dist/remote-web/assets/index-L6EiQbJn.css +1 -0
- 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 +7 -0
- package/dist/remote-web/index.html +20 -0
- package/dist/remote-web/manifest.webmanifest +13 -0
- package/dist/remote-web/sw.js +12 -0
- package/dist/serve.js +9 -3
- package/dist/shared-execution-slots.js +17 -9
- package/dist/slog.js +11 -0
- package/dist/workspace.js +18 -17
- package/package.json +9 -10
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { REMOTE_PROTOCOL_VERSION } from "./protocol.js";
|
|
4
|
+
const Base64Url32Schema = z.string().regex(/^[A-Za-z0-9_-]{43}$/);
|
|
5
|
+
export const GatewayIdentitySchema = z.object({
|
|
6
|
+
gatewayId: z.string().uuid(),
|
|
7
|
+
pid: z.number().int().positive(),
|
|
8
|
+
protocolVersion: z.literal(REMOTE_PROTOCOL_VERSION),
|
|
9
|
+
proof: Base64Url32Schema,
|
|
10
|
+
}).strict();
|
|
11
|
+
export function createIdentitySecret() {
|
|
12
|
+
return randomBytes(32).toString("base64url");
|
|
13
|
+
}
|
|
14
|
+
export function createIdentityNonce() {
|
|
15
|
+
return randomBytes(32).toString("base64url");
|
|
16
|
+
}
|
|
17
|
+
export function signGatewayIdentity(identityKey, nonce, identity) {
|
|
18
|
+
const key = Base64Url32Schema.parse(identityKey);
|
|
19
|
+
const parsedNonce = Base64Url32Schema.parse(nonce);
|
|
20
|
+
return createHmac("sha256", Buffer.from(key, "base64url"))
|
|
21
|
+
.update(JSON.stringify([
|
|
22
|
+
"nowcrew.remote.identity.v1",
|
|
23
|
+
parsedNonce,
|
|
24
|
+
identity.gatewayId,
|
|
25
|
+
identity.pid,
|
|
26
|
+
identity.protocolVersion,
|
|
27
|
+
]))
|
|
28
|
+
.digest("base64url");
|
|
29
|
+
}
|
|
30
|
+
export function verifyGatewayIdentity(identityKey, nonce, identity) {
|
|
31
|
+
try {
|
|
32
|
+
const expected = Buffer.from(signGatewayIdentity(identityKey, nonce, identity), "base64url");
|
|
33
|
+
const actual = Buffer.from(identity.proof, "base64url");
|
|
34
|
+
return expected.length === actual.length && timingSafeEqual(expected, actual);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { acquireRemoteOwnerLock, readRemoteOwnerLock, releaseRemoteOwnerLock, } from "./config.js";
|
|
3
|
+
import { createIdentityNonce, createIdentitySecret, GatewayIdentitySchema, verifyGatewayIdentity, } from "./identity.js";
|
|
4
|
+
function ownerBaseUrl(lock) {
|
|
5
|
+
const host = lock.host === "0.0.0.0" || lock.host === "::" ? "127.0.0.1" : lock.host;
|
|
6
|
+
return `http://${host}:${lock.port}`;
|
|
7
|
+
}
|
|
8
|
+
export function isProcessAlive(pid) {
|
|
9
|
+
try {
|
|
10
|
+
process.kill(pid, 0);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
return error.code === "EPERM";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function authenticateRemoteGateway(config) {
|
|
18
|
+
let lock;
|
|
19
|
+
try {
|
|
20
|
+
lock = await readRemoteOwnerLock(config.paths.lock);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
if (!lock?.identityKey)
|
|
26
|
+
return null;
|
|
27
|
+
const nonce = createIdentityNonce();
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(`${ownerBaseUrl(lock)}/api/v1/identity?nonce=${encodeURIComponent(nonce)}`, {
|
|
30
|
+
signal: AbortSignal.timeout(700),
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok)
|
|
33
|
+
return null;
|
|
34
|
+
const identity = GatewayIdentitySchema.parse(await response.json());
|
|
35
|
+
if (identity.gatewayId !== lock.gatewayId || identity.pid !== lock.pid)
|
|
36
|
+
return null;
|
|
37
|
+
if (!verifyGatewayIdentity(lock.identityKey, nonce, identity))
|
|
38
|
+
return null;
|
|
39
|
+
return {
|
|
40
|
+
gatewayId: identity.gatewayId,
|
|
41
|
+
pid: identity.pid,
|
|
42
|
+
protocolVersion: identity.protocolVersion,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function acquireGatewayOwnership(config, options = {}) {
|
|
50
|
+
const attempts = options.attempts ?? 50;
|
|
51
|
+
const intervalMs = options.intervalMs ?? 100;
|
|
52
|
+
const candidate = {
|
|
53
|
+
version: 1,
|
|
54
|
+
gatewayId: randomUUID(),
|
|
55
|
+
pid: process.pid,
|
|
56
|
+
host: config.host,
|
|
57
|
+
port: config.port,
|
|
58
|
+
startedAt: new Date().toISOString(),
|
|
59
|
+
identityKey: createIdentitySecret(),
|
|
60
|
+
};
|
|
61
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
62
|
+
if (await acquireRemoteOwnerLock(config.paths.lock, candidate)) {
|
|
63
|
+
return { kind: "acquired", lock: candidate };
|
|
64
|
+
}
|
|
65
|
+
const identity = await authenticateRemoteGateway(config);
|
|
66
|
+
if (identity)
|
|
67
|
+
return { kind: "existing", identity };
|
|
68
|
+
const current = await readRemoteOwnerLock(config.paths.lock).catch(() => null);
|
|
69
|
+
if (current && !isProcessAlive(current.pid)) {
|
|
70
|
+
await releaseRemoteOwnerLock(config.paths.lock, current.gatewayId);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (attempt + 1 < attempts)
|
|
74
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
75
|
+
}
|
|
76
|
+
throw new Error("NowCrew remote gateway owner exists but could not be authenticated");
|
|
77
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const REMOTE_PROTOCOL_VERSION = 2;
|
|
3
|
+
export const MAX_REMOTE_PROMPT_BYTES = 64 * 1024;
|
|
4
|
+
export const MAX_CLAUDE_INPUT_QUEUE_ITEMS = 16;
|
|
5
|
+
export const MAX_CLAUDE_INPUT_QUEUE_BYTES = 1024 * 1024;
|
|
6
|
+
export const MAX_REMOTE_WEBSOCKET_BUFFER_BYTES = 1024 * 1024;
|
|
7
|
+
export const RemoteRuntimeSchema = z.enum(["claude", "codex"]);
|
|
8
|
+
export const RemoteControlStateSchema = z.enum([
|
|
9
|
+
"live",
|
|
10
|
+
"read_only",
|
|
11
|
+
"offline",
|
|
12
|
+
"unsupported",
|
|
13
|
+
]);
|
|
14
|
+
export const RemoteSessionSourceSchema = z.enum(["channel", "app_server", "history"]);
|
|
15
|
+
export const RemoteSessionSummarySchema = z.object({
|
|
16
|
+
id: z.string().min(1).max(128),
|
|
17
|
+
runtime: RemoteRuntimeSchema,
|
|
18
|
+
title: z.string().min(1).max(240),
|
|
19
|
+
cwd: z.string().max(4096),
|
|
20
|
+
updatedAt: z.string().datetime(),
|
|
21
|
+
controlState: RemoteControlStateSchema,
|
|
22
|
+
busy: z.boolean(),
|
|
23
|
+
source: RemoteSessionSourceSchema,
|
|
24
|
+
}).strict();
|
|
25
|
+
const TimelineBaseSchema = z.object({
|
|
26
|
+
id: z.string().min(1).max(256),
|
|
27
|
+
createdAt: z.string().datetime(),
|
|
28
|
+
});
|
|
29
|
+
export const RemoteQuestionsSchema = z.array(z.object({
|
|
30
|
+
id: z.string().min(1).max(160),
|
|
31
|
+
header: z.string().max(160),
|
|
32
|
+
question: z.string().min(1).max(4_000),
|
|
33
|
+
isSecret: z.boolean(),
|
|
34
|
+
options: z.array(z.object({
|
|
35
|
+
label: z.string().min(1).max(240),
|
|
36
|
+
description: z.string().max(1_000),
|
|
37
|
+
}).strict()).max(32).nullable(),
|
|
38
|
+
}).strict()).min(1).max(4);
|
|
39
|
+
export const RemoteTimelineItemSchema = z.discriminatedUnion("kind", [
|
|
40
|
+
TimelineBaseSchema.extend({
|
|
41
|
+
kind: z.literal("message"),
|
|
42
|
+
role: z.enum(["user", "assistant"]),
|
|
43
|
+
text: z.string().min(1),
|
|
44
|
+
}).strict(),
|
|
45
|
+
TimelineBaseSchema.extend({
|
|
46
|
+
kind: z.literal("tool"),
|
|
47
|
+
name: z.string().min(1).max(160),
|
|
48
|
+
summary: z.string().max(12_000),
|
|
49
|
+
status: z.string().min(1).max(80),
|
|
50
|
+
}).strict(),
|
|
51
|
+
TimelineBaseSchema.extend({
|
|
52
|
+
kind: z.literal("approval"),
|
|
53
|
+
requestId: z.string().min(1).max(160),
|
|
54
|
+
tool: z.string().min(1).max(160),
|
|
55
|
+
preview: z.string().max(12_000),
|
|
56
|
+
}).strict(),
|
|
57
|
+
TimelineBaseSchema.extend({
|
|
58
|
+
kind: z.literal("question"),
|
|
59
|
+
requestId: z.string().uuid(),
|
|
60
|
+
questions: RemoteQuestionsSchema,
|
|
61
|
+
}).strict(),
|
|
62
|
+
TimelineBaseSchema.extend({
|
|
63
|
+
kind: z.literal("status"),
|
|
64
|
+
text: z.string().min(1).max(2_000),
|
|
65
|
+
}).strict(),
|
|
66
|
+
]);
|
|
67
|
+
export const SubmitRemoteInputSchema = z.object({
|
|
68
|
+
text: z.string().trim().min(1).refine((value) => Buffer.byteLength(value, "utf8") <= MAX_REMOTE_PROMPT_BYTES, `prompt must not exceed ${MAX_REMOTE_PROMPT_BYTES} UTF-8 bytes`),
|
|
69
|
+
idempotencyKey: z.string().uuid(),
|
|
70
|
+
}).strict();
|
|
71
|
+
export const ResolveRemoteApprovalSchema = z.object({
|
|
72
|
+
decision: z.enum(["allow", "deny"]),
|
|
73
|
+
}).strict();
|
|
74
|
+
export const ResolveRemoteQuestionSchema = z.object({
|
|
75
|
+
answers: z.record(z.array(z.string().max(4_000)).max(16)),
|
|
76
|
+
}).strict();
|
|
77
|
+
export const ChannelRegistrationSchema = z.object({
|
|
78
|
+
type: z.literal("channel.register"),
|
|
79
|
+
protocolVersion: z.literal(REMOTE_PROTOCOL_VERSION),
|
|
80
|
+
sessionId: z.string().uuid(),
|
|
81
|
+
cwd: z.string().min(1).max(4096),
|
|
82
|
+
pid: z.number().int().positive(),
|
|
83
|
+
generation: z.string().uuid(),
|
|
84
|
+
title: z.string().min(1).max(240).optional(),
|
|
85
|
+
}).strict();
|
|
86
|
+
export const ChannelReplySchema = z.object({
|
|
87
|
+
type: z.literal("channel.reply"),
|
|
88
|
+
protocolVersion: z.literal(REMOTE_PROTOCOL_VERSION),
|
|
89
|
+
sessionId: z.string().uuid(),
|
|
90
|
+
requestId: z.string().uuid(),
|
|
91
|
+
text: z.string().max(200_000),
|
|
92
|
+
}).strict();
|
|
93
|
+
export const BridgeUserMessageSchema = z.object({
|
|
94
|
+
type: z.literal("bridge.user_message"),
|
|
95
|
+
protocolVersion: z.literal(REMOTE_PROTOCOL_VERSION),
|
|
96
|
+
sessionId: z.string().uuid(),
|
|
97
|
+
messageId: z.string().uuid(),
|
|
98
|
+
text: z.string().min(1).refine((value) => Buffer.byteLength(value, "utf8") <= MAX_REMOTE_PROMPT_BYTES, `message must not exceed ${MAX_REMOTE_PROMPT_BYTES} UTF-8 bytes`),
|
|
99
|
+
createdAt: z.string().datetime(),
|
|
100
|
+
}).strict();
|
|
101
|
+
export const ChannelPermissionRequestSchema = z.object({
|
|
102
|
+
type: z.literal("channel.permission"),
|
|
103
|
+
protocolVersion: z.literal(REMOTE_PROTOCOL_VERSION),
|
|
104
|
+
sessionId: z.string().uuid(),
|
|
105
|
+
requestId: z.string().regex(/^[a-km-z]{5}$/),
|
|
106
|
+
tool: z.string().min(1).max(160),
|
|
107
|
+
description: z.string().max(4_000),
|
|
108
|
+
preview: z.string().max(16_000),
|
|
109
|
+
}).strict();
|
|
110
|
+
export const BridgeApprovalRequestSchema = z.object({
|
|
111
|
+
type: z.literal("bridge.approval"),
|
|
112
|
+
protocolVersion: z.literal(REMOTE_PROTOCOL_VERSION),
|
|
113
|
+
sessionId: z.string().uuid(),
|
|
114
|
+
requestId: z.string().uuid(),
|
|
115
|
+
tool: z.string().min(1).max(160),
|
|
116
|
+
preview: z.string().max(16_000),
|
|
117
|
+
}).strict();
|
|
118
|
+
export const BridgeQuestionRequestSchema = z.object({
|
|
119
|
+
type: z.literal("bridge.question"),
|
|
120
|
+
protocolVersion: z.literal(REMOTE_PROTOCOL_VERSION),
|
|
121
|
+
sessionId: z.string().uuid(),
|
|
122
|
+
requestId: z.string().uuid(),
|
|
123
|
+
questions: RemoteQuestionsSchema,
|
|
124
|
+
}).strict();
|
|
125
|
+
export const BridgeStatusSchema = z.object({
|
|
126
|
+
type: z.literal("bridge.status"),
|
|
127
|
+
protocolVersion: z.literal(REMOTE_PROTOCOL_VERSION),
|
|
128
|
+
sessionId: z.string().uuid(),
|
|
129
|
+
busy: z.boolean(),
|
|
130
|
+
}).strict();
|
|
131
|
+
export const BridgeResolvedSchema = z.object({
|
|
132
|
+
type: z.literal("bridge.resolved"),
|
|
133
|
+
protocolVersion: z.literal(REMOTE_PROTOCOL_VERSION),
|
|
134
|
+
sessionId: z.string().uuid(),
|
|
135
|
+
requestId: z.string().uuid(),
|
|
136
|
+
}).strict();
|
|
137
|
+
export const ChannelDeliveryAckSchema = z.object({
|
|
138
|
+
type: z.literal("channel.delivery_ack"),
|
|
139
|
+
protocolVersion: z.literal(REMOTE_PROTOCOL_VERSION),
|
|
140
|
+
sessionId: z.string().uuid(),
|
|
141
|
+
generation: z.string().uuid(),
|
|
142
|
+
deliveryId: z.string().uuid(),
|
|
143
|
+
status: z.enum(["accepted", "queue_full", "not_pending"]),
|
|
144
|
+
}).strict();
|
|
145
|
+
export const ChannelToGatewaySchema = z.discriminatedUnion("type", [
|
|
146
|
+
ChannelRegistrationSchema,
|
|
147
|
+
ChannelReplySchema,
|
|
148
|
+
BridgeUserMessageSchema,
|
|
149
|
+
ChannelPermissionRequestSchema,
|
|
150
|
+
BridgeApprovalRequestSchema,
|
|
151
|
+
BridgeQuestionRequestSchema,
|
|
152
|
+
BridgeStatusSchema,
|
|
153
|
+
BridgeResolvedSchema,
|
|
154
|
+
ChannelDeliveryAckSchema,
|
|
155
|
+
]);
|
|
156
|
+
export const GatewayToChannelSchema = z.discriminatedUnion("type", [
|
|
157
|
+
z.object({
|
|
158
|
+
type: z.literal("channel.registered"),
|
|
159
|
+
protocolVersion: z.literal(REMOTE_PROTOCOL_VERSION),
|
|
160
|
+
sessionId: z.string().uuid(),
|
|
161
|
+
generation: z.string().uuid(),
|
|
162
|
+
}).strict(),
|
|
163
|
+
z.object({
|
|
164
|
+
type: z.literal("channel.prompt"),
|
|
165
|
+
protocolVersion: z.literal(REMOTE_PROTOCOL_VERSION),
|
|
166
|
+
deliveryId: z.string().uuid(),
|
|
167
|
+
requestId: z.string().uuid(),
|
|
168
|
+
text: z.string().min(1).max(MAX_REMOTE_PROMPT_BYTES),
|
|
169
|
+
}).strict(),
|
|
170
|
+
z.object({
|
|
171
|
+
type: z.literal("bridge.approval_response"),
|
|
172
|
+
protocolVersion: z.literal(REMOTE_PROTOCOL_VERSION),
|
|
173
|
+
deliveryId: z.string().uuid(),
|
|
174
|
+
requestId: z.string().uuid(),
|
|
175
|
+
decision: z.enum(["allow", "deny"]),
|
|
176
|
+
}).strict(),
|
|
177
|
+
z.object({
|
|
178
|
+
type: z.literal("bridge.question_response"),
|
|
179
|
+
protocolVersion: z.literal(REMOTE_PROTOCOL_VERSION),
|
|
180
|
+
deliveryId: z.string().uuid(),
|
|
181
|
+
requestId: z.string().uuid(),
|
|
182
|
+
answers: ResolveRemoteQuestionSchema.shape.answers,
|
|
183
|
+
}).strict(),
|
|
184
|
+
z.object({
|
|
185
|
+
type: z.literal("channel.permission_response"),
|
|
186
|
+
protocolVersion: z.literal(REMOTE_PROTOCOL_VERSION),
|
|
187
|
+
deliveryId: z.string().uuid(),
|
|
188
|
+
requestId: z.string().regex(/^[a-km-z]{5}$/),
|
|
189
|
+
decision: z.enum(["allow", "deny"]),
|
|
190
|
+
}).strict(),
|
|
191
|
+
]);
|
|
192
|
+
export const GatewayClientEventSchema = z.discriminatedUnion("type", [
|
|
193
|
+
z.object({
|
|
194
|
+
type: z.literal("sessions.changed"),
|
|
195
|
+
protocolVersion: z.literal(REMOTE_PROTOCOL_VERSION),
|
|
196
|
+
}).strict(),
|
|
197
|
+
z.object({
|
|
198
|
+
type: z.literal("timeline.appended"),
|
|
199
|
+
protocolVersion: z.literal(REMOTE_PROTOCOL_VERSION),
|
|
200
|
+
runtime: RemoteRuntimeSchema,
|
|
201
|
+
sessionId: z.string().min(1).max(128),
|
|
202
|
+
item: RemoteTimelineItemSchema,
|
|
203
|
+
}).strict(),
|
|
204
|
+
z.object({
|
|
205
|
+
type: z.literal("interaction.resolved"),
|
|
206
|
+
protocolVersion: z.literal(REMOTE_PROTOCOL_VERSION),
|
|
207
|
+
runtime: RemoteRuntimeSchema,
|
|
208
|
+
sessionId: z.string().min(1).max(128),
|
|
209
|
+
requestId: z.string().min(1).max(160),
|
|
210
|
+
}).strict(),
|
|
211
|
+
]);
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { networkInterfaces } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { statSync } from "node:fs";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
import spawn from "cross-spawn";
|
|
8
|
+
import { runClaudeBridge } from "./claude-bridge.js";
|
|
9
|
+
import { loadOrCreateRemoteConfig, releaseRemoteOwnerLock, updateRemoteBinding, } from "./config.js";
|
|
10
|
+
import { startRemoteGateway } from "./gateway.js";
|
|
11
|
+
import { startCodexRuntime } from "./codex-runtime.js";
|
|
12
|
+
import { acquireGatewayOwnership, authenticateRemoteGateway } from "./owner.js";
|
|
13
|
+
import { buildCodexWrappedInvocation } from "./wrapper.js";
|
|
14
|
+
import { probeRuntime, } from "./runtime-probe.js";
|
|
15
|
+
function existingDirectory(path) {
|
|
16
|
+
try {
|
|
17
|
+
return statSync(path).isDirectory() ? path : null;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function bundledUiDir() {
|
|
24
|
+
const embedded = existingDirectory(join(dirname(fileURLToPath(import.meta.url)), "..", "remote-web"));
|
|
25
|
+
if (embedded)
|
|
26
|
+
return embedded;
|
|
27
|
+
try {
|
|
28
|
+
const require = createRequire(import.meta.url);
|
|
29
|
+
return existingDirectory(join(dirname(require.resolve("@nowcrew/remote-web/package.json")), "dist"));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function displayHost(host) {
|
|
36
|
+
if (host !== "0.0.0.0" && host !== "::")
|
|
37
|
+
return host;
|
|
38
|
+
for (const values of Object.values(networkInterfaces())) {
|
|
39
|
+
const address = values?.find((value) => value.family === "IPv4" && !value.internal)?.address;
|
|
40
|
+
if (address)
|
|
41
|
+
return address;
|
|
42
|
+
}
|
|
43
|
+
return "127.0.0.1";
|
|
44
|
+
}
|
|
45
|
+
function baseUrl(config) {
|
|
46
|
+
return `http://${displayHost(config.host)}:${config.port}`;
|
|
47
|
+
}
|
|
48
|
+
function localBaseUrl(config) {
|
|
49
|
+
const host = config.host === "0.0.0.0" || config.host === "::" ? "127.0.0.1" : config.host;
|
|
50
|
+
return `http://${host}:${config.port}`;
|
|
51
|
+
}
|
|
52
|
+
function pairingUrl(config) {
|
|
53
|
+
return `${baseUrl(config)}/#token=${encodeURIComponent(config.token)}`;
|
|
54
|
+
}
|
|
55
|
+
function currentDaemonInvocation() {
|
|
56
|
+
const entry = process.argv[1];
|
|
57
|
+
if (!entry)
|
|
58
|
+
throw new Error("Unable to locate the crew-daemon entrypoint");
|
|
59
|
+
return { command: process.execPath, args: [...process.execArgv, entry] };
|
|
60
|
+
}
|
|
61
|
+
function spawnAndWait(bin, args, env) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const child = spawn(bin, [...args], { env, stdio: "inherit" });
|
|
64
|
+
child.once("error", reject);
|
|
65
|
+
child.once("close", (code, signal) => resolve(code ?? (signal ? 1 : 0)));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function reportDegradedRuntime(result) {
|
|
69
|
+
if (result.state === "supported")
|
|
70
|
+
return;
|
|
71
|
+
process.stderr.write(`NowCrew remote: ${result.runtime} live control ${result.state}: ${result.reason}\n`);
|
|
72
|
+
}
|
|
73
|
+
async function ensureGateway(config) {
|
|
74
|
+
if (await authenticateRemoteGateway(config))
|
|
75
|
+
return;
|
|
76
|
+
const invocation = currentDaemonInvocation();
|
|
77
|
+
const child = spawn(invocation.command, [...invocation.args, "remote", "serve", "--background"], {
|
|
78
|
+
env: process.env,
|
|
79
|
+
detached: true,
|
|
80
|
+
stdio: "ignore",
|
|
81
|
+
});
|
|
82
|
+
child.unref();
|
|
83
|
+
for (let attempt = 0; attempt < 50; attempt += 1) {
|
|
84
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
85
|
+
if (await authenticateRemoteGateway(config))
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`NowCrew remote gateway did not start at ${localBaseUrl(config)}`);
|
|
89
|
+
}
|
|
90
|
+
function usage() {
|
|
91
|
+
return [
|
|
92
|
+
"NowCrew mobile runtime control:",
|
|
93
|
+
" crew-daemon remote setup [--host 127.0.0.1|0.0.0.0] [--port 4317]",
|
|
94
|
+
" crew-daemon remote serve [--host ...] [--port ...] [--ui-dir <path>]",
|
|
95
|
+
" crew-daemon remote status|url",
|
|
96
|
+
" crew-daemon remote run claude -- [claude options]",
|
|
97
|
+
" crew-daemon remote run codex -- [codex options]",
|
|
98
|
+
].join("\n") + "\n";
|
|
99
|
+
}
|
|
100
|
+
async function serveCommand(argv) {
|
|
101
|
+
const parsed = parseArgs({
|
|
102
|
+
args: [...argv],
|
|
103
|
+
strict: true,
|
|
104
|
+
options: {
|
|
105
|
+
host: { type: "string" },
|
|
106
|
+
port: { type: "string" },
|
|
107
|
+
"ui-dir": { type: "string" },
|
|
108
|
+
background: { type: "boolean", default: false },
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
const port = parsed.values.port === undefined ? undefined : Number(parsed.values.port);
|
|
112
|
+
const config = await loadOrCreateRemoteConfig({
|
|
113
|
+
...(parsed.values.host === undefined ? {} : { host: parsed.values.host }),
|
|
114
|
+
...(port === undefined ? {} : { port }),
|
|
115
|
+
});
|
|
116
|
+
const ownership = await acquireGatewayOwnership(config);
|
|
117
|
+
if (ownership.kind === "existing")
|
|
118
|
+
return 0;
|
|
119
|
+
const owner = ownership.lock;
|
|
120
|
+
if (!owner.identityKey)
|
|
121
|
+
throw new Error("New remote gateway owner is missing its identity key");
|
|
122
|
+
let codexRuntime = null;
|
|
123
|
+
try {
|
|
124
|
+
const [claudeProbe, codexProbe] = await Promise.all([
|
|
125
|
+
probeRuntime("claude"),
|
|
126
|
+
probeRuntime("codex"),
|
|
127
|
+
]);
|
|
128
|
+
reportDegradedRuntime(claudeProbe);
|
|
129
|
+
reportDegradedRuntime(codexProbe);
|
|
130
|
+
const runtimeStates = {
|
|
131
|
+
...(claudeProbe.state === "supported" ? {} : { claude: claudeProbe.state }),
|
|
132
|
+
...(codexProbe.state === "supported" ? {} : { codex: codexProbe.state }),
|
|
133
|
+
};
|
|
134
|
+
if (codexProbe.state === "supported") {
|
|
135
|
+
codexRuntime = await startCodexRuntime(config.paths.codexSocket).catch((error) => {
|
|
136
|
+
runtimeStates.codex = "offline";
|
|
137
|
+
process.stderr.write(`NowCrew remote: Codex live control offline: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
138
|
+
return null;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
let gateway = null;
|
|
142
|
+
try {
|
|
143
|
+
gateway = await startRemoteGateway({
|
|
144
|
+
config,
|
|
145
|
+
gatewayId: owner.gatewayId,
|
|
146
|
+
identityKey: owner.identityKey,
|
|
147
|
+
...(codexRuntime === null ? {} : { codex: codexRuntime.adapter }),
|
|
148
|
+
runtimeStates,
|
|
149
|
+
uiDir: parsed.values["ui-dir"] ?? process.env.NOWCREW_REMOTE_UI_DIR ?? bundledUiDir(),
|
|
150
|
+
});
|
|
151
|
+
if (!parsed.values.background)
|
|
152
|
+
process.stdout.write(`NowCrew remote listening at ${baseUrl(config)}\n`);
|
|
153
|
+
await new Promise((resolve) => {
|
|
154
|
+
process.once("SIGINT", resolve);
|
|
155
|
+
process.once("SIGTERM", resolve);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
await gateway?.close();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
try {
|
|
164
|
+
await codexRuntime?.close();
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
await releaseRemoteOwnerLock(config.paths.lock, owner.gatewayId);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
async function setupCommand(argv) {
|
|
173
|
+
const parsed = parseArgs({
|
|
174
|
+
args: [...argv],
|
|
175
|
+
strict: true,
|
|
176
|
+
options: {
|
|
177
|
+
host: { type: "string" },
|
|
178
|
+
port: { type: "string" },
|
|
179
|
+
help: { type: "boolean", short: "h", default: false },
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
if (parsed.values.help) {
|
|
183
|
+
process.stdout.write(usage());
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
let config = await loadOrCreateRemoteConfig();
|
|
187
|
+
if (parsed.values.host !== undefined || parsed.values.port !== undefined) {
|
|
188
|
+
config = await updateRemoteBinding(config, {
|
|
189
|
+
host: parsed.values.host ?? config.host,
|
|
190
|
+
port: parsed.values.port === undefined ? config.port : Number(parsed.values.port),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
process.stdout.write([
|
|
194
|
+
`Phone URL: ${pairingUrl(config)}`,
|
|
195
|
+
"",
|
|
196
|
+
"Add these shell functions to your zsh configuration:",
|
|
197
|
+
"claude() { crew-daemon remote run claude -- \"$@\"; }",
|
|
198
|
+
"codex() { crew-daemon remote run codex -- \"$@\"; }",
|
|
199
|
+
"",
|
|
200
|
+
config.host === "127.0.0.1"
|
|
201
|
+
? "Loopback mode is enabled. Use --host 0.0.0.0 for a trusted LAN, or expose this URL through private HTTPS."
|
|
202
|
+
: "LAN mode is enabled. Keep the capability URL private and do not expose this HTTP port to the public internet.",
|
|
203
|
+
].join("\n") + "\n");
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
export async function runRemoteCommand(argv) {
|
|
207
|
+
if (argv[0] !== "remote")
|
|
208
|
+
return null;
|
|
209
|
+
const action = argv[1];
|
|
210
|
+
if (!action || action === "help" || action === "--help" || action === "-h") {
|
|
211
|
+
process.stdout.write(usage());
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
if (action === "serve")
|
|
215
|
+
return serveCommand(argv.slice(2));
|
|
216
|
+
if (action === "setup")
|
|
217
|
+
return setupCommand(argv.slice(2));
|
|
218
|
+
const config = await loadOrCreateRemoteConfig();
|
|
219
|
+
if (action === "status") {
|
|
220
|
+
const current = await authenticateRemoteGateway(config);
|
|
221
|
+
process.stdout.write(`${JSON.stringify({
|
|
222
|
+
running: current !== null,
|
|
223
|
+
url: baseUrl(config),
|
|
224
|
+
...(current ?? {}),
|
|
225
|
+
}, null, 2)}\n`);
|
|
226
|
+
return current ? 0 : 3;
|
|
227
|
+
}
|
|
228
|
+
if (action === "url") {
|
|
229
|
+
process.stdout.write(`${pairingUrl(config)}\n`);
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
if (action !== "run" || !["claude", "codex"].includes(argv[2] ?? "")) {
|
|
233
|
+
process.stderr.write(usage());
|
|
234
|
+
return 2;
|
|
235
|
+
}
|
|
236
|
+
const runtime = argv[2];
|
|
237
|
+
const probe = await probeRuntime(runtime);
|
|
238
|
+
if (probe.state !== "supported") {
|
|
239
|
+
reportDegradedRuntime(probe);
|
|
240
|
+
return 4;
|
|
241
|
+
}
|
|
242
|
+
await ensureGateway(config);
|
|
243
|
+
const separator = argv.indexOf("--", 3);
|
|
244
|
+
const runtimeArgs = argv.slice(separator >= 0 ? separator + 1 : 3);
|
|
245
|
+
if (runtime === "claude") {
|
|
246
|
+
return runClaudeBridge(runtimeArgs, {
|
|
247
|
+
gatewayUrl: localBaseUrl(config),
|
|
248
|
+
token: config.token,
|
|
249
|
+
cwd: process.cwd(),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
const invocation = buildCodexWrappedInvocation(runtimeArgs, config.paths.codexSocket, process.env.NOWCREW_CODEX_BIN ?? "codex");
|
|
253
|
+
return spawnAndWait(invocation.bin, invocation.args, invocation.env);
|
|
254
|
+
}
|