relay-companion 0.1.0 → 0.1.2
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 +21 -8
- package/overlay/inbox.html +18 -1
- package/overlay/main.cjs +54 -9
- package/package.json +1 -1
- package/src/client.js +28 -0
- package/src/config.js +5 -2
- package/src/install.js +79 -6
- package/src/materializer.js +43 -1
- package/src/mcp.js +80 -0
- package/src/notifications.js +107 -1
- package/src/task-daemon.js +72 -8
package/bin/relay.js
CHANGED
|
@@ -7,11 +7,12 @@ import { spawnSync, spawn } from "node:child_process";
|
|
|
7
7
|
import readline from "node:readline/promises";
|
|
8
8
|
import { stdin, stdout } from "node:process";
|
|
9
9
|
import { RelayClient } from "../src/client.js";
|
|
10
|
-
import { writeConfig, readConfig, apiUrl,
|
|
10
|
+
import { writeConfig, readConfig, apiUrl, DEFAULT_API_URL, DEFAULT_WEB_URL } from "../src/config.js";
|
|
11
11
|
import { runTaskDaemon, pollTaskRuntimeOnce } from "../src/task-daemon.js";
|
|
12
12
|
import { runMcpServer } from "../src/mcp.js";
|
|
13
13
|
import { runSetupInstall, runUninstall } from "../src/install.js";
|
|
14
14
|
import { openRelay, openTask } from "../src/materializer.js";
|
|
15
|
+
import { resetCompanionStateForAccount } from "../src/notifications.js";
|
|
15
16
|
|
|
16
17
|
function parseFlags(argv) {
|
|
17
18
|
const flags = {};
|
|
@@ -38,22 +39,29 @@ async function prompt(question, fallback = "") {
|
|
|
38
39
|
return answer || fallback;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
async function cmdPair(flags) {
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
async function cmdPair(flags, { promptForDefaults = true } = {}) {
|
|
43
|
+
let url = flags.api || process.env.RELAY_API_URL;
|
|
44
|
+
if (!url) {
|
|
45
|
+
url = promptForDefaults ? await prompt(`Relay API URL [${apiUrl()}]: `, apiUrl()) : DEFAULT_API_URL;
|
|
46
|
+
}
|
|
47
|
+
const appUrl = flags.web || process.env.RELAY_WEB_URL || DEFAULT_WEB_URL;
|
|
44
48
|
let code = flags.code;
|
|
45
49
|
if (!code) code = await prompt("Pairing code (from your Relay app): ");
|
|
46
|
-
|
|
50
|
+
let name = flags.name;
|
|
51
|
+
if (!name) {
|
|
52
|
+
name = promptForDefaults ? await prompt(`Device name [${os.hostname()}]: `, os.hostname()) : os.hostname();
|
|
53
|
+
}
|
|
47
54
|
writeConfig({ apiUrl: url, webUrl: appUrl });
|
|
48
55
|
const client = new RelayClient({ url });
|
|
49
56
|
const res = await client.registerDevice({ pairingCode: code, name, platform: process.platform });
|
|
50
57
|
writeConfig({ apiUrl: url, webUrl: appUrl, deviceToken: res.deviceToken, deviceId: res.deviceId, user: res.user });
|
|
58
|
+
resetCompanionStateForAccount({ user: res.user, deviceId: res.deviceId });
|
|
51
59
|
console.log(`Paired as ${res.user.name} <${res.user.email}>. This device is now connected to Relay.`);
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
/** Register the Relay tools into Claude Code + Codex and start the receive daemon. */
|
|
55
63
|
function applyInstall() {
|
|
56
|
-
const { installed, missing, daemon } = runSetupInstall();
|
|
64
|
+
const { installed, missing, daemon, pill } = runSetupInstall();
|
|
57
65
|
if (installed.length) console.log(`Added Relay to ${installed.join(" and ")} on this machine.`);
|
|
58
66
|
for (const m of missing) {
|
|
59
67
|
if (!/registration failed/.test(m)) console.log(`${m} was not found here, so it was skipped.`);
|
|
@@ -63,12 +71,16 @@ function applyInstall() {
|
|
|
63
71
|
else if (daemon.reason === "autostart_unsupported_platform") {
|
|
64
72
|
console.log("Run `relay daemon` to receive relays (background autostart is macOS-only for now).");
|
|
65
73
|
}
|
|
74
|
+
if (pill?.ok) console.log("The Relay pill is open and will start automatically when you log in.");
|
|
75
|
+
else if (pill?.reason === "pill_runtime_missing") {
|
|
76
|
+
console.log("Run `relay pill` to open the Relay pill.");
|
|
77
|
+
}
|
|
66
78
|
if (installed.length) console.log("Open Claude Code or Codex and your Relay tools are ready.");
|
|
67
79
|
}
|
|
68
80
|
|
|
69
81
|
/** Full setup: pair this device, then install the tools + daemon. */
|
|
70
82
|
async function cmdSetup(flags) {
|
|
71
|
-
await cmdPair(flags);
|
|
83
|
+
await cmdPair(flags, { promptForDefaults: Boolean(flags.interactive) });
|
|
72
84
|
console.log("");
|
|
73
85
|
applyInstall();
|
|
74
86
|
}
|
|
@@ -137,7 +149,7 @@ function openUrl(url) {
|
|
|
137
149
|
// detail. Prints the identical JSON contract.
|
|
138
150
|
async function cmdOpen(positional, flags) {
|
|
139
151
|
const log = (m) => process.stderr.write(`[relay] ${m}\n`);
|
|
140
|
-
const host = String(flags.host || "claude").toLowerCase();
|
|
152
|
+
const host = String(flags.host || flags.in || "claude").toLowerCase();
|
|
141
153
|
if (flags.task) {
|
|
142
154
|
const result = await openTask({ taskId: String(flags.task), host, log });
|
|
143
155
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -239,6 +251,7 @@ async function main() {
|
|
|
239
251
|
"",
|
|
240
252
|
"Usage:",
|
|
241
253
|
" relay setup [--code CODE] [--name NAME] Pair this machine and add Relay to Claude Code + Codex",
|
|
254
|
+
" relay setup --interactive Prompt for API URL and device name during setup",
|
|
242
255
|
" relay install Add Relay to your agents (device already paired)",
|
|
243
256
|
" relay uninstall Remove Relay from your agents and stop the daemon",
|
|
244
257
|
" relay pair [--api URL] [--web URL] [--code CODE] Pair this machine only (no agent install)",
|
package/overlay/inbox.html
CHANGED
|
@@ -66,6 +66,21 @@
|
|
|
66
66
|
.card.has-unread .sq0 { fill:var(--accent); }
|
|
67
67
|
.word { font-family:var(--serif); font-size:19px; font-weight:500; letter-spacing:-.01em; color:var(--ink); line-height:1; }
|
|
68
68
|
.spacer { flex:1 1 auto; }
|
|
69
|
+
.minimize-hint {
|
|
70
|
+
margin-right:10px;
|
|
71
|
+
font-size:11px;
|
|
72
|
+
font-weight:500;
|
|
73
|
+
color:var(--muted-3);
|
|
74
|
+
line-height:1;
|
|
75
|
+
transition:color .18s var(--settle), opacity .18s var(--settle), width .18s var(--settle), margin .18s var(--settle);
|
|
76
|
+
}
|
|
77
|
+
.lockup:hover .minimize-hint { color:var(--muted-2); }
|
|
78
|
+
.card.collapsed .minimize-hint {
|
|
79
|
+
width:0;
|
|
80
|
+
margin-right:0;
|
|
81
|
+
overflow:hidden;
|
|
82
|
+
opacity:0;
|
|
83
|
+
}
|
|
69
84
|
.count {
|
|
70
85
|
font-family:var(--mono); font-size:13px; font-weight:500; font-variant-numeric:tabular-nums;
|
|
71
86
|
color:var(--accent); line-height:1; transition:color .3s var(--settle);
|
|
@@ -381,6 +396,7 @@
|
|
|
381
396
|
</svg>
|
|
382
397
|
<span class="word">relay</span>
|
|
383
398
|
<span class="spacer"></span>
|
|
399
|
+
<span class="minimize-hint">Minimize</span>
|
|
384
400
|
<span class="count zero" id="count">0</span>
|
|
385
401
|
</div>
|
|
386
402
|
|
|
@@ -700,6 +716,7 @@
|
|
|
700
716
|
task_cancelled: "Task cancelled",
|
|
701
717
|
task_failed: "Task failed",
|
|
702
718
|
connector_reauth: "Reconnect needed",
|
|
719
|
+
plain_relay: "Relay",
|
|
703
720
|
};
|
|
704
721
|
function relayKindLabel(kind) { return RELAY_KIND_LABEL[kind] || "Relay"; }
|
|
705
722
|
// The clean split: Relays shows ONLY the urgent quick-reply (human_question) and
|
|
@@ -721,7 +738,7 @@
|
|
|
721
738
|
// single-line subjects. Source: HumanResponse.question (protocol.ts), the real question.
|
|
722
739
|
return firstLine(body, 240) || title || relayKindLabel(kind);
|
|
723
740
|
}
|
|
724
|
-
if (kind === "task_completed" || kind === "result") {
|
|
741
|
+
if (kind === "plain_relay" || kind === "task_completed" || kind === "result") {
|
|
725
742
|
return title || firstLine(body, 90) || relayKindLabel(kind);
|
|
726
743
|
}
|
|
727
744
|
// plain message / notice: subject is the first meaningful body line
|
package/overlay/main.cjs
CHANGED
|
@@ -69,6 +69,7 @@ async function relayClient() {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
app.setName("Relay");
|
|
72
|
+
const gotSingleInstanceLock = app.requestSingleInstanceLock();
|
|
72
73
|
|
|
73
74
|
// ---- config / account ----------------------------------------------------
|
|
74
75
|
|
|
@@ -121,7 +122,35 @@ function taskWebUrl(taskId) {
|
|
|
121
122
|
|
|
122
123
|
function readStore() {
|
|
123
124
|
try {
|
|
124
|
-
|
|
125
|
+
const store = JSON.parse(fs.readFileSync(STATE_PATH, "utf8")) || {};
|
|
126
|
+
const cfg = readConfigFile();
|
|
127
|
+
const current = {
|
|
128
|
+
userId: (cfg.user && cfg.user.id) || "",
|
|
129
|
+
email: (cfg.user && cfg.user.email) || "",
|
|
130
|
+
deviceId: cfg.deviceId || "",
|
|
131
|
+
};
|
|
132
|
+
if (current.userId || current.email) {
|
|
133
|
+
const account = store.account || {};
|
|
134
|
+
const missingAccount = !account.userId && !account.email;
|
|
135
|
+
const wrongUserId = account.userId && current.userId && account.userId !== current.userId;
|
|
136
|
+
const wrongEmail = account.email && current.email && account.email !== current.email;
|
|
137
|
+
if (missingAccount || wrongUserId || wrongEmail) {
|
|
138
|
+
const next = {
|
|
139
|
+
version: 1,
|
|
140
|
+
account: { ...current, resetAt: new Date().toISOString() },
|
|
141
|
+
profile: { name: "", handle: "", email: "", inboxDir: "", contactCardRoots: [], transport: { type: "relay_api" } },
|
|
142
|
+
contacts: [],
|
|
143
|
+
packets: {},
|
|
144
|
+
meetingNotes: {},
|
|
145
|
+
setup: {},
|
|
146
|
+
emailThreads: {},
|
|
147
|
+
chats: {},
|
|
148
|
+
};
|
|
149
|
+
writeStateAtomic(next);
|
|
150
|
+
return next;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return store;
|
|
125
154
|
} catch {
|
|
126
155
|
return {};
|
|
127
156
|
}
|
|
@@ -294,12 +323,18 @@ function ackPacket(packetId) {
|
|
|
294
323
|
if (!packetId) return;
|
|
295
324
|
const store = readStore();
|
|
296
325
|
if (!store.packets || !store.packets[packetId]) return;
|
|
326
|
+
const row = store.packets[packetId];
|
|
297
327
|
store.packets[packetId] = {
|
|
298
|
-
...
|
|
328
|
+
...row,
|
|
299
329
|
state: "read",
|
|
300
330
|
updatedAt: new Date().toISOString(),
|
|
301
331
|
};
|
|
302
332
|
writeStateAtomic(store);
|
|
333
|
+
if (row.relayNotificationKind === "plain_relay") {
|
|
334
|
+
relayClient()
|
|
335
|
+
.then((client) => client.markRead(packetId, { idempotencyKey: idempotencyKey("read") }))
|
|
336
|
+
.catch((error) => console.error("[overlay] relay mark-read failed:", error && error.message));
|
|
337
|
+
}
|
|
303
338
|
pushInbox(true).catch(() => {});
|
|
304
339
|
}
|
|
305
340
|
|
|
@@ -752,11 +787,21 @@ ipcMain.handle("relay:soundBytes", (_e, name) => {
|
|
|
752
787
|
return null; // gracefully null when no sound is available
|
|
753
788
|
});
|
|
754
789
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
790
|
+
if (!gotSingleInstanceLock) {
|
|
791
|
+
app.quit();
|
|
792
|
+
} else {
|
|
793
|
+
app.on("second-instance", () => {
|
|
794
|
+
if (!win || win.isDestroyed()) return;
|
|
795
|
+
pollHosts();
|
|
796
|
+
maybeShow();
|
|
797
|
+
});
|
|
759
798
|
|
|
760
|
-
app.
|
|
761
|
-
|
|
762
|
-
|
|
799
|
+
app.whenReady().then(() => {
|
|
800
|
+
if (process.platform === "darwin" && app.dock) app.dock.hide(); // accessory: no dock icon, no NC banners
|
|
801
|
+
createWindow();
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
app.on("window-all-closed", () => {
|
|
805
|
+
/* keep running as a background agent; relaunched at login by launchd */
|
|
806
|
+
});
|
|
807
|
+
}
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -46,6 +46,34 @@ export class RelayClient {
|
|
|
46
46
|
return this.#req("GET", "/v1/task-relays");
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
sendRelay(payload) {
|
|
50
|
+
return this.#req("POST", "/v1/relays", payload);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
inbox() {
|
|
54
|
+
return this.#req("GET", "/v1/inbox");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fetchRelay(id) {
|
|
58
|
+
return this.#req("GET", `/v1/relays/${encodeURIComponent(id)}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
acknowledge(id, payload = {}) {
|
|
62
|
+
return this.#req("POST", `/v1/relays/${encodeURIComponent(id)}/ack`, payload);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
markRead(id, payload = {}) {
|
|
66
|
+
return this.#req("POST", `/v1/relays/${encodeURIComponent(id)}/read`, payload);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
openRelay(token) {
|
|
70
|
+
return this.#req("GET", `/v1/open/${encodeURIComponent(token)}`, undefined, { auth: false });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
openRelayPacket(token) {
|
|
74
|
+
return this.#req("GET", `/v1/open/${encodeURIComponent(token)}/packet`, undefined, { auth: false });
|
|
75
|
+
}
|
|
76
|
+
|
|
49
77
|
createFileUpload(payload) {
|
|
50
78
|
return this.#req("POST", "/v1/files", payload);
|
|
51
79
|
}
|
package/src/config.js
CHANGED
|
@@ -2,6 +2,9 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
+
export const DEFAULT_API_URL = "https://aia6vj5pgp.us-east-1.awsapprunner.com";
|
|
6
|
+
export const DEFAULT_WEB_URL = "https://sendrelays.com";
|
|
7
|
+
|
|
5
8
|
/**
|
|
6
9
|
* Companion configuration lives in ~/.relay/config.json. It holds the device
|
|
7
10
|
* token issued at pairing and the API base URL. Environment variables override
|
|
@@ -36,7 +39,7 @@ export function apiUrl() {
|
|
|
36
39
|
return (
|
|
37
40
|
process.env.RELAY_API_URL ||
|
|
38
41
|
readConfig().apiUrl ||
|
|
39
|
-
|
|
42
|
+
DEFAULT_API_URL
|
|
40
43
|
);
|
|
41
44
|
}
|
|
42
45
|
|
|
@@ -44,7 +47,7 @@ export function webUrl() {
|
|
|
44
47
|
return (
|
|
45
48
|
process.env.RELAY_WEB_URL ||
|
|
46
49
|
readConfig().webUrl ||
|
|
47
|
-
|
|
50
|
+
DEFAULT_WEB_URL
|
|
48
51
|
).replace(/\/$/, "");
|
|
49
52
|
}
|
|
50
53
|
|
package/src/install.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
|
|
7
8
|
// Persistent install of the Relay companion into the user's local agents.
|
|
@@ -12,6 +13,7 @@ import { fileURLToPath } from "node:url";
|
|
|
12
13
|
// It also installs a launchd agent that keeps the receive daemon running.
|
|
13
14
|
|
|
14
15
|
const DAEMON_LAUNCH_LABEL = "work.relay.companion";
|
|
16
|
+
const PILL_LAUNCH_LABEL = "work.relay.companion.pill";
|
|
15
17
|
|
|
16
18
|
export const PACKAGE_NAME = "relay-companion";
|
|
17
19
|
|
|
@@ -20,6 +22,17 @@ export function relayBinPath() {
|
|
|
20
22
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../bin/relay.js");
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
function packageRootForBin(bin = relayBinPath()) {
|
|
26
|
+
return path.resolve(path.dirname(bin), "..");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function plistEscape(value) {
|
|
30
|
+
return String(value)
|
|
31
|
+
.replaceAll("&", "&")
|
|
32
|
+
.replaceAll("<", "<")
|
|
33
|
+
.replaceAll(">", ">");
|
|
34
|
+
}
|
|
35
|
+
|
|
23
36
|
/**
|
|
24
37
|
* A STABLE path to the companion CLI that the agents and the daemon can keep
|
|
25
38
|
* launching. When `setup` runs from npx (an ephemeral cache that gets cleaned),
|
|
@@ -49,6 +62,16 @@ function run(cmd, args) {
|
|
|
49
62
|
};
|
|
50
63
|
}
|
|
51
64
|
|
|
65
|
+
function electronPathForPackageRoot(packageRoot) {
|
|
66
|
+
try {
|
|
67
|
+
const require = createRequire(path.join(packageRoot, "package.json"));
|
|
68
|
+
const electronPath = require("electron");
|
|
69
|
+
return typeof electronPath === "string" && electronPath ? electronPath : null;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
52
75
|
/** Whether an agent CLI is on PATH. */
|
|
53
76
|
export function commandExists(cmd) {
|
|
54
77
|
const res = spawnSync(cmd, ["--version"], { encoding: "utf8", timeout: 8000 });
|
|
@@ -85,17 +108,59 @@ export function installDaemonAutostart(bin = relayBinPath(), node = process.exec
|
|
|
85
108
|
<key>Label</key><string>${DAEMON_LAUNCH_LABEL}</string>
|
|
86
109
|
<key>ProgramArguments</key>
|
|
87
110
|
<array>
|
|
88
|
-
<string>${node}</string>
|
|
89
|
-
<string>${bin}</string>
|
|
111
|
+
<string>${plistEscape(node)}</string>
|
|
112
|
+
<string>${plistEscape(bin)}</string>
|
|
90
113
|
<string>daemon</string>
|
|
91
114
|
</array>
|
|
92
115
|
<key>RunAtLoad</key><true/>
|
|
93
116
|
<key>KeepAlive</key><true/>
|
|
94
|
-
<key>StandardOutPath</key><string>${logPath}</string>
|
|
95
|
-
<key>StandardErrorPath</key><string>${logPath}</string>
|
|
117
|
+
<key>StandardOutPath</key><string>${plistEscape(logPath)}</string>
|
|
118
|
+
<key>StandardErrorPath</key><string>${plistEscape(logPath)}</string>
|
|
119
|
+
<key>EnvironmentVariables</key>
|
|
120
|
+
<dict>
|
|
121
|
+
<key>PATH</key><string>${plistEscape(pathEnv)}</string>
|
|
122
|
+
</dict>
|
|
123
|
+
</dict>
|
|
124
|
+
</plist>
|
|
125
|
+
`;
|
|
126
|
+
fs.writeFileSync(plistPath, plist);
|
|
127
|
+
run("launchctl", ["unload", plistPath]); // ignore if not loaded
|
|
128
|
+
const res = run("launchctl", ["load", plistPath]);
|
|
129
|
+
return { ok: res.ok, plistPath, logPath };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Install + load a launchd agent that starts the visible Relay pill (macOS). */
|
|
133
|
+
export function installPillAutostart(bin = relayBinPath()) {
|
|
134
|
+
if (process.platform !== "darwin") return { ok: false, reason: "autostart_unsupported_platform" };
|
|
135
|
+
const packageRoot = packageRootForBin(bin);
|
|
136
|
+
const electronPath = electronPathForPackageRoot(packageRoot);
|
|
137
|
+
const overlayMain = path.join(packageRoot, "overlay", "main.cjs");
|
|
138
|
+
if (!electronPath || !fs.existsSync(overlayMain)) {
|
|
139
|
+
return { ok: false, reason: "pill_runtime_missing", electronPath, overlayMain };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const home = os.homedir();
|
|
143
|
+
const plistPath = path.join(home, "Library", "LaunchAgents", `${PILL_LAUNCH_LABEL}.plist`);
|
|
144
|
+
const logPath = path.join(home, ".relay", "pill.log");
|
|
145
|
+
fs.mkdirSync(path.dirname(plistPath), { recursive: true });
|
|
146
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
147
|
+
const pathEnv = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
|
|
148
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
149
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
150
|
+
<plist version="1.0">
|
|
151
|
+
<dict>
|
|
152
|
+
<key>Label</key><string>${PILL_LAUNCH_LABEL}</string>
|
|
153
|
+
<key>ProgramArguments</key>
|
|
154
|
+
<array>
|
|
155
|
+
<string>${plistEscape(electronPath)}</string>
|
|
156
|
+
<string>${plistEscape(overlayMain)}</string>
|
|
157
|
+
</array>
|
|
158
|
+
<key>RunAtLoad</key><true/>
|
|
159
|
+
<key>StandardOutPath</key><string>${plistEscape(logPath)}</string>
|
|
160
|
+
<key>StandardErrorPath</key><string>${plistEscape(logPath)}</string>
|
|
96
161
|
<key>EnvironmentVariables</key>
|
|
97
162
|
<dict>
|
|
98
|
-
<key>PATH</key><string>${pathEnv}</string>
|
|
163
|
+
<key>PATH</key><string>${plistEscape(pathEnv)}</string>
|
|
99
164
|
</dict>
|
|
100
165
|
</dict>
|
|
101
166
|
</plist>
|
|
@@ -123,7 +188,8 @@ export function runSetupInstall() {
|
|
|
123
188
|
else missing.push("Codex (registration failed)");
|
|
124
189
|
} else missing.push("Codex");
|
|
125
190
|
const daemon = installDaemonAutostart(bin);
|
|
126
|
-
|
|
191
|
+
const pill = installPillAutostart(bin);
|
|
192
|
+
return { installed, missing, daemon, pill };
|
|
127
193
|
}
|
|
128
194
|
|
|
129
195
|
/** Remove the Relay MCP from both agents and stop/clear the background daemon. */
|
|
@@ -138,5 +204,12 @@ export function runUninstall() {
|
|
|
138
204
|
} catch {
|
|
139
205
|
/* already gone */
|
|
140
206
|
}
|
|
207
|
+
const pillPlistPath = path.join(os.homedir(), "Library", "LaunchAgents", `${PILL_LAUNCH_LABEL}.plist`);
|
|
208
|
+
run("launchctl", ["unload", pillPlistPath]);
|
|
209
|
+
try {
|
|
210
|
+
fs.unlinkSync(pillPlistPath);
|
|
211
|
+
} catch {
|
|
212
|
+
/* already gone */
|
|
213
|
+
}
|
|
141
214
|
}
|
|
142
215
|
}
|
package/src/materializer.js
CHANGED
|
@@ -87,7 +87,7 @@ function readRowContent(rowState) {
|
|
|
87
87
|
// real API. Returns a normalized "row" the host writers consume.
|
|
88
88
|
async function resolveRow(id, { log = () => {} } = {}) {
|
|
89
89
|
const rowState = getRowState(id);
|
|
90
|
-
if (!rowState)
|
|
90
|
+
if (!rowState) return resolvePublicOpenToken(id, { log });
|
|
91
91
|
const content = readRowContent(rowState);
|
|
92
92
|
// The staged content packet carries the same fields under different names
|
|
93
93
|
// (briefingMarkdown/displayTitle/sender). Merge so either source satisfies the
|
|
@@ -132,6 +132,48 @@ async function resolveRow(id, { log = () => {} } = {}) {
|
|
|
132
132
|
return { row, rowState };
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
async function resolvePublicOpenToken(token, { log = () => {} } = {}) {
|
|
136
|
+
try {
|
|
137
|
+
const { RelayClient } = await import("./client.js");
|
|
138
|
+
const client = new RelayClient();
|
|
139
|
+
const response = await client.openRelayPacket(token);
|
|
140
|
+
const packet = response?.packet;
|
|
141
|
+
if (!packet || typeof packet !== "object") throw new Error("packet missing");
|
|
142
|
+
const rowState = {
|
|
143
|
+
id: token,
|
|
144
|
+
direction: "inbound",
|
|
145
|
+
state: "unread",
|
|
146
|
+
kind: packet.kind || "message",
|
|
147
|
+
relayNotificationKind: "plain_relay",
|
|
148
|
+
senderName: packet.sender?.name || "Someone",
|
|
149
|
+
title: packet.title || "Relay",
|
|
150
|
+
displayTitle: packet.title || "Relay",
|
|
151
|
+
bodyMarkdown: packet.bodyMarkdown || "",
|
|
152
|
+
briefingMarkdown: packet.bodyMarkdown || "",
|
|
153
|
+
createdAt: packet.createdAt || new Date().toISOString(),
|
|
154
|
+
attachments: packet.attachments || [],
|
|
155
|
+
attachmentUrls: response.attachmentUrls || {},
|
|
156
|
+
materializationDeferredReason: "public_open_token",
|
|
157
|
+
materializedSurfaces: { codex: false, claudeCode: false, claudeCowork: false },
|
|
158
|
+
};
|
|
159
|
+
const row = {
|
|
160
|
+
...packet,
|
|
161
|
+
...rowState,
|
|
162
|
+
id: token,
|
|
163
|
+
sender: packet.sender || (rowState.senderName ? { name: rowState.senderName } : null),
|
|
164
|
+
recipient: packet.recipient || null,
|
|
165
|
+
displayTitle: rowState.displayTitle,
|
|
166
|
+
title: rowState.title,
|
|
167
|
+
bodyMarkdown: rowState.bodyMarkdown,
|
|
168
|
+
briefingMarkdown: renderRelayRowBriefing(rowState),
|
|
169
|
+
};
|
|
170
|
+
return { row, rowState };
|
|
171
|
+
} catch (error) {
|
|
172
|
+
log(`relay public open failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
173
|
+
throw new Error(`Unknown Relay row or open token: ${token}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
135
177
|
// Fetch the verified task object for a row that points at a task. The real API
|
|
136
178
|
// shape of GET /v1/tasks/:taskId (RelayClient.getTask) is `{ task: Task }` — a
|
|
137
179
|
// wrapper object, never a bare Task — so we unwrap response.task. Returns the
|
package/src/mcp.js
CHANGED
|
@@ -4,6 +4,66 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
|
|
|
4
4
|
import { RelayClient } from "./client.js";
|
|
5
5
|
|
|
6
6
|
export const TOOLS = [
|
|
7
|
+
{
|
|
8
|
+
name: "relay_send",
|
|
9
|
+
description:
|
|
10
|
+
"Send an ordinary Relay message or handoff to another person. Use this for the normal product flow when the human says things like 'send Sven a relay asking how he is doing' or 'relay this context to Jordan'. This is NOT a task and does not create an agent coordination run. If the recipient has Relay, it appears in their Relay companion pill and Relays list; if they do not have Relay, Relay emails them a public link saying the sender sent a relay, with options to open it in Claude or Codex without creating an account. Do not use task-only tools for this ordinary communication path.",
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
recipient: {
|
|
15
|
+
type: "object",
|
|
16
|
+
description: "Who should receive the relay. Prefer contactId or relayUserId when known; email is fine for someone not on Relay.",
|
|
17
|
+
properties: {
|
|
18
|
+
contactId: { type: "string" },
|
|
19
|
+
relayUserId: { type: "string" },
|
|
20
|
+
email: { type: "string" },
|
|
21
|
+
name: { type: "string" },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
kind: { type: "string", enum: ["message", "handoff"], description: "Use message for notes/questions; handoff for larger context takeovers." },
|
|
25
|
+
title: { type: "string" },
|
|
26
|
+
bodyMarkdown: { type: "string" },
|
|
27
|
+
userInstructions: { type: "string", description: "Optional private instruction for how the recipient's agent should handle a handoff." },
|
|
28
|
+
targetSurfaces: {
|
|
29
|
+
type: "array",
|
|
30
|
+
items: { type: "string", enum: ["codex", "claude_code", "claude_desktop", "claude_cowork"] },
|
|
31
|
+
},
|
|
32
|
+
attachments: { type: "array", items: { type: "object" } },
|
|
33
|
+
idempotencyKey: { type: "string" },
|
|
34
|
+
},
|
|
35
|
+
required: ["recipient", "title", "bodyMarkdown", "idempotencyKey"],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "relay_contacts_search",
|
|
40
|
+
description:
|
|
41
|
+
"Search this human's Relay contact book before sending an ordinary relay. Use it when the human gives a name like 'Sven' and you need the correct person/email. If results are ambiguous, ask the human which contact they meant rather than guessing.",
|
|
42
|
+
inputSchema: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: { query: { type: "string" } },
|
|
45
|
+
required: ["query"],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "relay_inbox_list",
|
|
50
|
+
description:
|
|
51
|
+
"List ordinary Relay messages addressed to this human. This does not list task coordination relays; use relay_task_status for task state.",
|
|
52
|
+
inputSchema: { type: "object", properties: {} },
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "relay_acknowledge",
|
|
56
|
+
description:
|
|
57
|
+
"Acknowledge an ordinary Relay after it has been handled locally. This removes it from the active ordinary inbox.",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: "object",
|
|
60
|
+
properties: {
|
|
61
|
+
relayId: { type: "string" },
|
|
62
|
+
idempotencyKey: { type: "string" },
|
|
63
|
+
},
|
|
64
|
+
required: ["relayId", "idempotencyKey"],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
7
67
|
{
|
|
8
68
|
name: "relay_task_create",
|
|
9
69
|
description:
|
|
@@ -260,6 +320,26 @@ function text(obj) {
|
|
|
260
320
|
|
|
261
321
|
export async function handleCall(client, name, args) {
|
|
262
322
|
switch (name) {
|
|
323
|
+
case "relay_send":
|
|
324
|
+
return text(
|
|
325
|
+
await client.sendRelay({
|
|
326
|
+
recipient: args.recipient,
|
|
327
|
+
kind: args.kind || "message",
|
|
328
|
+
title: args.title,
|
|
329
|
+
bodyMarkdown: args.bodyMarkdown,
|
|
330
|
+
userInstructions: args.userInstructions || "",
|
|
331
|
+
source: { host: "relay-mcp" },
|
|
332
|
+
targetSurfaces: args.targetSurfaces || [],
|
|
333
|
+
attachments: args.attachments || [],
|
|
334
|
+
idempotencyKey: args.idempotencyKey,
|
|
335
|
+
}),
|
|
336
|
+
);
|
|
337
|
+
case "relay_contacts_search":
|
|
338
|
+
return text(await client.searchContacts(args.query));
|
|
339
|
+
case "relay_inbox_list":
|
|
340
|
+
return text(await client.inbox());
|
|
341
|
+
case "relay_acknowledge":
|
|
342
|
+
return text(await client.acknowledge(args.relayId, { idempotencyKey: args.idempotencyKey }));
|
|
263
343
|
case "relay_task_create":
|
|
264
344
|
return text(
|
|
265
345
|
await client.createTask({
|
package/src/notifications.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { webUrl } from "./config.js";
|
|
4
|
+
import { currentUser, readConfig, webUrl } from "./config.js";
|
|
5
5
|
import { relayBinPath } from "./install.js";
|
|
6
6
|
|
|
7
7
|
const DEFAULT_COMPANION_STATE = {
|
|
@@ -224,6 +224,48 @@ function writeCompanionState(statePath, state) {
|
|
|
224
224
|
fs.renameSync(tmp, statePath);
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
function accountMarker({ user = currentUser(), deviceId = readConfig().deviceId || "" } = {}) {
|
|
228
|
+
return {
|
|
229
|
+
userId: user?.id || "",
|
|
230
|
+
email: user?.email || "",
|
|
231
|
+
deviceId: deviceId || "",
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function stateBelongsToAccount(state, marker) {
|
|
236
|
+
if (!marker.userId && !marker.email) return true;
|
|
237
|
+
const account = state.account || {};
|
|
238
|
+
if (!account.userId && !account.email) return false;
|
|
239
|
+
if (account.userId && marker.userId && account.userId !== marker.userId) return false;
|
|
240
|
+
if (account.email && marker.email && account.email !== marker.email) return false;
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function resetCompanionStateForAccount(
|
|
245
|
+
{ user = currentUser(), deviceId = readConfig().deviceId || "", force = false } = {},
|
|
246
|
+
{ statePath = companionStatePath() } = {},
|
|
247
|
+
) {
|
|
248
|
+
const marker = accountMarker({ user, deviceId });
|
|
249
|
+
const existing = readCompanionState(statePath);
|
|
250
|
+
if (!force && stateBelongsToAccount(existing, marker)) {
|
|
251
|
+
if (!existing.account) {
|
|
252
|
+
existing.account = { ...marker, resetAt: null };
|
|
253
|
+
writeCompanionState(statePath, existing);
|
|
254
|
+
}
|
|
255
|
+
return { reset: false, statePath };
|
|
256
|
+
}
|
|
257
|
+
const next = {
|
|
258
|
+
...structuredClone(DEFAULT_COMPANION_STATE),
|
|
259
|
+
account: { ...marker, resetAt: new Date().toISOString() },
|
|
260
|
+
profile: {
|
|
261
|
+
...structuredClone(DEFAULT_COMPANION_STATE.profile),
|
|
262
|
+
transport: { type: "relay_api" },
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
writeCompanionState(statePath, next);
|
|
266
|
+
return { reset: true, statePath };
|
|
267
|
+
}
|
|
268
|
+
|
|
227
269
|
function notificationUrgency(kind) {
|
|
228
270
|
if (kind === "human_question") return "blocking";
|
|
229
271
|
if (kind === "task_request" || kind === "share_approval") return "action_required";
|
|
@@ -410,3 +452,67 @@ export function stageRelayCompanionItem(notification, { statePath = companionSta
|
|
|
410
452
|
export function defaultStageRelayCompanionItem(notification) {
|
|
411
453
|
return stageRelayCompanionItem(notification);
|
|
412
454
|
}
|
|
455
|
+
|
|
456
|
+
export function stagePlainRelayItem({ item, packet, attachmentUrls = {} }, { statePath = companionStatePath() } = {}) {
|
|
457
|
+
if (!item?.relayId) throw new Error("stagePlainRelayItem requires an inbox item with relayId");
|
|
458
|
+
const state = readCompanionState(statePath);
|
|
459
|
+
state.packets ||= {};
|
|
460
|
+
state.chats ||= {};
|
|
461
|
+
const now = new Date().toISOString();
|
|
462
|
+
const existing = state.packets[item.relayId] || {};
|
|
463
|
+
const content = {
|
|
464
|
+
...(packet || {}),
|
|
465
|
+
id: item.relayId,
|
|
466
|
+
briefingMarkdown: packet?.bodyMarkdown || item.preview || "",
|
|
467
|
+
attachmentUrls,
|
|
468
|
+
delivery: {
|
|
469
|
+
transport: "relay_api",
|
|
470
|
+
targetSurfaces: packet?.targetSurfaces || ["codex", "claude_code"],
|
|
471
|
+
recipientInboxDir: "",
|
|
472
|
+
outboxPath: null,
|
|
473
|
+
inboxPath: null,
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
const contentPath = writeNotificationPacketContent({ id: item.relayId }, content, statePath);
|
|
477
|
+
const readState = existing.state === "read" || item.state === "read" ? "read" : "unread";
|
|
478
|
+
const row = {
|
|
479
|
+
...existing,
|
|
480
|
+
id: item.relayId,
|
|
481
|
+
direction: "inbound",
|
|
482
|
+
state: readState,
|
|
483
|
+
kind: item.kind || packet?.kind || "message",
|
|
484
|
+
relayNotificationKind: "plain_relay",
|
|
485
|
+
urgency: "normal",
|
|
486
|
+
senderName: item.sender?.name || packet?.sender?.name || "Someone",
|
|
487
|
+
title: item.title || packet?.title || item.preview || "Relay",
|
|
488
|
+
displayTitle: item.title || packet?.title || item.displayTitle || "Relay",
|
|
489
|
+
briefingMarkdown: packet?.bodyMarkdown || item.preview || "",
|
|
490
|
+
bodyMarkdown: packet?.bodyMarkdown || item.preview || "",
|
|
491
|
+
createdAt: item.createdAt || packet?.createdAt || now,
|
|
492
|
+
updatedAt: item.updatedAt || now,
|
|
493
|
+
stagedAt: now,
|
|
494
|
+
actionUrl: "/app/relays",
|
|
495
|
+
action: { type: "open", url: "/app/relays" },
|
|
496
|
+
quickReply: false,
|
|
497
|
+
taskId: null,
|
|
498
|
+
participantId: null,
|
|
499
|
+
messageId: null,
|
|
500
|
+
provider: null,
|
|
501
|
+
contentPath,
|
|
502
|
+
filePath: contentPath,
|
|
503
|
+
attachments: packet?.attachments || [],
|
|
504
|
+
attachmentUrls,
|
|
505
|
+
materializationDeferredReason: "relay_pill",
|
|
506
|
+
materializedSurfaces: existing.materializedSurfaces || { codex: false, claudeCode: false, claudeCowork: false },
|
|
507
|
+
};
|
|
508
|
+
state.packets[item.relayId] = row;
|
|
509
|
+
writeCompanionState(statePath, state);
|
|
510
|
+
return {
|
|
511
|
+
ok: true,
|
|
512
|
+
transport: "relay_companion",
|
|
513
|
+
statePath,
|
|
514
|
+
itemId: item.relayId,
|
|
515
|
+
actionUrl: row.actionUrl,
|
|
516
|
+
contentPath,
|
|
517
|
+
};
|
|
518
|
+
}
|
package/src/task-daemon.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
defaultStageRelayCompanionItem,
|
|
14
14
|
freshNotifications,
|
|
15
15
|
markNotificationsProcessed,
|
|
16
|
+
stagePlainRelayItem as defaultStagePlainRelayItem,
|
|
16
17
|
} from "./notifications.js";
|
|
17
18
|
|
|
18
19
|
function idempotencyKey(prefix) {
|
|
@@ -55,6 +56,26 @@ function markTaskEventsProcessed(ledger, events) {
|
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
function freshPlainRelays(ledger, items) {
|
|
60
|
+
ledger.plainRelays ||= {};
|
|
61
|
+
return items.filter((item) => {
|
|
62
|
+
const seen = ledger.plainRelays[item.relayId];
|
|
63
|
+
return !seen || seen.updatedAt !== item.updatedAt || seen.state !== item.state;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function markPlainRelaysProcessed(ledger, items) {
|
|
68
|
+
ledger.plainRelays ||= {};
|
|
69
|
+
const processedAt = new Date().toISOString();
|
|
70
|
+
for (const item of items) {
|
|
71
|
+
ledger.plainRelays[item.relayId] = {
|
|
72
|
+
state: item.state,
|
|
73
|
+
updatedAt: item.updatedAt || item.createdAt || "",
|
|
74
|
+
processedAt,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
58
79
|
async function pollVisibleTaskEvents({ client, ledger, tasks, log }) {
|
|
59
80
|
if (typeof client.taskEvents !== "function") return [];
|
|
60
81
|
const visibleEvents = [];
|
|
@@ -76,7 +97,32 @@ async function pollVisibleTaskEvents({ client, ledger, tasks, log }) {
|
|
|
76
97
|
return visibleEvents;
|
|
77
98
|
}
|
|
78
99
|
|
|
79
|
-
async function
|
|
100
|
+
async function pollPlainInbox({ client, ledger, stagePlainRelay, log }) {
|
|
101
|
+
if (typeof client.inbox !== "function" || typeof client.fetchRelay !== "function") return [];
|
|
102
|
+
try {
|
|
103
|
+
const inbox = await client.inbox();
|
|
104
|
+
const fresh = freshPlainRelays(ledger, inbox.items || []);
|
|
105
|
+
const staged = [];
|
|
106
|
+
for (const item of fresh) {
|
|
107
|
+
try {
|
|
108
|
+
const relay = await client.fetchRelay(item.relayId);
|
|
109
|
+
stagePlainRelay({ item, packet: relay.packet, attachmentUrls: relay.attachmentUrls || {} });
|
|
110
|
+
staged.push(item);
|
|
111
|
+
log(`staged ordinary Relay from ${item.sender?.name || "someone"}: ${item.title || item.relayId}`);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
log(`ordinary Relay staging failed for ${item.relayId}: ${err.message}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (staged.length) markPlainRelaysProcessed(ledger, staged);
|
|
117
|
+
return staged;
|
|
118
|
+
} catch (err) {
|
|
119
|
+
log(`ordinary Relay inbox polling failed: ${err.message}`);
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function pollHumanNotifications({ client, ledger, stageCompanionItem, stagePlainRelay, log }) {
|
|
125
|
+
const ordinaryRelays = await pollPlainInbox({ client, ledger, stagePlainRelay, log });
|
|
80
126
|
try {
|
|
81
127
|
const [me, tasks, relays, connectors] = await Promise.all([
|
|
82
128
|
client.me(),
|
|
@@ -103,10 +149,10 @@ async function pollHumanNotifications({ client, ledger, stageCompanionItem, log
|
|
|
103
149
|
}
|
|
104
150
|
}
|
|
105
151
|
if (staged.length) markNotificationsProcessed(ledger, staged);
|
|
106
|
-
return { notifications: staged, events };
|
|
152
|
+
return { notifications: staged, ordinaryRelays, events };
|
|
107
153
|
} catch (err) {
|
|
108
154
|
log(`Relay companion attention polling failed: ${err.message}`);
|
|
109
|
-
return { notifications: [], events: [] };
|
|
155
|
+
return { notifications: [], ordinaryRelays, events: [] };
|
|
110
156
|
}
|
|
111
157
|
}
|
|
112
158
|
|
|
@@ -114,10 +160,16 @@ export async function pollTaskRuntimeOnce({
|
|
|
114
160
|
client = new RelayClient(),
|
|
115
161
|
log = () => {},
|
|
116
162
|
stageCompanionItem = defaultStageRelayCompanionItem,
|
|
163
|
+
stagePlainRelay = defaultStagePlainRelayItem,
|
|
117
164
|
adapters,
|
|
118
165
|
} = {}) {
|
|
119
166
|
const ledger = readTaskLedger();
|
|
120
|
-
|
|
167
|
+
let inbox = { messages: [], sessions: [] };
|
|
168
|
+
try {
|
|
169
|
+
inbox = await client.agentInbox();
|
|
170
|
+
} catch (err) {
|
|
171
|
+
log(`task agent inbox polling failed: ${err.message}`);
|
|
172
|
+
}
|
|
121
173
|
const fresh = freshMessages(ledger, inbox.messages || []);
|
|
122
174
|
const processedMessageIds = new Set();
|
|
123
175
|
const processedMessages = [];
|
|
@@ -179,9 +231,15 @@ export async function pollTaskRuntimeOnce({
|
|
|
179
231
|
}
|
|
180
232
|
}
|
|
181
233
|
|
|
182
|
-
const humanPolling = await pollHumanNotifications({ client, ledger, stageCompanionItem, log });
|
|
234
|
+
const humanPolling = await pollHumanNotifications({ client, ledger, stageCompanionItem, stagePlainRelay, log });
|
|
183
235
|
writeTaskLedger(ledger);
|
|
184
|
-
return {
|
|
236
|
+
return {
|
|
237
|
+
sessions: touched,
|
|
238
|
+
messages: processedMessages,
|
|
239
|
+
notifications: humanPolling.notifications,
|
|
240
|
+
ordinaryRelays: humanPolling.ordinaryRelays,
|
|
241
|
+
events: humanPolling.events,
|
|
242
|
+
};
|
|
185
243
|
}
|
|
186
244
|
|
|
187
245
|
export async function runTaskDaemon({ intervalMs = 4000 } = {}) {
|
|
@@ -194,9 +252,15 @@ export async function runTaskDaemon({ intervalMs = 4000 } = {}) {
|
|
|
194
252
|
for (;;) {
|
|
195
253
|
try {
|
|
196
254
|
const result = await pollTaskRuntimeOnce({ client, log });
|
|
197
|
-
if (
|
|
255
|
+
if (
|
|
256
|
+
result.sessions.length ||
|
|
257
|
+
result.messages.length ||
|
|
258
|
+
result.notifications.length ||
|
|
259
|
+
result.ordinaryRelays.length ||
|
|
260
|
+
result.events.length
|
|
261
|
+
) {
|
|
198
262
|
log(
|
|
199
|
-
`processed ${result.sessions.length} session(s), ${result.messages.length} message(s), ${result.notifications.length}
|
|
263
|
+
`processed ${result.sessions.length} session(s), ${result.messages.length} message(s), ${result.notifications.length} task attention item(s), ${result.ordinaryRelays.length} ordinary relay(s), ${result.events.length} event(s)`,
|
|
200
264
|
);
|
|
201
265
|
}
|
|
202
266
|
} catch (err) {
|