anywhere-ai 0.0.15 → 0.0.16

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
@@ -10,7 +10,7 @@ import { spawnSync, execSync } from "child_process";
10
10
  import readline from "readline";
11
11
  var args = process.argv.slice(2);
12
12
  if (args.includes("--version") || args.includes("-v")) {
13
- console.log(`anywhere-ai v${"0.0.15"}`);
13
+ console.log(`anywhere-ai v${"0.0.16"}`);
14
14
  process.exit(0);
15
15
  }
16
16
  function ask(question, preserveCase = false) {
@@ -188,4 +188,4 @@ if (isVPS) {
188
188
  console.log(" Make sure port " + port + " is open in your firewall.");
189
189
  }
190
190
  console.log();
191
- import("./server-2HM57DQY.js");
191
+ import("./server-HGN3DO3S.js");
@@ -0,0 +1,782 @@
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 os2 from "os";
11
+ import path2 from "path";
12
+ import { readdir, readFile, stat } 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 PROJECTS_DIR = path.join(ANYWHERE_DIR, "projects");
28
+ var WORKTREES_DIR = path.join(ANYWHERE_DIR, "worktrees");
29
+ var sessions = /* @__PURE__ */ new Map();
30
+ var sessionConfig = /* @__PURE__ */ new Map();
31
+ var activeSessions = /* @__PURE__ */ new Set();
32
+ var pendingPermissions = /* @__PURE__ */ new Map();
33
+ function getAssistantText(msg) {
34
+ if (msg.type !== "assistant") return null;
35
+ return msg.message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
36
+ }
37
+ var permissionCallbacks = /* @__PURE__ */ new Map();
38
+ function setPermissionCallback(sessionId, cb) {
39
+ if (cb) {
40
+ permissionCallbacks.set(sessionId, cb);
41
+ } else {
42
+ permissionCallbacks.delete(sessionId);
43
+ }
44
+ }
45
+ var permReqCounter = 0;
46
+ function makeCanUseTool(sessionHint, permissionMode) {
47
+ return async (toolName, input, options) => {
48
+ console.log(
49
+ `[canUseTool] tool=${toolName} reason=${options.decisionReason ?? "none"} mode=${permissionMode}`
50
+ );
51
+ const requestId = `perm_${++permReqCounter}_${Date.now()}`;
52
+ const req = {
53
+ requestId,
54
+ toolName,
55
+ input,
56
+ description: input.description,
57
+ decisionReason: options.decisionReason,
58
+ suggestions: options.suggestions
59
+ };
60
+ const cb = permissionCallbacks.get(sessionHint);
61
+ if (cb) cb(req);
62
+ return new Promise((resolve) => {
63
+ pendingPermissions.set(requestId, resolve);
64
+ const timeout = setTimeout(
65
+ () => {
66
+ if (pendingPermissions.has(requestId)) {
67
+ pendingPermissions.delete(requestId);
68
+ resolve({
69
+ behavior: "deny",
70
+ message: "Permission request timed out"
71
+ });
72
+ }
73
+ },
74
+ 5 * 60 * 1e3
75
+ );
76
+ options.signal.addEventListener("abort", () => {
77
+ clearTimeout(timeout);
78
+ if (pendingPermissions.has(requestId)) {
79
+ pendingPermissions.delete(requestId);
80
+ resolve({ behavior: "deny", message: "Request aborted" });
81
+ }
82
+ });
83
+ const origResolve = resolve;
84
+ pendingPermissions.set(requestId, (result) => {
85
+ clearTimeout(timeout);
86
+ pendingPermissions.delete(requestId);
87
+ origResolve(result);
88
+ });
89
+ });
90
+ };
91
+ }
92
+ var setupRepo = async (repo) => {
93
+ console.log(`[setupRepo] repo=${repo}`);
94
+ await fs.mkdir(PROJECTS_DIR, { recursive: true });
95
+ await fs.mkdir(WORKTREES_DIR, { recursive: true });
96
+ const repoPath = path.join(PROJECTS_DIR, repo);
97
+ try {
98
+ await fs.access(repoPath);
99
+ console.log(`[setupRepo] repo exists at ${repoPath}, fetching origin`);
100
+ execSync("git fetch origin", { cwd: repoPath, encoding: "utf-8" });
101
+ } catch {
102
+ console.log(`[setupRepo] cloning repo ${repo} to ${repoPath}`);
103
+ execSync(`gh repo clone ${repo} ${repoPath}`, { encoding: "utf-8" });
104
+ }
105
+ const branchName = generate({ words: 3 }).dashed;
106
+ console.log(`[setupRepo] branchName=${branchName}`);
107
+ const worktreePath = path.join(WORKTREES_DIR, branchName);
108
+ console.log(`[setupRepo] creating worktree at ${worktreePath}`);
109
+ execSync(`git worktree add ${worktreePath} -b ${branchName}`, {
110
+ cwd: repoPath,
111
+ encoding: "utf-8"
112
+ });
113
+ console.log(`[setupRepo] worktree ready at ${worktreePath}`);
114
+ return worktreePath;
115
+ };
116
+ var createChat = async ({
117
+ model,
118
+ permission,
119
+ repo,
120
+ prompt
121
+ }) => {
122
+ console.log(`[createChat] model=${model} permission=${permission} repo=${repo} prompt="${prompt?.slice(0, 50)}"`);
123
+ const repoPath = repo ? await setupRepo(repo) : void 0;
124
+ console.log(`[createChat] cwd=${repoPath || process.cwd()}`);
125
+ const tempId = `temp_${Date.now()}`;
126
+ const mode = permission || "acceptEdits";
127
+ console.log(`[createChat] tempId=${tempId} mode=${mode}`);
128
+ const originalCwd = process.cwd();
129
+ if (repoPath) process.chdir(repoPath);
130
+ const session = unstable_v2_createSession({
131
+ model: model || "claude-opus-4-6",
132
+ permissionMode: mode,
133
+ canUseTool: makeCanUseTool(tempId, mode)
134
+ });
135
+ if (repoPath) process.chdir(originalCwd);
136
+ return { session, tempId, repoPath };
137
+ };
138
+ var sendMessage = async ({
139
+ prompt,
140
+ session
141
+ }) => {
142
+ await session.send(prompt);
143
+ };
144
+ var getSession = async ({
145
+ sessionId,
146
+ model,
147
+ permission
148
+ }) => {
149
+ let session = sessions.get(sessionId);
150
+ const currentConfig = sessionConfig.get(sessionId);
151
+ const requestedModel = model || "claude-opus-4-6";
152
+ const requestedPermission = permission || "acceptEdits";
153
+ const configChanged = session && currentConfig && (requestedModel !== currentConfig.model || requestedPermission !== currentConfig.permission);
154
+ if (session && configChanged) {
155
+ console.log(`[getSession] ${sessionId} re-creating (model=${currentConfig.model}->${requestedModel} permission=${currentConfig.permission}->${requestedPermission})`);
156
+ session.close();
157
+ sessions.delete(sessionId);
158
+ sessionConfig.delete(sessionId);
159
+ session = void 0;
160
+ }
161
+ if (!session) {
162
+ console.log(`[getSession] ${sessionId} resuming from JSONL`);
163
+ session = unstable_v2_resumeSession(sessionId, {
164
+ model: requestedModel,
165
+ permissionMode: requestedPermission,
166
+ canUseTool: makeCanUseTool(sessionId, requestedPermission)
167
+ });
168
+ sessions.set(sessionId, session);
169
+ sessionConfig.set(sessionId, { model: requestedModel, permission: requestedPermission });
170
+ } else {
171
+ console.log(`[getSession] ${sessionId} found in memory`);
172
+ }
173
+ return session;
174
+ };
175
+
176
+ // src/routes/chats/index.ts
177
+ var repoNameCache = /* @__PURE__ */ new Map();
178
+ function isHomeDir(dir) {
179
+ const home = os2.homedir();
180
+ if (dir === home) return true;
181
+ if (dir === "/root" || dir === "/") return true;
182
+ if (/^\/home\/[^/]+\/?$/.test(dir)) return true;
183
+ if (/^\/Users\/[^/]+\/?$/.test(dir)) return true;
184
+ return false;
185
+ }
186
+ function deriveRepoName(cwd) {
187
+ if (!cwd) return "";
188
+ if (repoNameCache.has(cwd)) return repoNameCache.get(cwd);
189
+ if (isHomeDir(cwd)) {
190
+ repoNameCache.set(cwd, "");
191
+ return "";
192
+ }
193
+ let name = "";
194
+ let tryDir = cwd;
195
+ let foundGit = false;
196
+ while (tryDir && !isHomeDir(tryDir) && tryDir !== "/") {
197
+ try {
198
+ const url = execSync2("git remote get-url origin 2>/dev/null", {
199
+ cwd: tryDir,
200
+ encoding: "utf-8",
201
+ timeout: 3e3
202
+ }).trim();
203
+ const match = url.match(/[:/]([^/]+\/[^/.]+?)(?:\.git)?$/);
204
+ if (match?.[1]) name = match[1];
205
+ foundGit = true;
206
+ break;
207
+ } catch {
208
+ const parent = path2.dirname(tryDir);
209
+ if (parent === tryDir) break;
210
+ tryDir = parent;
211
+ }
212
+ }
213
+ if (!foundGit) {
214
+ tryDir = cwd;
215
+ while (tryDir && !isHomeDir(tryDir) && tryDir !== "/") {
216
+ try {
217
+ const root = execSync2("git rev-parse --show-toplevel 2>/dev/null", {
218
+ cwd: tryDir,
219
+ encoding: "utf-8",
220
+ timeout: 3e3
221
+ }).trim();
222
+ if (!isHomeDir(root)) {
223
+ name = root.split("/").filter(Boolean).pop() || "";
224
+ }
225
+ break;
226
+ } catch {
227
+ const parent = path2.dirname(tryDir);
228
+ if (parent === tryDir) break;
229
+ tryDir = parent;
230
+ }
231
+ }
232
+ }
233
+ repoNameCache.set(cwd, name);
234
+ return name;
235
+ }
236
+ async function findSessionFile(sessionId) {
237
+ const projectsBase = path2.join(os2.homedir(), ".claude", "projects");
238
+ try {
239
+ const dirs = await readdir(projectsBase);
240
+ for (const dir of dirs) {
241
+ const filePath = path2.join(projectsBase, dir, `${sessionId}.jsonl`);
242
+ try {
243
+ await stat(filePath);
244
+ return filePath;
245
+ } catch {
246
+ }
247
+ }
248
+ } catch {
249
+ }
250
+ return null;
251
+ }
252
+ var chats = new Hono();
253
+ chats.post("/new", async (c) => {
254
+ const { prompt, model, permission, repo } = await c.req.json();
255
+ console.log(`[POST /new] prompt="${prompt?.slice(0, 50)}" model=${model} repo=${repo}`);
256
+ if (!prompt) return c.json({ error: "Prompt is required" }, 400);
257
+ try {
258
+ const { session, tempId, repoPath } = await createChat({ model, permission, repo, prompt });
259
+ await sendMessage({ prompt, session });
260
+ return streamSSE(c, async (stream) => {
261
+ let finalSessionId = "";
262
+ console.log(`[POST /new] SSE stream started, tempId=${tempId}`);
263
+ const sendPerm = async (req) => {
264
+ try {
265
+ await stream.writeSSE({
266
+ data: JSON.stringify({
267
+ type: "permission_request",
268
+ ...req,
269
+ sessionId: finalSessionId || "pending"
270
+ })
271
+ });
272
+ } catch (e) {
273
+ console.error("Failed to send permission SSE:", e);
274
+ }
275
+ };
276
+ setPermissionCallback(tempId, sendPerm);
277
+ try {
278
+ for await (const msg of session.stream()) {
279
+ if (msg.session_id && !finalSessionId) {
280
+ finalSessionId = msg.session_id;
281
+ console.log(`[POST /new] sessionId received: ${finalSessionId}`);
282
+ activeSessions.add(finalSessionId);
283
+ setPermissionCallback(tempId, null);
284
+ setPermissionCallback(finalSessionId, sendPerm);
285
+ }
286
+ const text = getAssistantText(msg);
287
+ if (text)
288
+ await stream.writeSSE({
289
+ data: JSON.stringify({
290
+ response: text.trim(),
291
+ sessionId: msg.session_id
292
+ })
293
+ });
294
+ }
295
+ if (finalSessionId) {
296
+ sessions.set(finalSessionId, session);
297
+ sessionConfig.set(finalSessionId, {
298
+ model: model || "claude-opus-4-6",
299
+ permission: permission || "acceptEdits"
300
+ });
301
+ console.log(`[POST /new] session stored: ${finalSessionId}${repoPath ? ` cwd=${repoPath}` : ""}`);
302
+ }
303
+ } catch (err) {
304
+ console.error(err);
305
+ await stream.writeSSE({
306
+ data: JSON.stringify({ error: "Stream error" })
307
+ });
308
+ } finally {
309
+ setPermissionCallback(tempId, null);
310
+ if (finalSessionId) {
311
+ setPermissionCallback(finalSessionId, null);
312
+ activeSessions.delete(finalSessionId);
313
+ }
314
+ }
315
+ });
316
+ } catch (error) {
317
+ console.error(error);
318
+ return c.json({ error: "Failed to create new chat" }, 400);
319
+ }
320
+ });
321
+ chats.post("/:id/message", async (c) => {
322
+ const { prompt, model, permission } = await c.req.json();
323
+ const sessionId = c.req.param("id");
324
+ console.log(`[POST /:id/message] sessionId=${sessionId} prompt="${prompt?.slice(0, 50)}"`);
325
+ if (!prompt || !sessionId || /[\/\\]/.test(sessionId))
326
+ return c.json({ error: "Invalid request" }, 400);
327
+ try {
328
+ const session = await getSession({ sessionId, model, permission });
329
+ if (!session) return c.json({ error: "Session is required" }, 400);
330
+ await sendMessage({ prompt, session });
331
+ return streamSSE(c, async (stream) => {
332
+ activeSessions.add(sessionId);
333
+ setPermissionCallback(sessionId, async (req) => {
334
+ try {
335
+ await stream.writeSSE({
336
+ data: JSON.stringify({
337
+ type: "permission_request",
338
+ ...req,
339
+ sessionId
340
+ })
341
+ });
342
+ } catch (e) {
343
+ console.error("Failed to send permission SSE:", e);
344
+ }
345
+ });
346
+ try {
347
+ for await (const msg of session.stream()) {
348
+ const text = getAssistantText(msg);
349
+ if (text)
350
+ await stream.writeSSE({
351
+ data: JSON.stringify({
352
+ response: text?.trim() || null,
353
+ sessionId: msg.session_id
354
+ })
355
+ });
356
+ }
357
+ } catch (err) {
358
+ console.error(err);
359
+ await stream.writeSSE({
360
+ data: JSON.stringify({ error: "Stream error" })
361
+ });
362
+ } finally {
363
+ setPermissionCallback(sessionId, null);
364
+ activeSessions.delete(sessionId);
365
+ }
366
+ });
367
+ } catch (error) {
368
+ console.error(error);
369
+ return c.json({ error: "Failed to send message" }, 400);
370
+ }
371
+ });
372
+ chats.post("/:id/permission", async (c) => {
373
+ const { requestId, behavior, updatedPermissions } = await c.req.json();
374
+ if (!requestId || !behavior)
375
+ return c.json({ error: "requestId and behavior are required" }, 400);
376
+ const resolve = pendingPermissions.get(requestId);
377
+ if (!resolve)
378
+ return c.json({ error: "No pending permission request found" }, 404);
379
+ if (behavior === "allow") {
380
+ resolve({
381
+ behavior: "allow",
382
+ ...updatedPermissions ? { updatedPermissions } : {}
383
+ });
384
+ } else {
385
+ resolve({ behavior: "deny", message: "User denied permission" });
386
+ }
387
+ return c.json({ ok: true });
388
+ });
389
+ chats.get("/", async (c) => {
390
+ try {
391
+ const projectsBase = path2.join(os2.homedir(), ".claude", "projects");
392
+ const dirs = await readdir(projectsBase);
393
+ const allChats = [];
394
+ for (const dir of dirs) {
395
+ const dirPath = path2.join(projectsBase, dir);
396
+ try {
397
+ const s = await stat(dirPath);
398
+ if (!s.isDirectory()) continue;
399
+ } catch {
400
+ continue;
401
+ }
402
+ const files = (await readdir(dirPath)).filter(
403
+ (f) => f.endsWith(".jsonl")
404
+ );
405
+ for (const file of files) {
406
+ const filePath = path2.join(dirPath, file);
407
+ const content = await readFile(filePath, "utf-8");
408
+ const lines = content.split("\n").filter(Boolean);
409
+ const sessionId = file.replace(".jsonl", "");
410
+ let preview = "";
411
+ let timestamp = "";
412
+ let cwd = "";
413
+ let gitBranch = "";
414
+ let lastMessage = "";
415
+ let lastTimestamp = "";
416
+ for (const line of lines) {
417
+ try {
418
+ const obj = JSON.parse(line);
419
+ if (obj.type === "user") {
420
+ if (!cwd && obj.cwd) cwd = obj.cwd;
421
+ if (!gitBranch && obj.gitBranch) gitBranch = obj.gitBranch;
422
+ if (!obj.isMeta) {
423
+ if (!preview) {
424
+ const msgContent = obj.message.content;
425
+ if (typeof msgContent === "string") {
426
+ preview = msgContent.slice(0, 100);
427
+ } else {
428
+ preview = msgContent.filter((b) => b.type === "text").map((b) => b.text).join("").slice(0, 100);
429
+ }
430
+ timestamp = obj.timestamp;
431
+ }
432
+ if (obj.timestamp) lastTimestamp = obj.timestamp;
433
+ }
434
+ }
435
+ if (obj.type === "assistant") {
436
+ if (obj.timestamp) lastTimestamp = obj.timestamp;
437
+ const content2 = obj.message?.content;
438
+ if (typeof content2 === "string") {
439
+ lastMessage = content2.slice(0, 120);
440
+ } else if (Array.isArray(content2)) {
441
+ const text = content2.filter((b) => b.type === "text").map((b) => b.text).join("").trim();
442
+ if (text) lastMessage = text.slice(0, 120);
443
+ }
444
+ }
445
+ } catch {
446
+ continue;
447
+ }
448
+ }
449
+ if (!timestamp) continue;
450
+ const fileStat = await stat(filePath);
451
+ const updatedAt = lastTimestamp || fileStat.mtime.toISOString();
452
+ const repo = deriveRepoName(cwd);
453
+ allChats.push({
454
+ sessionId,
455
+ text: preview,
456
+ timestamp,
457
+ updatedAt,
458
+ lastMessage,
459
+ isActive: activeSessions.has(sessionId),
460
+ repo,
461
+ cwd,
462
+ gitBranch
463
+ });
464
+ }
465
+ }
466
+ return c.json({ result: allChats });
467
+ } catch (error) {
468
+ console.error("[GET /chats] error:", error);
469
+ return c.json({ result: [] });
470
+ }
471
+ });
472
+ chats.get("/:id", async (c) => {
473
+ const sessionId = c.req.param("id");
474
+ if (!sessionId || /[\/\\]/.test(sessionId))
475
+ return c.json({ error: "Invalid session id" }, 400);
476
+ try {
477
+ const file = await findSessionFile(sessionId);
478
+ if (!file) return c.json({ error: "Chat not found" }, 404);
479
+ const content = await readFile(file, "utf-8");
480
+ const lines = content.split("\n").filter(Boolean);
481
+ const parsed = lines.map((line) => JSON.parse(line));
482
+ let model;
483
+ let permissionMode;
484
+ for (const obj of [...parsed].reverse()) {
485
+ if (!model && obj.type === "assistant") model = obj.message?.model;
486
+ if (!permissionMode && obj.type === "user" && !obj.isMeta)
487
+ permissionMode = obj.permissionMode;
488
+ if (model && permissionMode) break;
489
+ }
490
+ const messages = parsed.filter(
491
+ (obj) => obj.type === "user" && !obj.isMeta || obj.type === "assistant"
492
+ ).flatMap((obj) => {
493
+ const content2 = obj.message.content;
494
+ if (typeof content2 === "string") {
495
+ return [
496
+ {
497
+ role: obj.message.role,
498
+ type: "text",
499
+ text: content2.trim(),
500
+ timestamp: obj.timestamp
501
+ }
502
+ ];
503
+ }
504
+ return content2.filter(
505
+ (b) => ["text", "tool_use", "tool_result"].includes(b.type)
506
+ ).map((b) => {
507
+ if (b.type === "text") {
508
+ return {
509
+ role: obj.message.role,
510
+ type: "text",
511
+ text: b.text.trim(),
512
+ timestamp: obj.timestamp
513
+ };
514
+ }
515
+ if (b.type === "tool_use") {
516
+ return {
517
+ role: obj.message.role,
518
+ type: "tool_use",
519
+ name: b.name,
520
+ input: b.input,
521
+ timestamp: obj.timestamp
522
+ };
523
+ }
524
+ if (b.type === "tool_result") {
525
+ const resultText = Array.isArray(b.content) ? b.content.filter((r) => r.type === "text").map((r) => r.text).join("") : b.content ?? "";
526
+ return {
527
+ role: obj.message.role,
528
+ type: "tool_result",
529
+ text: resultText.trim(),
530
+ timestamp: obj.timestamp
531
+ };
532
+ }
533
+ }).filter(Boolean);
534
+ });
535
+ return c.json({ messages, model, permissionMode });
536
+ } catch {
537
+ return c.json({ error: "Chat not found" }, 404);
538
+ }
539
+ });
540
+
541
+ // src/routes/git/index.ts
542
+ import { Hono as Hono2 } from "hono";
543
+ import { exec } from "child_process";
544
+ import { readFile as readFile2 } from "fs/promises";
545
+ import path3 from "path";
546
+ import { promisify } from "util";
547
+ var execAsync = promisify(exec);
548
+ var _repoRoot = null;
549
+ async function getRepoRoot() {
550
+ if (_repoRoot === null) {
551
+ try {
552
+ const { stdout } = await execAsync("git rev-parse --show-toplevel");
553
+ _repoRoot = stdout.trim();
554
+ } catch {
555
+ _repoRoot = false;
556
+ }
557
+ }
558
+ return _repoRoot || null;
559
+ }
560
+ var git = new Hono2();
561
+ git.get("/status", async (c) => {
562
+ try {
563
+ const root = await getRepoRoot();
564
+ if (!root) return c.json({ files: [], error: "Not a git repository" });
565
+ const { stdout: statusOut } = await execAsync("git status --porcelain", {
566
+ cwd: root
567
+ });
568
+ const [{ stdout: unstagedNum }, { stdout: stagedNum }] = await Promise.all([
569
+ execAsync("git diff --numstat", { cwd: root }).catch(() => ({
570
+ stdout: ""
571
+ })),
572
+ execAsync("git diff --cached --numstat", { cwd: root }).catch(() => ({
573
+ stdout: ""
574
+ }))
575
+ ]);
576
+ const parseNumstat = (raw) => {
577
+ const map = /* @__PURE__ */ new Map();
578
+ for (const line of raw.split("\n").filter(Boolean)) {
579
+ const [add, del, file] = line.split(" ");
580
+ if (file) {
581
+ map.set(file, {
582
+ additions: add === "-" ? 0 : parseInt(add, 10),
583
+ deletions: del === "-" ? 0 : parseInt(del, 10)
584
+ });
585
+ }
586
+ }
587
+ return map;
588
+ };
589
+ const unstagedStats = parseNumstat(unstagedNum);
590
+ const stagedStats = parseNumstat(stagedNum);
591
+ const files = [];
592
+ for (const line of statusOut.split("\n").filter(Boolean)) {
593
+ const xy = line.slice(0, 2);
594
+ const filePath = line.slice(3).trim();
595
+ let status = "M";
596
+ let staged = false;
597
+ if (xy === "??") {
598
+ status = "?";
599
+ } else if (xy === "!!") {
600
+ continue;
601
+ } else {
602
+ const indexStatus = xy[0];
603
+ const worktreeStatus = xy[1];
604
+ if (indexStatus !== " " && indexStatus !== "?") {
605
+ staged = true;
606
+ if (indexStatus === "A") status = "A";
607
+ else if (indexStatus === "D") status = "D";
608
+ else if (indexStatus === "R") status = "R";
609
+ else status = "M";
610
+ } else {
611
+ if (worktreeStatus === "D") status = "D";
612
+ else status = "M";
613
+ }
614
+ }
615
+ const stats = staged ? stagedStats.get(filePath) : unstagedStats.get(filePath);
616
+ let additions = stats?.additions ?? 0;
617
+ let deletions = stats?.deletions ?? 0;
618
+ if (status === "?") {
619
+ try {
620
+ const content = await readFile2(path3.join(root, filePath), "utf-8");
621
+ additions = content.split("\n").length;
622
+ } catch {
623
+ additions = 0;
624
+ }
625
+ }
626
+ files.push({ path: filePath, status, staged, additions, deletions });
627
+ }
628
+ return c.json({ files });
629
+ } catch (error) {
630
+ console.error("git status error:", error);
631
+ return c.json({ files: [] });
632
+ }
633
+ });
634
+ git.get("/diff", async (c) => {
635
+ const filePath = c.req.query("file");
636
+ if (!filePath) return c.json({ error: "file parameter required" }, 400);
637
+ if (filePath.includes("..")) return c.json({ error: "Invalid path" }, 400);
638
+ const root = await getRepoRoot();
639
+ if (!root) return c.json({ error: "Not a git repository" }, 400);
640
+ const safePath = filePath.replace(/'/g, "'\\''");
641
+ try {
642
+ let oldContent = "";
643
+ let newContent = "";
644
+ try {
645
+ const { stdout } = await execAsync(`git show 'HEAD:${safePath}'`, {
646
+ cwd: root,
647
+ maxBuffer: 10 * 1024 * 1024
648
+ });
649
+ oldContent = stdout;
650
+ } catch {
651
+ oldContent = "";
652
+ }
653
+ try {
654
+ newContent = await readFile2(path3.join(root, filePath), "utf-8");
655
+ } catch (e) {
656
+ console.error("readFile error for", filePath, e);
657
+ newContent = "";
658
+ }
659
+ return c.json({ path: filePath, oldContent, newContent });
660
+ } catch (error) {
661
+ console.error("git diff error:", error);
662
+ return c.json({ error: "Failed to get diff" }, 500);
663
+ }
664
+ });
665
+ git.get("/branch", async (c) => {
666
+ try {
667
+ const root = await getRepoRoot();
668
+ if (!root) return c.json({ error: "Not a git repository" }, 400);
669
+ const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", { cwd: root });
670
+ return c.json({ branch: stdout.trim() });
671
+ } catch (error) {
672
+ console.error("git branch error:", error);
673
+ return c.json({ error: "Failed to get branch" }, 500);
674
+ }
675
+ });
676
+ git.post("/branch/rename", async (c) => {
677
+ try {
678
+ const root = await getRepoRoot();
679
+ if (!root) return c.json({ error: "Not a git repository" }, 400);
680
+ const { newName } = await c.req.json();
681
+ if (!newName || !/^[a-zA-Z0-9._\/-]+$/.test(newName)) {
682
+ return c.json({ error: "Invalid branch name" }, 400);
683
+ }
684
+ await execAsync(`git branch -m ${newName}`, { cwd: root });
685
+ return c.json({ ok: true, branch: newName });
686
+ } catch (error) {
687
+ console.error("git branch rename error:", error);
688
+ return c.json({ error: "Failed to rename branch" }, 500);
689
+ }
690
+ });
691
+
692
+ // src/routes/gh/index.ts
693
+ import { Hono as Hono3 } from "hono";
694
+ import { exec as exec2, execSync as execSync3 } from "child_process";
695
+ import { promisify as promisify2 } from "util";
696
+ var execAsync2 = promisify2(exec2);
697
+ var gh = new Hono3();
698
+ gh.get("/repos", async (c) => {
699
+ const fields = "--json name,nameWithOwner,description,url,isPrivate,updatedAt --limit 100";
700
+ try {
701
+ const personal = JSON.parse(
702
+ execSync3(`gh repo list ${fields}`, { encoding: "utf-8" })
703
+ );
704
+ let orgRepos = [];
705
+ try {
706
+ const orgs = execSync3("gh api user/orgs --jq '.[].login'", { encoding: "utf-8" }).trim().split("\n").filter(Boolean);
707
+ for (const org of orgs) {
708
+ const repos = JSON.parse(
709
+ execSync3(`gh repo list ${org} ${fields}`, { encoding: "utf-8" })
710
+ );
711
+ orgRepos.push(...repos);
712
+ }
713
+ } catch {
714
+ }
715
+ const username = execSync3("gh api user --jq '.login'", { encoding: "utf-8" }).trim();
716
+ const all = [...personal, ...orgRepos].sort(
717
+ (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
718
+ );
719
+ return c.json({ username, repos: all });
720
+ } catch (error) {
721
+ return c.json([]);
722
+ }
723
+ });
724
+ gh.get("/pr", async (c) => {
725
+ try {
726
+ const root = await getRepoRoot();
727
+ if (!root) return c.json({ exists: false });
728
+ const { stdout } = await execAsync2(
729
+ "gh pr view --json number,title,url",
730
+ { cwd: root }
731
+ );
732
+ const pr = JSON.parse(stdout);
733
+ return c.json({ exists: true, pr });
734
+ } catch {
735
+ return c.json({ exists: false });
736
+ }
737
+ });
738
+ gh.post("/pr", async (c) => {
739
+ try {
740
+ const root = await getRepoRoot();
741
+ if (!root) return c.json({ error: "Not a git repository" }, 400);
742
+ await execAsync2("git push -u origin HEAD", { cwd: root });
743
+ await execAsync2("gh pr create --fill", { cwd: root });
744
+ const { stdout } = await execAsync2(
745
+ "gh pr view --json number,title,url",
746
+ { cwd: root }
747
+ );
748
+ const pr = JSON.parse(stdout);
749
+ return c.json(pr);
750
+ } catch (error) {
751
+ console.error("gh pr create error:", error);
752
+ const msg = error?.stderr || error?.message || "Failed to create PR";
753
+ return c.json({ error: msg }, 500);
754
+ }
755
+ });
756
+
757
+ // src/server.ts
758
+ import { bearerAuth } from "hono/bearer-auth";
759
+ var app = new Hono4();
760
+ var token = process.env.AUTH_TOKEN;
761
+ if (!token) {
762
+ console.error("AUTH_TOKEN is not set. Run `anywhere-ai init` first.");
763
+ process.exit(1);
764
+ }
765
+ app.use("*", cors());
766
+ app.get("/health", async (c) => {
767
+ return c.json({ message: "server is healthy" }, 200);
768
+ });
769
+ app.use("/v1/*", bearerAuth({ token }));
770
+ app.route("/v1/chats", chats);
771
+ app.route("/v1/git", git);
772
+ app.route("/v1/gh", gh);
773
+ serve(
774
+ {
775
+ fetch: app.fetch,
776
+ hostname: "0.0.0.0",
777
+ port: parseInt(process.env.PORT || "3847")
778
+ },
779
+ (info) => {
780
+ console.log("Server is running");
781
+ }
782
+ );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anywhere-ai",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "type": "module",
5
5
  "description": "Code on any repo from your phone",
6
6
  "bin": {