agent-office-cli 0.0.1

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/src/index.js ADDED
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env node
2
+
3
+ try {
4
+ process.loadEnvFile?.();
5
+ } catch {}
6
+
7
+ const fs = require("node:fs");
8
+ const path = require("node:path");
9
+ const { spawnSync } = require("node:child_process");
10
+ const { createAppServer } = require("./server");
11
+ const {
12
+ createSessionStore,
13
+ printClaudeHooksConfig,
14
+ DEFAULT_LAN_HOST,
15
+ DEFAULT_PORT,
16
+ DEFAULT_SERVER_URL
17
+ } = require("./core");
18
+ const {
19
+ createPtyManager,
20
+ defaultTransportForProvider,
21
+ ensureNodePtySpawnHelper,
22
+ listSessionRecords,
23
+ removeSessionRecord,
24
+ applyClaudeHookConfig,
25
+ commandExists,
26
+ hasClaudeHookConfig,
27
+ networkUrls,
28
+ resolveCommand,
29
+ listAgentOfficeSessions,
30
+ killSession,
31
+ tmuxPath
32
+ } = require("./runtime");
33
+ const { createTunnelClient } = require("./tunnel");
34
+
35
+ const DEFAULT_RELAY_URL = "https://agentoffice.top";
36
+
37
+ function readEnvValue(name) {
38
+ const value = process.env[name];
39
+ if (typeof value !== "string") {
40
+ return null;
41
+ }
42
+ const trimmed = value.trim();
43
+ return trimmed ? trimmed : null;
44
+ }
45
+
46
+ function resolveHostedOptions(options) {
47
+ const optionKey = typeof options.key === "string" ? options.key.trim() : "";
48
+ const optionRelay = typeof options.relay === "string" ? options.relay.trim() : "";
49
+
50
+ return {
51
+ key: optionKey || readEnvValue("AGENTOFFICE_API_KEY"),
52
+ relayUrl: optionRelay || readEnvValue("AGENTOFFICE_RELAY_URL") || DEFAULT_RELAY_URL,
53
+ keySource: optionKey ? "--key" : readEnvValue("AGENTOFFICE_API_KEY") ? "AGENTOFFICE_API_KEY" : null,
54
+ relaySource: optionRelay ? "--relay" : readEnvValue("AGENTOFFICE_RELAY_URL") ? "AGENTOFFICE_RELAY_URL" : "default"
55
+ };
56
+ }
57
+
58
+ function parseArgs(argv) {
59
+ const args = argv.slice(2);
60
+ const action = args.shift() || "start";
61
+ let subaction = null;
62
+ const options = {};
63
+ const commandParts = [];
64
+ let commandMode = false;
65
+
66
+ if (args[0] && !args[0].startsWith("-") && args[0] !== "--") {
67
+ subaction = args.shift();
68
+ }
69
+
70
+ while (args.length > 0) {
71
+ const token = args.shift();
72
+ if (commandMode) {
73
+ commandParts.push(token);
74
+ continue;
75
+ }
76
+ if (token === "--") {
77
+ if (action !== "run") {
78
+ continue;
79
+ }
80
+ commandMode = true;
81
+ continue;
82
+ }
83
+ if (token === "-t") {
84
+ options.title = args.shift();
85
+ continue;
86
+ }
87
+ if (token.startsWith("--")) {
88
+ const key = token.slice(2);
89
+ const next = args[0];
90
+ if (!next || next.startsWith("--")) {
91
+ options[key] = true;
92
+ } else {
93
+ options[key] = args.shift();
94
+ }
95
+ continue;
96
+ }
97
+ commandParts.push(token);
98
+ }
99
+
100
+ return { action, subaction, options, command: commandParts.join(" ").trim() };
101
+ }
102
+
103
+ async function postJson(url, payload) {
104
+ const response = await fetch(url, {
105
+ method: "POST",
106
+ headers: { "Content-Type": "application/json" },
107
+ body: JSON.stringify(payload)
108
+ });
109
+ if (!response.ok) {
110
+ const text = await response.text();
111
+ throw new Error(text || response.statusText);
112
+ }
113
+ return response.json();
114
+ }
115
+
116
+ async function resolveAttachTarget({ target, server }) {
117
+ if (!target) {
118
+ return null;
119
+ }
120
+
121
+ if (target.startsWith("agentoffice_")) {
122
+ return target;
123
+ }
124
+
125
+ const record = listSessionRecords().find((entry) => entry.sessionId === target);
126
+ if (record && record.meta && record.meta.tmuxSession) {
127
+ return record.meta.tmuxSession;
128
+ }
129
+
130
+ try {
131
+ const response = await fetch(`${server}/api/sessions/${encodeURIComponent(target)}`);
132
+ if (response.ok) {
133
+ const session = await response.json();
134
+ if (session && session.meta && session.meta.tmuxSession) {
135
+ return session.meta.tmuxSession;
136
+ }
137
+ }
138
+ } catch {
139
+ // Ignore server lookup failures and fall back to local registry only.
140
+ }
141
+
142
+ return null;
143
+ }
144
+
145
+ async function main() {
146
+ const { action, subaction, options, command } = parseArgs(process.argv);
147
+
148
+ if (action === "start") {
149
+ const nodePtySetup = ensureNodePtySpawnHelper();
150
+ if (nodePtySetup.changed.length > 0) {
151
+ console.log(`AgentOffice repaired node-pty spawn-helper permissions for ${nodePtySetup.changed.join(", ")}`);
152
+ }
153
+
154
+ const host = options.host || DEFAULT_LAN_HOST;
155
+ const port = Number(options.port || DEFAULT_PORT);
156
+ const localServerUrl = `http://127.0.0.1:${port}`;
157
+ const handlerPath = path.resolve(__dirname, "index.js");
158
+
159
+ console.log("AgentOffice preflight");
160
+ console.log(`- tmux: ${commandExists("tmux") ? resolveCommand("tmux") : "missing"}`);
161
+ console.log(`- claude: ${commandExists("claude") ? resolveCommand("claude") : "missing"}`);
162
+ console.log(`- codex: ${commandExists("codex") ? resolveCommand("codex") : "missing"}`);
163
+
164
+ if (!hasClaudeHookConfig({ serverUrl: localServerUrl, handlerPath })) {
165
+ const settingsPath = applyClaudeHookConfig({ serverUrl: localServerUrl, handlerPath });
166
+ console.log(`- claude hooks: installed into ${settingsPath}`);
167
+ } else {
168
+ console.log("- claude hooks: configured");
169
+ }
170
+
171
+ if (!commandExists("tmux")) {
172
+ throw new Error("tmux is required for AgentOffice local sessions. Install it first, for example with `brew install tmux`.");
173
+ }
174
+
175
+ const store = createSessionStore();
176
+ const ptyManager = createPtyManager({ store });
177
+ const restored = ptyManager.restoreManagedSessions();
178
+ createAppServer({ host, port, store, ptyManager });
179
+ console.log(`AgentOffice restored ${restored.length} session(s).`);
180
+ console.log("AgentOffice URLs");
181
+ for (const url of networkUrls({ host, port })) {
182
+ console.log(`- ${url}`);
183
+ }
184
+ console.log("");
185
+
186
+ // --- Hosted mode: connect tunnel to relay ---
187
+ const hosted = resolveHostedOptions(options);
188
+ if (hosted.key) {
189
+ const localServerUrl = `http://127.0.0.1:${port}`;
190
+ const tunnel = createTunnelClient({
191
+ key: hosted.key,
192
+ relayUrl: hosted.relayUrl,
193
+ localServerUrl
194
+ });
195
+ console.log(`AgentOffice tunnel connecting to relay: ${hosted.relayUrl}`);
196
+ console.log(`- hosted auth: key from ${hosted.keySource}, relay from ${hosted.relaySource}`);
197
+ tunnel.sendStatusSummary(store.listSessionSummaries());
198
+
199
+ let statusDebounceTimer = null;
200
+ function scheduleStatusSummary() {
201
+ if (statusDebounceTimer) return;
202
+ statusDebounceTimer = setTimeout(() => {
203
+ statusDebounceTimer = null;
204
+ tunnel.sendStatusSummary(store.listSessionSummaries());
205
+ }, 500);
206
+ }
207
+
208
+ store.emitter.on("session:update", scheduleStatusSummary);
209
+ store.emitter.on("session:remove", scheduleStatusSummary);
210
+ }
211
+
212
+ return;
213
+ }
214
+
215
+ if (action === "attach") {
216
+ if (!commandExists("tmux")) {
217
+ throw new Error("tmux is required to attach to an AgentOffice worker.");
218
+ }
219
+
220
+ const target = subaction || command || options.session || options.id;
221
+ const server = options.server || DEFAULT_SERVER_URL;
222
+ const tmuxSession = await resolveAttachTarget({ target, server });
223
+ if (!tmuxSession) {
224
+ throw new Error(`unable to find a tmux worker for ${target || "<missing-session-id>"}`);
225
+ }
226
+
227
+ console.log(`Attaching local terminal to ${tmuxSession}`);
228
+ const attached = spawnSync(tmuxPath(), ["attach-session", "-t", tmuxSession], {
229
+ stdio: "inherit"
230
+ });
231
+ if (attached.status !== 0) {
232
+ throw new Error((attached.stderr || attached.stdout || "tmux attach failed").toString().trim());
233
+ }
234
+ return;
235
+ }
236
+
237
+ if (action === "codex" || action === "claude") {
238
+ const provider = action;
239
+ const binary = options.bin || resolveCommand(provider) || provider;
240
+ const server = options.server || DEFAULT_SERVER_URL;
241
+ const transport = options.transport || "tmux";
242
+ const sessionCommand = command || binary;
243
+ const result = await postJson(`${server}/api/sessions/launch`, {
244
+ provider,
245
+ title: options.title || `${provider[0].toUpperCase()}${provider.slice(1)} Session`,
246
+ cwd: options.cwd || process.cwd(),
247
+ command: sessionCommand,
248
+ transport
249
+ });
250
+ if (transport === "tmux") {
251
+ console.log(`Attaching local terminal to ${result.session.meta.tmuxSession}`);
252
+ const attached = spawnSync(tmuxPath(), ["attach-session", "-t", result.session.meta.tmuxSession], {
253
+ stdio: "inherit"
254
+ });
255
+ if (attached.status !== 0) {
256
+ throw new Error((attached.stderr || attached.stdout || "tmux attach failed").toString().trim());
257
+ }
258
+ return;
259
+ }
260
+ console.log(JSON.stringify(result.session, null, 2));
261
+ return;
262
+ }
263
+
264
+ if (action === "cleanup") {
265
+ if (!commandExists("tmux")) {
266
+ console.log("No cleanup needed: tmux is not installed.");
267
+ return;
268
+ }
269
+ const sessions = listAgentOfficeSessions();
270
+ const records = listSessionRecords();
271
+ if (sessions.length === 0) {
272
+ for (const record of records) {
273
+ removeSessionRecord(record.sessionId);
274
+ }
275
+ console.log(records.length > 0 ? `Removed ${records.length} stale AgentOffice record(s).` : "No AgentOffice tmux sessions found.");
276
+ return;
277
+ }
278
+ for (const sessionName of sessions) {
279
+ killSession(sessionName);
280
+ console.log(`Removed ${sessionName}`);
281
+ }
282
+ for (const record of records) {
283
+ removeSessionRecord(record.sessionId);
284
+ }
285
+ console.log(`Cleanup complete. Removed ${sessions.length} AgentOffice session(s).`);
286
+ return;
287
+ }
288
+
289
+ if (action === "run") {
290
+ if (!command) {
291
+ throw new Error("missing command after --");
292
+ }
293
+ const server = options.server || DEFAULT_SERVER_URL;
294
+ const transport = options.transport || defaultTransportForProvider(options.provider || "generic");
295
+ const result = await postJson(`${server}/api/sessions/launch`, {
296
+ provider: options.provider || "generic",
297
+ title: options.title || command,
298
+ cwd: options.cwd || process.cwd(),
299
+ command,
300
+ transport
301
+ });
302
+ if (options.attach) {
303
+ if (result.session.transport !== "tmux" || !result.session.meta || !result.session.meta.tmuxSession) {
304
+ throw new Error("--attach currently requires transport=tmux");
305
+ }
306
+ console.log(`Attaching local terminal to ${result.session.meta.tmuxSession}`);
307
+ const attached = spawnSync(tmuxPath(), ["attach-session", "-t", result.session.meta.tmuxSession], {
308
+ stdio: "inherit"
309
+ });
310
+ if (attached.status !== 0) {
311
+ throw new Error((attached.stderr || attached.stdout || "tmux attach failed").toString().trim());
312
+ }
313
+ return;
314
+ }
315
+ console.log(JSON.stringify(result.session, null, 2));
316
+ return;
317
+ }
318
+
319
+ if (action === "claude-hook") {
320
+ const server = options.server || DEFAULT_SERVER_URL;
321
+ try {
322
+ const input = fs.readFileSync(0, "utf8");
323
+ if (input.trim()) {
324
+ await postJson(`${server}/api/providers/claude/hook`, JSON.parse(input));
325
+ }
326
+ } catch (error) {
327
+ console.error(`AgentOffice claude-hook ignored error: ${error.message}`);
328
+ }
329
+ return;
330
+ }
331
+
332
+ if (action === "print-claude-hooks") {
333
+ const server = options.server || DEFAULT_SERVER_URL;
334
+ const settings = printClaudeHooksConfig({
335
+ serverUrl: server,
336
+ handlerPath: path.resolve(__dirname, "index.js")
337
+ });
338
+ console.log(JSON.stringify(settings, null, 2));
339
+ return;
340
+ }
341
+
342
+ throw new Error(`unknown action: ${action}`);
343
+ }
344
+
345
+ main().catch((error) => {
346
+ console.error(error.message);
347
+ process.exit(1);
348
+ });
@@ -0,0 +1,90 @@
1
+ const fs = require("node:fs");
2
+ const os = require("node:os");
3
+ const path = require("node:path");
4
+ const { spawnSync } = require("node:child_process");
5
+ const { printClaudeHooksConfig } = require("../core");
6
+
7
+ function commandExists(command) {
8
+ const result = spawnSync("/usr/bin/env", ["bash", "-lc", `command -v ${JSON.stringify(command)} >/dev/null 2>&1`]);
9
+ return result.status === 0;
10
+ }
11
+
12
+ function resolveCommand(command) {
13
+ const result = spawnSync("/usr/bin/env", ["bash", "-lc", `command -v ${JSON.stringify(command)}`], { encoding: "utf8" });
14
+ if (result.status !== 0) {
15
+ return null;
16
+ }
17
+ return (result.stdout || "").trim() || null;
18
+ }
19
+
20
+ function networkUrls({ host, port }) {
21
+ const urls = [];
22
+ if (host === "0.0.0.0") {
23
+ urls.push(`http://127.0.0.1:${port}`);
24
+ const interfaces = os.networkInterfaces();
25
+ for (const entries of Object.values(interfaces)) {
26
+ for (const entry of entries || []) {
27
+ if (!entry || entry.family !== "IPv4" || entry.internal) {
28
+ continue;
29
+ }
30
+ urls.push(`http://${entry.address}:${port}`);
31
+ }
32
+ }
33
+ } else {
34
+ urls.push(`http://${host}:${port}`);
35
+ }
36
+ return [...new Set(urls)];
37
+ }
38
+
39
+ function claudeSettingsPath() {
40
+ return path.join(os.homedir(), ".claude", "settings.json");
41
+ }
42
+
43
+ function readJson(filePath) {
44
+ try {
45
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ function hasClaudeHookConfig({ serverUrl, handlerPath }) {
52
+ const settings = readJson(claudeSettingsPath());
53
+ if (!settings || !settings.hooks) {
54
+ return false;
55
+ }
56
+ const expected = printClaudeHooksConfig({ serverUrl, handlerPath });
57
+ const hookEvent = expected.hooks && expected.hooks.SessionStart;
58
+ const actualEvent = settings.hooks && settings.hooks.SessionStart;
59
+ if (!hookEvent || !actualEvent) {
60
+ return false;
61
+ }
62
+ const expectedCommand = hookEvent[0] && hookEvent[0].hooks && hookEvent[0].hooks[0] && hookEvent[0].hooks[0].command;
63
+ const actualCommand = actualEvent[0] && actualEvent[0].hooks && actualEvent[0].hooks[0] && actualEvent[0].hooks[0].command;
64
+ return Boolean(expectedCommand && actualCommand && expectedCommand === actualCommand);
65
+ }
66
+
67
+ function applyClaudeHookConfig({ serverUrl, handlerPath }) {
68
+ const filePath = claudeSettingsPath();
69
+ const nextHooks = printClaudeHooksConfig({ serverUrl, handlerPath }).hooks;
70
+ const current = readJson(filePath) || {};
71
+ const next = {
72
+ ...current,
73
+ hooks: {
74
+ ...(current.hooks || {}),
75
+ ...nextHooks
76
+ }
77
+ };
78
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
79
+ fs.writeFileSync(filePath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
80
+ return filePath;
81
+ }
82
+
83
+ module.exports = {
84
+ applyClaudeHookConfig,
85
+ claudeSettingsPath,
86
+ commandExists,
87
+ hasClaudeHookConfig,
88
+ networkUrls,
89
+ resolveCommand
90
+ };
@@ -0,0 +1,49 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ function ensureExecutable(filePath) {
5
+ try {
6
+ const stats = fs.statSync(filePath);
7
+ const mode = stats.mode & 0o777;
8
+ if ((mode & 0o111) === 0o111) {
9
+ return false;
10
+ }
11
+ fs.chmodSync(filePath, mode | 0o755);
12
+ return true;
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+
18
+ function ensureNodePtySpawnHelper() {
19
+ let packageJsonPath;
20
+ try {
21
+ packageJsonPath = require.resolve("node-pty/package.json");
22
+ } catch {
23
+ return { changed: [], checked: [] };
24
+ }
25
+
26
+ const packageRoot = path.dirname(packageJsonPath);
27
+ const targets = [
28
+ path.join(packageRoot, "prebuilds", "darwin-arm64", "spawn-helper"),
29
+ path.join(packageRoot, "prebuilds", "darwin-x64", "spawn-helper")
30
+ ];
31
+
32
+ const checked = [];
33
+ const changed = [];
34
+ for (const target of targets) {
35
+ checked.push(target);
36
+ if (!fs.existsSync(target)) {
37
+ continue;
38
+ }
39
+ if (ensureExecutable(target)) {
40
+ changed.push(target);
41
+ }
42
+ }
43
+
44
+ return { changed, checked };
45
+ }
46
+
47
+ module.exports = {
48
+ ensureNodePtySpawnHelper
49
+ };
@@ -0,0 +1,54 @@
1
+ const { createPtyManager, defaultTransportForProvider } = require("./pty-manager");
2
+ const {
3
+ AGENTOFFICE_TMUX_PREFIX,
4
+ attachClient,
5
+ capturePane,
6
+ createTmuxSession,
7
+ describePane,
8
+ killSession,
9
+ listAgentOfficeSessions,
10
+ localAttachCommand,
11
+ sessionExists,
12
+ tmuxPath
13
+ } = require("./tmux");
14
+ const {
15
+ REGISTRY_DIR,
16
+ listSessionRecords,
17
+ persistSessionRecord,
18
+ removeSessionRecord
19
+ } = require("./session-registry");
20
+ const { ensureNodePtySpawnHelper } = require("./ensure-node-pty");
21
+ const {
22
+ applyClaudeHookConfig,
23
+ claudeSettingsPath,
24
+ commandExists,
25
+ hasClaudeHookConfig,
26
+ networkUrls,
27
+ resolveCommand
28
+ } = require("./cli-helpers");
29
+
30
+ module.exports = {
31
+ createPtyManager,
32
+ defaultTransportForProvider,
33
+ AGENTOFFICE_TMUX_PREFIX,
34
+ attachClient,
35
+ capturePane,
36
+ createTmuxSession,
37
+ describePane,
38
+ killSession,
39
+ listAgentOfficeSessions,
40
+ localAttachCommand,
41
+ sessionExists,
42
+ tmuxPath,
43
+ REGISTRY_DIR,
44
+ listSessionRecords,
45
+ persistSessionRecord,
46
+ removeSessionRecord,
47
+ ensureNodePtySpawnHelper,
48
+ applyClaudeHookConfig,
49
+ claudeSettingsPath,
50
+ commandExists,
51
+ hasClaudeHookConfig,
52
+ networkUrls,
53
+ resolveCommand
54
+ };