agent-office-cli 0.1.5 → 0.1.7
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/src/index.js +11 -0
- package/src/runtime/index.js +6 -0
- package/src/runtime/sleep-inhibitor.js +31 -0
- package/src/runtime/sleep-inhibitor.test.js +63 -0
- package/src/runtime/tunnel-log.js +56 -0
- package/src/runtime/tunnel-log.test.js +43 -0
- package/src/tunnel.js +14 -9
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -19,6 +19,7 @@ const {
|
|
|
19
19
|
createPtyManager,
|
|
20
20
|
defaultTransportForProvider,
|
|
21
21
|
ensureNodePtySpawnHelper,
|
|
22
|
+
startSleepInhibitor,
|
|
22
23
|
listSessionRecords,
|
|
23
24
|
removeSessionRecord,
|
|
24
25
|
applyClaudeHookConfig,
|
|
@@ -172,6 +173,13 @@ async function main() {
|
|
|
172
173
|
throw new Error("tmux is required for AgentOffice local sessions. Install it first, for example with `brew install tmux`.");
|
|
173
174
|
}
|
|
174
175
|
|
|
176
|
+
const sleepInhibitor = startSleepInhibitor({ commandExists });
|
|
177
|
+
if (sleepInhibitor.started) {
|
|
178
|
+
console.log("- sleep guard: caffeinate active");
|
|
179
|
+
} else if (process.platform === "darwin") {
|
|
180
|
+
console.log("- sleep guard: skipped (caffeinate unavailable)");
|
|
181
|
+
}
|
|
182
|
+
|
|
175
183
|
const store = createSessionStore();
|
|
176
184
|
const ptyManager = createPtyManager({ store });
|
|
177
185
|
const restored = ptyManager.restoreManagedSessions();
|
|
@@ -194,6 +202,9 @@ async function main() {
|
|
|
194
202
|
});
|
|
195
203
|
console.log(`AgentOffice tunnel connecting to relay: ${hosted.relayUrl}`);
|
|
196
204
|
console.log(`- hosted auth: key from ${hosted.keySource}, relay from ${hosted.relaySource}`);
|
|
205
|
+
if (tunnel.logPath) {
|
|
206
|
+
console.log(`- tunnel log: ${tunnel.logPath}`);
|
|
207
|
+
}
|
|
197
208
|
tunnel.sendStatusSummary(store.listSessionSummaries());
|
|
198
209
|
|
|
199
210
|
let statusDebounceTimer = null;
|
package/src/runtime/index.js
CHANGED
|
@@ -18,6 +18,8 @@ const {
|
|
|
18
18
|
removeSessionRecord
|
|
19
19
|
} = require("./session-registry");
|
|
20
20
|
const { ensureNodePtySpawnHelper } = require("./ensure-node-pty");
|
|
21
|
+
const { startSleepInhibitor } = require("./sleep-inhibitor");
|
|
22
|
+
const { createTunnelLogger, TUNNEL_LOG_PATH, describeWebSocketClose } = require("./tunnel-log");
|
|
21
23
|
const {
|
|
22
24
|
applyClaudeHookConfig,
|
|
23
25
|
claudeSettingsPath,
|
|
@@ -45,6 +47,10 @@ module.exports = {
|
|
|
45
47
|
persistSessionRecord,
|
|
46
48
|
removeSessionRecord,
|
|
47
49
|
ensureNodePtySpawnHelper,
|
|
50
|
+
startSleepInhibitor,
|
|
51
|
+
createTunnelLogger,
|
|
52
|
+
TUNNEL_LOG_PATH,
|
|
53
|
+
describeWebSocketClose,
|
|
48
54
|
applyClaudeHookConfig,
|
|
49
55
|
claudeSettingsPath,
|
|
50
56
|
commandExists,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const { spawn } = require("node:child_process");
|
|
2
|
+
|
|
3
|
+
function startSleepInhibitor({
|
|
4
|
+
pid = process.pid,
|
|
5
|
+
platform = process.platform,
|
|
6
|
+
commandExists = () => true,
|
|
7
|
+
spawn: spawnProcess = spawn
|
|
8
|
+
} = {}) {
|
|
9
|
+
if (platform !== "darwin") {
|
|
10
|
+
return { started: false, reason: "unsupported_platform" };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!commandExists("caffeinate")) {
|
|
14
|
+
return { started: false, reason: "missing_command" };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const child = spawnProcess("caffeinate", ["-dimsu", "-w", String(pid)], {
|
|
18
|
+
stdio: "ignore"
|
|
19
|
+
});
|
|
20
|
+
child.unref?.();
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
started: true,
|
|
24
|
+
reason: "started",
|
|
25
|
+
child
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = {
|
|
30
|
+
startSleepInhibitor
|
|
31
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const test = require("node:test");
|
|
2
|
+
const assert = require("node:assert/strict");
|
|
3
|
+
|
|
4
|
+
const { startSleepInhibitor } = require("./sleep-inhibitor");
|
|
5
|
+
|
|
6
|
+
test("startSleepInhibitor starts caffeinate by default on macOS", () => {
|
|
7
|
+
const calls = [];
|
|
8
|
+
const child = {
|
|
9
|
+
unrefCalled: false,
|
|
10
|
+
unref() {
|
|
11
|
+
this.unrefCalled = true;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const result = startSleepInhibitor({
|
|
16
|
+
pid: 4321,
|
|
17
|
+
platform: "darwin",
|
|
18
|
+
commandExists: (command) => command === "caffeinate",
|
|
19
|
+
spawn: (command, args, options) => {
|
|
20
|
+
calls.push({ command, args, options });
|
|
21
|
+
return child;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
assert.deepEqual(calls, [
|
|
26
|
+
{
|
|
27
|
+
command: "caffeinate",
|
|
28
|
+
args: ["-dimsu", "-w", "4321"],
|
|
29
|
+
options: { stdio: "ignore" }
|
|
30
|
+
}
|
|
31
|
+
]);
|
|
32
|
+
assert.equal(child.unrefCalled, true);
|
|
33
|
+
assert.equal(result.started, true);
|
|
34
|
+
assert.equal(result.reason, "started");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("startSleepInhibitor skips non-macOS platforms", () => {
|
|
38
|
+
const result = startSleepInhibitor({
|
|
39
|
+
pid: 4321,
|
|
40
|
+
platform: "linux",
|
|
41
|
+
commandExists: () => true,
|
|
42
|
+
spawn: () => {
|
|
43
|
+
throw new Error("spawn should not be called");
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
assert.equal(result.started, false);
|
|
48
|
+
assert.equal(result.reason, "unsupported_platform");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("startSleepInhibitor skips when caffeinate is unavailable", () => {
|
|
52
|
+
const result = startSleepInhibitor({
|
|
53
|
+
pid: 4321,
|
|
54
|
+
platform: "darwin",
|
|
55
|
+
commandExists: () => false,
|
|
56
|
+
spawn: () => {
|
|
57
|
+
throw new Error("spawn should not be called");
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
assert.equal(result.started, false);
|
|
62
|
+
assert.equal(result.reason, "missing_command");
|
|
63
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const os = require("node:os");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
|
|
5
|
+
const TUNNEL_LOG_PATH = path.join(os.homedir(), ".agentoffice", "logs", "tunnel.log");
|
|
6
|
+
|
|
7
|
+
function describeWebSocketClose({ code, reason }) {
|
|
8
|
+
const parts = [];
|
|
9
|
+
|
|
10
|
+
if (typeof code === "number") {
|
|
11
|
+
parts.push(`code=${code}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (reason) {
|
|
15
|
+
parts.push(`reason=${reason}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return parts.length > 0 ? parts.join(" ") : "no close details";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createTunnelLogger({
|
|
22
|
+
logPath = TUNNEL_LOG_PATH,
|
|
23
|
+
now = () => new Date().toISOString(),
|
|
24
|
+
mkdirSync = fs.mkdirSync,
|
|
25
|
+
appendFileSync = fs.appendFileSync,
|
|
26
|
+
consoleObj = console,
|
|
27
|
+
} = {}) {
|
|
28
|
+
function write(level, message) {
|
|
29
|
+
const line = `[${now()}] [${level}] ${message}`;
|
|
30
|
+
const print = level === "error" ? consoleObj.error : consoleObj.log;
|
|
31
|
+
print.call(consoleObj, line);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
mkdirSync(path.dirname(logPath), { recursive: true });
|
|
35
|
+
appendFileSync(logPath, `${line}\n`, "utf8");
|
|
36
|
+
} catch {
|
|
37
|
+
// Logging should never crash the tunnel client.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
logPath,
|
|
43
|
+
info(message) {
|
|
44
|
+
write("info", message);
|
|
45
|
+
},
|
|
46
|
+
error(message) {
|
|
47
|
+
write("error", message);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
TUNNEL_LOG_PATH,
|
|
54
|
+
createTunnelLogger,
|
|
55
|
+
describeWebSocketClose,
|
|
56
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const test = require("node:test");
|
|
2
|
+
const assert = require("node:assert/strict");
|
|
3
|
+
|
|
4
|
+
const { createTunnelLogger, describeWebSocketClose } = require("./tunnel-log");
|
|
5
|
+
|
|
6
|
+
test("describeWebSocketClose includes code and reason when present", () => {
|
|
7
|
+
assert.equal(
|
|
8
|
+
describeWebSocketClose({ code: 1006, reason: "network_reset" }),
|
|
9
|
+
"code=1006 reason=network_reset"
|
|
10
|
+
);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("describeWebSocketClose falls back when no details are available", () => {
|
|
14
|
+
assert.equal(describeWebSocketClose({}), "no close details");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("createTunnelLogger mirrors lines to console and appends a local tunnel log", () => {
|
|
18
|
+
const writes = [];
|
|
19
|
+
const consoleLines = [];
|
|
20
|
+
|
|
21
|
+
const logger = createTunnelLogger({
|
|
22
|
+
logPath: "/tmp/agentoffice-tunnel.log",
|
|
23
|
+
now: () => "2026-03-20T08:12:00.000Z",
|
|
24
|
+
mkdirSync: () => {},
|
|
25
|
+
appendFileSync: (_path, content) => writes.push(content),
|
|
26
|
+
consoleObj: {
|
|
27
|
+
log: (line) => consoleLines.push(["log", line]),
|
|
28
|
+
error: (line) => consoleLines.push(["error", line]),
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
logger.info("connected to relay");
|
|
33
|
+
logger.error("ws error: socket hang up");
|
|
34
|
+
|
|
35
|
+
assert.deepEqual(consoleLines, [
|
|
36
|
+
["log", "[2026-03-20T08:12:00.000Z] [info] connected to relay"],
|
|
37
|
+
["error", "[2026-03-20T08:12:00.000Z] [error] ws error: socket hang up"],
|
|
38
|
+
]);
|
|
39
|
+
assert.deepEqual(writes, [
|
|
40
|
+
"[2026-03-20T08:12:00.000Z] [info] connected to relay\n",
|
|
41
|
+
"[2026-03-20T08:12:00.000Z] [error] ws error: socket hang up\n",
|
|
42
|
+
]);
|
|
43
|
+
});
|
package/src/tunnel.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { WebSocket } = require("ws");
|
|
2
2
|
const { toSessionSummary } = require("./core");
|
|
3
|
+
const { createTunnelLogger, describeWebSocketClose } = require("./runtime/tunnel-log");
|
|
3
4
|
|
|
4
5
|
const RECONNECT_BASE_MS = 1000;
|
|
5
6
|
const RECONNECT_MAX_MS = 30000;
|
|
@@ -40,7 +41,7 @@ function buildLocalRequestHeaders(headers, localServerUrl) {
|
|
|
40
41
|
return nextHeaders;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
function createTunnelClient({ key, relayUrl, localServerUrl }) {
|
|
44
|
+
function createTunnelClient({ key, relayUrl, localServerUrl, logger = createTunnelLogger() }) {
|
|
44
45
|
let ws = null;
|
|
45
46
|
let reconnectDelay = RECONNECT_BASE_MS;
|
|
46
47
|
let stopped = false;
|
|
@@ -68,7 +69,7 @@ function createTunnelClient({ key, relayUrl, localServerUrl }) {
|
|
|
68
69
|
|
|
69
70
|
ws.on("open", () => {
|
|
70
71
|
reconnectDelay = RECONNECT_BASE_MS;
|
|
71
|
-
|
|
72
|
+
logger.info("[tunnel] connected to relay, authenticating...");
|
|
72
73
|
ws.send(JSON.stringify({ type: "auth", key }));
|
|
73
74
|
});
|
|
74
75
|
|
|
@@ -91,12 +92,12 @@ function createTunnelClient({ key, relayUrl, localServerUrl }) {
|
|
|
91
92
|
|
|
92
93
|
if (msg.type === "auth:ok") {
|
|
93
94
|
authenticated = true;
|
|
94
|
-
|
|
95
|
+
logger.info(`[tunnel] authenticated with relay: ${relayUrl} (userId=${msg.userId})`);
|
|
95
96
|
flushStatusSummary();
|
|
96
97
|
return;
|
|
97
98
|
}
|
|
98
99
|
if (msg.type === "auth:error") {
|
|
99
|
-
|
|
100
|
+
logger.error(`[tunnel] authentication failed: ${msg.error || "invalid key"}`);
|
|
100
101
|
stopped = true;
|
|
101
102
|
ws.close();
|
|
102
103
|
return;
|
|
@@ -108,20 +109,23 @@ function createTunnelClient({ key, relayUrl, localServerUrl }) {
|
|
|
108
109
|
|
|
109
110
|
await handleRelayMessage(msg);
|
|
110
111
|
} catch (err) {
|
|
111
|
-
|
|
112
|
+
logger.error(`[tunnel] message error: ${err.message}`);
|
|
112
113
|
}
|
|
113
114
|
});
|
|
114
115
|
|
|
115
|
-
ws.on("close", (code) => {
|
|
116
|
+
ws.on("close", (code, reasonBuffer) => {
|
|
117
|
+
const reason = Buffer.isBuffer(reasonBuffer) ? reasonBuffer.toString("utf8") : String(reasonBuffer || "");
|
|
118
|
+
const closeDetails = describeWebSocketClose({ code, reason });
|
|
116
119
|
if (stopped) {
|
|
120
|
+
logger.info(`[tunnel] stopped with close ${closeDetails}`);
|
|
117
121
|
return;
|
|
118
122
|
}
|
|
119
123
|
if (code === 4401) {
|
|
120
|
-
|
|
124
|
+
logger.error(`[tunnel] authentication rejected by relay (${closeDetails}). Not reconnecting.`);
|
|
121
125
|
stopped = true;
|
|
122
126
|
return;
|
|
123
127
|
}
|
|
124
|
-
|
|
128
|
+
logger.info(`[tunnel] disconnected (${closeDetails}). Reconnecting in ${reconnectDelay}ms...`);
|
|
125
129
|
setTimeout(() => {
|
|
126
130
|
reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
|
|
127
131
|
connect();
|
|
@@ -129,7 +133,7 @@ function createTunnelClient({ key, relayUrl, localServerUrl }) {
|
|
|
129
133
|
});
|
|
130
134
|
|
|
131
135
|
ws.on("error", (err) => {
|
|
132
|
-
|
|
136
|
+
logger.error(`[tunnel] ws error: ${err.message}`);
|
|
133
137
|
});
|
|
134
138
|
}
|
|
135
139
|
|
|
@@ -250,6 +254,7 @@ function createTunnelClient({ key, relayUrl, localServerUrl }) {
|
|
|
250
254
|
connect();
|
|
251
255
|
|
|
252
256
|
return {
|
|
257
|
+
logPath: logger.logPath,
|
|
253
258
|
sendStatusSummary,
|
|
254
259
|
stop
|
|
255
260
|
};
|