ztile-cli 0.0.1 → 0.3.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.
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+
9
+ export {
10
+ __require
11
+ };
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ __require
4
+ } from "./chunk-PDX44BCA.js";
5
+
6
+ // src/lib/credentials.ts
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
8
+ import { join } from "path";
9
+ function credentialsPath() {
10
+ const home = process.env["HOME"] ?? process.env["USERPROFILE"] ?? "/tmp";
11
+ return join(home, ".ztile", "credentials.json");
12
+ }
13
+ function loadCredentials() {
14
+ const path = credentialsPath();
15
+ if (!existsSync(path)) return null;
16
+ try {
17
+ return JSON.parse(readFileSync(path, "utf-8"));
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+ function saveCredentials(creds) {
23
+ const path = credentialsPath();
24
+ const dir = join(path, "..");
25
+ mkdirSync(dir, { recursive: true });
26
+ writeFileSync(path, JSON.stringify(creds, null, 2) + "\n", { mode: 384 });
27
+ }
28
+ function resolveApiKey() {
29
+ const envKey = process.env["ZTILE_API_KEY"];
30
+ if (envKey) return envKey;
31
+ const creds = loadCredentials();
32
+ if (creds?.apiKey) return creds.apiKey;
33
+ return "";
34
+ }
35
+ function resolveServerUrl() {
36
+ const envUrl = process.env["ZTILE_SERVER_URL"];
37
+ if (envUrl) return envUrl;
38
+ const creds = loadCredentials();
39
+ if (creds?.serverUrl) return creds.serverUrl;
40
+ return "https://ztile.dev";
41
+ }
42
+ function resolveMachineId() {
43
+ const envId = process.env["ZTILE_MACHINE_ID"];
44
+ if (envId) return envId;
45
+ const creds = loadCredentials();
46
+ if (creds?.machineId) return creds.machineId;
47
+ return "";
48
+ }
49
+ function pidFilePath() {
50
+ const home = process.env["HOME"] ?? process.env["USERPROFILE"] ?? "/tmp";
51
+ return join(home, ".ztile", "connect.pid");
52
+ }
53
+ function savePid(pid) {
54
+ const path = pidFilePath();
55
+ const dir = join(path, "..");
56
+ mkdirSync(dir, { recursive: true });
57
+ writeFileSync(path, String(pid));
58
+ }
59
+ function loadPid() {
60
+ const path = pidFilePath();
61
+ if (!existsSync(path)) return null;
62
+ try {
63
+ const pid = parseInt(readFileSync(path, "utf-8").trim(), 10);
64
+ return isNaN(pid) ? null : pid;
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+ function removePid() {
70
+ const path = pidFilePath();
71
+ try {
72
+ const { unlinkSync } = __require("fs");
73
+ unlinkSync(path);
74
+ } catch {
75
+ }
76
+ }
77
+ function isProcessRunning(pid) {
78
+ try {
79
+ process.kill(pid, 0);
80
+ return true;
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+
86
+ export {
87
+ loadCredentials,
88
+ saveCredentials,
89
+ resolveApiKey,
90
+ resolveServerUrl,
91
+ resolveMachineId,
92
+ savePid,
93
+ loadPid,
94
+ removePid,
95
+ isProcessRunning
96
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/cli.js ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ var args = process.argv.slice(2);
5
+ if (args[0] === "hook") {
6
+ const { handleHook } = await import("./hook-ZR4EVEUT.js");
7
+ await handleHook();
8
+ process.exit(0);
9
+ }
10
+ if (args[0] === "login") {
11
+ const { login } = await import("./login-3IOCLQWW.js");
12
+ await login({
13
+ apiKey: findArg(args, "--api-key"),
14
+ serverUrl: findArg(args, "--server-url"),
15
+ machineId: findArg(args, "--machine-id")
16
+ });
17
+ process.exit(0);
18
+ }
19
+ if (args[0] === "install") {
20
+ const { resolveApiKey } = await import("./credentials-OO2RZFLG.js");
21
+ const apiKey = resolveApiKey();
22
+ if (!apiKey) {
23
+ console.error("Error: Not logged in.");
24
+ console.error(" Run `ztile login` first.");
25
+ process.exit(1);
26
+ }
27
+ const { setupClaudeCode } = await import("./setup-VFHRFRZH.js");
28
+ await setupClaudeCode();
29
+ process.exit(0);
30
+ }
31
+ if (args[0] === "connect") {
32
+ const { runConnect } = await import("./connect-ZGETP6WZ.js");
33
+ const projectDir = findArg(args, "--project") ?? process.env["ZTILE_PROJECT"] ?? process.cwd();
34
+ const daemon = args.includes("--daemon") || args.includes("-d");
35
+ await runConnect({ projectDir, daemon });
36
+ }
37
+ if (args[0] === "disconnect") {
38
+ const { disconnect } = await import("./disconnect-TZ3MQUZS.js");
39
+ disconnect();
40
+ process.exit(0);
41
+ }
42
+ if (args.length === 0 || !["hook", "install", "connect", "disconnect", "login"].includes(args[0])) {
43
+ if (args.length > 0) console.error(`Unknown command: ${args[0]}`);
44
+ console.log("Usage:");
45
+ console.log(" ztile login Save API key and server config");
46
+ console.log(" ztile install Configure Claude Code hooks");
47
+ console.log(" ztile connect Connect to dashboard for remote control");
48
+ console.log(" ztile disconnect Stop background connection");
49
+ console.log(" ztile hook Handle Claude Code hook (internal)");
50
+ console.log("");
51
+ console.log("Options:");
52
+ console.log(" ztile connect -d Run in background (daemon mode)");
53
+ process.exit(args.length > 0 ? 1 : 0);
54
+ }
55
+ function findArg(args2, name) {
56
+ const idx = args2.indexOf(name);
57
+ if (idx === -1 || idx + 1 >= args2.length) return void 0;
58
+ return args2[idx + 1];
59
+ }
@@ -0,0 +1,370 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ isProcessRunning,
4
+ loadPid,
5
+ removePid,
6
+ resolveApiKey,
7
+ resolveServerUrl,
8
+ savePid
9
+ } from "./chunk-XSAVKZSF.js";
10
+ import "./chunk-PDX44BCA.js";
11
+
12
+ // src/commands/connect.ts
13
+ import { hostname } from "os";
14
+
15
+ // src/lib/workspace.ts
16
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
17
+ import { join } from "path";
18
+
19
+ // ../../node_modules/.pnpm/nanoid@5.1.6/node_modules/nanoid/index.js
20
+ import { webcrypto as crypto } from "crypto";
21
+
22
+ // ../../node_modules/.pnpm/nanoid@5.1.6/node_modules/nanoid/url-alphabet/index.js
23
+ var urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
24
+
25
+ // ../../node_modules/.pnpm/nanoid@5.1.6/node_modules/nanoid/index.js
26
+ var POOL_SIZE_MULTIPLIER = 128;
27
+ var pool;
28
+ var poolOffset;
29
+ function fillPool(bytes) {
30
+ if (!pool || pool.length < bytes) {
31
+ pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER);
32
+ crypto.getRandomValues(pool);
33
+ poolOffset = 0;
34
+ } else if (poolOffset + bytes > pool.length) {
35
+ crypto.getRandomValues(pool);
36
+ poolOffset = 0;
37
+ }
38
+ poolOffset += bytes;
39
+ }
40
+ function nanoid(size = 21) {
41
+ fillPool(size |= 0);
42
+ let id = "";
43
+ for (let i = poolOffset - size; i < poolOffset; i++) {
44
+ id += urlAlphabet[pool[i] & 63];
45
+ }
46
+ return id;
47
+ }
48
+
49
+ // src/lib/workspace.ts
50
+ function getWorkspaceId(projectDir) {
51
+ const dir = join(projectDir, ".ztile");
52
+ const path = join(dir, "workspace-id");
53
+ if (existsSync(path)) return readFileSync(path, "utf-8").trim();
54
+ mkdirSync(dir, { recursive: true });
55
+ const id = nanoid(12);
56
+ writeFileSync(path, id);
57
+ return id;
58
+ }
59
+
60
+ // src/lib/server-client.ts
61
+ function getConfig() {
62
+ return {
63
+ serverUrl: resolveServerUrl(),
64
+ apiKey: resolveApiKey()
65
+ };
66
+ }
67
+ async function request(method, path, body) {
68
+ const { serverUrl, apiKey } = getConfig();
69
+ const response = await fetch(`${serverUrl}${path}`, {
70
+ method,
71
+ headers: {
72
+ "Content-Type": "application/json",
73
+ "x-api-key": apiKey
74
+ },
75
+ body: body ? JSON.stringify(body) : void 0
76
+ });
77
+ const data = await response.json();
78
+ return { ok: response.ok, status: response.status, data };
79
+ }
80
+ async function sendHeartbeat(workspaceId, hostname2, sessions2) {
81
+ const result = await request("POST", "/api/workspaces/heartbeat", {
82
+ workspaceId,
83
+ hostname: hostname2,
84
+ sessions: sessions2
85
+ });
86
+ return result.ok;
87
+ }
88
+ async function pollCommands(workspaceId) {
89
+ const result = await request("GET", `/api/commands?workspaceId=${workspaceId}`);
90
+ if (!result.ok) return [];
91
+ return result.data?.commands ?? [];
92
+ }
93
+ async function updateCommand(commandId, status, result) {
94
+ const res = await request("PATCH", `/api/commands/${commandId}`, {
95
+ status,
96
+ result
97
+ });
98
+ return res.ok;
99
+ }
100
+ async function sendStreamData(sessionId, workspaceId, data) {
101
+ const res = await request("POST", `/api/sessions/${sessionId}/stream`, {
102
+ workspaceId,
103
+ data
104
+ });
105
+ return res.ok;
106
+ }
107
+
108
+ // src/lib/session-manager.ts
109
+ import { spawn as ptySpawn } from "node-pty";
110
+ var sessions = /* @__PURE__ */ new Map();
111
+ function log(msg) {
112
+ console.log(`[session] ${msg}`);
113
+ }
114
+ function attachStreamParser(pty, info, commandId) {
115
+ let buffer = "";
116
+ pty.onData((data) => {
117
+ buffer += data;
118
+ const lines = buffer.split("\n");
119
+ buffer = lines.pop() ?? "";
120
+ for (const line of lines) {
121
+ const trimmed = line.trim();
122
+ if (!trimmed) continue;
123
+ try {
124
+ const msg = JSON.parse(trimmed);
125
+ if (msg.type === "system" && msg.subtype === "init") {
126
+ info.sessionId = msg.session_id;
127
+ info.status = "running";
128
+ log(`session started: ${info.sessionId}`);
129
+ }
130
+ if (msg.type === "result") {
131
+ info.status = "idle";
132
+ log(`session completed: ${info.sessionId} (${msg.duration_ms}ms, $${msg.total_cost_usd})`);
133
+ }
134
+ if (info.sessionId) {
135
+ sendStreamData(info.sessionId, info.workspaceId, msg).catch((err) => {
136
+ log(`stream send error: ${err instanceof Error ? err.message : err}`);
137
+ });
138
+ }
139
+ } catch {
140
+ }
141
+ }
142
+ });
143
+ pty.onExit(({ exitCode }) => {
144
+ log(`process exited: code=${exitCode}, session=${info.sessionId}`);
145
+ info.status = "idle";
146
+ info.pty = null;
147
+ updateCommand(commandId, exitCode === 0 ? "completed" : "failed", {
148
+ sessionId: info.sessionId || void 0,
149
+ error: exitCode !== 0 ? `exit code ${exitCode}` : void 0
150
+ }).catch(() => {
151
+ });
152
+ });
153
+ }
154
+ function spawnSession(commandId, workspaceId, prompt, workdir, allowedTools) {
155
+ const args = ["-p", prompt, "--output-format", "stream-json", "--verbose"];
156
+ if (allowedTools?.length) {
157
+ args.push("--allowedTools", ...allowedTools);
158
+ }
159
+ const info = {
160
+ sessionId: "",
161
+ status: "starting",
162
+ pty: null,
163
+ workspaceId,
164
+ workdir
165
+ };
166
+ log(`spawning: claude ${args.join(" ").slice(0, 100)}...`);
167
+ const env = { ...process.env };
168
+ delete env.CLAUDECODE;
169
+ delete env.CLAUDE_CODE_SSE_PORT;
170
+ const pty = ptySpawn("claude", args, {
171
+ name: "xterm-256color",
172
+ cols: 200,
173
+ rows: 50,
174
+ cwd: workdir,
175
+ env
176
+ });
177
+ info.pty = pty;
178
+ attachStreamParser(pty, info, commandId);
179
+ sessions.set(commandId, info);
180
+ }
181
+ function sendPrompt(commandId, workspaceId, sessionId, prompt) {
182
+ let workdir = process.cwd();
183
+ for (const info2 of sessions.values()) {
184
+ if (info2.sessionId === sessionId) {
185
+ if (info2.status === "running") {
186
+ log(`session ${sessionId} is still running, rejecting prompt`);
187
+ updateCommand(commandId, "failed", { error: "Session is still running" }).catch(() => {
188
+ });
189
+ return;
190
+ }
191
+ workdir = info2.workdir;
192
+ break;
193
+ }
194
+ }
195
+ const args = [
196
+ "-p",
197
+ prompt,
198
+ "--resume",
199
+ sessionId,
200
+ "--output-format",
201
+ "stream-json",
202
+ "--verbose"
203
+ ];
204
+ log(`resume: session=${sessionId}`);
205
+ const env = { ...process.env };
206
+ delete env.CLAUDECODE;
207
+ delete env.CLAUDE_CODE_SSE_PORT;
208
+ const pty = ptySpawn("claude", args, {
209
+ name: "xterm-256color",
210
+ cols: 200,
211
+ rows: 50,
212
+ cwd: workdir,
213
+ env
214
+ });
215
+ const info = {
216
+ sessionId,
217
+ status: "running",
218
+ pty,
219
+ workspaceId,
220
+ workdir
221
+ };
222
+ attachStreamParser(pty, info, commandId);
223
+ sessions.set(sessionId, info);
224
+ }
225
+ function getSessionList() {
226
+ const result = [];
227
+ for (const info of sessions.values()) {
228
+ if (info.sessionId) {
229
+ result.push({
230
+ sessionId: info.sessionId,
231
+ mode: "operate",
232
+ status: info.status
233
+ });
234
+ }
235
+ }
236
+ return result;
237
+ }
238
+
239
+ // src/commands/connect.ts
240
+ var POLL_INTERVAL_MS = 5e3;
241
+ var HEARTBEAT_INTERVAL_MS = 15e3;
242
+ var DIM = "\x1B[2m";
243
+ var RESET = "\x1B[0m";
244
+ var GREEN = "\x1B[32m";
245
+ var YELLOW = "\x1B[33m";
246
+ var CYAN = "\x1B[36m";
247
+ var RED = "\x1B[31m";
248
+ var BOLD = "\x1B[1m";
249
+ function timestamp() {
250
+ return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
251
+ }
252
+ function log2(icon, msg, color = "") {
253
+ console.log(`${DIM}${timestamp()}${RESET} ${color}${icon}${RESET} ${msg}`);
254
+ }
255
+ async function handleCommand(cmd, workspaceId, projectDir) {
256
+ switch (cmd.type) {
257
+ case "spawn": {
258
+ const prompt = cmd.payload.prompt;
259
+ const workdir = cmd.payload.workdir ?? projectDir;
260
+ const allowedTools = cmd.payload.allowedTools;
261
+ log2("\u26A1", `Spawn session: "${prompt.slice(0, 60)}${prompt.length > 60 ? "..." : ""}"`, CYAN);
262
+ await updateCommand(cmd.id, "processing");
263
+ spawnSession(cmd.id, workspaceId, prompt, workdir, allowedTools);
264
+ break;
265
+ }
266
+ case "prompt": {
267
+ if (!cmd.sessionId) {
268
+ log2("\u2717", `Command failed: missing sessionId`, RED);
269
+ await updateCommand(cmd.id, "failed", { error: "Missing sessionId" });
270
+ return;
271
+ }
272
+ const prompt = cmd.payload.prompt;
273
+ log2("\u2192", `Prompt sent: "${prompt.slice(0, 60)}${prompt.length > 60 ? "..." : ""}"`, GREEN);
274
+ await updateCommand(cmd.id, "processing");
275
+ sendPrompt(cmd.id, workspaceId, cmd.sessionId, prompt);
276
+ break;
277
+ }
278
+ default:
279
+ log2("?", `Unknown command: ${cmd.type}`, YELLOW);
280
+ await updateCommand(cmd.id, "failed", { error: `Unknown command type: ${cmd.type}` });
281
+ }
282
+ }
283
+ async function runConnect(options) {
284
+ const apiKey = resolveApiKey();
285
+ if (!apiKey) {
286
+ console.error("Error: Not logged in. Run `ztile login` first.");
287
+ process.exit(1);
288
+ }
289
+ const { projectDir, daemon } = options;
290
+ const workspaceId = getWorkspaceId(projectDir);
291
+ const host = hostname();
292
+ const serverUrl = resolveServerUrl();
293
+ const existingPid = loadPid();
294
+ if (existingPid && isProcessRunning(existingPid)) {
295
+ console.error(`Error: Already connected (PID: ${existingPid})`);
296
+ console.error(" Run `ztile disconnect` first.");
297
+ process.exit(1);
298
+ }
299
+ if (daemon) {
300
+ const { spawn } = await import("child_process");
301
+ const child = spawn(process.argv[0], [...process.argv.slice(1).filter((a) => a !== "--daemon" && a !== "-d")], {
302
+ detached: true,
303
+ stdio: "ignore"
304
+ });
305
+ child.unref();
306
+ savePid(child.pid);
307
+ console.log(`Ztile daemon started (PID: ${child.pid})`);
308
+ console.log(` Workspace: ${workspaceId}`);
309
+ console.log(` Server: ${serverUrl}`);
310
+ console.log("");
311
+ console.log("Run `ztile disconnect` to stop.");
312
+ process.exit(0);
313
+ }
314
+ savePid(process.pid);
315
+ console.log("");
316
+ console.log(`${BOLD}Ztile Connect${RESET}`);
317
+ console.log(`${DIM}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${RESET}`);
318
+ console.log(` Server: ${serverUrl}`);
319
+ console.log(` Workspace: ${workspaceId}`);
320
+ console.log(` Hostname: ${host}`);
321
+ console.log(` Project: ${projectDir}`);
322
+ console.log(`${DIM}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${RESET}`);
323
+ console.log("");
324
+ const ok = await sendHeartbeat(workspaceId, host, []);
325
+ if (ok) {
326
+ log2("\u25CF", `Connected to ${serverUrl}`, GREEN);
327
+ } else {
328
+ log2("\u25CF", `Failed to connect (will retry)`, RED);
329
+ }
330
+ log2("\u25CE", `Waiting for commands...`, DIM);
331
+ let lastSessionCount = 0;
332
+ const heartbeatTimer = setInterval(async () => {
333
+ const sessions2 = getSessionList();
334
+ const ok2 = await sendHeartbeat(workspaceId, host, sessions2).catch(() => false);
335
+ if (sessions2.length !== lastSessionCount) {
336
+ log2("\u25CE", `${sessions2.length} active session${sessions2.length !== 1 ? "s" : ""}`, DIM);
337
+ lastSessionCount = sessions2.length;
338
+ }
339
+ if (!ok2) {
340
+ log2("!", `Heartbeat failed, retrying...`, YELLOW);
341
+ }
342
+ }, HEARTBEAT_INTERVAL_MS);
343
+ let commandCount = 0;
344
+ const pollTimer = setInterval(async () => {
345
+ try {
346
+ const commands = await pollCommands(workspaceId);
347
+ for (const cmd of commands) {
348
+ commandCount++;
349
+ await handleCommand(cmd, workspaceId, projectDir);
350
+ }
351
+ } catch (err) {
352
+ log2("!", `Poll error: ${err instanceof Error ? err.message : err}`, RED);
353
+ }
354
+ }, POLL_INTERVAL_MS);
355
+ const shutdown = () => {
356
+ console.log("");
357
+ log2("\u25CF", `Disconnected (${commandCount} command${commandCount !== 1 ? "s" : ""} processed)`, DIM);
358
+ clearInterval(heartbeatTimer);
359
+ clearInterval(pollTimer);
360
+ removePid();
361
+ process.exit(0);
362
+ };
363
+ process.on("SIGINT", shutdown);
364
+ process.on("SIGTERM", shutdown);
365
+ await new Promise(() => {
366
+ });
367
+ }
368
+ export {
369
+ runConnect
370
+ };
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ isProcessRunning,
4
+ loadCredentials,
5
+ loadPid,
6
+ removePid,
7
+ resolveApiKey,
8
+ resolveMachineId,
9
+ resolveServerUrl,
10
+ saveCredentials,
11
+ savePid
12
+ } from "./chunk-XSAVKZSF.js";
13
+ import "./chunk-PDX44BCA.js";
14
+ export {
15
+ isProcessRunning,
16
+ loadCredentials,
17
+ loadPid,
18
+ removePid,
19
+ resolveApiKey,
20
+ resolveMachineId,
21
+ resolveServerUrl,
22
+ saveCredentials,
23
+ savePid
24
+ };
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ isProcessRunning,
4
+ loadPid,
5
+ removePid
6
+ } from "./chunk-XSAVKZSF.js";
7
+ import "./chunk-PDX44BCA.js";
8
+
9
+ // src/commands/disconnect.ts
10
+ function disconnect() {
11
+ const pid = loadPid();
12
+ if (!pid) {
13
+ console.log("No active connection found.");
14
+ return;
15
+ }
16
+ if (!isProcessRunning(pid)) {
17
+ console.log(`Stale PID file (process ${pid} not running). Cleaning up.`);
18
+ removePid();
19
+ return;
20
+ }
21
+ try {
22
+ process.kill(pid, "SIGTERM");
23
+ removePid();
24
+ console.log(`Disconnected (PID: ${pid})`);
25
+ } catch (err) {
26
+ console.error(`Error stopping process ${pid}: ${err instanceof Error ? err.message : err}`);
27
+ removePid();
28
+ }
29
+ }
30
+ export {
31
+ disconnect
32
+ };
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+ import "./chunk-PDX44BCA.js";
3
+
4
+ // src/commands/hook.ts
5
+ import { readFile as readFile2, stat } from "fs/promises";
6
+
7
+ // src/state.ts
8
+ import { readFile, writeFile, mkdir, open, unlink } from "fs/promises";
9
+ import { join } from "path";
10
+ import { existsSync } from "fs";
11
+ var STATE_DIR = join(process.env["HOME"] ?? process.env["USERPROFILE"] ?? "/tmp", ".ztile");
12
+ var STATE_FILE = join(STATE_DIR, "state.json");
13
+ var LOCK_FILE = join(STATE_DIR, "hook.lock");
14
+ var LOCK_TIMEOUT_MS = 5e3;
15
+ var LOCK_RETRY_MS = 50;
16
+ var LOCK_STALE_MS = 1e4;
17
+ async function loadState() {
18
+ try {
19
+ const content = await readFile(STATE_FILE, "utf-8");
20
+ return JSON.parse(content);
21
+ } catch {
22
+ return { transcripts: {} };
23
+ }
24
+ }
25
+ async function saveState(state) {
26
+ if (!existsSync(STATE_DIR)) {
27
+ await mkdir(STATE_DIR, { recursive: true });
28
+ }
29
+ await writeFile(STATE_FILE, JSON.stringify(state, null, 2) + "\n");
30
+ }
31
+ async function getByteOffset(transcriptPath) {
32
+ const state = await loadState();
33
+ return state.transcripts[transcriptPath]?.last_byte_offset ?? 0;
34
+ }
35
+ async function updateByteOffset(transcriptPath, offset, sessionId) {
36
+ const state = await loadState();
37
+ state.transcripts[transcriptPath] = {
38
+ last_byte_offset: offset,
39
+ session_id: sessionId,
40
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
41
+ };
42
+ await saveState(state);
43
+ }
44
+ async function acquireLock() {
45
+ if (!existsSync(STATE_DIR)) {
46
+ await mkdir(STATE_DIR, { recursive: true });
47
+ }
48
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
49
+ while (Date.now() < deadline) {
50
+ try {
51
+ const fd = await open(LOCK_FILE, "wx");
52
+ await fd.writeFile(String(Date.now()));
53
+ await fd.close();
54
+ return true;
55
+ } catch (err) {
56
+ if (err.code === "EEXIST") {
57
+ try {
58
+ const content = await readFile(LOCK_FILE, "utf-8");
59
+ const lockTime = parseInt(content, 10);
60
+ if (!isNaN(lockTime) && Date.now() - lockTime > LOCK_STALE_MS) {
61
+ await unlink(LOCK_FILE).catch(() => {
62
+ });
63
+ continue;
64
+ }
65
+ } catch {
66
+ }
67
+ await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
68
+ continue;
69
+ }
70
+ throw err;
71
+ }
72
+ }
73
+ return false;
74
+ }
75
+ async function releaseLock() {
76
+ await unlink(LOCK_FILE).catch(() => {
77
+ });
78
+ }
79
+
80
+ // src/outputs/server.ts
81
+ var DEFAULT_SERVER_URL = "https://ztile.dev";
82
+ function getServerConfig() {
83
+ return {
84
+ serverUrl: process.env["ZTILE_SERVER_URL"] ?? DEFAULT_SERVER_URL,
85
+ apiKey: process.env["ZTILE_API_KEY"]
86
+ };
87
+ }
88
+ async function sendRawToServer(payload) {
89
+ const { serverUrl, apiKey } = getServerConfig();
90
+ if (!apiKey) {
91
+ console.error("[server] No API key configured (ZTILE_API_KEY)");
92
+ return false;
93
+ }
94
+ try {
95
+ const response = await fetch(`${serverUrl}/api/hooks/ingest`, {
96
+ method: "POST",
97
+ headers: {
98
+ "Content-Type": "application/json",
99
+ "x-api-key": apiKey
100
+ },
101
+ body: JSON.stringify(payload)
102
+ });
103
+ if (!response.ok) {
104
+ console.error(
105
+ `[server] Failed to send raw data: ${response.status} ${response.statusText}`
106
+ );
107
+ return false;
108
+ }
109
+ return true;
110
+ } catch (error) {
111
+ console.error(
112
+ `[server] Connection error: ${error instanceof Error ? error.message : error}`
113
+ );
114
+ return false;
115
+ }
116
+ }
117
+
118
+ // src/commands/hook.ts
119
+ import { appendFileSync } from "fs";
120
+ import { join as join2 } from "path";
121
+ import { hostname } from "os";
122
+ var LOG_FILE = join2(process.env["HOME"] ?? "/tmp", ".ztile", "hook-debug.log");
123
+ function debugLog(msg) {
124
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
125
+ `;
126
+ try {
127
+ appendFileSync(LOG_FILE, line);
128
+ } catch {
129
+ }
130
+ console.error(msg);
131
+ }
132
+ async function readStdin() {
133
+ const chunks = [];
134
+ for await (const chunk of process.stdin) {
135
+ chunks.push(chunk);
136
+ }
137
+ const text = Buffer.concat(chunks).toString("utf-8").trim();
138
+ if (!text) {
139
+ throw new Error("No input received on stdin");
140
+ }
141
+ return JSON.parse(text);
142
+ }
143
+ async function handleHook() {
144
+ const payload = await readStdin();
145
+ const hookEvent = payload["hook_event_name"];
146
+ const transcriptPath = payload["transcript_path"];
147
+ const sessionId = payload["session_id"];
148
+ const cwd = payload["cwd"];
149
+ debugLog(`[hook] ${hookEvent}: payload keys = ${Object.keys(payload).join(", ")}`);
150
+ if (!transcriptPath) {
151
+ debugLog("[hook] No transcript_path in payload, skipping");
152
+ return;
153
+ }
154
+ if (!sessionId) {
155
+ debugLog("[hook] No session_id in payload, skipping");
156
+ return;
157
+ }
158
+ const machineId = process.env["ZTILE_MACHINE_ID"] ?? hostname();
159
+ if (hookEvent === "UserPromptSubmit") {
160
+ const prompt = payload["prompt"];
161
+ if (prompt) {
162
+ const ok = await sendRawToServer({
163
+ hook_event: hookEvent,
164
+ session_id: sessionId,
165
+ machine_id: machineId,
166
+ project: cwd,
167
+ prompt
168
+ });
169
+ debugLog(`[hook] ${hookEvent}: prompt sent` + (ok ? " \u2192 server" : " (send failed)"));
170
+ } else {
171
+ debugLog(`[hook] ${hookEvent}: no prompt in payload, skipping`);
172
+ }
173
+ return;
174
+ }
175
+ if (hookEvent === "PermissionRequest") {
176
+ const toolName = payload["tool_name"];
177
+ const toolInput = payload["tool_input"];
178
+ const ok = await sendRawToServer({
179
+ hook_event: hookEvent,
180
+ session_id: sessionId,
181
+ machine_id: machineId,
182
+ project: cwd,
183
+ tool_name: toolName,
184
+ tool_input: toolInput
185
+ });
186
+ debugLog(`[hook] ${hookEvent}: ${toolName}` + (ok ? " \u2192 server" : " (send failed)"));
187
+ return;
188
+ }
189
+ if (hookEvent === "SessionEnd") {
190
+ const ok = await sendRawToServer({
191
+ hook_event: hookEvent,
192
+ session_id: sessionId,
193
+ machine_id: machineId,
194
+ project: cwd
195
+ });
196
+ debugLog(`[hook] ${hookEvent}` + (ok ? " \u2192 server" : " (send failed)"));
197
+ return;
198
+ }
199
+ const locked = await acquireLock();
200
+ if (!locked) {
201
+ debugLog(`[hook] ${hookEvent}: could not acquire lock, skipping`);
202
+ return;
203
+ }
204
+ try {
205
+ const fromOffset = await getByteOffset(transcriptPath);
206
+ const fileInfo = await stat(transcriptPath);
207
+ if (fileInfo.size <= fromOffset) {
208
+ debugLog(`[hook] ${hookEvent}: no diff (offset ${fromOffset} \u2192 ${fileInfo.size})`);
209
+ await updateByteOffset(transcriptPath, fileInfo.size, sessionId);
210
+ return;
211
+ }
212
+ const buffer = await readFile2(transcriptPath);
213
+ const newData = buffer.subarray(fromOffset);
214
+ const text = newData.toString("utf-8");
215
+ const lines = text.split("\n").filter((l) => l.trim() !== "");
216
+ if (lines.length === 0) {
217
+ debugLog(`[hook] ${hookEvent}: no lines in diff`);
218
+ await updateByteOffset(transcriptPath, fileInfo.size, sessionId);
219
+ return;
220
+ }
221
+ const ok = await sendRawToServer({
222
+ hook_event: hookEvent ?? "unknown",
223
+ session_id: sessionId,
224
+ machine_id: machineId,
225
+ project: cwd,
226
+ lines
227
+ });
228
+ debugLog(
229
+ `[hook] ${hookEvent}: ${lines.length} raw lines` + (ok ? " \u2192 server" : " (send failed)")
230
+ );
231
+ await updateByteOffset(transcriptPath, fileInfo.size, sessionId);
232
+ } finally {
233
+ await releaseLock();
234
+ }
235
+ }
236
+ export {
237
+ handleHook
238
+ };
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ loadCredentials,
4
+ saveCredentials
5
+ } from "./chunk-XSAVKZSF.js";
6
+ import {
7
+ __require
8
+ } from "./chunk-PDX44BCA.js";
9
+
10
+ // src/commands/login.ts
11
+ import { createInterface } from "readline";
12
+ import { hostname } from "os";
13
+ async function prompt(question) {
14
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
15
+ return new Promise((resolve) => {
16
+ rl.question(question, (answer) => {
17
+ rl.close();
18
+ resolve(answer.trim());
19
+ });
20
+ });
21
+ }
22
+ function openBrowser(url) {
23
+ const { execSync } = __require("child_process");
24
+ try {
25
+ const platform = process.platform;
26
+ if (platform === "darwin") execSync(`open "${url}"`);
27
+ else if (platform === "win32") execSync(`start "${url}"`);
28
+ else execSync(`xdg-open "${url}" 2>/dev/null || echo ""`);
29
+ } catch {
30
+ }
31
+ }
32
+ async function login(options) {
33
+ const existing = loadCredentials();
34
+ const defaultServer = existing?.serverUrl ?? "https://ztile.dev";
35
+ const defaultMachine = existing?.machineId ?? hostname();
36
+ let apiKey = options.apiKey;
37
+ let serverUrl = options.serverUrl ?? defaultServer;
38
+ const machineId = options.machineId ?? defaultMachine;
39
+ if (apiKey) {
40
+ await verifyAndSave(apiKey, serverUrl, machineId);
41
+ return;
42
+ }
43
+ console.log("");
44
+ console.log("How would you like to authenticate?");
45
+ console.log("");
46
+ console.log(" 1) Open browser to get API key (recommended)");
47
+ console.log(" 2) Enter API key directly");
48
+ console.log("");
49
+ const choice = await prompt("Choice [1]: ");
50
+ const method = choice === "2" ? "direct" : "browser";
51
+ if (method === "browser") {
52
+ const settingsUrl = `${serverUrl}/settings`;
53
+ console.log("");
54
+ console.log(`Opening ${settingsUrl} ...`);
55
+ console.log("Copy your API key from the Settings page and paste it below.");
56
+ console.log("");
57
+ openBrowser(settingsUrl);
58
+ apiKey = await prompt("API Key: ");
59
+ } else {
60
+ apiKey = await prompt("API Key: ");
61
+ }
62
+ if (!apiKey) {
63
+ console.error("Error: API key is required.");
64
+ process.exit(1);
65
+ }
66
+ if (!options.serverUrl) {
67
+ const input = await prompt(`Server URL [${defaultServer}]: `);
68
+ if (input) serverUrl = input;
69
+ }
70
+ await verifyAndSave(apiKey, serverUrl, machineId);
71
+ }
72
+ async function verifyAndSave(apiKey, serverUrl, machineId) {
73
+ process.stderr.write("Verifying... ");
74
+ try {
75
+ const res = await fetch(`${serverUrl}/api/hooks/ingest`, {
76
+ method: "POST",
77
+ headers: { "Content-Type": "application/json", "x-api-key": apiKey },
78
+ body: JSON.stringify({})
79
+ });
80
+ if (res.status === 401) {
81
+ console.error("failed");
82
+ console.error("Error: Invalid API key.");
83
+ process.exit(1);
84
+ }
85
+ } catch {
86
+ console.error("failed");
87
+ console.error(`Error: Could not connect to ${serverUrl}`);
88
+ process.exit(1);
89
+ }
90
+ console.error("ok");
91
+ saveCredentials({ apiKey, serverUrl, machineId });
92
+ console.log("");
93
+ console.log("Logged in successfully!");
94
+ console.log(` Server: ${serverUrl}`);
95
+ console.log(` Machine ID: ${machineId}`);
96
+ console.log(` Saved to: ~/.ztile/credentials.json`);
97
+ console.log("");
98
+ console.log("Next: run `ztile install` to configure Claude Code hooks.");
99
+ }
100
+ export {
101
+ login
102
+ };
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ import "./chunk-PDX44BCA.js";
3
+
4
+ // src/commands/setup.ts
5
+ import { readFile, writeFile } from "fs/promises";
6
+ import { join } from "path";
7
+ import { existsSync } from "fs";
8
+ import { execSync } from "child_process";
9
+ var HOOK_EVENTS = [
10
+ "UserPromptSubmit",
11
+ "PreToolUse",
12
+ "PostToolUse",
13
+ "PostToolUseFailure",
14
+ "PermissionRequest",
15
+ "SubagentStart",
16
+ "SubagentStop",
17
+ "Stop",
18
+ "PreCompact",
19
+ "Notification",
20
+ "SessionEnd"
21
+ ];
22
+ async function setupClaudeCode(_options) {
23
+ const ztileBin = findGlobalBin();
24
+ if (!ztileBin) {
25
+ console.error("Error: ztile is not globally installed.");
26
+ console.error("");
27
+ console.error(" npm install -g ztile");
28
+ console.error(" ztile setup --api-key <key>");
29
+ console.error("");
30
+ process.exit(1);
31
+ }
32
+ const homeDir = process.env["HOME"] ?? process.env["USERPROFILE"] ?? "/tmp";
33
+ const claudeDir = join(homeDir, ".claude");
34
+ const settingsPath = join(claudeDir, "settings.json");
35
+ if (!existsSync(claudeDir)) {
36
+ console.error(
37
+ "Claude Code not found (~/.claude/ does not exist). Skipping."
38
+ );
39
+ return;
40
+ }
41
+ let settings = {};
42
+ try {
43
+ const content = await readFile(settingsPath, "utf-8");
44
+ settings = JSON.parse(content);
45
+ } catch {
46
+ }
47
+ const hooks = settings["hooks"] ?? {};
48
+ const hookCommand = `${ztileBin} hook`;
49
+ const ztileHook = { type: "command", command: hookCommand };
50
+ for (const event of Object.keys(hooks)) {
51
+ const existing = hooks[event] ?? [];
52
+ hooks[event] = existing.filter(
53
+ (entry) => !entry.hooks?.some(
54
+ (h) => h.type === "command" && (h.command?.includes("ztile hook") || h.command?.includes("ztile/apps/collector")) || h.type === "http" && h.url?.includes("/api/hooks/claude")
55
+ )
56
+ );
57
+ if (hooks[event].length === 0) {
58
+ delete hooks[event];
59
+ }
60
+ }
61
+ let addedCount = 0;
62
+ for (const event of HOOK_EVENTS) {
63
+ const existing = hooks[event] ?? [];
64
+ hooks[event] = [...existing, { hooks: [ztileHook] }];
65
+ addedCount++;
66
+ }
67
+ settings["hooks"] = hooks;
68
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n");
69
+ console.log(`Claude Code: configured ${addedCount} hook(s) in ${settingsPath}`);
70
+ console.log(` Binary: ${ztileBin}`);
71
+ console.log(` Events: ${HOOK_EVENTS.join(", ")}`);
72
+ console.log();
73
+ console.log("Next steps:");
74
+ console.log(" \u2022 Restart Claude Code for hooks to take effect");
75
+ console.log(" \u2022 Run `ztile connect` to enable remote control from the dashboard");
76
+ }
77
+ function findGlobalBin() {
78
+ try {
79
+ const resolved = execSync("which ztile", { encoding: "utf-8" }).trim();
80
+ return resolved || null;
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+ export {
86
+ setupClaudeCode
87
+ };
package/package.json CHANGED
@@ -1,7 +1,44 @@
1
1
  {
2
2
  "name": "ztile-cli",
3
- "version": "0.0.1",
4
- "description": "Mission control for autonomous coding agents",
5
- "main": "index.js",
6
- "license": "MIT"
3
+ "version": "0.3.0",
4
+ "description": "Mission control for autonomous coding agents (Claude Code, Codex, Gemini CLI)",
5
+ "bin": {
6
+ "ztile": "dist/cli.js"
7
+ },
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsup",
13
+ "dev": "tsup --watch",
14
+ "lint": "tsc --noEmit",
15
+ "check-types": "tsc --noEmit"
16
+ },
17
+ "dependencies": {
18
+ "nanoid": "^5.1.5"
19
+ },
20
+ "optionalDependencies": {
21
+ "node-pty": "^1.1.0"
22
+ },
23
+ "devDependencies": {
24
+ "@ztile/tsconfig": "workspace:*",
25
+ "@types/node": "25.3.5",
26
+ "tsup": "^8.0.0",
27
+ "typescript": "5.9.2"
28
+ },
29
+ "keywords": [
30
+ "ai",
31
+ "coding-agent",
32
+ "claude-code",
33
+ "codex",
34
+ "gemini-cli",
35
+ "monitoring",
36
+ "observability"
37
+ ],
38
+ "license": "MIT",
39
+ "type": "module",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/shunyooo/ztile.git"
43
+ }
7
44
  }
package/index.js DELETED
@@ -1 +0,0 @@
1
- // placeholder