ztile-cli 0.0.1 → 0.2.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/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/cli.js ADDED
@@ -0,0 +1,43 @@
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-SN73JWB3.js");
7
+ await handleHook();
8
+ process.exit(0);
9
+ }
10
+ if (args[0] === "setup") {
11
+ const { setupClaudeCode } = await import("./setup-6PUWCUFR.js");
12
+ const apiKey = process.env["ZTILE_API_KEY"] ?? findArg(args, "--api-key");
13
+ if (!apiKey) {
14
+ console.error("Error: API key is required.");
15
+ console.error(" Set ZTILE_API_KEY env var, or pass --api-key <key>");
16
+ process.exit(1);
17
+ }
18
+ await setupClaudeCode({
19
+ apiKey,
20
+ serverUrl: process.env["ZTILE_SERVER_URL"] ?? findArg(args, "--server-url"),
21
+ machineId: process.env["ZTILE_MACHINE_ID"] ?? findArg(args, "--machine-id"),
22
+ project: process.env["ZTILE_PROJECT"] ?? findArg(args, "--project")
23
+ });
24
+ process.exit(0);
25
+ }
26
+ if (args[0] === "daemon") {
27
+ const { runDaemon } = await import("./daemon-KSDSRNAZ.js");
28
+ const projectDir = findArg(args, "--project") ?? process.env["ZTILE_PROJECT"] ?? process.cwd();
29
+ await runDaemon(projectDir);
30
+ }
31
+ if (args.length === 0 || !["hook", "setup", "daemon"].includes(args[0])) {
32
+ if (args.length > 0) console.error(`Unknown command: ${args[0]}`);
33
+ console.log("Usage:");
34
+ console.log(" ztile hook Handle Claude Code hook (stdin \u2192 transcript diff \u2192 server)");
35
+ console.log(" ztile setup Configure Claude Code hooks");
36
+ console.log(" ztile daemon Start daemon for remote control (poll server for commands)");
37
+ process.exit(args.length > 0 ? 1 : 0);
38
+ }
39
+ function findArg(args2, name) {
40
+ const idx = args2.indexOf(name);
41
+ if (idx === -1 || idx + 1 >= args2.length) return void 0;
42
+ return args2[idx + 1];
43
+ }
@@ -0,0 +1,311 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/daemon.ts
4
+ import { hostname } from "os";
5
+
6
+ // src/lib/workspace.ts
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
8
+ import { join } from "path";
9
+
10
+ // ../../node_modules/.pnpm/nanoid@5.1.6/node_modules/nanoid/index.js
11
+ import { webcrypto as crypto } from "crypto";
12
+
13
+ // ../../node_modules/.pnpm/nanoid@5.1.6/node_modules/nanoid/url-alphabet/index.js
14
+ var urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
15
+
16
+ // ../../node_modules/.pnpm/nanoid@5.1.6/node_modules/nanoid/index.js
17
+ var POOL_SIZE_MULTIPLIER = 128;
18
+ var pool;
19
+ var poolOffset;
20
+ function fillPool(bytes) {
21
+ if (!pool || pool.length < bytes) {
22
+ pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER);
23
+ crypto.getRandomValues(pool);
24
+ poolOffset = 0;
25
+ } else if (poolOffset + bytes > pool.length) {
26
+ crypto.getRandomValues(pool);
27
+ poolOffset = 0;
28
+ }
29
+ poolOffset += bytes;
30
+ }
31
+ function nanoid(size = 21) {
32
+ fillPool(size |= 0);
33
+ let id = "";
34
+ for (let i = poolOffset - size; i < poolOffset; i++) {
35
+ id += urlAlphabet[pool[i] & 63];
36
+ }
37
+ return id;
38
+ }
39
+
40
+ // src/lib/workspace.ts
41
+ function getWorkspaceId(projectDir) {
42
+ const dir = join(projectDir, ".ztile");
43
+ const path = join(dir, "workspace-id");
44
+ if (existsSync(path)) return readFileSync(path, "utf-8").trim();
45
+ mkdirSync(dir, { recursive: true });
46
+ const id = nanoid(12);
47
+ writeFileSync(path, id);
48
+ return id;
49
+ }
50
+
51
+ // src/lib/server-client.ts
52
+ var DEFAULT_SERVER_URL = "https://ztile.dev";
53
+ function getConfig() {
54
+ return {
55
+ serverUrl: process.env["ZTILE_SERVER_URL"] ?? DEFAULT_SERVER_URL,
56
+ apiKey: process.env["ZTILE_API_KEY"] ?? ""
57
+ };
58
+ }
59
+ async function request(method, path, body) {
60
+ const { serverUrl, apiKey } = getConfig();
61
+ const response = await fetch(`${serverUrl}${path}`, {
62
+ method,
63
+ headers: {
64
+ "Content-Type": "application/json",
65
+ "x-api-key": apiKey
66
+ },
67
+ body: body ? JSON.stringify(body) : void 0
68
+ });
69
+ const data = await response.json();
70
+ return { ok: response.ok, status: response.status, data };
71
+ }
72
+ async function sendHeartbeat(workspaceId, hostname2, sessions2) {
73
+ const result = await request("POST", "/api/workspaces/heartbeat", {
74
+ workspaceId,
75
+ hostname: hostname2,
76
+ sessions: sessions2
77
+ });
78
+ return result.ok;
79
+ }
80
+ async function pollCommands(workspaceId) {
81
+ const result = await request("GET", `/api/commands?workspaceId=${workspaceId}`);
82
+ if (!result.ok) return [];
83
+ return result.data?.commands ?? [];
84
+ }
85
+ async function updateCommand(commandId, status, result) {
86
+ const res = await request("PATCH", `/api/commands/${commandId}`, {
87
+ status,
88
+ result
89
+ });
90
+ return res.ok;
91
+ }
92
+ async function sendStreamData(sessionId, workspaceId, data) {
93
+ const res = await request("POST", `/api/sessions/${sessionId}/stream`, {
94
+ workspaceId,
95
+ data
96
+ });
97
+ return res.ok;
98
+ }
99
+
100
+ // src/lib/session-manager.ts
101
+ import { spawn as ptySpawn } from "node-pty";
102
+ var sessions = /* @__PURE__ */ new Map();
103
+ function log(msg) {
104
+ console.log(`[session] ${msg}`);
105
+ }
106
+ function attachStreamParser(pty, info, commandId) {
107
+ let buffer = "";
108
+ pty.onData((data) => {
109
+ buffer += data;
110
+ const lines = buffer.split("\n");
111
+ buffer = lines.pop() ?? "";
112
+ for (const line of lines) {
113
+ const trimmed = line.trim();
114
+ if (!trimmed) continue;
115
+ try {
116
+ const msg = JSON.parse(trimmed);
117
+ if (msg.type === "system" && msg.subtype === "init") {
118
+ info.sessionId = msg.session_id;
119
+ info.status = "running";
120
+ log(`session started: ${info.sessionId}`);
121
+ }
122
+ if (msg.type === "result") {
123
+ info.status = "idle";
124
+ log(`session completed: ${info.sessionId} (${msg.duration_ms}ms, $${msg.total_cost_usd})`);
125
+ }
126
+ if (info.sessionId) {
127
+ sendStreamData(info.sessionId, info.workspaceId, msg).catch((err) => {
128
+ log(`stream send error: ${err instanceof Error ? err.message : err}`);
129
+ });
130
+ }
131
+ } catch {
132
+ }
133
+ }
134
+ });
135
+ pty.onExit(({ exitCode }) => {
136
+ log(`process exited: code=${exitCode}, session=${info.sessionId}`);
137
+ info.status = "idle";
138
+ info.pty = null;
139
+ updateCommand(commandId, exitCode === 0 ? "completed" : "failed", {
140
+ sessionId: info.sessionId || void 0,
141
+ error: exitCode !== 0 ? `exit code ${exitCode}` : void 0
142
+ }).catch(() => {
143
+ });
144
+ });
145
+ }
146
+ function spawnSession(commandId, workspaceId, prompt, workdir, allowedTools) {
147
+ const args = ["-p", prompt, "--output-format", "stream-json", "--verbose"];
148
+ if (allowedTools?.length) {
149
+ args.push("--allowedTools", ...allowedTools);
150
+ }
151
+ const info = {
152
+ sessionId: "",
153
+ status: "starting",
154
+ pty: null,
155
+ workspaceId,
156
+ workdir
157
+ };
158
+ log(`spawning: claude ${args.join(" ").slice(0, 100)}...`);
159
+ const env = { ...process.env };
160
+ delete env.CLAUDECODE;
161
+ delete env.CLAUDE_CODE_SSE_PORT;
162
+ const pty = ptySpawn("claude", args, {
163
+ name: "xterm-256color",
164
+ cols: 200,
165
+ rows: 50,
166
+ cwd: workdir,
167
+ env
168
+ });
169
+ info.pty = pty;
170
+ attachStreamParser(pty, info, commandId);
171
+ sessions.set(commandId, info);
172
+ }
173
+ function sendPrompt(commandId, workspaceId, sessionId, prompt) {
174
+ let workdir = process.cwd();
175
+ for (const info2 of sessions.values()) {
176
+ if (info2.sessionId === sessionId) {
177
+ if (info2.status === "running") {
178
+ log(`session ${sessionId} is still running, rejecting prompt`);
179
+ updateCommand(commandId, "failed", { error: "Session is still running" }).catch(() => {
180
+ });
181
+ return;
182
+ }
183
+ workdir = info2.workdir;
184
+ break;
185
+ }
186
+ }
187
+ const args = [
188
+ "-p",
189
+ prompt,
190
+ "--resume",
191
+ sessionId,
192
+ "--output-format",
193
+ "stream-json",
194
+ "--verbose"
195
+ ];
196
+ log(`resume: session=${sessionId}`);
197
+ const env = { ...process.env };
198
+ delete env.CLAUDECODE;
199
+ delete env.CLAUDE_CODE_SSE_PORT;
200
+ const pty = ptySpawn("claude", args, {
201
+ name: "xterm-256color",
202
+ cols: 200,
203
+ rows: 50,
204
+ cwd: workdir,
205
+ env
206
+ });
207
+ const info = {
208
+ sessionId,
209
+ status: "running",
210
+ pty,
211
+ workspaceId,
212
+ workdir
213
+ };
214
+ attachStreamParser(pty, info, commandId);
215
+ sessions.set(sessionId, info);
216
+ }
217
+ function getSessionList() {
218
+ const result = [];
219
+ for (const info of sessions.values()) {
220
+ if (info.sessionId) {
221
+ result.push({
222
+ sessionId: info.sessionId,
223
+ mode: "operate",
224
+ status: info.status
225
+ });
226
+ }
227
+ }
228
+ return result;
229
+ }
230
+
231
+ // src/commands/daemon.ts
232
+ var POLL_INTERVAL_MS = 5e3;
233
+ var HEARTBEAT_INTERVAL_MS = 15e3;
234
+ function log2(msg) {
235
+ console.log(`[daemon] ${msg}`);
236
+ }
237
+ async function handleCommand(cmd, workspaceId, projectDir) {
238
+ log2(`command: ${cmd.type} (${cmd.id})`);
239
+ await updateCommand(cmd.id, "processing");
240
+ switch (cmd.type) {
241
+ case "spawn": {
242
+ const prompt = cmd.payload.prompt;
243
+ const workdir = cmd.payload.workdir ?? projectDir;
244
+ const allowedTools = cmd.payload.allowedTools;
245
+ spawnSession(cmd.id, workspaceId, prompt, workdir, allowedTools);
246
+ break;
247
+ }
248
+ case "prompt": {
249
+ if (!cmd.sessionId) {
250
+ await updateCommand(cmd.id, "failed", { error: "Missing sessionId" });
251
+ return;
252
+ }
253
+ const prompt = cmd.payload.prompt;
254
+ sendPrompt(cmd.id, workspaceId, cmd.sessionId, prompt);
255
+ break;
256
+ }
257
+ default:
258
+ log2(`unknown command type: ${cmd.type}`);
259
+ await updateCommand(cmd.id, "failed", { error: `Unknown command type: ${cmd.type}` });
260
+ }
261
+ }
262
+ async function runDaemon(projectDir) {
263
+ const apiKey = process.env["ZTILE_API_KEY"];
264
+ if (!apiKey) {
265
+ console.error("Error: ZTILE_API_KEY is required.");
266
+ process.exit(1);
267
+ }
268
+ const workspaceId = getWorkspaceId(projectDir);
269
+ const host = hostname();
270
+ log2(`starting daemon`);
271
+ log2(` workspace: ${workspaceId}`);
272
+ log2(` hostname: ${host}`);
273
+ log2(` project: ${projectDir}`);
274
+ log2(` server: ${process.env["ZTILE_SERVER_URL"] ?? "https://ztile.dev"}`);
275
+ log2(` poll interval: ${POLL_INTERVAL_MS}ms`);
276
+ const ok = await sendHeartbeat(workspaceId, host, []);
277
+ if (ok) {
278
+ log2(`registered with server`);
279
+ } else {
280
+ log2(`failed to register with server (will retry)`);
281
+ }
282
+ const heartbeatTimer = setInterval(async () => {
283
+ const sessions2 = getSessionList();
284
+ await sendHeartbeat(workspaceId, host, sessions2).catch(() => {
285
+ });
286
+ }, HEARTBEAT_INTERVAL_MS);
287
+ log2(`polling for commands...`);
288
+ const pollTimer = setInterval(async () => {
289
+ try {
290
+ const commands = await pollCommands(workspaceId);
291
+ for (const cmd of commands) {
292
+ await handleCommand(cmd, workspaceId, projectDir);
293
+ }
294
+ } catch (err) {
295
+ log2(`poll error: ${err instanceof Error ? err.message : err}`);
296
+ }
297
+ }, POLL_INTERVAL_MS);
298
+ const shutdown = () => {
299
+ log2(`shutting down...`);
300
+ clearInterval(heartbeatTimer);
301
+ clearInterval(pollTimer);
302
+ process.exit(0);
303
+ };
304
+ process.on("SIGINT", shutdown);
305
+ process.on("SIGTERM", shutdown);
306
+ await new Promise(() => {
307
+ });
308
+ }
309
+ export {
310
+ runDaemon
311
+ };
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/hook.ts
4
+ import { readFile as readFile2, stat } from "fs/promises";
5
+
6
+ // src/state.ts
7
+ import { readFile, writeFile, mkdir, open, unlink } from "fs/promises";
8
+ import { join } from "path";
9
+ import { existsSync } from "fs";
10
+ var STATE_DIR = join(process.env["HOME"] ?? process.env["USERPROFILE"] ?? "/tmp", ".ztile");
11
+ var STATE_FILE = join(STATE_DIR, "state.json");
12
+ var LOCK_FILE = join(STATE_DIR, "hook.lock");
13
+ var LOCK_TIMEOUT_MS = 5e3;
14
+ var LOCK_RETRY_MS = 50;
15
+ var LOCK_STALE_MS = 1e4;
16
+ async function loadState() {
17
+ try {
18
+ const content = await readFile(STATE_FILE, "utf-8");
19
+ return JSON.parse(content);
20
+ } catch {
21
+ return { transcripts: {} };
22
+ }
23
+ }
24
+ async function saveState(state) {
25
+ if (!existsSync(STATE_DIR)) {
26
+ await mkdir(STATE_DIR, { recursive: true });
27
+ }
28
+ await writeFile(STATE_FILE, JSON.stringify(state, null, 2) + "\n");
29
+ }
30
+ async function getByteOffset(transcriptPath) {
31
+ const state = await loadState();
32
+ return state.transcripts[transcriptPath]?.last_byte_offset ?? 0;
33
+ }
34
+ async function updateByteOffset(transcriptPath, offset, sessionId) {
35
+ const state = await loadState();
36
+ state.transcripts[transcriptPath] = {
37
+ last_byte_offset: offset,
38
+ session_id: sessionId,
39
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
40
+ };
41
+ await saveState(state);
42
+ }
43
+ async function acquireLock() {
44
+ if (!existsSync(STATE_DIR)) {
45
+ await mkdir(STATE_DIR, { recursive: true });
46
+ }
47
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
48
+ while (Date.now() < deadline) {
49
+ try {
50
+ const fd = await open(LOCK_FILE, "wx");
51
+ await fd.writeFile(String(Date.now()));
52
+ await fd.close();
53
+ return true;
54
+ } catch (err) {
55
+ if (err.code === "EEXIST") {
56
+ try {
57
+ const content = await readFile(LOCK_FILE, "utf-8");
58
+ const lockTime = parseInt(content, 10);
59
+ if (!isNaN(lockTime) && Date.now() - lockTime > LOCK_STALE_MS) {
60
+ await unlink(LOCK_FILE).catch(() => {
61
+ });
62
+ continue;
63
+ }
64
+ } catch {
65
+ }
66
+ await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
67
+ continue;
68
+ }
69
+ throw err;
70
+ }
71
+ }
72
+ return false;
73
+ }
74
+ async function releaseLock() {
75
+ await unlink(LOCK_FILE).catch(() => {
76
+ });
77
+ }
78
+
79
+ // src/outputs/server.ts
80
+ var DEFAULT_SERVER_URL = "https://ztile.dev";
81
+ function getServerConfig() {
82
+ return {
83
+ serverUrl: process.env["ZTILE_SERVER_URL"] ?? DEFAULT_SERVER_URL,
84
+ apiKey: process.env["ZTILE_API_KEY"]
85
+ };
86
+ }
87
+ async function sendRawToServer(payload) {
88
+ const { serverUrl, apiKey } = getServerConfig();
89
+ if (!apiKey) {
90
+ console.error("[server] No API key configured (ZTILE_API_KEY)");
91
+ return false;
92
+ }
93
+ try {
94
+ const response = await fetch(`${serverUrl}/api/hooks/ingest`, {
95
+ method: "POST",
96
+ headers: {
97
+ "Content-Type": "application/json",
98
+ "x-api-key": apiKey
99
+ },
100
+ body: JSON.stringify(payload)
101
+ });
102
+ if (!response.ok) {
103
+ console.error(
104
+ `[server] Failed to send raw data: ${response.status} ${response.statusText}`
105
+ );
106
+ return false;
107
+ }
108
+ return true;
109
+ } catch (error) {
110
+ console.error(
111
+ `[server] Connection error: ${error instanceof Error ? error.message : error}`
112
+ );
113
+ return false;
114
+ }
115
+ }
116
+
117
+ // src/commands/hook.ts
118
+ import { appendFileSync } from "fs";
119
+ import { join as join2 } from "path";
120
+ import { hostname } from "os";
121
+ var LOG_FILE = join2(process.env["HOME"] ?? "/tmp", ".ztile", "hook-debug.log");
122
+ function debugLog(msg) {
123
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
124
+ `;
125
+ try {
126
+ appendFileSync(LOG_FILE, line);
127
+ } catch {
128
+ }
129
+ console.error(msg);
130
+ }
131
+ async function readStdin() {
132
+ const chunks = [];
133
+ for await (const chunk of process.stdin) {
134
+ chunks.push(chunk);
135
+ }
136
+ const text = Buffer.concat(chunks).toString("utf-8").trim();
137
+ if (!text) {
138
+ throw new Error("No input received on stdin");
139
+ }
140
+ return JSON.parse(text);
141
+ }
142
+ async function handleHook() {
143
+ const payload = await readStdin();
144
+ const hookEvent = payload["hook_event_name"];
145
+ const transcriptPath = payload["transcript_path"];
146
+ const sessionId = payload["session_id"];
147
+ const cwd = payload["cwd"];
148
+ debugLog(`[hook] ${hookEvent}: payload keys = ${Object.keys(payload).join(", ")}`);
149
+ if (!transcriptPath) {
150
+ debugLog("[hook] No transcript_path in payload, skipping");
151
+ return;
152
+ }
153
+ if (!sessionId) {
154
+ debugLog("[hook] No session_id in payload, skipping");
155
+ return;
156
+ }
157
+ const machineId = process.env["ZTILE_MACHINE_ID"] ?? hostname();
158
+ if (hookEvent === "UserPromptSubmit") {
159
+ const prompt = payload["prompt"];
160
+ if (prompt) {
161
+ const ok = await sendRawToServer({
162
+ hook_event: hookEvent,
163
+ session_id: sessionId,
164
+ machine_id: machineId,
165
+ project: cwd,
166
+ prompt
167
+ });
168
+ debugLog(`[hook] ${hookEvent}: prompt sent` + (ok ? " \u2192 server" : " (send failed)"));
169
+ } else {
170
+ debugLog(`[hook] ${hookEvent}: no prompt in payload, skipping`);
171
+ }
172
+ return;
173
+ }
174
+ if (hookEvent === "PermissionRequest") {
175
+ const toolName = payload["tool_name"];
176
+ const toolInput = payload["tool_input"];
177
+ const ok = await sendRawToServer({
178
+ hook_event: hookEvent,
179
+ session_id: sessionId,
180
+ machine_id: machineId,
181
+ project: cwd,
182
+ tool_name: toolName,
183
+ tool_input: toolInput
184
+ });
185
+ debugLog(`[hook] ${hookEvent}: ${toolName}` + (ok ? " \u2192 server" : " (send failed)"));
186
+ return;
187
+ }
188
+ if (hookEvent === "SessionEnd") {
189
+ const ok = await sendRawToServer({
190
+ hook_event: hookEvent,
191
+ session_id: sessionId,
192
+ machine_id: machineId,
193
+ project: cwd
194
+ });
195
+ debugLog(`[hook] ${hookEvent}` + (ok ? " \u2192 server" : " (send failed)"));
196
+ return;
197
+ }
198
+ const locked = await acquireLock();
199
+ if (!locked) {
200
+ debugLog(`[hook] ${hookEvent}: could not acquire lock, skipping`);
201
+ return;
202
+ }
203
+ try {
204
+ const fromOffset = await getByteOffset(transcriptPath);
205
+ const fileInfo = await stat(transcriptPath);
206
+ if (fileInfo.size <= fromOffset) {
207
+ debugLog(`[hook] ${hookEvent}: no diff (offset ${fromOffset} \u2192 ${fileInfo.size})`);
208
+ await updateByteOffset(transcriptPath, fileInfo.size, sessionId);
209
+ return;
210
+ }
211
+ const buffer = await readFile2(transcriptPath);
212
+ const newData = buffer.subarray(fromOffset);
213
+ const text = newData.toString("utf-8");
214
+ const lines = text.split("\n").filter((l) => l.trim() !== "");
215
+ if (lines.length === 0) {
216
+ debugLog(`[hook] ${hookEvent}: no lines in diff`);
217
+ await updateByteOffset(transcriptPath, fileInfo.size, sessionId);
218
+ return;
219
+ }
220
+ const ok = await sendRawToServer({
221
+ hook_event: hookEvent ?? "unknown",
222
+ session_id: sessionId,
223
+ machine_id: machineId,
224
+ project: cwd,
225
+ lines
226
+ });
227
+ debugLog(
228
+ `[hook] ${hookEvent}: ${lines.length} raw lines` + (ok ? " \u2192 server" : " (send failed)")
229
+ );
230
+ await updateByteOffset(transcriptPath, fileInfo.size, sessionId);
231
+ } finally {
232
+ await releaseLock();
233
+ }
234
+ }
235
+ export {
236
+ handleHook
237
+ };
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/setup.ts
4
+ import { readFile, writeFile } from "fs/promises";
5
+ import { join } from "path";
6
+ import { existsSync } from "fs";
7
+ import { hostname } from "os";
8
+ import { execSync } from "child_process";
9
+ var DEFAULT_SERVER_URL = "https://ztile.dev";
10
+ var HOOK_EVENTS = [
11
+ "UserPromptSubmit",
12
+ "PreToolUse",
13
+ "PostToolUse",
14
+ "PostToolUseFailure",
15
+ "PermissionRequest",
16
+ "SubagentStart",
17
+ "SubagentStop",
18
+ "Stop",
19
+ "PreCompact",
20
+ "Notification",
21
+ "SessionEnd"
22
+ ];
23
+ async function setupClaudeCode(options) {
24
+ const ztileBin = findGlobalBin();
25
+ if (!ztileBin) {
26
+ console.error("Error: ztile is not globally installed.");
27
+ console.error("");
28
+ console.error(" npm install -g ztile");
29
+ console.error(" ztile setup --api-key <key>");
30
+ console.error("");
31
+ process.exit(1);
32
+ }
33
+ const homeDir = process.env["HOME"] ?? process.env["USERPROFILE"] ?? "/tmp";
34
+ const claudeDir = join(homeDir, ".claude");
35
+ const settingsPath = join(claudeDir, "settings.json");
36
+ if (!existsSync(claudeDir)) {
37
+ console.error(
38
+ "Claude Code not found (~/.claude/ does not exist). Skipping."
39
+ );
40
+ return;
41
+ }
42
+ let settings = {};
43
+ try {
44
+ const content = await readFile(settingsPath, "utf-8");
45
+ settings = JSON.parse(content);
46
+ } catch {
47
+ }
48
+ const hooks = settings["hooks"] ?? {};
49
+ const serverUrl = options.serverUrl ?? DEFAULT_SERVER_URL;
50
+ const envPrefix = [
51
+ `ZTILE_API_KEY=${options.apiKey}`,
52
+ `ZTILE_SERVER_URL=${serverUrl}`,
53
+ `ZTILE_MACHINE_ID=${options.machineId ?? hostname()}`,
54
+ ...options.project ? [`ZTILE_PROJECT=${options.project}`] : []
55
+ ].join(" ");
56
+ const hookCommand = `${envPrefix} ${ztileBin} hook`;
57
+ const ztileHook = { type: "command", command: hookCommand };
58
+ for (const event of Object.keys(hooks)) {
59
+ const existing = hooks[event] ?? [];
60
+ hooks[event] = existing.filter(
61
+ (entry) => !entry.hooks?.some(
62
+ (h) => h.type === "command" && (h.command?.includes("ztile hook") || h.command?.includes("ztile/apps/collector")) || h.type === "http" && h.url?.includes("/api/hooks/claude")
63
+ )
64
+ );
65
+ if (hooks[event].length === 0) {
66
+ delete hooks[event];
67
+ }
68
+ }
69
+ let addedCount = 0;
70
+ for (const event of HOOK_EVENTS) {
71
+ const existing = hooks[event] ?? [];
72
+ hooks[event] = [...existing, { hooks: [ztileHook] }];
73
+ addedCount++;
74
+ }
75
+ settings["hooks"] = hooks;
76
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n");
77
+ console.log(`Claude Code: configured ${addedCount} hook(s) in ${settingsPath}`);
78
+ console.log(` Mode: command (transcript-based)`);
79
+ console.log(` Server: ${serverUrl}`);
80
+ console.log(` Machine ID: ${options.machineId ?? hostname()}`);
81
+ if (options.project) console.log(` Project: ${options.project}`);
82
+ console.log(` Binary: ${ztileBin}`);
83
+ console.log(` Events: ${HOOK_EVENTS.join(", ")}`);
84
+ console.log();
85
+ console.log("Note: Restart Claude Code sessions for hooks to take effect.");
86
+ }
87
+ function findGlobalBin() {
88
+ try {
89
+ const resolved = execSync("which ztile", { encoding: "utf-8" }).trim();
90
+ return resolved || null;
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+ export {
96
+ setupClaudeCode
97
+ };
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.2.2",
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/newdefs/ztile.git"
43
+ }
7
44
  }
package/index.js DELETED
@@ -1 +0,0 @@
1
- // placeholder