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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-office-cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Run and manage AI agent sessions locally, with optional relay to agentoffice.top",
5
5
  "license": "MIT",
6
6
  "engines": {
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;
@@ -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
- console.log("[tunnel] connected to relay, authenticating...");
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
- console.log(`[tunnel] authenticated with relay: ${relayUrl} (userId=${msg.userId})`);
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
- console.error(`[tunnel] authentication failed: ${msg.error || "invalid key"}`);
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
- console.error(`[tunnel] message error: ${err.message}`);
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
- console.error("[tunnel] authentication rejected by relay. Not reconnecting.");
124
+ logger.error(`[tunnel] authentication rejected by relay (${closeDetails}). Not reconnecting.`);
121
125
  stopped = true;
122
126
  return;
123
127
  }
124
- console.log(`[tunnel] disconnected (${code}). Reconnecting in ${reconnectDelay}ms...`);
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
- console.error(`[tunnel] ws error: ${err.message}`);
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
  };