relay-companion 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/bin/relay.js +262 -0
- package/overlay/inbox.html +1398 -0
- package/overlay/main.cjs +762 -0
- package/overlay/preload.cjs +37 -0
- package/overlay/sounds/tink.wav +0 -0
- package/package.json +25 -0
- package/src/claude-materializer.js +85 -0
- package/src/claude-session-writer.js +629 -0
- package/src/client.js +168 -0
- package/src/codex-app-server.js +120 -0
- package/src/codex-desktop.js +276 -0
- package/src/codex-session-writer.js +170 -0
- package/src/codex-state.js +114 -0
- package/src/config.js +62 -0
- package/src/host-json.js +14 -0
- package/src/host-paths.js +67 -0
- package/src/install.js +142 -0
- package/src/materializer.js +378 -0
- package/src/mcp.js +419 -0
- package/src/notifications.js +412 -0
- package/src/pinning.js +43 -0
- package/src/relay-briefing.js +344 -0
- package/src/runtime.js +1141 -0
- package/src/task-daemon.js +216 -0
package/bin/relay.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { spawnSync, spawn } from "node:child_process";
|
|
7
|
+
import readline from "node:readline/promises";
|
|
8
|
+
import { stdin, stdout } from "node:process";
|
|
9
|
+
import { RelayClient } from "../src/client.js";
|
|
10
|
+
import { writeConfig, readConfig, apiUrl, webUrl } from "../src/config.js";
|
|
11
|
+
import { runTaskDaemon, pollTaskRuntimeOnce } from "../src/task-daemon.js";
|
|
12
|
+
import { runMcpServer } from "../src/mcp.js";
|
|
13
|
+
import { runSetupInstall, runUninstall } from "../src/install.js";
|
|
14
|
+
import { openRelay, openTask } from "../src/materializer.js";
|
|
15
|
+
|
|
16
|
+
function parseFlags(argv) {
|
|
17
|
+
const flags = {};
|
|
18
|
+
const positional = [];
|
|
19
|
+
for (let i = 0; i < argv.length; i++) {
|
|
20
|
+
const a = argv[i];
|
|
21
|
+
if (a.startsWith("--")) {
|
|
22
|
+
const key = a.slice(2);
|
|
23
|
+
const next = argv[i + 1];
|
|
24
|
+
if (next === undefined || next.startsWith("--")) flags[key] = true;
|
|
25
|
+
else {
|
|
26
|
+
flags[key] = next;
|
|
27
|
+
i++;
|
|
28
|
+
}
|
|
29
|
+
} else positional.push(a);
|
|
30
|
+
}
|
|
31
|
+
return { flags, positional };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function prompt(question, fallback = "") {
|
|
35
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
36
|
+
const answer = (await rl.question(question)).trim();
|
|
37
|
+
rl.close();
|
|
38
|
+
return answer || fallback;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function cmdPair(flags) {
|
|
42
|
+
const url = flags.api || (await prompt(`Relay API URL [${apiUrl()}]: `, apiUrl()));
|
|
43
|
+
const appUrl = flags.web || webUrl();
|
|
44
|
+
let code = flags.code;
|
|
45
|
+
if (!code) code = await prompt("Pairing code (from your Relay app): ");
|
|
46
|
+
const name = flags.name || (await prompt(`Device name [${os.hostname()}]: `, os.hostname()));
|
|
47
|
+
writeConfig({ apiUrl: url, webUrl: appUrl });
|
|
48
|
+
const client = new RelayClient({ url });
|
|
49
|
+
const res = await client.registerDevice({ pairingCode: code, name, platform: process.platform });
|
|
50
|
+
writeConfig({ apiUrl: url, webUrl: appUrl, deviceToken: res.deviceToken, deviceId: res.deviceId, user: res.user });
|
|
51
|
+
console.log(`Paired as ${res.user.name} <${res.user.email}>. This device is now connected to Relay.`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Register the Relay tools into Claude Code + Codex and start the receive daemon. */
|
|
55
|
+
function applyInstall() {
|
|
56
|
+
const { installed, missing, daemon } = runSetupInstall();
|
|
57
|
+
if (installed.length) console.log(`Added Relay to ${installed.join(" and ")} on this machine.`);
|
|
58
|
+
for (const m of missing) {
|
|
59
|
+
if (!/registration failed/.test(m)) console.log(`${m} was not found here, so it was skipped.`);
|
|
60
|
+
else console.log(`Could not register ${m}.`);
|
|
61
|
+
}
|
|
62
|
+
if (daemon.ok) console.log("Relay is running in the background and will start automatically when you log in.");
|
|
63
|
+
else if (daemon.reason === "autostart_unsupported_platform") {
|
|
64
|
+
console.log("Run `relay daemon` to receive relays (background autostart is macOS-only for now).");
|
|
65
|
+
}
|
|
66
|
+
if (installed.length) console.log("Open Claude Code or Codex and your Relay tools are ready.");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Full setup: pair this device, then install the tools + daemon. */
|
|
70
|
+
async function cmdSetup(flags) {
|
|
71
|
+
await cmdPair(flags);
|
|
72
|
+
console.log("");
|
|
73
|
+
applyInstall();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Install the tools + daemon on a device that is already paired. */
|
|
77
|
+
async function cmdInstall() {
|
|
78
|
+
if (!readConfig().deviceToken) {
|
|
79
|
+
throw new Error("This device is not paired yet. Run `relay setup` with a pairing code first.");
|
|
80
|
+
}
|
|
81
|
+
applyInstall();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Remove the Relay tools from both agents and stop the background daemon. */
|
|
85
|
+
async function cmdUninstall() {
|
|
86
|
+
runUninstall();
|
|
87
|
+
console.log("Removed Relay from Claude Code and Codex, and stopped the background daemon.");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function cmdWhoami() {
|
|
91
|
+
const cfg = readConfig();
|
|
92
|
+
if (!cfg.deviceToken) {
|
|
93
|
+
console.log("Not paired. Run: relay pair");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const client = new RelayClient();
|
|
97
|
+
const me = await client.me();
|
|
98
|
+
console.log(`${me.user.name} <${me.user.email}> (api: ${apiUrl()})`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function cmdTasksOnce() {
|
|
102
|
+
const out = await pollTaskRuntimeOnce({ log: (m) => console.log(`[relay] ${m}`) });
|
|
103
|
+
console.log(
|
|
104
|
+
`Processed ${out.sessions.length} task session(s), ${out.messages.length} message(s), ${out.notifications.length} Relay companion item(s).`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function absoluteWebTarget(pathOrUrl = "/app/tasks") {
|
|
109
|
+
if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
|
|
110
|
+
const path = pathOrUrl.startsWith("/") ? pathOrUrl : `/${pathOrUrl}`;
|
|
111
|
+
return `${webUrl()}${path}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function openUrl(url) {
|
|
115
|
+
const command = process.platform === "darwin"
|
|
116
|
+
? "open"
|
|
117
|
+
: process.platform === "win32"
|
|
118
|
+
? "cmd"
|
|
119
|
+
: "xdg-open";
|
|
120
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
121
|
+
const result = spawnSync(command, args, { stdio: "ignore" });
|
|
122
|
+
if (result.status !== 0) {
|
|
123
|
+
console.log(url);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// `relay open <id> --host <host>` — materialize the staged Relay row into a real
|
|
130
|
+
// native agent session inside the already-running Claude Desktop or Codex, then
|
|
131
|
+
// print { url, skipExternalOpen, openedInHost } so the overlay can fall back to
|
|
132
|
+
// shell.openExternal(url) when the host wasn't foregrounded directly.
|
|
133
|
+
//
|
|
134
|
+
// `relay open --task <taskId> --host <host>` — same materialization, but seeded
|
|
135
|
+
// from a Relay task (active or completed): the session opens with the task's
|
|
136
|
+
// objective + state and an instruction to call relay_task_status(taskId) for live
|
|
137
|
+
// detail. Prints the identical JSON contract.
|
|
138
|
+
async function cmdOpen(positional, flags) {
|
|
139
|
+
const log = (m) => process.stderr.write(`[relay] ${m}\n`);
|
|
140
|
+
const host = String(flags.host || "claude").toLowerCase();
|
|
141
|
+
if (flags.task) {
|
|
142
|
+
const result = await openTask({ taskId: String(flags.task), host, log });
|
|
143
|
+
console.log(JSON.stringify(result, null, 2));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const id = positional[0] || flags.id;
|
|
147
|
+
if (!id) throw new Error("Usage: relay open <id> --host claude|codex (or: relay open --task <taskId> --host claude|codex)");
|
|
148
|
+
const result = await openRelay({ id, host, log });
|
|
149
|
+
console.log(JSON.stringify(result, null, 2));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function cmdOpenNotification(flags) {
|
|
153
|
+
let target = flags.url;
|
|
154
|
+
if (!target && flags.task) target = `/app/tasks/${encodeURIComponent(flags.task)}`;
|
|
155
|
+
if (!target && flags.message) target = "/app/relays";
|
|
156
|
+
target ||= "/app/tasks";
|
|
157
|
+
const url = absoluteWebTarget(target);
|
|
158
|
+
const opened = openUrl(url);
|
|
159
|
+
console.log(opened ? `Opened ${url}` : `Open this Relay URL: ${url}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function cmdAnswerQuestion(flags) {
|
|
163
|
+
const taskId = flags.task || flags.taskId;
|
|
164
|
+
const messageId = flags.message || flags.messageId;
|
|
165
|
+
let answer = flags.answer;
|
|
166
|
+
if (!taskId || !messageId) {
|
|
167
|
+
throw new Error("Usage: relay answer-question --task TASK_ID --message MESSAGE_ID --answer ANSWER");
|
|
168
|
+
}
|
|
169
|
+
if (!answer) answer = await prompt("Answer: ");
|
|
170
|
+
const client = new RelayClient();
|
|
171
|
+
await client.answerHumanQuestion(taskId, messageId, {
|
|
172
|
+
answerMarkdown: answer,
|
|
173
|
+
idempotencyKey: `human_answer_${Date.now()}`,
|
|
174
|
+
});
|
|
175
|
+
console.log("Sent answer to Relay. The waiting task agent will resume on the next daemon poll.");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Launch the desktop Relay companion pill (Electron overlay). */
|
|
179
|
+
function cmdPill() {
|
|
180
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
181
|
+
const overlayMain = path.resolve(here, "../overlay/main.cjs");
|
|
182
|
+
// require("electron") returns the path to the electron binary when resolved under node.
|
|
183
|
+
let electronPath;
|
|
184
|
+
try {
|
|
185
|
+
const require = createRequire(import.meta.url);
|
|
186
|
+
electronPath = require("electron");
|
|
187
|
+
} catch (error) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
"Electron is not installed. Run `npm install` in packages/companion first. (" +
|
|
190
|
+
(error && error.message ? error.message : error) +
|
|
191
|
+
")",
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
if (typeof electronPath !== "string") {
|
|
195
|
+
throw new Error("Could not resolve the Electron binary. Run `npm install` in packages/companion.");
|
|
196
|
+
}
|
|
197
|
+
const child = spawn(electronPath, [overlayMain], {
|
|
198
|
+
detached: true,
|
|
199
|
+
stdio: "ignore",
|
|
200
|
+
env: { ...process.env },
|
|
201
|
+
});
|
|
202
|
+
child.unref();
|
|
203
|
+
console.log("Relay pill launched. It shows when Claude Code or Codex is running.");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function main() {
|
|
207
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
208
|
+
const { flags, positional } = parseFlags(rest);
|
|
209
|
+
switch (command) {
|
|
210
|
+
case "setup":
|
|
211
|
+
return cmdSetup(flags);
|
|
212
|
+
case "pair":
|
|
213
|
+
return cmdPair(flags);
|
|
214
|
+
case "install":
|
|
215
|
+
return cmdInstall();
|
|
216
|
+
case "uninstall":
|
|
217
|
+
return cmdUninstall();
|
|
218
|
+
case "whoami":
|
|
219
|
+
return cmdWhoami();
|
|
220
|
+
case "tasks":
|
|
221
|
+
return cmdTasksOnce();
|
|
222
|
+
case "open":
|
|
223
|
+
return cmdOpen(positional, flags);
|
|
224
|
+
case "open-notification":
|
|
225
|
+
return cmdOpenNotification(flags);
|
|
226
|
+
case "answer-question":
|
|
227
|
+
case "answer":
|
|
228
|
+
return cmdAnswerQuestion(flags);
|
|
229
|
+
case "daemon":
|
|
230
|
+
return runTaskDaemon({ intervalMs: Number(flags.interval) || 4000 });
|
|
231
|
+
case "pill":
|
|
232
|
+
return cmdPill();
|
|
233
|
+
case "mcp":
|
|
234
|
+
return runMcpServer();
|
|
235
|
+
default:
|
|
236
|
+
console.log(
|
|
237
|
+
[
|
|
238
|
+
"Relay companion",
|
|
239
|
+
"",
|
|
240
|
+
"Usage:",
|
|
241
|
+
" relay setup [--code CODE] [--name NAME] Pair this machine and add Relay to Claude Code + Codex",
|
|
242
|
+
" relay install Add Relay to your agents (device already paired)",
|
|
243
|
+
" relay uninstall Remove Relay from your agents and stop the daemon",
|
|
244
|
+
" relay pair [--api URL] [--web URL] [--code CODE] Pair this machine only (no agent install)",
|
|
245
|
+
" relay tasks Pull and process task agent runtime once",
|
|
246
|
+
" relay open <id> --host claude|codex Materialize a staged Relay row into a native agent session",
|
|
247
|
+
" relay open --task <taskId> --host claude|codex Materialize a Relay task into a native agent session",
|
|
248
|
+
" relay open-notification [--url PATH] [--task ID] Open a Relay companion target in the browser",
|
|
249
|
+
" relay answer-question --task ID --message ID --answer TEXT",
|
|
250
|
+
" relay daemon [--interval MS] Run the task runtime daemon",
|
|
251
|
+
" relay pill Launch the desktop Relay companion pill",
|
|
252
|
+
" relay mcp Run the MCP server (for your agent)",
|
|
253
|
+
" relay whoami Show the paired account",
|
|
254
|
+
].join("\n"),
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
main().catch((err) => {
|
|
260
|
+
console.error(err.message || err);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
});
|