anywhere-ai 0.0.1

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/.env ADDED
@@ -0,0 +1,4 @@
1
+ # SERVER_URL=168.119.231.95
2
+ SERVER_URL=http://localhost:8080
3
+ AUTH_TOKEN=E4PR6tWWyvluPgbNbrUsh9T9LerW803Obu7oaXQSRJA=
4
+ CLAUDE_CODE_OAUTH_TOKEN=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
package/dist/cli.js ADDED
@@ -0,0 +1,99 @@
1
+ // src/cli.ts
2
+ import fs from "fs/promises";
3
+ import { readFileSync } from "fs";
4
+ import os from "os";
5
+ import path from "path";
6
+ import crypto from "crypto";
7
+ import { spawnSync, execSync } from "child_process";
8
+ var ANYWHERE_DIR = path.join(os.homedir(), ".anywhere");
9
+ var CONFIG_PATH = path.join(ANYWHERE_DIR, "config.json");
10
+ async function createConfigFile() {
11
+ try {
12
+ await fs.access(CONFIG_PATH);
13
+ console.log("Config.json already exists");
14
+ } catch (error) {
15
+ console.log("Config.json does not exists");
16
+ const token = crypto.randomBytes(16).toString("hex");
17
+ await fs.writeFile(
18
+ CONFIG_PATH,
19
+ JSON.stringify({ authToken: token }, null, 2)
20
+ );
21
+ console.log("Config.json created");
22
+ }
23
+ }
24
+ function isGhInstalled() {
25
+ try {
26
+ execSync("which gh", { stdio: "ignore" });
27
+ return true;
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+ function ensureGhInstalled() {
33
+ if (isGhInstalled()) return;
34
+ const platform = os.platform();
35
+ console.log("gh CLI not found. Installing...");
36
+ if (platform === "darwin") {
37
+ const result = spawnSync("brew", ["install", "gh"], { stdio: "inherit" });
38
+ if (result.status !== 0) {
39
+ console.error(
40
+ "Failed to install gh via Homebrew. Install manually: https://cli.github.com"
41
+ );
42
+ process.exit(1);
43
+ }
44
+ } else if (platform === "linux") {
45
+ const commands = [
46
+ "curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg",
47
+ 'echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null',
48
+ "sudo apt update",
49
+ "sudo apt install -y gh"
50
+ ];
51
+ for (const cmd of commands) {
52
+ const result = spawnSync("sh", ["-c", cmd], { stdio: "inherit" });
53
+ if (result.status !== 0) {
54
+ console.error(`Failed during: ${cmd}`);
55
+ console.error("Install gh manually: https://cli.github.com");
56
+ process.exit(1);
57
+ }
58
+ }
59
+ } else {
60
+ console.error(
61
+ `Unsupported platform: ${platform}. Install gh manually: https://cli.github.com`
62
+ );
63
+ process.exit(1);
64
+ }
65
+ if (!isGhInstalled()) {
66
+ console.error("gh installation completed but binary not found in PATH.");
67
+ process.exit(1);
68
+ }
69
+ console.log("gh CLI installed successfully.");
70
+ }
71
+ function isGhAuthed() {
72
+ try {
73
+ execSync("gh auth status", { encoding: "utf-8" });
74
+ return true;
75
+ } catch {
76
+ return false;
77
+ }
78
+ }
79
+ function ensureGhAuth() {
80
+ if (isGhAuthed()) {
81
+ console.log("Gh authenticated");
82
+ return;
83
+ }
84
+ console.log("gh not authenticated, Launching gh auth login...");
85
+ const result = spawnSync("gh", ["auth", "login"], {
86
+ stdio: "inherit"
87
+ });
88
+ if (result.status !== 0) {
89
+ console.error("GitHub login failed");
90
+ process.exit(1);
91
+ }
92
+ }
93
+ await fs.mkdir(ANYWHERE_DIR, { recursive: true });
94
+ await createConfigFile();
95
+ ensureGhInstalled();
96
+ ensureGhAuth();
97
+ var config = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
98
+ process.env.AUTH_TOKEN = config.authToken;
99
+ import("./server-5PJ4IEOJ.js");
@@ -0,0 +1,459 @@
1
+ // src/server.ts
2
+ import { Hono as Hono3 } from "hono";
3
+ import { serve } from "@hono/node-server";
4
+ import { cors } from "hono/cors";
5
+
6
+ // src/routes/chats/index.ts
7
+ import { Hono } from "hono";
8
+ import os from "os";
9
+ import path from "path";
10
+ import { readdir, readFile } from "fs/promises";
11
+ import { streamSSE } from "hono/streaming";
12
+
13
+ // src/chats.ts
14
+ import {
15
+ unstable_v2_createSession,
16
+ unstable_v2_resumeSession
17
+ } from "@anthropic-ai/claude-agent-sdk";
18
+ var sessions = /* @__PURE__ */ new Map();
19
+ var activeSessions = /* @__PURE__ */ new Set();
20
+ var pendingPermissions = /* @__PURE__ */ new Map();
21
+ function getAssistantText(msg) {
22
+ if (msg.type !== "assistant") return null;
23
+ return msg.message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
24
+ }
25
+ var permissionCallbacks = /* @__PURE__ */ new Map();
26
+ function setPermissionCallback(sessionId, cb) {
27
+ if (cb) {
28
+ permissionCallbacks.set(sessionId, cb);
29
+ } else {
30
+ permissionCallbacks.delete(sessionId);
31
+ }
32
+ }
33
+ var permReqCounter = 0;
34
+ function makeCanUseTool(sessionHint, permissionMode) {
35
+ return async (toolName, input, options) => {
36
+ console.log(`[canUseTool] tool=${toolName} reason=${options.decisionReason ?? "none"} mode=${permissionMode}`);
37
+ const requestId = `perm_${++permReqCounter}_${Date.now()}`;
38
+ const req = {
39
+ requestId,
40
+ toolName,
41
+ input,
42
+ description: input.description,
43
+ decisionReason: options.decisionReason,
44
+ suggestions: options.suggestions
45
+ };
46
+ const cb = permissionCallbacks.get(sessionHint);
47
+ if (cb) cb(req);
48
+ return new Promise((resolve) => {
49
+ pendingPermissions.set(requestId, resolve);
50
+ const timeout = setTimeout(() => {
51
+ if (pendingPermissions.has(requestId)) {
52
+ pendingPermissions.delete(requestId);
53
+ resolve({ behavior: "deny", message: "Permission request timed out" });
54
+ }
55
+ }, 5 * 60 * 1e3);
56
+ options.signal.addEventListener("abort", () => {
57
+ clearTimeout(timeout);
58
+ if (pendingPermissions.has(requestId)) {
59
+ pendingPermissions.delete(requestId);
60
+ resolve({ behavior: "deny", message: "Request aborted" });
61
+ }
62
+ });
63
+ const origResolve = resolve;
64
+ pendingPermissions.set(requestId, (result) => {
65
+ clearTimeout(timeout);
66
+ pendingPermissions.delete(requestId);
67
+ origResolve(result);
68
+ });
69
+ });
70
+ };
71
+ }
72
+ var createChat = async ({
73
+ model,
74
+ permission
75
+ }) => {
76
+ const tempId = `temp_${Date.now()}`;
77
+ const mode = permission || "acceptEdits";
78
+ const session = unstable_v2_createSession({
79
+ model: model || "claude-opus-4-6",
80
+ permissionMode: mode,
81
+ canUseTool: makeCanUseTool(tempId, mode)
82
+ });
83
+ return { session, tempId };
84
+ };
85
+ var sendMessage = async ({
86
+ prompt,
87
+ session
88
+ }) => {
89
+ await session.send(prompt);
90
+ };
91
+ var getSession = async ({
92
+ sessionId,
93
+ model,
94
+ permission
95
+ }) => {
96
+ let session = sessions.get(sessionId);
97
+ if (session && (model || permission)) {
98
+ session.close();
99
+ sessions.delete(sessionId);
100
+ session = void 0;
101
+ }
102
+ if (!session) {
103
+ const mode = permission || "acceptEdits";
104
+ session = unstable_v2_resumeSession(sessionId, {
105
+ model: model || "claude-opus-4-6",
106
+ permissionMode: mode,
107
+ canUseTool: makeCanUseTool(sessionId, mode)
108
+ });
109
+ sessions.set(sessionId, session);
110
+ }
111
+ return session;
112
+ };
113
+
114
+ // src/routes/chats/index.ts
115
+ var chats = new Hono();
116
+ chats.post("/new", async (c) => {
117
+ const { prompt, model, permission } = await c.req.json();
118
+ if (!prompt) return c.json({ error: "Prompt is required" }, 400);
119
+ try {
120
+ const { session, tempId } = await createChat({ model, permission });
121
+ await sendMessage({ prompt, session });
122
+ return streamSSE(c, async (stream) => {
123
+ let finalSessionId = "";
124
+ const sendPerm = async (req) => {
125
+ try {
126
+ await stream.writeSSE({
127
+ data: JSON.stringify({
128
+ type: "permission_request",
129
+ ...req,
130
+ sessionId: finalSessionId || "pending"
131
+ })
132
+ });
133
+ } catch (e) {
134
+ console.error("Failed to send permission SSE:", e);
135
+ }
136
+ };
137
+ setPermissionCallback(tempId, sendPerm);
138
+ try {
139
+ for await (const msg of session.stream()) {
140
+ if (msg.session_id && !finalSessionId) {
141
+ finalSessionId = msg.session_id;
142
+ activeSessions.add(finalSessionId);
143
+ setPermissionCallback(tempId, null);
144
+ setPermissionCallback(finalSessionId, sendPerm);
145
+ }
146
+ const text = getAssistantText(msg);
147
+ if (text)
148
+ await stream.writeSSE({
149
+ data: JSON.stringify({ response: text.trim(), sessionId: msg.session_id })
150
+ });
151
+ }
152
+ if (finalSessionId) sessions.set(finalSessionId, session);
153
+ } catch (err) {
154
+ console.error(err);
155
+ await stream.writeSSE({ data: JSON.stringify({ error: "Stream error" }) });
156
+ } finally {
157
+ setPermissionCallback(tempId, null);
158
+ if (finalSessionId) {
159
+ setPermissionCallback(finalSessionId, null);
160
+ activeSessions.delete(finalSessionId);
161
+ }
162
+ }
163
+ });
164
+ } catch (error) {
165
+ console.error(error);
166
+ return c.json({ error: "Failed to create new chat" }, 400);
167
+ }
168
+ });
169
+ chats.post("/:id/message", async (c) => {
170
+ const { prompt, model, permission } = await c.req.json();
171
+ const sessionId = c.req.param("id");
172
+ if (!prompt || !sessionId || /[\/\\]/.test(sessionId))
173
+ return c.json({ error: "Invalid request" }, 400);
174
+ try {
175
+ const session = await getSession({ sessionId, model, permission });
176
+ if (!session) return c.json({ error: "Session is required" }, 400);
177
+ await sendMessage({ prompt, session });
178
+ return streamSSE(c, async (stream) => {
179
+ activeSessions.add(sessionId);
180
+ setPermissionCallback(sessionId, async (req) => {
181
+ try {
182
+ await stream.writeSSE({
183
+ data: JSON.stringify({
184
+ type: "permission_request",
185
+ ...req,
186
+ sessionId
187
+ })
188
+ });
189
+ } catch (e) {
190
+ console.error("Failed to send permission SSE:", e);
191
+ }
192
+ });
193
+ try {
194
+ for await (const msg of session.stream()) {
195
+ const text = getAssistantText(msg);
196
+ if (text)
197
+ await stream.writeSSE({
198
+ data: JSON.stringify({
199
+ response: text?.trim() || null,
200
+ sessionId: msg.session_id
201
+ })
202
+ });
203
+ }
204
+ } catch (err) {
205
+ console.error(err);
206
+ await stream.writeSSE({ data: JSON.stringify({ error: "Stream error" }) });
207
+ } finally {
208
+ setPermissionCallback(sessionId, null);
209
+ activeSessions.delete(sessionId);
210
+ }
211
+ });
212
+ } catch (error) {
213
+ console.error(error);
214
+ return c.json({ error: "Failed to send message" }, 400);
215
+ }
216
+ });
217
+ chats.post("/:id/permission", async (c) => {
218
+ const { requestId, behavior, updatedPermissions } = await c.req.json();
219
+ if (!requestId || !behavior)
220
+ return c.json({ error: "requestId and behavior are required" }, 400);
221
+ const resolve = pendingPermissions.get(requestId);
222
+ if (!resolve)
223
+ return c.json({ error: "No pending permission request found" }, 404);
224
+ if (behavior === "allow") {
225
+ resolve({
226
+ behavior: "allow",
227
+ ...updatedPermissions ? { updatedPermissions } : {}
228
+ });
229
+ } else {
230
+ resolve({ behavior: "deny", message: "User denied permission" });
231
+ }
232
+ return c.json({ ok: true });
233
+ });
234
+ chats.get("/", async (c) => {
235
+ try {
236
+ const cwdSlug = process.cwd().replaceAll("/", "-");
237
+ const chatsDir = path.join(os.homedir(), ".claude", "projects", cwdSlug);
238
+ const files = (await readdir(chatsDir)).filter((f) => f.endsWith(".jsonl"));
239
+ const result = await Promise.all(
240
+ files.map(async (file) => {
241
+ const filePath = path.join(chatsDir, file);
242
+ const content = await readFile(filePath, "utf-8");
243
+ const lines = content.split("\n").filter(Boolean);
244
+ const sessionId = file.replace(".jsonl", "");
245
+ let preview = "";
246
+ let timestamp = "";
247
+ for (const line of lines) {
248
+ const obj = JSON.parse(line);
249
+ if (obj.type === "user" && !obj.isMeta) {
250
+ const content2 = obj.message.content;
251
+ if (typeof content2 === "string") {
252
+ preview = content2.slice(0, 100);
253
+ } else {
254
+ preview = content2.filter((b) => b.type === "text").map((b) => b.text).join("").slice(0, 100);
255
+ }
256
+ timestamp = obj.timestamp;
257
+ break;
258
+ }
259
+ }
260
+ return { sessionId, text: preview, timestamp, isActive: activeSessions.has(sessionId) };
261
+ })
262
+ );
263
+ return c.json({ result });
264
+ } catch (error) {
265
+ return c.json({ result: [] });
266
+ }
267
+ });
268
+ chats.get("/:id", async (c) => {
269
+ const sessionId = c.req.param("id");
270
+ if (!sessionId || /[\/\\]/.test(sessionId))
271
+ return c.json({ error: "Invalid session id" }, 400);
272
+ try {
273
+ const cwdSlug = process.cwd().replaceAll("/", "-");
274
+ const chatsDir = path.join(os.homedir(), ".claude", "projects", cwdSlug);
275
+ const file = path.join(chatsDir, `${sessionId}.jsonl`);
276
+ const content = await readFile(file, "utf-8");
277
+ const lines = content.split("\n").filter(Boolean);
278
+ const parsed = lines.map((line) => JSON.parse(line));
279
+ let model;
280
+ let permissionMode;
281
+ for (const obj of [...parsed].reverse()) {
282
+ if (!model && obj.type === "assistant") model = obj.message?.model;
283
+ if (!permissionMode && obj.type === "user" && !obj.isMeta) permissionMode = obj.permissionMode;
284
+ if (model && permissionMode) break;
285
+ }
286
+ const messages = parsed.filter(
287
+ (obj) => obj.type === "user" && !obj.isMeta || obj.type === "assistant"
288
+ ).flatMap((obj) => {
289
+ const content2 = obj.message.content;
290
+ if (typeof content2 === "string") {
291
+ return [{ role: obj.message.role, type: "text", text: content2.trim(), timestamp: obj.timestamp }];
292
+ }
293
+ return content2.filter((b) => ["text", "tool_use", "tool_result"].includes(b.type)).map((b) => {
294
+ if (b.type === "text") {
295
+ return { role: obj.message.role, type: "text", text: b.text.trim(), timestamp: obj.timestamp };
296
+ }
297
+ if (b.type === "tool_use") {
298
+ return { role: obj.message.role, type: "tool_use", name: b.name, input: b.input, timestamp: obj.timestamp };
299
+ }
300
+ if (b.type === "tool_result") {
301
+ const resultText = Array.isArray(b.content) ? b.content.filter((r) => r.type === "text").map((r) => r.text).join("") : b.content ?? "";
302
+ return { role: obj.message.role, type: "tool_result", text: resultText.trim(), timestamp: obj.timestamp };
303
+ }
304
+ }).filter(Boolean);
305
+ });
306
+ return c.json({ messages, model, permissionMode });
307
+ } catch {
308
+ return c.json({ error: "Chat not found" }, 404);
309
+ }
310
+ });
311
+
312
+ // src/routes/git/index.ts
313
+ import { Hono as Hono2 } from "hono";
314
+ import { exec } from "child_process";
315
+ import { readFile as readFile2 } from "fs/promises";
316
+ import path2 from "path";
317
+ import { promisify } from "util";
318
+ var execAsync = promisify(exec);
319
+ var _repoRoot = null;
320
+ async function getRepoRoot() {
321
+ if (!_repoRoot) {
322
+ try {
323
+ const { stdout } = await execAsync("git rev-parse --show-toplevel");
324
+ _repoRoot = stdout.trim();
325
+ } catch {
326
+ _repoRoot = process.cwd();
327
+ }
328
+ }
329
+ return _repoRoot;
330
+ }
331
+ var git = new Hono2();
332
+ git.get("/status", async (c) => {
333
+ try {
334
+ const root = await getRepoRoot();
335
+ const { stdout: statusOut } = await execAsync("git status --porcelain", {
336
+ cwd: root
337
+ });
338
+ const [{ stdout: unstagedNum }, { stdout: stagedNum }] = await Promise.all([
339
+ execAsync("git diff --numstat", { cwd: root }).catch(() => ({
340
+ stdout: ""
341
+ })),
342
+ execAsync("git diff --cached --numstat", { cwd: root }).catch(() => ({
343
+ stdout: ""
344
+ }))
345
+ ]);
346
+ const parseNumstat = (raw) => {
347
+ const map = /* @__PURE__ */ new Map();
348
+ for (const line of raw.split("\n").filter(Boolean)) {
349
+ const [add, del, file] = line.split(" ");
350
+ if (file) {
351
+ map.set(file, {
352
+ additions: add === "-" ? 0 : parseInt(add, 10),
353
+ deletions: del === "-" ? 0 : parseInt(del, 10)
354
+ });
355
+ }
356
+ }
357
+ return map;
358
+ };
359
+ const unstagedStats = parseNumstat(unstagedNum);
360
+ const stagedStats = parseNumstat(stagedNum);
361
+ const files = [];
362
+ for (const line of statusOut.split("\n").filter(Boolean)) {
363
+ const xy = line.slice(0, 2);
364
+ const filePath = line.slice(3).trim();
365
+ let status = "M";
366
+ let staged = false;
367
+ if (xy === "??") {
368
+ status = "?";
369
+ } else if (xy === "!!") {
370
+ continue;
371
+ } else {
372
+ const indexStatus = xy[0];
373
+ const worktreeStatus = xy[1];
374
+ if (indexStatus !== " " && indexStatus !== "?") {
375
+ staged = true;
376
+ if (indexStatus === "A") status = "A";
377
+ else if (indexStatus === "D") status = "D";
378
+ else if (indexStatus === "R") status = "R";
379
+ else status = "M";
380
+ } else {
381
+ if (worktreeStatus === "D") status = "D";
382
+ else status = "M";
383
+ }
384
+ }
385
+ const stats = staged ? stagedStats.get(filePath) : unstagedStats.get(filePath);
386
+ let additions = stats?.additions ?? 0;
387
+ let deletions = stats?.deletions ?? 0;
388
+ if (status === "?") {
389
+ try {
390
+ const content = await readFile2(path2.join(root, filePath), "utf-8");
391
+ additions = content.split("\n").length;
392
+ } catch {
393
+ additions = 0;
394
+ }
395
+ }
396
+ files.push({ path: filePath, status, staged, additions, deletions });
397
+ }
398
+ return c.json({ files });
399
+ } catch (error) {
400
+ console.error("git status error:", error);
401
+ return c.json({ files: [] });
402
+ }
403
+ });
404
+ git.get("/diff", async (c) => {
405
+ const filePath = c.req.query("file");
406
+ if (!filePath) return c.json({ error: "file parameter required" }, 400);
407
+ if (filePath.includes("..")) return c.json({ error: "Invalid path" }, 400);
408
+ const root = await getRepoRoot();
409
+ const safePath = filePath.replace(/'/g, "'\\''");
410
+ try {
411
+ let oldContent = "";
412
+ let newContent = "";
413
+ try {
414
+ const { stdout } = await execAsync(`git show 'HEAD:${safePath}'`, {
415
+ cwd: root,
416
+ maxBuffer: 10 * 1024 * 1024
417
+ });
418
+ oldContent = stdout;
419
+ } catch {
420
+ oldContent = "";
421
+ }
422
+ try {
423
+ newContent = await readFile2(path2.join(root, filePath), "utf-8");
424
+ } catch (e) {
425
+ console.error("readFile error for", filePath, e);
426
+ newContent = "";
427
+ }
428
+ return c.json({ path: filePath, oldContent, newContent });
429
+ } catch (error) {
430
+ console.error("git diff error:", error);
431
+ return c.json({ error: "Failed to get diff" }, 500);
432
+ }
433
+ });
434
+
435
+ // src/server.ts
436
+ import { bearerAuth } from "hono/bearer-auth";
437
+ var app = new Hono3();
438
+ var token = process.env.AUTH_TOKEN;
439
+ if (!token) {
440
+ console.error("AUTH_TOKEN is not set. Run `anywhere-ai init` first.");
441
+ process.exit(1);
442
+ }
443
+ app.use("*", cors());
444
+ app.get("/health", async (c) => {
445
+ return c.json({ message: "server is healthy" }, 200);
446
+ });
447
+ app.use("/v1/*", bearerAuth({ token }));
448
+ app.route("/v1/chats", chats);
449
+ app.route("/v1/git", git);
450
+ serve(
451
+ {
452
+ fetch: app.fetch,
453
+ hostname: "0.0.0.0",
454
+ port: parseInt(process.env.PORT || "3847")
455
+ },
456
+ (info) => {
457
+ console.log(`Server is running on http://localhost:${info.port}`);
458
+ }
459
+ );
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "anywhere-ai",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "Code on any repo from your phone",
6
+ "bin": {
7
+ "anywhere-ai": "./dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1",
11
+ "dev": "tsx watch src/server.ts",
12
+ "start": "tsx src/server.ts",
13
+ "build": "tsup src/cli.ts --format esm --outDir dist"
14
+ },
15
+ "keywords": [
16
+ "claude",
17
+ "mobile",
18
+ "coding",
19
+ "agent"
20
+ ],
21
+ "author": "yogesharc",
22
+ "license": "MIT",
23
+ "packageManager": "pnpm@10.30.3",
24
+ "dependencies": {
25
+ "@anthropic-ai/claude-agent-sdk": "^0.2.69",
26
+ "@hono/node-server": "^1.19.11",
27
+ "dotenv": "^17.3.1",
28
+ "hono": "^4.12.5"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^25.3.3",
32
+ "tsup": "^8.5.1",
33
+ "tsx": "^4.21.0",
34
+ "typescript": "^5.9.3"
35
+ }
36
+ }
package/src/chats.ts ADDED
@@ -0,0 +1,203 @@
1
+ import {
2
+ unstable_v2_createSession,
3
+ PermissionMode,
4
+ PermissionResult,
5
+ SDKMessage,
6
+ SDKSession,
7
+ unstable_v2_resumeSession,
8
+ } from "@anthropic-ai/claude-agent-sdk";
9
+
10
+ export const sessions = new Map<string, SDKSession>();
11
+
12
+ // Track which sessions are currently streaming
13
+ export const activeSessions = new Set<string>();
14
+
15
+ // Pending permission requests: requestId -> resolve function
16
+ export const pendingPermissions = new Map<
17
+ string,
18
+ (result: PermissionResult) => void
19
+ >();
20
+
21
+ export function getAssistantText(msg: SDKMessage): string | null {
22
+ if (msg.type !== "assistant") return null;
23
+ return msg.message.content
24
+ .filter((block: any) => block.type === "text")
25
+ .map((block: any) => block.text)
26
+ .join("");
27
+ }
28
+
29
+ // Permission suggestion sent to the client
30
+ export interface PermissionSuggestion {
31
+ type: string; // 'addRules' | 'replaceRules' | 'setMode' etc.
32
+ rules?: Array<{ toolName: string; ruleContent?: string }>;
33
+ behavior?: string;
34
+ destination?: string;
35
+ mode?: string;
36
+ }
37
+
38
+ // Permission request info sent to the client via SSE
39
+ export interface PermissionRequest {
40
+ requestId: string;
41
+ toolName: string;
42
+ input: Record<string, unknown>;
43
+ description?: string;
44
+ decisionReason?: string;
45
+ suggestions?: PermissionSuggestion[];
46
+ }
47
+
48
+ // Callback for notifying the SSE stream about permission requests
49
+ type PermissionCallback = (req: PermissionRequest) => void;
50
+
51
+ // Active permission callbacks per session (set during streaming)
52
+ const permissionCallbacks = new Map<string, PermissionCallback>();
53
+
54
+ export function setPermissionCallback(
55
+ sessionId: string,
56
+ cb: PermissionCallback | null,
57
+ ) {
58
+ if (cb) {
59
+ permissionCallbacks.set(sessionId, cb);
60
+ } else {
61
+ permissionCallbacks.delete(sessionId);
62
+ }
63
+ }
64
+
65
+ let permReqCounter = 0;
66
+
67
+ function makeCanUseTool(sessionHint: string, permissionMode: PermissionMode) {
68
+ return async (
69
+ toolName: string,
70
+ input: Record<string, unknown>,
71
+ options: {
72
+ signal: AbortSignal;
73
+ suggestions?: any[];
74
+ blockedPath?: string;
75
+ decisionReason?: string;
76
+ toolUseID: string;
77
+ agentID?: string;
78
+ },
79
+ ): Promise<PermissionResult> => {
80
+ console.log(`[canUseTool] tool=${toolName} reason=${options.decisionReason ?? "none"} mode=${permissionMode}`);
81
+
82
+ // In acceptEdits mode: auto-allow file operations, only prompt for others
83
+ // The SDK calls canUseTool for tools that need a permission decision.
84
+ // We forward the decision to the client for interactive approval.
85
+ const requestId = `perm_${++permReqCounter}_${Date.now()}`;
86
+
87
+ const req: PermissionRequest = {
88
+ requestId,
89
+ toolName,
90
+ input,
91
+ description: (input as any).description,
92
+ decisionReason: options.decisionReason,
93
+ suggestions: options.suggestions as PermissionSuggestion[] | undefined,
94
+ };
95
+
96
+ // Notify the active SSE stream (if any)
97
+ const cb = permissionCallbacks.get(sessionHint);
98
+ if (cb) cb(req);
99
+
100
+ // Wait for the client to respond
101
+ return new Promise<PermissionResult>((resolve) => {
102
+ pendingPermissions.set(requestId, resolve);
103
+
104
+ // Timeout after 5 minutes — deny if no response
105
+ const timeout = setTimeout(() => {
106
+ if (pendingPermissions.has(requestId)) {
107
+ pendingPermissions.delete(requestId);
108
+ resolve({ behavior: "deny", message: "Permission request timed out" });
109
+ }
110
+ }, 5 * 60 * 1000);
111
+
112
+ // Clean up timeout if abort signal fires
113
+ options.signal.addEventListener("abort", () => {
114
+ clearTimeout(timeout);
115
+ if (pendingPermissions.has(requestId)) {
116
+ pendingPermissions.delete(requestId);
117
+ resolve({ behavior: "deny", message: "Request aborted" });
118
+ }
119
+ });
120
+
121
+ // Also clean up on resolve
122
+ const origResolve = resolve;
123
+ pendingPermissions.set(requestId, (result) => {
124
+ clearTimeout(timeout);
125
+ pendingPermissions.delete(requestId);
126
+ origResolve(result);
127
+ });
128
+ });
129
+ };
130
+ }
131
+
132
+ export const createChat = async ({
133
+ model,
134
+ permission,
135
+ }: {
136
+ model?: string;
137
+ permission?: PermissionMode;
138
+ }) => {
139
+ // We pass a placeholder sessionId hint — we'll update the callback key
140
+ // once we know the real sessionId from the first streamed message
141
+ const tempId = `temp_${Date.now()}`;
142
+ const mode = permission || "acceptEdits";
143
+ const session = unstable_v2_createSession({
144
+ model: model || "claude-opus-4-6",
145
+ permissionMode: mode,
146
+ canUseTool: makeCanUseTool(tempId, mode),
147
+ });
148
+ return { session, tempId };
149
+ };
150
+
151
+ export const sendMessage = async ({
152
+ prompt,
153
+ session,
154
+ }: {
155
+ prompt: string;
156
+ session: SDKSession;
157
+ }): Promise<void> => {
158
+ await session.send(prompt);
159
+ };
160
+
161
+ export const streamMessage = async (
162
+ session: SDKSession,
163
+ ): Promise<{ text: string | null; sessionId: string }> => {
164
+ let sessionId = "";
165
+ let response = "";
166
+ for await (const msg of session.stream()) {
167
+ sessionId = msg.session_id;
168
+ const text = getAssistantText(msg);
169
+ if (text) response += text;
170
+ }
171
+ return { text: response || null, sessionId };
172
+ };
173
+
174
+ export const closeSession = (session: SDKSession) => {
175
+ session.close();
176
+ };
177
+
178
+ export const getSession = async ({
179
+ sessionId,
180
+ model,
181
+ permission,
182
+ }: {
183
+ sessionId: string;
184
+ model?: string;
185
+ permission?: PermissionMode;
186
+ }): Promise<SDKSession> => {
187
+ let session = sessions.get(sessionId);
188
+ if (session && (model || permission)) {
189
+ session.close();
190
+ sessions.delete(sessionId);
191
+ session = undefined;
192
+ }
193
+ if (!session) {
194
+ const mode = permission || "acceptEdits";
195
+ session = unstable_v2_resumeSession(sessionId, {
196
+ model: model || "claude-opus-4-6",
197
+ permissionMode: mode,
198
+ canUseTool: makeCanUseTool(sessionId, mode),
199
+ });
200
+ sessions.set(sessionId, session);
201
+ }
202
+ return session;
203
+ };
package/src/cli.ts ADDED
@@ -0,0 +1,114 @@
1
+ import fs from "fs/promises";
2
+ import { readFileSync } from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import crypto from "crypto";
6
+ import { spawnSync, execSync } from "child_process";
7
+
8
+ const ANYWHERE_DIR = path.join(os.homedir(), ".anywhere");
9
+ const CONFIG_PATH = path.join(ANYWHERE_DIR, "config.json");
10
+
11
+ async function createAnywhereDir() {
12
+ await fs.mkdir(ANYWHERE_DIR, { recursive: true });
13
+ }
14
+
15
+ async function createConfigFile() {
16
+ try {
17
+ await fs.access(CONFIG_PATH);
18
+ console.log("Config.json already exists");
19
+ } catch (error) {
20
+ console.log("Config.json does not exists");
21
+ const token = crypto.randomBytes(16).toString("hex");
22
+ await fs.writeFile(
23
+ CONFIG_PATH,
24
+ JSON.stringify({ authToken: token }, null, 2),
25
+ );
26
+ console.log("Config.json created");
27
+ }
28
+ }
29
+
30
+ function isGhInstalled(): boolean {
31
+ try {
32
+ execSync("which gh", { stdio: "ignore" });
33
+ return true;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ function ensureGhInstalled() {
40
+ if (isGhInstalled()) return;
41
+
42
+ const platform = os.platform();
43
+ console.log("gh CLI not found. Installing...");
44
+
45
+ if (platform === "darwin") {
46
+ const result = spawnSync("brew", ["install", "gh"], { stdio: "inherit" });
47
+ if (result.status !== 0) {
48
+ console.error(
49
+ "Failed to install gh via Homebrew. Install manually: https://cli.github.com",
50
+ );
51
+ process.exit(1);
52
+ }
53
+ } else if (platform === "linux") {
54
+ const commands = [
55
+ "curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg",
56
+ 'echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null',
57
+ "sudo apt update",
58
+ "sudo apt install -y gh",
59
+ ];
60
+ for (const cmd of commands) {
61
+ const result = spawnSync("sh", ["-c", cmd], { stdio: "inherit" });
62
+ if (result.status !== 0) {
63
+ console.error(`Failed during: ${cmd}`);
64
+ console.error("Install gh manually: https://cli.github.com");
65
+ process.exit(1);
66
+ }
67
+ }
68
+ } else {
69
+ console.error(
70
+ `Unsupported platform: ${platform}. Install gh manually: https://cli.github.com`,
71
+ );
72
+ process.exit(1);
73
+ }
74
+
75
+ if (!isGhInstalled()) {
76
+ console.error("gh installation completed but binary not found in PATH.");
77
+ process.exit(1);
78
+ }
79
+ console.log("gh CLI installed successfully.");
80
+ }
81
+
82
+ function isGhAuthed(): boolean {
83
+ try {
84
+ execSync("gh auth status", { encoding: "utf-8" });
85
+ return true;
86
+ } catch {
87
+ return false;
88
+ }
89
+ }
90
+ function ensureGhAuth() {
91
+ if (isGhAuthed()) {
92
+ console.log("Gh authenticated");
93
+ return;
94
+ }
95
+ console.log("gh not authenticated, Launching gh auth login...");
96
+ const result = spawnSync("gh", ["auth", "login"], {
97
+ stdio: "inherit",
98
+ });
99
+ if (result.status !== 0) {
100
+ console.error("GitHub login failed");
101
+ process.exit(1);
102
+ }
103
+ }
104
+
105
+ // Setup
106
+ await fs.mkdir(ANYWHERE_DIR, { recursive: true });
107
+ await createConfigFile();
108
+ ensureGhInstalled();
109
+ ensureGhAuth();
110
+
111
+ // Start server
112
+ const config = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
113
+ process.env.AUTH_TOKEN = config.authToken;
114
+ import("./server.js");
@@ -0,0 +1,257 @@
1
+ import { Hono } from "hono";
2
+ import os from "os";
3
+ import path from "path";
4
+ import { readdir, readFile } from "fs/promises";
5
+ import { streamSSE } from "hono/streaming";
6
+
7
+ import {
8
+ sessions,
9
+ activeSessions,
10
+ createChat,
11
+ sendMessage,
12
+ getSession,
13
+ getAssistantText,
14
+ pendingPermissions,
15
+ setPermissionCallback,
16
+ type PermissionRequest,
17
+ } from "../../chats";
18
+
19
+ export const chats = new Hono();
20
+
21
+ chats.post("/new", async (c) => {
22
+ const { prompt, model, permission } = await c.req.json();
23
+ if (!prompt) return c.json({ error: "Prompt is required" }, 400);
24
+
25
+ try {
26
+ const { session, tempId } = await createChat({ model, permission });
27
+ await sendMessage({ prompt, session });
28
+
29
+ return streamSSE(c, async (stream) => {
30
+ let finalSessionId = "";
31
+
32
+ // Send permission requests directly from canUseTool callback
33
+ const sendPerm = async (req: PermissionRequest) => {
34
+ try {
35
+ await stream.writeSSE({
36
+ data: JSON.stringify({
37
+ type: "permission_request",
38
+ ...req,
39
+ sessionId: finalSessionId || "pending",
40
+ }),
41
+ });
42
+ } catch (e) {
43
+ console.error("Failed to send permission SSE:", e);
44
+ }
45
+ };
46
+ setPermissionCallback(tempId, sendPerm);
47
+
48
+ try {
49
+ for await (const msg of session.stream()) {
50
+ if (msg.session_id && !finalSessionId) {
51
+ finalSessionId = msg.session_id;
52
+ activeSessions.add(finalSessionId);
53
+
54
+ setPermissionCallback(tempId, null);
55
+ setPermissionCallback(finalSessionId, sendPerm);
56
+ }
57
+
58
+ const text = getAssistantText(msg);
59
+ if (text)
60
+ await stream.writeSSE({
61
+ data: JSON.stringify({ response: text.trim(), sessionId: msg.session_id }),
62
+ });
63
+ }
64
+ if (finalSessionId) sessions.set(finalSessionId, session);
65
+ } catch (err) {
66
+ console.error(err);
67
+ await stream.writeSSE({ data: JSON.stringify({ error: "Stream error" }) });
68
+ } finally {
69
+ setPermissionCallback(tempId, null);
70
+ if (finalSessionId) {
71
+ setPermissionCallback(finalSessionId, null);
72
+ activeSessions.delete(finalSessionId);
73
+ }
74
+ }
75
+ });
76
+ } catch (error) {
77
+ console.error(error);
78
+ return c.json({ error: "Failed to create new chat" }, 400);
79
+ }
80
+ });
81
+
82
+ chats.post("/:id/message", async (c) => {
83
+ const { prompt, model, permission } = await c.req.json();
84
+ const sessionId = c.req.param("id");
85
+ if (!prompt || !sessionId || /[\/\\]/.test(sessionId))
86
+ return c.json({ error: "Invalid request" }, 400);
87
+
88
+ try {
89
+ const session = await getSession({ sessionId, model, permission });
90
+ if (!session) return c.json({ error: "Session is required" }, 400);
91
+
92
+ await sendMessage({ prompt, session });
93
+
94
+ return streamSSE(c, async (stream) => {
95
+ activeSessions.add(sessionId);
96
+
97
+ // Permission requests are sent directly from the canUseTool callback
98
+ // (which runs on a different async context than the stream loop)
99
+ setPermissionCallback(sessionId, async (req) => {
100
+ try {
101
+ await stream.writeSSE({
102
+ data: JSON.stringify({
103
+ type: "permission_request",
104
+ ...req,
105
+ sessionId,
106
+ }),
107
+ });
108
+ } catch (e) {
109
+ console.error("Failed to send permission SSE:", e);
110
+ }
111
+ });
112
+
113
+ try {
114
+ for await (const msg of session.stream()) {
115
+ const text = getAssistantText(msg);
116
+ if (text)
117
+ await stream.writeSSE({
118
+ data: JSON.stringify({
119
+ response: text?.trim() || null,
120
+ sessionId: msg.session_id,
121
+ }),
122
+ });
123
+ }
124
+ } catch (err) {
125
+ console.error(err);
126
+ await stream.writeSSE({ data: JSON.stringify({ error: "Stream error" }) });
127
+ } finally {
128
+ setPermissionCallback(sessionId, null);
129
+ activeSessions.delete(sessionId);
130
+ }
131
+ });
132
+ } catch (error) {
133
+ console.error(error);
134
+ return c.json({ error: "Failed to send message" }, 400);
135
+ }
136
+ });
137
+
138
+ // Respond to a pending permission request
139
+ chats.post("/:id/permission", async (c) => {
140
+ const { requestId, behavior, updatedPermissions } = await c.req.json();
141
+ if (!requestId || !behavior)
142
+ return c.json({ error: "requestId and behavior are required" }, 400);
143
+
144
+ const resolve = pendingPermissions.get(requestId);
145
+ if (!resolve)
146
+ return c.json({ error: "No pending permission request found" }, 404);
147
+
148
+ if (behavior === "allow") {
149
+ resolve({
150
+ behavior: "allow",
151
+ ...(updatedPermissions ? { updatedPermissions } : {}),
152
+ });
153
+ } else {
154
+ resolve({ behavior: "deny", message: "User denied permission" });
155
+ }
156
+
157
+ return c.json({ ok: true });
158
+ });
159
+
160
+ chats.get("/", async (c) => {
161
+ try {
162
+ const cwdSlug = process.cwd().replaceAll("/", "-");
163
+ const chatsDir = path.join(os.homedir(), ".claude", "projects", cwdSlug);
164
+ const files = (await readdir(chatsDir)).filter((f) => f.endsWith(".jsonl"));
165
+
166
+ const result = await Promise.all(
167
+ files.map(async (file) => {
168
+ const filePath = path.join(chatsDir, file);
169
+ const content = await readFile(filePath, "utf-8");
170
+ const lines = content.split("\n").filter(Boolean);
171
+ const sessionId = file.replace(".jsonl", "");
172
+
173
+ let preview = "";
174
+ let timestamp = "";
175
+ for (const line of lines) {
176
+ const obj = JSON.parse(line);
177
+ if (obj.type === "user" && !obj.isMeta) {
178
+ const content = obj.message.content;
179
+ if (typeof content === "string") {
180
+ preview = content.slice(0, 100);
181
+ } else {
182
+ preview = content
183
+ .filter((b: any) => b.type === "text")
184
+ .map((b: any) => b.text)
185
+ .join("")
186
+ .slice(0, 100);
187
+ }
188
+ timestamp = obj.timestamp;
189
+ break;
190
+ }
191
+ }
192
+ return { sessionId, text: preview, timestamp, isActive: activeSessions.has(sessionId) };
193
+ }),
194
+ );
195
+ return c.json({ result });
196
+ } catch (error) {
197
+ return c.json({ result: [] });
198
+ }
199
+ });
200
+
201
+ chats.get("/:id", async (c) => {
202
+ const sessionId = c.req.param("id");
203
+ if (!sessionId || /[\/\\]/.test(sessionId))
204
+ return c.json({ error: "Invalid session id" }, 400);
205
+
206
+ try {
207
+ const cwdSlug = process.cwd().replaceAll("/", "-");
208
+ const chatsDir = path.join(os.homedir(), ".claude", "projects", cwdSlug);
209
+ const file = path.join(chatsDir, `${sessionId}.jsonl`);
210
+ const content = await readFile(file, "utf-8");
211
+ const lines = content.split("\n").filter(Boolean);
212
+
213
+ const parsed = lines.map((line) => JSON.parse(line));
214
+
215
+ // Extract last known model and permissionMode from JSONL
216
+ let model: string | undefined;
217
+ let permissionMode: string | undefined;
218
+ for (const obj of [...parsed].reverse()) {
219
+ if (!model && obj.type === "assistant") model = obj.message?.model;
220
+ if (!permissionMode && obj.type === "user" && !obj.isMeta) permissionMode = obj.permissionMode;
221
+ if (model && permissionMode) break;
222
+ }
223
+
224
+ const messages = parsed
225
+ .filter(
226
+ (obj) =>
227
+ (obj.type === "user" && !obj.isMeta) || obj.type === "assistant",
228
+ )
229
+ .flatMap((obj) => {
230
+ const content = obj.message.content;
231
+ if (typeof content === "string") {
232
+ return [{ role: obj.message.role, type: "text", text: content.trim(), timestamp: obj.timestamp }];
233
+ }
234
+ return content
235
+ .filter((b: any) => ["text", "tool_use", "tool_result"].includes(b.type))
236
+ .map((b: any) => {
237
+ if (b.type === "text") {
238
+ return { role: obj.message.role, type: "text", text: b.text.trim(), timestamp: obj.timestamp };
239
+ }
240
+ if (b.type === "tool_use") {
241
+ return { role: obj.message.role, type: "tool_use", name: b.name, input: b.input, timestamp: obj.timestamp };
242
+ }
243
+ if (b.type === "tool_result") {
244
+ const resultText = Array.isArray(b.content)
245
+ ? b.content.filter((r: any) => r.type === "text").map((r: any) => r.text).join("")
246
+ : b.content ?? "";
247
+ return { role: obj.message.role, type: "tool_result", text: resultText.trim(), timestamp: obj.timestamp };
248
+ }
249
+ })
250
+ .filter(Boolean);
251
+ });
252
+
253
+ return c.json({ messages, model, permissionMode });
254
+ } catch {
255
+ return c.json({ error: "Chat not found" }, 404);
256
+ }
257
+ });
@@ -0,0 +1,156 @@
1
+ import { Hono } from "hono";
2
+ import { exec } from "child_process";
3
+ import { readFile } from "fs/promises";
4
+ import path from "path";
5
+ import { promisify } from "util";
6
+
7
+ const execAsync = promisify(exec);
8
+
9
+ // Resolve git repo root lazily (process.cwd() may be a subdir like api/)
10
+ let _repoRoot: string | null = null;
11
+ async function getRepoRoot(): Promise<string> {
12
+ if (!_repoRoot) {
13
+ try {
14
+ const { stdout } = await execAsync("git rev-parse --show-toplevel");
15
+ _repoRoot = stdout.trim();
16
+ } catch {
17
+ _repoRoot = process.cwd();
18
+ }
19
+ }
20
+ return _repoRoot;
21
+ }
22
+
23
+ export const git = new Hono();
24
+
25
+ interface FileChange {
26
+ path: string;
27
+ status: "M" | "A" | "D" | "?" | "R";
28
+ staged: boolean;
29
+ additions: number;
30
+ deletions: number;
31
+ }
32
+
33
+ git.get("/status", async (c) => {
34
+ try {
35
+ const root = await getRepoRoot();
36
+
37
+ const { stdout: statusOut } = await execAsync("git status --porcelain", {
38
+ cwd: root,
39
+ });
40
+
41
+ const [{ stdout: unstagedNum }, { stdout: stagedNum }] = await Promise.all([
42
+ execAsync("git diff --numstat", { cwd: root }).catch(() => ({
43
+ stdout: "",
44
+ })),
45
+ execAsync("git diff --cached --numstat", { cwd: root }).catch(() => ({
46
+ stdout: "",
47
+ })),
48
+ ]);
49
+
50
+ const parseNumstat = (raw: string) => {
51
+ const map = new Map<string, { additions: number; deletions: number }>();
52
+ for (const line of raw.split("\n").filter(Boolean)) {
53
+ const [add, del, file] = line.split("\t");
54
+ if (file) {
55
+ map.set(file, {
56
+ additions: add === "-" ? 0 : parseInt(add, 10),
57
+ deletions: del === "-" ? 0 : parseInt(del, 10),
58
+ });
59
+ }
60
+ }
61
+ return map;
62
+ };
63
+
64
+ const unstagedStats = parseNumstat(unstagedNum);
65
+ const stagedStats = parseNumstat(stagedNum);
66
+
67
+ const files: FileChange[] = [];
68
+ for (const line of statusOut.split("\n").filter(Boolean)) {
69
+ const xy = line.slice(0, 2);
70
+ const filePath = line.slice(3).trim();
71
+
72
+ let status: FileChange["status"] = "M";
73
+ let staged = false;
74
+
75
+ if (xy === "??") {
76
+ status = "?";
77
+ } else if (xy === "!!") {
78
+ continue;
79
+ } else {
80
+ const indexStatus = xy[0];
81
+ const worktreeStatus = xy[1];
82
+
83
+ if (indexStatus !== " " && indexStatus !== "?") {
84
+ staged = true;
85
+ if (indexStatus === "A") status = "A";
86
+ else if (indexStatus === "D") status = "D";
87
+ else if (indexStatus === "R") status = "R";
88
+ else status = "M";
89
+ } else {
90
+ if (worktreeStatus === "D") status = "D";
91
+ else status = "M";
92
+ }
93
+ }
94
+
95
+ const stats = staged
96
+ ? stagedStats.get(filePath)
97
+ : unstagedStats.get(filePath);
98
+
99
+ let additions = stats?.additions ?? 0;
100
+ let deletions = stats?.deletions ?? 0;
101
+
102
+ if (status === "?") {
103
+ try {
104
+ const content = await readFile(path.join(root, filePath), "utf-8");
105
+ additions = content.split("\n").length;
106
+ } catch {
107
+ additions = 0;
108
+ }
109
+ }
110
+
111
+ files.push({ path: filePath, status, staged, additions, deletions });
112
+ }
113
+
114
+ return c.json({ files });
115
+ } catch (error) {
116
+ console.error("git status error:", error);
117
+ return c.json({ files: [] });
118
+ }
119
+ });
120
+
121
+ git.get("/diff", async (c) => {
122
+ const filePath = c.req.query("file");
123
+ if (!filePath) return c.json({ error: "file parameter required" }, 400);
124
+
125
+ if (filePath.includes("..")) return c.json({ error: "Invalid path" }, 400);
126
+
127
+ const root = await getRepoRoot();
128
+ const safePath = filePath.replace(/'/g, "'\\''");
129
+
130
+ try {
131
+ let oldContent = "";
132
+ let newContent = "";
133
+
134
+ try {
135
+ const { stdout } = await execAsync(`git show 'HEAD:${safePath}'`, {
136
+ cwd: root,
137
+ maxBuffer: 10 * 1024 * 1024,
138
+ });
139
+ oldContent = stdout;
140
+ } catch {
141
+ oldContent = "";
142
+ }
143
+
144
+ try {
145
+ newContent = await readFile(path.join(root, filePath), "utf-8");
146
+ } catch (e) {
147
+ console.error("readFile error for", filePath, e);
148
+ newContent = "";
149
+ }
150
+
151
+ return c.json({ path: filePath, oldContent, newContent });
152
+ } catch (error) {
153
+ console.error("git diff error:", error);
154
+ return c.json({ error: "Failed to get diff" }, 500);
155
+ }
156
+ });
package/src/server.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { Hono } from "hono";
2
+ import { serve } from "@hono/node-server";
3
+ import { cors } from "hono/cors";
4
+ import { chats } from "./routes/chats";
5
+ import { git } from "./routes/git";
6
+ import { bearerAuth } from "hono/bearer-auth";
7
+ const app = new Hono();
8
+ const token = process.env.AUTH_TOKEN;
9
+
10
+ if (!token) {
11
+ console.error("AUTH_TOKEN is not set. Run `anywhere-ai init` first.");
12
+ process.exit(1);
13
+ }
14
+
15
+ app.use("*", cors());
16
+
17
+ app.get("/health", async (c) => {
18
+ return c.json({ message: "server is healthy" }, 200);
19
+ });
20
+
21
+ app.use("/v1/*", bearerAuth({ token }));
22
+ app.route("/v1/chats", chats);
23
+ app.route("/v1/git", git);
24
+
25
+ serve(
26
+ {
27
+ fetch: app.fetch,
28
+ hostname: "0.0.0.0",
29
+ port: parseInt(process.env.PORT || "3847"),
30
+ },
31
+ (info) => {
32
+ console.log(`Server is running on http://localhost:${info.port}`);
33
+ },
34
+ );
package/test.js ADDED
@@ -0,0 +1,12 @@
1
+ const greet = (name) => `Yo, what's up ${name}?!`;
2
+ console.log(greet("Yogesh"));
3
+
4
+ const add = (a, b) => a + b;
5
+ const multiply = (a, b) => a * b;
6
+ const subtract = (a, b) => a - b;
7
+
8
+ console.log(`2 + 3 = ${add(2, 3)}`);
9
+ console.log(`4 x 5 = ${multiply(4, 5)}`);
10
+
11
+ const fruits = ["apple", "banana", "mango", "kiwi"];
12
+ fruits.forEach((fruit, i) => console.log(`${i + 1}. ${fruit}`));
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "esModuleInterop": true,
7
+ "strict": true,
8
+ "outDir": "dist",
9
+ "rootDir": "src",
10
+ "skipLibCheck": true
11
+ },
12
+ "include": ["src"]
13
+ }