anywhere-ai 0.0.3 → 0.0.4

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
@@ -1,3 +1,5 @@
1
+ #!/usr/bin/env node
2
+
1
3
  // src/cli.ts
2
4
  import fs from "fs/promises";
3
5
  import { readFileSync } from "fs";
@@ -173,4 +175,4 @@ if (isVPS) {
173
175
  console.log(" Make sure port " + port + " is open in your firewall.");
174
176
  }
175
177
  console.log();
176
- import("./server-AGLOR4NI.js");
178
+ import("./server-Q5IYQIHO.js");
@@ -0,0 +1,585 @@
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 } from "fs/promises";
13
+ import { streamSSE } from "hono/streaming";
14
+
15
+ // src/chats.ts
16
+ import {
17
+ unstable_v2_createSession,
18
+ unstable_v2_prompt,
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
+ var ANYWHERE_DIR = path.join(os.homedir(), ".anywhere");
26
+ var PROJECTS_DIR = path.join(ANYWHERE_DIR, "projects");
27
+ var WORKTREES_DIR = path.join(ANYWHERE_DIR, "worktrees");
28
+ var sessions = /* @__PURE__ */ new Map();
29
+ var activeSessions = /* @__PURE__ */ new Set();
30
+ var pendingPermissions = /* @__PURE__ */ new Map();
31
+ function getAssistantText(msg) {
32
+ if (msg.type !== "assistant") return null;
33
+ return msg.message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
34
+ }
35
+ var permissionCallbacks = /* @__PURE__ */ new Map();
36
+ function setPermissionCallback(sessionId, cb) {
37
+ if (cb) {
38
+ permissionCallbacks.set(sessionId, cb);
39
+ } else {
40
+ permissionCallbacks.delete(sessionId);
41
+ }
42
+ }
43
+ var permReqCounter = 0;
44
+ function makeCanUseTool(sessionHint, permissionMode) {
45
+ return async (toolName, input, options) => {
46
+ console.log(
47
+ `[canUseTool] tool=${toolName} reason=${options.decisionReason ?? "none"} mode=${permissionMode}`
48
+ );
49
+ const requestId = `perm_${++permReqCounter}_${Date.now()}`;
50
+ const req = {
51
+ requestId,
52
+ toolName,
53
+ input,
54
+ description: input.description,
55
+ decisionReason: options.decisionReason,
56
+ suggestions: options.suggestions
57
+ };
58
+ const cb = permissionCallbacks.get(sessionHint);
59
+ if (cb) cb(req);
60
+ return new Promise((resolve) => {
61
+ pendingPermissions.set(requestId, resolve);
62
+ const timeout = setTimeout(
63
+ () => {
64
+ if (pendingPermissions.has(requestId)) {
65
+ pendingPermissions.delete(requestId);
66
+ resolve({
67
+ behavior: "deny",
68
+ message: "Permission request timed out"
69
+ });
70
+ }
71
+ },
72
+ 5 * 60 * 1e3
73
+ );
74
+ options.signal.addEventListener("abort", () => {
75
+ clearTimeout(timeout);
76
+ if (pendingPermissions.has(requestId)) {
77
+ pendingPermissions.delete(requestId);
78
+ resolve({ behavior: "deny", message: "Request aborted" });
79
+ }
80
+ });
81
+ const origResolve = resolve;
82
+ pendingPermissions.set(requestId, (result) => {
83
+ clearTimeout(timeout);
84
+ pendingPermissions.delete(requestId);
85
+ origResolve(result);
86
+ });
87
+ });
88
+ };
89
+ }
90
+ async function generateBranchName(prompt) {
91
+ try {
92
+ const start = Date.now();
93
+ const result = await unstable_v2_prompt(
94
+ `Generate a short git branch name (2-4 words, lowercase, kebab-case, no prefix) for this task: "${prompt.slice(0, 200)}". Reply with ONLY the branch name, nothing else.`,
95
+ { model: "claude-haiku-4-5" }
96
+ );
97
+ console.log(`[branch-name] took ${Date.now() - start}ms`);
98
+ if (result.subtype === "success") {
99
+ return result.result.trim().replace(/[^a-z0-9-]/g, "").slice(0, 40);
100
+ }
101
+ } catch (e) {
102
+ console.error("[branch-name] failed:", e);
103
+ }
104
+ return `chat-${Date.now()}`;
105
+ }
106
+ var setupRepo = async (repo, prompt) => {
107
+ await fs.mkdir(PROJECTS_DIR, { recursive: true });
108
+ await fs.mkdir(WORKTREES_DIR, { recursive: true });
109
+ const repoPath = path.join(PROJECTS_DIR, repo);
110
+ try {
111
+ await fs.access(repoPath);
112
+ execSync("git fetch origin", { cwd: repoPath, encoding: "utf-8" });
113
+ } catch {
114
+ execSync(`gh repo clone ${repo} ${repoPath}`, { encoding: "utf-8" });
115
+ }
116
+ const branchName = `chat/${await generateBranchName(prompt)}`;
117
+ const worktreePath = path.join(WORKTREES_DIR, branchName);
118
+ execSync(`git worktree add ${worktreePath} -b ${branchName}`, {
119
+ cwd: repoPath,
120
+ encoding: "utf-8"
121
+ });
122
+ return worktreePath;
123
+ };
124
+ var createChat = async ({
125
+ model,
126
+ permission,
127
+ repo,
128
+ prompt
129
+ }) => {
130
+ const repoPath = repo ? await setupRepo(repo, prompt || "chat") : void 0;
131
+ const tempId = `temp_${Date.now()}`;
132
+ const mode = permission || "acceptEdits";
133
+ const session = unstable_v2_createSession({
134
+ model: model || "claude-opus-4-6",
135
+ permissionMode: mode,
136
+ canUseTool: makeCanUseTool(tempId, mode),
137
+ ...repoPath ? { cwd: repoPath } : {}
138
+ });
139
+ return { session, tempId };
140
+ };
141
+ var sendMessage = async ({
142
+ prompt,
143
+ session
144
+ }) => {
145
+ await session.send(prompt);
146
+ };
147
+ var getSession = async ({
148
+ sessionId,
149
+ model,
150
+ permission
151
+ }) => {
152
+ let session = sessions.get(sessionId);
153
+ if (session && (model || permission)) {
154
+ session.close();
155
+ sessions.delete(sessionId);
156
+ session = void 0;
157
+ }
158
+ if (!session) {
159
+ const mode = permission || "acceptEdits";
160
+ session = unstable_v2_resumeSession(sessionId, {
161
+ model: model || "claude-opus-4-6",
162
+ permissionMode: mode,
163
+ canUseTool: makeCanUseTool(sessionId, mode)
164
+ });
165
+ sessions.set(sessionId, session);
166
+ }
167
+ return session;
168
+ };
169
+
170
+ // src/routes/chats/index.ts
171
+ var chats = new Hono();
172
+ chats.post("/new", async (c) => {
173
+ const { prompt, model, permission, repo } = await c.req.json();
174
+ if (!prompt) return c.json({ error: "Prompt is required" }, 400);
175
+ try {
176
+ const { session, tempId } = await createChat({ model, permission, repo, prompt });
177
+ await sendMessage({ prompt, session });
178
+ return streamSSE(c, async (stream) => {
179
+ let finalSessionId = "";
180
+ const sendPerm = async (req) => {
181
+ try {
182
+ await stream.writeSSE({
183
+ data: JSON.stringify({
184
+ type: "permission_request",
185
+ ...req,
186
+ sessionId: finalSessionId || "pending"
187
+ })
188
+ });
189
+ } catch (e) {
190
+ console.error("Failed to send permission SSE:", e);
191
+ }
192
+ };
193
+ setPermissionCallback(tempId, sendPerm);
194
+ try {
195
+ for await (const msg of session.stream()) {
196
+ if (msg.session_id && !finalSessionId) {
197
+ finalSessionId = msg.session_id;
198
+ activeSessions.add(finalSessionId);
199
+ setPermissionCallback(tempId, null);
200
+ setPermissionCallback(finalSessionId, sendPerm);
201
+ }
202
+ const text = getAssistantText(msg);
203
+ if (text)
204
+ await stream.writeSSE({
205
+ data: JSON.stringify({
206
+ response: text.trim(),
207
+ sessionId: msg.session_id
208
+ })
209
+ });
210
+ }
211
+ if (finalSessionId) sessions.set(finalSessionId, session);
212
+ } catch (err) {
213
+ console.error(err);
214
+ await stream.writeSSE({
215
+ data: JSON.stringify({ error: "Stream error" })
216
+ });
217
+ } finally {
218
+ setPermissionCallback(tempId, null);
219
+ if (finalSessionId) {
220
+ setPermissionCallback(finalSessionId, null);
221
+ activeSessions.delete(finalSessionId);
222
+ }
223
+ }
224
+ });
225
+ } catch (error) {
226
+ console.error(error);
227
+ return c.json({ error: "Failed to create new chat" }, 400);
228
+ }
229
+ });
230
+ chats.post("/:id/message", async (c) => {
231
+ const { prompt, model, permission } = await c.req.json();
232
+ const sessionId = c.req.param("id");
233
+ if (!prompt || !sessionId || /[\/\\]/.test(sessionId))
234
+ return c.json({ error: "Invalid request" }, 400);
235
+ try {
236
+ const session = await getSession({ sessionId, model, permission });
237
+ if (!session) return c.json({ error: "Session is required" }, 400);
238
+ await sendMessage({ prompt, session });
239
+ return streamSSE(c, async (stream) => {
240
+ activeSessions.add(sessionId);
241
+ setPermissionCallback(sessionId, async (req) => {
242
+ try {
243
+ await stream.writeSSE({
244
+ data: JSON.stringify({
245
+ type: "permission_request",
246
+ ...req,
247
+ sessionId
248
+ })
249
+ });
250
+ } catch (e) {
251
+ console.error("Failed to send permission SSE:", e);
252
+ }
253
+ });
254
+ try {
255
+ for await (const msg of session.stream()) {
256
+ const text = getAssistantText(msg);
257
+ if (text)
258
+ await stream.writeSSE({
259
+ data: JSON.stringify({
260
+ response: text?.trim() || null,
261
+ sessionId: msg.session_id
262
+ })
263
+ });
264
+ }
265
+ } catch (err) {
266
+ console.error(err);
267
+ await stream.writeSSE({
268
+ data: JSON.stringify({ error: "Stream error" })
269
+ });
270
+ } finally {
271
+ setPermissionCallback(sessionId, null);
272
+ activeSessions.delete(sessionId);
273
+ }
274
+ });
275
+ } catch (error) {
276
+ console.error(error);
277
+ return c.json({ error: "Failed to send message" }, 400);
278
+ }
279
+ });
280
+ chats.post("/:id/permission", async (c) => {
281
+ const { requestId, behavior, updatedPermissions } = await c.req.json();
282
+ if (!requestId || !behavior)
283
+ return c.json({ error: "requestId and behavior are required" }, 400);
284
+ const resolve = pendingPermissions.get(requestId);
285
+ if (!resolve)
286
+ return c.json({ error: "No pending permission request found" }, 404);
287
+ if (behavior === "allow") {
288
+ resolve({
289
+ behavior: "allow",
290
+ ...updatedPermissions ? { updatedPermissions } : {}
291
+ });
292
+ } else {
293
+ resolve({ behavior: "deny", message: "User denied permission" });
294
+ }
295
+ return c.json({ ok: true });
296
+ });
297
+ chats.get("/", async (c) => {
298
+ try {
299
+ const cwdSlug = process.cwd().replaceAll("/", "-");
300
+ const chatsDir = path2.join(os2.homedir(), ".claude", "projects", cwdSlug);
301
+ const files = (await readdir(chatsDir)).filter((f) => f.endsWith(".jsonl"));
302
+ const result = await Promise.all(
303
+ files.map(async (file) => {
304
+ const filePath = path2.join(chatsDir, file);
305
+ const content = await readFile(filePath, "utf-8");
306
+ const lines = content.split("\n").filter(Boolean);
307
+ const sessionId = file.replace(".jsonl", "");
308
+ let preview = "";
309
+ let timestamp = "";
310
+ for (const line of lines) {
311
+ const obj = JSON.parse(line);
312
+ if (obj.type === "user" && !obj.isMeta) {
313
+ const content2 = obj.message.content;
314
+ if (typeof content2 === "string") {
315
+ preview = content2.slice(0, 100);
316
+ } else {
317
+ preview = content2.filter((b) => b.type === "text").map((b) => b.text).join("").slice(0, 100);
318
+ }
319
+ timestamp = obj.timestamp;
320
+ break;
321
+ }
322
+ }
323
+ return {
324
+ sessionId,
325
+ text: preview,
326
+ timestamp,
327
+ isActive: activeSessions.has(sessionId)
328
+ };
329
+ })
330
+ );
331
+ return c.json({ result });
332
+ } catch (error) {
333
+ return c.json({ result: [] });
334
+ }
335
+ });
336
+ chats.get("/:id", async (c) => {
337
+ const sessionId = c.req.param("id");
338
+ if (!sessionId || /[\/\\]/.test(sessionId))
339
+ return c.json({ error: "Invalid session id" }, 400);
340
+ try {
341
+ const cwdSlug = process.cwd().replaceAll("/", "-");
342
+ const chatsDir = path2.join(os2.homedir(), ".claude", "projects", cwdSlug);
343
+ const file = path2.join(chatsDir, `${sessionId}.jsonl`);
344
+ const content = await readFile(file, "utf-8");
345
+ const lines = content.split("\n").filter(Boolean);
346
+ const parsed = lines.map((line) => JSON.parse(line));
347
+ let model;
348
+ let permissionMode;
349
+ for (const obj of [...parsed].reverse()) {
350
+ if (!model && obj.type === "assistant") model = obj.message?.model;
351
+ if (!permissionMode && obj.type === "user" && !obj.isMeta)
352
+ permissionMode = obj.permissionMode;
353
+ if (model && permissionMode) break;
354
+ }
355
+ const messages = parsed.filter(
356
+ (obj) => obj.type === "user" && !obj.isMeta || obj.type === "assistant"
357
+ ).flatMap((obj) => {
358
+ const content2 = obj.message.content;
359
+ if (typeof content2 === "string") {
360
+ return [
361
+ {
362
+ role: obj.message.role,
363
+ type: "text",
364
+ text: content2.trim(),
365
+ timestamp: obj.timestamp
366
+ }
367
+ ];
368
+ }
369
+ return content2.filter(
370
+ (b) => ["text", "tool_use", "tool_result"].includes(b.type)
371
+ ).map((b) => {
372
+ if (b.type === "text") {
373
+ return {
374
+ role: obj.message.role,
375
+ type: "text",
376
+ text: b.text.trim(),
377
+ timestamp: obj.timestamp
378
+ };
379
+ }
380
+ if (b.type === "tool_use") {
381
+ return {
382
+ role: obj.message.role,
383
+ type: "tool_use",
384
+ name: b.name,
385
+ input: b.input,
386
+ timestamp: obj.timestamp
387
+ };
388
+ }
389
+ if (b.type === "tool_result") {
390
+ const resultText = Array.isArray(b.content) ? b.content.filter((r) => r.type === "text").map((r) => r.text).join("") : b.content ?? "";
391
+ return {
392
+ role: obj.message.role,
393
+ type: "tool_result",
394
+ text: resultText.trim(),
395
+ timestamp: obj.timestamp
396
+ };
397
+ }
398
+ }).filter(Boolean);
399
+ });
400
+ return c.json({ messages, model, permissionMode });
401
+ } catch {
402
+ return c.json({ error: "Chat not found" }, 404);
403
+ }
404
+ });
405
+
406
+ // src/routes/git/index.ts
407
+ import { Hono as Hono2 } from "hono";
408
+ import { exec } from "child_process";
409
+ import { readFile as readFile2 } from "fs/promises";
410
+ import path3 from "path";
411
+ import { promisify } from "util";
412
+ var execAsync = promisify(exec);
413
+ var _repoRoot = null;
414
+ async function getRepoRoot() {
415
+ if (!_repoRoot) {
416
+ try {
417
+ const { stdout } = await execAsync("git rev-parse --show-toplevel");
418
+ _repoRoot = stdout.trim();
419
+ } catch {
420
+ _repoRoot = process.cwd();
421
+ }
422
+ }
423
+ return _repoRoot;
424
+ }
425
+ var git = new Hono2();
426
+ git.get("/status", async (c) => {
427
+ try {
428
+ const root = await getRepoRoot();
429
+ const { stdout: statusOut } = await execAsync("git status --porcelain", {
430
+ cwd: root
431
+ });
432
+ const [{ stdout: unstagedNum }, { stdout: stagedNum }] = await Promise.all([
433
+ execAsync("git diff --numstat", { cwd: root }).catch(() => ({
434
+ stdout: ""
435
+ })),
436
+ execAsync("git diff --cached --numstat", { cwd: root }).catch(() => ({
437
+ stdout: ""
438
+ }))
439
+ ]);
440
+ const parseNumstat = (raw) => {
441
+ const map = /* @__PURE__ */ new Map();
442
+ for (const line of raw.split("\n").filter(Boolean)) {
443
+ const [add, del, file] = line.split(" ");
444
+ if (file) {
445
+ map.set(file, {
446
+ additions: add === "-" ? 0 : parseInt(add, 10),
447
+ deletions: del === "-" ? 0 : parseInt(del, 10)
448
+ });
449
+ }
450
+ }
451
+ return map;
452
+ };
453
+ const unstagedStats = parseNumstat(unstagedNum);
454
+ const stagedStats = parseNumstat(stagedNum);
455
+ const files = [];
456
+ for (const line of statusOut.split("\n").filter(Boolean)) {
457
+ const xy = line.slice(0, 2);
458
+ const filePath = line.slice(3).trim();
459
+ let status = "M";
460
+ let staged = false;
461
+ if (xy === "??") {
462
+ status = "?";
463
+ } else if (xy === "!!") {
464
+ continue;
465
+ } else {
466
+ const indexStatus = xy[0];
467
+ const worktreeStatus = xy[1];
468
+ if (indexStatus !== " " && indexStatus !== "?") {
469
+ staged = true;
470
+ if (indexStatus === "A") status = "A";
471
+ else if (indexStatus === "D") status = "D";
472
+ else if (indexStatus === "R") status = "R";
473
+ else status = "M";
474
+ } else {
475
+ if (worktreeStatus === "D") status = "D";
476
+ else status = "M";
477
+ }
478
+ }
479
+ const stats = staged ? stagedStats.get(filePath) : unstagedStats.get(filePath);
480
+ let additions = stats?.additions ?? 0;
481
+ let deletions = stats?.deletions ?? 0;
482
+ if (status === "?") {
483
+ try {
484
+ const content = await readFile2(path3.join(root, filePath), "utf-8");
485
+ additions = content.split("\n").length;
486
+ } catch {
487
+ additions = 0;
488
+ }
489
+ }
490
+ files.push({ path: filePath, status, staged, additions, deletions });
491
+ }
492
+ return c.json({ files });
493
+ } catch (error) {
494
+ console.error("git status error:", error);
495
+ return c.json({ files: [] });
496
+ }
497
+ });
498
+ git.get("/diff", async (c) => {
499
+ const filePath = c.req.query("file");
500
+ if (!filePath) return c.json({ error: "file parameter required" }, 400);
501
+ if (filePath.includes("..")) return c.json({ error: "Invalid path" }, 400);
502
+ const root = await getRepoRoot();
503
+ const safePath = filePath.replace(/'/g, "'\\''");
504
+ try {
505
+ let oldContent = "";
506
+ let newContent = "";
507
+ try {
508
+ const { stdout } = await execAsync(`git show 'HEAD:${safePath}'`, {
509
+ cwd: root,
510
+ maxBuffer: 10 * 1024 * 1024
511
+ });
512
+ oldContent = stdout;
513
+ } catch {
514
+ oldContent = "";
515
+ }
516
+ try {
517
+ newContent = await readFile2(path3.join(root, filePath), "utf-8");
518
+ } catch (e) {
519
+ console.error("readFile error for", filePath, e);
520
+ newContent = "";
521
+ }
522
+ return c.json({ path: filePath, oldContent, newContent });
523
+ } catch (error) {
524
+ console.error("git diff error:", error);
525
+ return c.json({ error: "Failed to get diff" }, 500);
526
+ }
527
+ });
528
+
529
+ // src/routes/gh/index.ts
530
+ import { Hono as Hono3 } from "hono";
531
+ import { execSync as execSync2 } from "child_process";
532
+ var gh = new Hono3();
533
+ gh.get("/repos", async (c) => {
534
+ const fields = "--json name,nameWithOwner,description,url,isPrivate,updatedAt --limit 100";
535
+ try {
536
+ const personal = JSON.parse(
537
+ execSync2(`gh repo list ${fields}`, { encoding: "utf-8" })
538
+ );
539
+ let orgRepos = [];
540
+ try {
541
+ const orgs = execSync2("gh api user/orgs --jq '.[].login'", { encoding: "utf-8" }).trim().split("\n").filter(Boolean);
542
+ for (const org of orgs) {
543
+ const repos = JSON.parse(
544
+ execSync2(`gh repo list ${org} ${fields}`, { encoding: "utf-8" })
545
+ );
546
+ orgRepos.push(...repos);
547
+ }
548
+ } catch {
549
+ }
550
+ const username = execSync2("gh api user --jq '.login'", { encoding: "utf-8" }).trim();
551
+ const all = [...personal, ...orgRepos].sort(
552
+ (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
553
+ );
554
+ return c.json({ username, repos: all });
555
+ } catch (error) {
556
+ return c.json([]);
557
+ }
558
+ });
559
+
560
+ // src/server.ts
561
+ import { bearerAuth } from "hono/bearer-auth";
562
+ var app = new Hono4();
563
+ var token = process.env.AUTH_TOKEN;
564
+ if (!token) {
565
+ console.error("AUTH_TOKEN is not set. Run `anywhere-ai init` first.");
566
+ process.exit(1);
567
+ }
568
+ app.use("*", cors());
569
+ app.get("/health", async (c) => {
570
+ return c.json({ message: "server is healthy" }, 200);
571
+ });
572
+ app.use("/v1/*", bearerAuth({ token }));
573
+ app.route("/v1/chats", chats);
574
+ app.route("/v1/git", git);
575
+ app.route("/v1/gh", gh);
576
+ serve(
577
+ {
578
+ fetch: app.fetch,
579
+ hostname: "0.0.0.0",
580
+ port: parseInt(process.env.PORT || "3847")
581
+ },
582
+ (info) => {
583
+ console.log(`Server is running on http://localhost:${info.port}`);
584
+ }
585
+ );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anywhere-ai",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "type": "module",
5
5
  "description": "Code on any repo from your phone",
6
6
  "bin": {
@@ -33,6 +33,6 @@
33
33
  "test": "echo \"Error: no test specified\" && exit 1",
34
34
  "dev": "tsx watch --env-file=.env src/server.ts",
35
35
  "start": "tsx --env-file=.env src/server.ts",
36
- "build": "tsup src/cli.ts --format esm --outDir dist"
36
+ "build": "tsup"
37
37
  }
38
38
  }