anywhere-ai 0.0.22 → 0.0.24

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.js CHANGED
@@ -11,7 +11,7 @@ import readline from "readline";
11
11
  import qrcode from "qrcode";
12
12
  var args = process.argv.slice(2);
13
13
  if (args.includes("--version") || args.includes("-v")) {
14
- console.log(`anywhere-ai v${"0.0.22"}`);
14
+ console.log(`anywhere-ai v${"0.0.24"}`);
15
15
  process.exit(0);
16
16
  }
17
17
  function ask(question, preserveCase = false) {
@@ -170,7 +170,39 @@ try {
170
170
  } catch {
171
171
  }
172
172
  var isVPS = publicIP && localIP && publicIP !== localIP;
173
- await import("./server-SPM3PZ5G.js");
173
+ var MAX_RESTARTS = 5;
174
+ var RESTART_WINDOW = 6e4;
175
+ var RESTART_DELAY = 2e3;
176
+ var restartTimestamps = [];
177
+ var intentionalShutdown = false;
178
+ var serverProcess = null;
179
+ function startServer() {
180
+ const serverPath = new URL("./server.js", import.meta.url).pathname;
181
+ serverProcess = spawn(process.execPath, [serverPath], {
182
+ stdio: "inherit",
183
+ env: { ...process.env }
184
+ });
185
+ serverProcess.on("exit", (code, signal) => {
186
+ serverProcess = null;
187
+ if (intentionalShutdown) return;
188
+ const now = Date.now();
189
+ restartTimestamps.push(now);
190
+ while (restartTimestamps.length > 0 && now - restartTimestamps[0] > RESTART_WINDOW) {
191
+ restartTimestamps.shift();
192
+ }
193
+ if (restartTimestamps.length >= MAX_RESTARTS) {
194
+ console.error(`
195
+ Server crashed ${MAX_RESTARTS} times in ${RESTART_WINDOW / 1e3}s. Giving up.`);
196
+ cleanup();
197
+ return;
198
+ }
199
+ console.log(`
200
+ Server exited (code=${code}, signal=${signal}). Restarting in ${RESTART_DELAY / 1e3}s...`);
201
+ setTimeout(startServer, RESTART_DELAY);
202
+ });
203
+ }
204
+ startServer();
205
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
174
206
  var tunnelURL = null;
175
207
  var tunnelProcess = null;
176
208
  async function printBanner(serverURL2, localURL2) {
@@ -218,11 +250,12 @@ if (!args.includes("--no-tunnel")) {
218
250
  }
219
251
  }
220
252
  if (cloudflaredBin) {
221
- console.log("Starting tunnel...");
253
+ process.stdout.write("Starting tunnel");
222
254
  const tunnelArgs = cloudflaredBin === "npx" ? ["cloudflared", "tunnel", "--url", `http://localhost:${port}`] : ["tunnel", "--url", `http://localhost:${port}`];
223
255
  tunnelProcess = spawn(cloudflaredBin, tunnelArgs, {
224
256
  stdio: ["ignore", "pipe", "pipe"]
225
257
  });
258
+ const dots = setInterval(() => process.stdout.write("."), 1e3);
226
259
  tunnelURL = await new Promise((resolve) => {
227
260
  const timeout = setTimeout(() => resolve(null), 15e3);
228
261
  const urlRegex = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
@@ -247,10 +280,11 @@ if (!args.includes("--no-tunnel")) {
247
280
  resolve(null);
248
281
  });
249
282
  });
283
+ clearInterval(dots);
250
284
  if (tunnelURL) {
251
- console.log("Tunnel established!");
285
+ console.log(" done!");
252
286
  } else {
253
- console.log("Tunnel failed, using local address.");
287
+ console.log(" failed, using local address.");
254
288
  }
255
289
  }
256
290
  }
@@ -258,6 +292,11 @@ var localURL = isVPS ? "http://" + publicIP + ":" + port : "http://" + (localIP
258
292
  var serverURL = tunnelURL || localURL;
259
293
  await printBanner(serverURL, localURL);
260
294
  function cleanup() {
295
+ intentionalShutdown = true;
296
+ if (serverProcess) {
297
+ serverProcess.kill();
298
+ serverProcess = null;
299
+ }
261
300
  if (tunnelProcess) {
262
301
  tunnelProcess.kill();
263
302
  tunnelProcess = null;
package/dist/server.js ADDED
@@ -0,0 +1,901 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/server.ts
4
+ import { Hono as Hono4 } from "hono";
5
+ import { serve } from "@hono/node-server";
6
+ import { cors } from "hono/cors";
7
+
8
+ // src/routes/chats/index.ts
9
+ import { Hono } from "hono";
10
+ import os3 from "os";
11
+ import path3 from "path";
12
+ import { readdir as readdir2, readFile as readFile2, stat as stat2 } from "fs/promises";
13
+ import { execSync as execSync2 } from "child_process";
14
+ import { streamSSE } from "hono/streaming";
15
+
16
+ // src/chats.ts
17
+ import {
18
+ unstable_v2_createSession,
19
+ unstable_v2_resumeSession
20
+ } from "@anthropic-ai/claude-agent-sdk";
21
+ import fs from "fs/promises";
22
+ import path from "path";
23
+ import os from "os";
24
+ import { execSync } from "child_process";
25
+ import generate from "@good-ghosting/random-name-generator";
26
+ var ANYWHERE_DIR = path.join(os.homedir(), ".anywhere");
27
+ var cwdLock = Promise.resolve();
28
+ function withCwd(cwd, fn) {
29
+ const prev = cwdLock;
30
+ let release;
31
+ cwdLock = new Promise((r) => release = r);
32
+ return prev.then(() => {
33
+ const original = process.cwd();
34
+ if (cwd) process.chdir(cwd);
35
+ try {
36
+ return fn();
37
+ } finally {
38
+ if (cwd) process.chdir(original);
39
+ release();
40
+ }
41
+ });
42
+ }
43
+ var PROJECTS_DIR = path.join(ANYWHERE_DIR, "projects");
44
+ var WORKTREES_DIR = path.join(ANYWHERE_DIR, "worktrees");
45
+ var sessions = /* @__PURE__ */ new Map();
46
+ var sessionConfig = /* @__PURE__ */ new Map();
47
+ var sessionCwds = /* @__PURE__ */ new Map();
48
+ async function getCwdFromJSONL(sessionId) {
49
+ const projectsBase = path.join(os.homedir(), ".claude", "projects");
50
+ try {
51
+ const dirs = await fs.readdir(projectsBase);
52
+ for (const dir of dirs) {
53
+ const filePath = path.join(projectsBase, dir, `${sessionId}.jsonl`);
54
+ try {
55
+ const content = await fs.readFile(filePath, "utf-8");
56
+ for (const line of content.split("\n")) {
57
+ if (!line) continue;
58
+ const obj = JSON.parse(line);
59
+ if (obj.type === "user" && obj.cwd) return obj.cwd;
60
+ }
61
+ } catch {
62
+ }
63
+ }
64
+ } catch {
65
+ }
66
+ return void 0;
67
+ }
68
+ var activeSessions = /* @__PURE__ */ new Set();
69
+ var pendingPermissions = /* @__PURE__ */ new Map();
70
+ function getAssistantText(msg) {
71
+ if (msg.type !== "assistant") return null;
72
+ return msg.message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
73
+ }
74
+ var permissionCallbacks = /* @__PURE__ */ new Map();
75
+ function setPermissionCallback(sessionId, cb) {
76
+ if (cb) {
77
+ permissionCallbacks.set(sessionId, cb);
78
+ } else {
79
+ permissionCallbacks.delete(sessionId);
80
+ }
81
+ }
82
+ var permReqCounter = 0;
83
+ function makeCanUseTool(sessionHint, permissionMode) {
84
+ return async (toolName, input, options) => {
85
+ const requestId = `perm_${++permReqCounter}_${Date.now()}`;
86
+ const req = {
87
+ requestId,
88
+ toolName,
89
+ input,
90
+ description: input.description,
91
+ decisionReason: options.decisionReason,
92
+ suggestions: options.suggestions
93
+ };
94
+ const cb = permissionCallbacks.get(sessionHint);
95
+ if (cb) cb(req);
96
+ return new Promise((resolve) => {
97
+ pendingPermissions.set(requestId, resolve);
98
+ const timeout = setTimeout(
99
+ () => {
100
+ if (pendingPermissions.has(requestId)) {
101
+ pendingPermissions.delete(requestId);
102
+ resolve({
103
+ behavior: "deny",
104
+ message: "Permission request timed out"
105
+ });
106
+ }
107
+ },
108
+ 5 * 60 * 1e3
109
+ );
110
+ options.signal.addEventListener("abort", () => {
111
+ clearTimeout(timeout);
112
+ if (pendingPermissions.has(requestId)) {
113
+ pendingPermissions.delete(requestId);
114
+ resolve({ behavior: "deny", message: "Request aborted" });
115
+ }
116
+ });
117
+ const origResolve = resolve;
118
+ pendingPermissions.set(requestId, (result) => {
119
+ clearTimeout(timeout);
120
+ pendingPermissions.delete(requestId);
121
+ origResolve(result);
122
+ });
123
+ });
124
+ };
125
+ }
126
+ var setupRepo = async (repo) => {
127
+ await fs.mkdir(PROJECTS_DIR, { recursive: true });
128
+ await fs.mkdir(WORKTREES_DIR, { recursive: true });
129
+ const repoPath = path.join(PROJECTS_DIR, repo);
130
+ try {
131
+ await fs.access(repoPath);
132
+ execSync("git fetch origin", { cwd: repoPath, encoding: "utf-8" });
133
+ } catch {
134
+ execSync(`gh repo clone ${repo} ${repoPath}`, { encoding: "utf-8" });
135
+ }
136
+ const branchName = generate({ words: 3 }).dashed;
137
+ const worktreePath = path.join(WORKTREES_DIR, branchName);
138
+ execSync(`git worktree add ${worktreePath} -b ${branchName}`, {
139
+ cwd: repoPath,
140
+ encoding: "utf-8"
141
+ });
142
+ return worktreePath;
143
+ };
144
+ var createChat = async ({
145
+ model,
146
+ permission,
147
+ repo,
148
+ prompt
149
+ }) => {
150
+ const repoPath = repo ? await setupRepo(repo) : void 0;
151
+ const tempId = `temp_${Date.now()}`;
152
+ const mode = permission || "acceptEdits";
153
+ const session = await withCwd(
154
+ repoPath,
155
+ () => unstable_v2_createSession({
156
+ model: model || "claude-opus-4-6",
157
+ permissionMode: mode,
158
+ canUseTool: makeCanUseTool(tempId, mode)
159
+ })
160
+ );
161
+ if (repoPath) sessionCwds.set(tempId, repoPath);
162
+ return { session, tempId, repoPath };
163
+ };
164
+ var sendMessage = async ({
165
+ prompt,
166
+ session
167
+ }) => {
168
+ await session.send(prompt);
169
+ };
170
+ var getSession = async ({
171
+ sessionId,
172
+ model,
173
+ permission,
174
+ cwd
175
+ }) => {
176
+ let session = sessions.get(sessionId);
177
+ const currentConfig = sessionConfig.get(sessionId);
178
+ const requestedModel = model || "claude-opus-4-6";
179
+ const requestedPermission = permission || "acceptEdits";
180
+ const configChanged = session && currentConfig && (requestedModel !== currentConfig.model || requestedPermission !== currentConfig.permission);
181
+ if (session && configChanged) {
182
+ session.close();
183
+ sessions.delete(sessionId);
184
+ sessionConfig.delete(sessionId);
185
+ session = void 0;
186
+ }
187
+ if (!session) {
188
+ const sessionCwd = cwd || sessionCwds.get(sessionId) || await getCwdFromJSONL(sessionId);
189
+ session = await withCwd(
190
+ sessionCwd,
191
+ () => unstable_v2_resumeSession(sessionId, {
192
+ model: requestedModel,
193
+ permissionMode: requestedPermission,
194
+ canUseTool: makeCanUseTool(sessionId, requestedPermission)
195
+ })
196
+ );
197
+ sessions.set(sessionId, session);
198
+ sessionConfig.set(sessionId, { model: requestedModel, permission: requestedPermission });
199
+ if (sessionCwd) sessionCwds.set(sessionId, sessionCwd);
200
+ }
201
+ return session;
202
+ };
203
+
204
+ // src/chat-names.ts
205
+ import { unstable_v2_prompt } from "@anthropic-ai/claude-agent-sdk";
206
+ import { mkdir, readFile, writeFile, readdir, stat, unlink } from "fs/promises";
207
+ import path2 from "path";
208
+ import os2 from "os";
209
+ var ANYWHERE_DIR2 = path2.join(os2.homedir(), ".anywhere");
210
+ var NAMES_FILE = path2.join(ANYWHERE_DIR2, "chat-names.json");
211
+ var CLAUDE_PROJECTS_DIR = path2.join(os2.homedir(), ".claude", "projects");
212
+ var chatNames = {};
213
+ var loaded = false;
214
+ var pendingNames = /* @__PURE__ */ new Set();
215
+ var TRIVIAL_RE = /^(hi|hey|hello|sup|yo|what'?s\s*up|greetings|hola|howdy|gm|good\s*(morning|afternoon|evening))[\s!?.,:]*$/i;
216
+ function isTrivialPrompt(prompt) {
217
+ return TRIVIAL_RE.test(prompt.trim());
218
+ }
219
+ async function ensureDir() {
220
+ await mkdir(ANYWHERE_DIR2, { recursive: true });
221
+ }
222
+ async function load() {
223
+ if (loaded) return;
224
+ await ensureDir();
225
+ try {
226
+ chatNames = JSON.parse(await readFile(NAMES_FILE, "utf-8"));
227
+ } catch {
228
+ chatNames = {};
229
+ }
230
+ loaded = true;
231
+ }
232
+ async function saveNames() {
233
+ await ensureDir();
234
+ await writeFile(NAMES_FILE, JSON.stringify(chatNames, null, 2));
235
+ }
236
+ async function getChatName(sessionId) {
237
+ await load();
238
+ return chatNames[sessionId];
239
+ }
240
+ async function getAllChatNames() {
241
+ await load();
242
+ return { ...chatNames };
243
+ }
244
+ async function cleanupNamegenJsonl(namegenSessionId) {
245
+ try {
246
+ const dirs = await readdir(CLAUDE_PROJECTS_DIR);
247
+ for (const dir of dirs) {
248
+ const filePath = path2.join(CLAUDE_PROJECTS_DIR, dir, `${namegenSessionId}.jsonl`);
249
+ try {
250
+ await stat(filePath);
251
+ await unlink(filePath);
252
+ return;
253
+ } catch {
254
+ }
255
+ }
256
+ } catch {
257
+ }
258
+ }
259
+ async function generateChatName(prompt, sessionId) {
260
+ if (isTrivialPrompt(prompt)) return;
261
+ await load();
262
+ if (chatNames[sessionId]) return;
263
+ if (pendingNames.has(sessionId)) return;
264
+ pendingNames.add(sessionId);
265
+ try {
266
+ const namePrompt = `Generate a concise 3-6 word title for a coding chat that starts with this message. Output ONLY the title, nothing else. No quotes, no punctuation at the end.
267
+
268
+ ${prompt.slice(0, 500)}`;
269
+ const result = await unstable_v2_prompt(namePrompt, {
270
+ model: "claude-haiku-4-5"
271
+ });
272
+ if (result.subtype === "success") {
273
+ cleanupNamegenJsonl(result.session_id);
274
+ const name = result.result.trim().replace(/^["']|["']$/g, "").trim();
275
+ if (name && name.length > 0 && name.length < 80) {
276
+ chatNames[sessionId] = name;
277
+ await saveNames();
278
+ }
279
+ }
280
+ } catch (err) {
281
+ console.error(`[chatName] failed for ${sessionId}:`, err);
282
+ } finally {
283
+ pendingNames.delete(sessionId);
284
+ }
285
+ }
286
+
287
+ // src/routes/chats/index.ts
288
+ var repoNameCache = /* @__PURE__ */ new Map();
289
+ function isHomeDir(dir) {
290
+ const home = os3.homedir();
291
+ if (dir === home) return true;
292
+ if (dir === "/root" || dir === "/") return true;
293
+ if (/^\/home\/[^/]+\/?$/.test(dir)) return true;
294
+ if (/^\/Users\/[^/]+\/?$/.test(dir)) return true;
295
+ return false;
296
+ }
297
+ function deriveRepoName(cwd) {
298
+ if (!cwd) return "";
299
+ if (repoNameCache.has(cwd)) return repoNameCache.get(cwd);
300
+ if (isHomeDir(cwd)) {
301
+ repoNameCache.set(cwd, "");
302
+ return "";
303
+ }
304
+ let name = "";
305
+ let tryDir = cwd;
306
+ let foundGit = false;
307
+ while (tryDir && !isHomeDir(tryDir) && tryDir !== "/") {
308
+ try {
309
+ const url = execSync2("git remote get-url origin 2>/dev/null", {
310
+ cwd: tryDir,
311
+ encoding: "utf-8",
312
+ timeout: 3e3
313
+ }).trim();
314
+ const match = url.match(/[:/]([^/]+\/[^/.]+?)(?:\.git)?$/);
315
+ if (match?.[1]) name = match[1];
316
+ foundGit = true;
317
+ break;
318
+ } catch {
319
+ const parent = path3.dirname(tryDir);
320
+ if (parent === tryDir) break;
321
+ tryDir = parent;
322
+ }
323
+ }
324
+ if (!foundGit) {
325
+ tryDir = cwd;
326
+ while (tryDir && !isHomeDir(tryDir) && tryDir !== "/") {
327
+ try {
328
+ const root = execSync2("git rev-parse --show-toplevel 2>/dev/null", {
329
+ cwd: tryDir,
330
+ encoding: "utf-8",
331
+ timeout: 3e3
332
+ }).trim();
333
+ if (!isHomeDir(root)) {
334
+ name = root.split("/").filter(Boolean).pop() || "";
335
+ }
336
+ break;
337
+ } catch {
338
+ const parent = path3.dirname(tryDir);
339
+ if (parent === tryDir) break;
340
+ tryDir = parent;
341
+ }
342
+ }
343
+ }
344
+ repoNameCache.set(cwd, name);
345
+ return name;
346
+ }
347
+ async function findSessionFile(sessionId) {
348
+ const projectsBase = path3.join(os3.homedir(), ".claude", "projects");
349
+ try {
350
+ const dirs = await readdir2(projectsBase);
351
+ for (const dir of dirs) {
352
+ const filePath = path3.join(projectsBase, dir, `${sessionId}.jsonl`);
353
+ try {
354
+ await stat2(filePath);
355
+ return filePath;
356
+ } catch {
357
+ }
358
+ }
359
+ } catch {
360
+ }
361
+ return null;
362
+ }
363
+ var chats = new Hono();
364
+ chats.post("/new", async (c) => {
365
+ const { prompt, model, permission, repo } = await c.req.json();
366
+ if (!prompt) return c.json({ error: "Prompt is required" }, 400);
367
+ try {
368
+ const { session, tempId, repoPath } = await createChat({ model, permission, repo, prompt });
369
+ await sendMessage({ prompt, session });
370
+ return streamSSE(c, async (stream) => {
371
+ let finalSessionId = "";
372
+ const sendPerm = async (req) => {
373
+ try {
374
+ await stream.writeSSE({
375
+ data: JSON.stringify({
376
+ type: "permission_request",
377
+ ...req,
378
+ sessionId: finalSessionId || "pending"
379
+ })
380
+ });
381
+ } catch (e) {
382
+ console.error("Failed to send permission SSE:", e);
383
+ }
384
+ };
385
+ setPermissionCallback(tempId, sendPerm);
386
+ try {
387
+ for await (const msg of session.stream()) {
388
+ if (msg.session_id && !finalSessionId) {
389
+ finalSessionId = msg.session_id;
390
+ activeSessions.add(finalSessionId);
391
+ setPermissionCallback(finalSessionId, sendPerm);
392
+ }
393
+ const text = getAssistantText(msg);
394
+ if (text)
395
+ await stream.writeSSE({
396
+ data: JSON.stringify({
397
+ response: text.trim(),
398
+ sessionId: msg.session_id
399
+ })
400
+ });
401
+ }
402
+ if (finalSessionId) {
403
+ sessions.set(finalSessionId, session);
404
+ sessionConfig.set(finalSessionId, {
405
+ model: model || "claude-opus-4-6",
406
+ permission: permission || "acceptEdits"
407
+ });
408
+ if (repoPath) sessionCwds.set(finalSessionId, repoPath);
409
+ generateChatName(prompt, finalSessionId).catch(() => {
410
+ });
411
+ }
412
+ } catch (err) {
413
+ console.error(err);
414
+ await stream.writeSSE({
415
+ data: JSON.stringify({ error: "Stream error" })
416
+ });
417
+ } finally {
418
+ setPermissionCallback(tempId, null);
419
+ if (finalSessionId) {
420
+ setPermissionCallback(finalSessionId, null);
421
+ activeSessions.delete(finalSessionId);
422
+ }
423
+ }
424
+ });
425
+ } catch (error) {
426
+ console.error(error);
427
+ return c.json({ error: "Failed to create new chat" }, 400);
428
+ }
429
+ });
430
+ chats.post("/:id/message", async (c) => {
431
+ const { prompt, model, permission } = await c.req.json();
432
+ const sessionId = c.req.param("id");
433
+ if (!prompt || !sessionId || /[\/\\]/.test(sessionId))
434
+ return c.json({ error: "Invalid request" }, 400);
435
+ try {
436
+ const session = await getSession({ sessionId, model, permission });
437
+ if (!session) return c.json({ error: "Session is required" }, 400);
438
+ await sendMessage({ prompt, session });
439
+ return streamSSE(c, async (stream) => {
440
+ activeSessions.add(sessionId);
441
+ setPermissionCallback(sessionId, async (req) => {
442
+ try {
443
+ await stream.writeSSE({
444
+ data: JSON.stringify({
445
+ type: "permission_request",
446
+ ...req,
447
+ sessionId
448
+ })
449
+ });
450
+ } catch (e) {
451
+ console.error("Failed to send permission SSE:", e);
452
+ }
453
+ });
454
+ try {
455
+ for await (const msg of session.stream()) {
456
+ const text = getAssistantText(msg);
457
+ if (text)
458
+ await stream.writeSSE({
459
+ data: JSON.stringify({
460
+ response: text?.trim() || null,
461
+ sessionId: msg.session_id
462
+ })
463
+ });
464
+ }
465
+ } catch (err) {
466
+ console.error(err);
467
+ await stream.writeSSE({
468
+ data: JSON.stringify({ error: "Stream error" })
469
+ });
470
+ generateChatName(prompt, sessionId).catch(() => {
471
+ });
472
+ } finally {
473
+ setPermissionCallback(sessionId, null);
474
+ activeSessions.delete(sessionId);
475
+ }
476
+ });
477
+ } catch (error) {
478
+ console.error(error);
479
+ return c.json({ error: "Failed to send message" }, 400);
480
+ }
481
+ });
482
+ chats.post("/:id/permission", async (c) => {
483
+ const { requestId, behavior, updatedPermissions } = await c.req.json();
484
+ if (!requestId || !behavior)
485
+ return c.json({ error: "requestId and behavior are required" }, 400);
486
+ const resolve = pendingPermissions.get(requestId);
487
+ if (!resolve)
488
+ return c.json({ error: "No pending permission request found" }, 404);
489
+ if (behavior === "allow") {
490
+ resolve({
491
+ behavior: "allow",
492
+ ...updatedPermissions ? { updatedPermissions } : {}
493
+ });
494
+ } else {
495
+ resolve({ behavior: "deny", message: "User denied permission" });
496
+ }
497
+ return c.json({ ok: true });
498
+ });
499
+ chats.get("/", async (c) => {
500
+ try {
501
+ const projectsBase = path3.join(os3.homedir(), ".claude", "projects");
502
+ const dirs = await readdir2(projectsBase);
503
+ const chatNamesMap = await getAllChatNames();
504
+ const allChats = [];
505
+ for (const dir of dirs) {
506
+ const dirPath = path3.join(projectsBase, dir);
507
+ try {
508
+ const s = await stat2(dirPath);
509
+ if (!s.isDirectory()) continue;
510
+ } catch {
511
+ continue;
512
+ }
513
+ const files = (await readdir2(dirPath)).filter(
514
+ (f) => f.endsWith(".jsonl")
515
+ );
516
+ for (const file of files) {
517
+ const filePath = path3.join(dirPath, file);
518
+ const content = await readFile2(filePath, "utf-8");
519
+ const lines = content.split("\n").filter(Boolean);
520
+ const sessionId = file.replace(".jsonl", "");
521
+ let preview = "";
522
+ let timestamp = "";
523
+ let cwd = "";
524
+ let gitBranch = "";
525
+ let lastMessage = "";
526
+ let lastTimestamp = "";
527
+ for (const line of lines) {
528
+ try {
529
+ const obj = JSON.parse(line);
530
+ if (obj.type === "user") {
531
+ if (!cwd && obj.cwd) cwd = obj.cwd;
532
+ if (!gitBranch && obj.gitBranch) gitBranch = obj.gitBranch;
533
+ if (!obj.isMeta) {
534
+ if (!preview) {
535
+ const msgContent = obj.message.content;
536
+ if (typeof msgContent === "string") {
537
+ preview = msgContent.slice(0, 100);
538
+ } else {
539
+ preview = msgContent.filter((b) => b.type === "text").map((b) => b.text).join("").slice(0, 100);
540
+ }
541
+ timestamp = obj.timestamp;
542
+ }
543
+ if (obj.timestamp) lastTimestamp = obj.timestamp;
544
+ }
545
+ }
546
+ if (obj.type === "assistant") {
547
+ if (obj.timestamp) lastTimestamp = obj.timestamp;
548
+ const content2 = obj.message?.content;
549
+ if (typeof content2 === "string") {
550
+ lastMessage = content2.slice(0, 120);
551
+ } else if (Array.isArray(content2)) {
552
+ const text = content2.filter((b) => b.type === "text").map((b) => b.text).join("").trim();
553
+ if (text) lastMessage = text.slice(0, 120);
554
+ }
555
+ }
556
+ } catch {
557
+ continue;
558
+ }
559
+ }
560
+ if (!timestamp) continue;
561
+ const fileStat = await stat2(filePath);
562
+ const updatedAt = lastTimestamp || fileStat.mtime.toISOString();
563
+ const repo = deriveRepoName(cwd);
564
+ allChats.push({
565
+ sessionId,
566
+ text: preview,
567
+ timestamp,
568
+ updatedAt,
569
+ lastMessage,
570
+ isActive: activeSessions.has(sessionId),
571
+ repo,
572
+ cwd,
573
+ gitBranch,
574
+ chatName: chatNamesMap[sessionId] || ""
575
+ });
576
+ }
577
+ }
578
+ return c.json({ result: allChats });
579
+ } catch (error) {
580
+ console.error("[GET /chats] error:", error);
581
+ return c.json({ result: [] });
582
+ }
583
+ });
584
+ chats.get("/:id", async (c) => {
585
+ const sessionId = c.req.param("id");
586
+ if (!sessionId || /[\/\\]/.test(sessionId))
587
+ return c.json({ error: "Invalid session id" }, 400);
588
+ try {
589
+ const file = await findSessionFile(sessionId);
590
+ if (!file) return c.json({ error: "Chat not found" }, 404);
591
+ const content = await readFile2(file, "utf-8");
592
+ const lines = content.split("\n").filter(Boolean);
593
+ const parsed = lines.map((line) => JSON.parse(line));
594
+ let model;
595
+ let permissionMode;
596
+ let cwd;
597
+ for (const obj of parsed) {
598
+ if (obj.type === "user" && obj.cwd && !cwd) cwd = obj.cwd;
599
+ }
600
+ for (const obj of [...parsed].reverse()) {
601
+ if (!model && obj.type === "assistant") model = obj.message?.model;
602
+ if (!permissionMode && obj.type === "user" && !obj.isMeta)
603
+ permissionMode = obj.permissionMode;
604
+ if (model && permissionMode) break;
605
+ }
606
+ const messages = parsed.filter(
607
+ (obj) => obj.type === "user" && !obj.isMeta || obj.type === "assistant"
608
+ ).flatMap((obj) => {
609
+ const content2 = obj.message.content;
610
+ if (typeof content2 === "string") {
611
+ return [
612
+ {
613
+ role: obj.message.role,
614
+ type: "text",
615
+ text: content2.trim(),
616
+ timestamp: obj.timestamp
617
+ }
618
+ ];
619
+ }
620
+ return content2.filter(
621
+ (b) => ["text", "tool_use", "tool_result"].includes(b.type)
622
+ ).map((b) => {
623
+ if (b.type === "text") {
624
+ return {
625
+ role: obj.message.role,
626
+ type: "text",
627
+ text: b.text.trim(),
628
+ timestamp: obj.timestamp
629
+ };
630
+ }
631
+ if (b.type === "tool_use") {
632
+ return {
633
+ role: obj.message.role,
634
+ type: "tool_use",
635
+ name: b.name,
636
+ input: b.input,
637
+ timestamp: obj.timestamp
638
+ };
639
+ }
640
+ if (b.type === "tool_result") {
641
+ const resultText = Array.isArray(b.content) ? b.content.filter((r) => r.type === "text").map((r) => r.text).join("") : b.content ?? "";
642
+ return {
643
+ role: obj.message.role,
644
+ type: "tool_result",
645
+ text: resultText.trim(),
646
+ timestamp: obj.timestamp
647
+ };
648
+ }
649
+ }).filter(Boolean);
650
+ });
651
+ const chatName = await getChatName(sessionId);
652
+ return c.json({ messages, model, permissionMode, cwd, chatName: chatName || "" });
653
+ } catch {
654
+ return c.json({ error: "Chat not found" }, 404);
655
+ }
656
+ });
657
+
658
+ // src/routes/git/index.ts
659
+ import { Hono as Hono2 } from "hono";
660
+ import { exec } from "child_process";
661
+ import { readFile as readFile3 } from "fs/promises";
662
+ import path4 from "path";
663
+ import { promisify } from "util";
664
+ var execAsync = promisify(exec);
665
+ async function getRepoRoot(cwd) {
666
+ try {
667
+ const { stdout } = await execAsync("git rev-parse --show-toplevel", {
668
+ cwd: cwd || process.cwd()
669
+ });
670
+ return stdout.trim();
671
+ } catch {
672
+ return null;
673
+ }
674
+ }
675
+ var git = new Hono2();
676
+ git.get("/status", async (c) => {
677
+ try {
678
+ const cwd = c.req.query("cwd");
679
+ const root = await getRepoRoot(cwd);
680
+ if (!root) return c.json({ files: [], error: "Not a git repository" });
681
+ const { stdout: statusOut } = await execAsync("git status --porcelain", {
682
+ cwd: root
683
+ });
684
+ const [{ stdout: unstagedNum }, { stdout: stagedNum }] = await Promise.all([
685
+ execAsync("git diff --numstat", { cwd: root }).catch(() => ({
686
+ stdout: ""
687
+ })),
688
+ execAsync("git diff --cached --numstat", { cwd: root }).catch(() => ({
689
+ stdout: ""
690
+ }))
691
+ ]);
692
+ const parseNumstat = (raw) => {
693
+ const map = /* @__PURE__ */ new Map();
694
+ for (const line of raw.split("\n").filter(Boolean)) {
695
+ const [add, del, file] = line.split(" ");
696
+ if (file) {
697
+ map.set(file, {
698
+ additions: add === "-" ? 0 : parseInt(add, 10),
699
+ deletions: del === "-" ? 0 : parseInt(del, 10)
700
+ });
701
+ }
702
+ }
703
+ return map;
704
+ };
705
+ const unstagedStats = parseNumstat(unstagedNum);
706
+ const stagedStats = parseNumstat(stagedNum);
707
+ const files = [];
708
+ for (const line of statusOut.split("\n").filter(Boolean)) {
709
+ const xy = line.slice(0, 2);
710
+ const filePath = line.slice(3).trim();
711
+ let status = "M";
712
+ let staged = false;
713
+ if (xy === "??") {
714
+ status = "?";
715
+ } else if (xy === "!!") {
716
+ continue;
717
+ } else {
718
+ const indexStatus = xy[0];
719
+ const worktreeStatus = xy[1];
720
+ if (indexStatus !== " " && indexStatus !== "?") {
721
+ staged = true;
722
+ if (indexStatus === "A") status = "A";
723
+ else if (indexStatus === "D") status = "D";
724
+ else if (indexStatus === "R") status = "R";
725
+ else status = "M";
726
+ } else {
727
+ if (worktreeStatus === "D") status = "D";
728
+ else status = "M";
729
+ }
730
+ }
731
+ const stats = staged ? stagedStats.get(filePath) : unstagedStats.get(filePath);
732
+ let additions = stats?.additions ?? 0;
733
+ let deletions = stats?.deletions ?? 0;
734
+ if (status === "?") {
735
+ try {
736
+ const content = await readFile3(path4.join(root, filePath), "utf-8");
737
+ additions = content.split("\n").length;
738
+ } catch {
739
+ additions = 0;
740
+ }
741
+ }
742
+ files.push({ path: filePath, status, staged, additions, deletions });
743
+ }
744
+ return c.json({ files });
745
+ } catch (error) {
746
+ console.error("git status error:", error);
747
+ return c.json({ files: [] });
748
+ }
749
+ });
750
+ git.get("/diff", async (c) => {
751
+ const filePath = c.req.query("file");
752
+ if (!filePath) return c.json({ error: "file parameter required" }, 400);
753
+ if (filePath.includes("..")) return c.json({ error: "Invalid path" }, 400);
754
+ const cwd = c.req.query("cwd");
755
+ const root = await getRepoRoot(cwd);
756
+ if (!root) return c.json({ error: "Not a git repository" }, 400);
757
+ const safePath = filePath.replace(/'/g, "'\\''");
758
+ try {
759
+ let oldContent = "";
760
+ let newContent = "";
761
+ try {
762
+ const { stdout } = await execAsync(`git show 'HEAD:${safePath}'`, {
763
+ cwd: root,
764
+ maxBuffer: 10 * 1024 * 1024
765
+ });
766
+ oldContent = stdout;
767
+ } catch {
768
+ oldContent = "";
769
+ }
770
+ try {
771
+ newContent = await readFile3(path4.join(root, filePath), "utf-8");
772
+ } catch (e) {
773
+ console.error("readFile error for", filePath, e);
774
+ newContent = "";
775
+ }
776
+ return c.json({ path: filePath, oldContent, newContent });
777
+ } catch (error) {
778
+ console.error("git diff error:", error);
779
+ return c.json({ error: "Failed to get diff" }, 500);
780
+ }
781
+ });
782
+ git.get("/branch", async (c) => {
783
+ try {
784
+ const cwd = c.req.query("cwd");
785
+ const root = await getRepoRoot(cwd);
786
+ if (!root) return c.json({ error: "Not a git repository" }, 400);
787
+ const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", { cwd: root });
788
+ return c.json({ branch: stdout.trim() });
789
+ } catch (error) {
790
+ console.error("git branch error:", error);
791
+ return c.json({ error: "Failed to get branch" }, 500);
792
+ }
793
+ });
794
+ git.post("/branch/rename", async (c) => {
795
+ try {
796
+ const { newName, cwd } = await c.req.json();
797
+ if (!newName || !/^[a-zA-Z0-9._\/-]+$/.test(newName)) {
798
+ return c.json({ error: "Invalid branch name" }, 400);
799
+ }
800
+ const root = await getRepoRoot(cwd);
801
+ if (!root) return c.json({ error: "Not a git repository" }, 400);
802
+ await execAsync(`git branch -m ${newName}`, { cwd: root });
803
+ return c.json({ ok: true, branch: newName });
804
+ } catch (error) {
805
+ console.error("git branch rename error:", error);
806
+ return c.json({ error: "Failed to rename branch" }, 500);
807
+ }
808
+ });
809
+
810
+ // src/routes/gh/index.ts
811
+ import { Hono as Hono3 } from "hono";
812
+ import { exec as exec2, execSync as execSync3 } from "child_process";
813
+ import { promisify as promisify2 } from "util";
814
+ var execAsync2 = promisify2(exec2);
815
+ var gh = new Hono3();
816
+ gh.get("/repos", async (c) => {
817
+ const fields = "--json name,nameWithOwner,description,url,isPrivate,updatedAt --limit 100";
818
+ try {
819
+ const personal = JSON.parse(
820
+ execSync3(`gh repo list ${fields}`, { encoding: "utf-8" })
821
+ );
822
+ let orgRepos = [];
823
+ try {
824
+ const orgs = execSync3("gh api user/orgs --jq '.[].login'", { encoding: "utf-8" }).trim().split("\n").filter(Boolean);
825
+ for (const org of orgs) {
826
+ const repos = JSON.parse(
827
+ execSync3(`gh repo list ${org} ${fields}`, { encoding: "utf-8" })
828
+ );
829
+ orgRepos.push(...repos);
830
+ }
831
+ } catch {
832
+ }
833
+ const username = execSync3("gh api user --jq '.login'", { encoding: "utf-8" }).trim();
834
+ const all = [...personal, ...orgRepos].sort(
835
+ (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
836
+ );
837
+ return c.json({ username, repos: all });
838
+ } catch (error) {
839
+ return c.json([]);
840
+ }
841
+ });
842
+ gh.get("/pr", async (c) => {
843
+ try {
844
+ const cwd = c.req.query("cwd");
845
+ const root = await getRepoRoot(cwd);
846
+ if (!root) return c.json({ exists: false });
847
+ const { stdout } = await execAsync2(
848
+ "gh pr view --json number,title,url",
849
+ { cwd: root }
850
+ );
851
+ const pr = JSON.parse(stdout);
852
+ return c.json({ exists: true, pr });
853
+ } catch {
854
+ return c.json({ exists: false });
855
+ }
856
+ });
857
+ gh.post("/pr", async (c) => {
858
+ try {
859
+ const { cwd } = await c.req.json().catch(() => ({ cwd: void 0 }));
860
+ const root = await getRepoRoot(cwd);
861
+ if (!root) return c.json({ error: "Not a git repository" }, 400);
862
+ await execAsync2("git push -u origin HEAD", { cwd: root });
863
+ await execAsync2("gh pr create --fill", { cwd: root });
864
+ const { stdout } = await execAsync2(
865
+ "gh pr view --json number,title,url",
866
+ { cwd: root }
867
+ );
868
+ const pr = JSON.parse(stdout);
869
+ return c.json(pr);
870
+ } catch (error) {
871
+ console.error("gh pr create error:", error);
872
+ const msg = error?.stderr || error?.message || "Failed to create PR";
873
+ return c.json({ error: msg }, 500);
874
+ }
875
+ });
876
+
877
+ // src/server.ts
878
+ import { bearerAuth } from "hono/bearer-auth";
879
+ var app = new Hono4();
880
+ var token = process.env.AUTH_TOKEN;
881
+ if (!token) {
882
+ console.error("AUTH_TOKEN is not set. Run `anywhere-ai init` first.");
883
+ process.exit(1);
884
+ }
885
+ app.use("*", cors());
886
+ app.get("/health", async (c) => {
887
+ return c.json({ message: "server is healthy" }, 200);
888
+ });
889
+ app.use("/v1/*", bearerAuth({ token }));
890
+ app.route("/v1/chats", chats);
891
+ app.route("/v1/git", git);
892
+ app.route("/v1/gh", gh);
893
+ serve(
894
+ {
895
+ fetch: app.fetch,
896
+ hostname: "0.0.0.0",
897
+ port: parseInt(process.env.PORT || "3847")
898
+ },
899
+ (info) => {
900
+ }
901
+ );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anywhere-ai",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "type": "module",
5
5
  "description": "Code on any repo from your phone",
6
6
  "bin": {