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 +3 -1
- package/dist/server-Q5IYQIHO.js +585 -0
- package/package.json +2 -2
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-
|
|
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
|
+
"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
|
|
36
|
+
"build": "tsup"
|
|
37
37
|
}
|
|
38
38
|
}
|