anywhere-ai 0.0.9 → 0.0.10
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 +2 -2
- package/dist/server-QUVKJH7W.js +661 -0
- package/package.json +1 -1
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.
|
|
13
|
+
console.log(`anywhere-ai v${"0.0.10"}`);
|
|
14
14
|
process.exit(0);
|
|
15
15
|
}
|
|
16
16
|
function ask(question, preserveCase = false) {
|
|
@@ -184,4 +184,4 @@ if (isVPS) {
|
|
|
184
184
|
console.log(" Make sure port " + port + " is open in your firewall.");
|
|
185
185
|
}
|
|
186
186
|
console.log();
|
|
187
|
-
import("./server-
|
|
187
|
+
import("./server-QUVKJH7W.js");
|
|
@@ -0,0 +1,661 @@
|
|
|
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
|
+
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
|
+
var setupRepo = async (repo) => {
|
|
91
|
+
console.log(`[setupRepo] repo=${repo}`);
|
|
92
|
+
await fs.mkdir(PROJECTS_DIR, { recursive: true });
|
|
93
|
+
await fs.mkdir(WORKTREES_DIR, { recursive: true });
|
|
94
|
+
const repoPath = path.join(PROJECTS_DIR, repo);
|
|
95
|
+
try {
|
|
96
|
+
await fs.access(repoPath);
|
|
97
|
+
console.log(`[setupRepo] repo exists at ${repoPath}, fetching origin`);
|
|
98
|
+
execSync("git fetch origin", { cwd: repoPath, encoding: "utf-8" });
|
|
99
|
+
} catch {
|
|
100
|
+
console.log(`[setupRepo] cloning repo ${repo} to ${repoPath}`);
|
|
101
|
+
execSync(`gh repo clone ${repo} ${repoPath}`, { encoding: "utf-8" });
|
|
102
|
+
}
|
|
103
|
+
const branchName = `chat/chat-${Date.now()}`;
|
|
104
|
+
console.log(`[setupRepo] branchName=${branchName}`);
|
|
105
|
+
const worktreePath = path.join(WORKTREES_DIR, branchName);
|
|
106
|
+
console.log(`[setupRepo] creating worktree at ${worktreePath}`);
|
|
107
|
+
execSync(`git worktree add ${worktreePath} -b ${branchName}`, {
|
|
108
|
+
cwd: repoPath,
|
|
109
|
+
encoding: "utf-8"
|
|
110
|
+
});
|
|
111
|
+
console.log(`[setupRepo] worktree ready at ${worktreePath}`);
|
|
112
|
+
return worktreePath;
|
|
113
|
+
};
|
|
114
|
+
var createChat = async ({
|
|
115
|
+
model,
|
|
116
|
+
permission,
|
|
117
|
+
repo,
|
|
118
|
+
prompt
|
|
119
|
+
}) => {
|
|
120
|
+
console.log(`[createChat] model=${model} permission=${permission} repo=${repo} prompt="${prompt?.slice(0, 50)}"`);
|
|
121
|
+
const repoPath = repo ? await setupRepo(repo) : void 0;
|
|
122
|
+
console.log(`[createChat] cwd=${repoPath || process.cwd()}`);
|
|
123
|
+
const tempId = `temp_${Date.now()}`;
|
|
124
|
+
const mode = permission || "acceptEdits";
|
|
125
|
+
console.log(`[createChat] tempId=${tempId} mode=${mode}`);
|
|
126
|
+
const session = unstable_v2_createSession({
|
|
127
|
+
model: model || "claude-opus-4-6",
|
|
128
|
+
permissionMode: mode,
|
|
129
|
+
canUseTool: makeCanUseTool(tempId, mode),
|
|
130
|
+
...repoPath ? { cwd: repoPath } : {}
|
|
131
|
+
});
|
|
132
|
+
return { session, tempId };
|
|
133
|
+
};
|
|
134
|
+
var sendMessage = async ({
|
|
135
|
+
prompt,
|
|
136
|
+
session
|
|
137
|
+
}) => {
|
|
138
|
+
await session.send(prompt);
|
|
139
|
+
};
|
|
140
|
+
var getSession = async ({
|
|
141
|
+
sessionId,
|
|
142
|
+
model,
|
|
143
|
+
permission
|
|
144
|
+
}) => {
|
|
145
|
+
let session = sessions.get(sessionId);
|
|
146
|
+
if (session && (model || permission)) {
|
|
147
|
+
console.log(`[getSession] ${sessionId} re-creating (model=${model} permission=${permission})`);
|
|
148
|
+
session.close();
|
|
149
|
+
sessions.delete(sessionId);
|
|
150
|
+
session = void 0;
|
|
151
|
+
}
|
|
152
|
+
if (!session) {
|
|
153
|
+
console.log(`[getSession] ${sessionId} resuming from JSONL`);
|
|
154
|
+
const mode = permission || "acceptEdits";
|
|
155
|
+
session = unstable_v2_resumeSession(sessionId, {
|
|
156
|
+
model: model || "claude-opus-4-6",
|
|
157
|
+
permissionMode: mode,
|
|
158
|
+
canUseTool: makeCanUseTool(sessionId, mode)
|
|
159
|
+
});
|
|
160
|
+
sessions.set(sessionId, session);
|
|
161
|
+
} else {
|
|
162
|
+
console.log(`[getSession] ${sessionId} found in memory`);
|
|
163
|
+
}
|
|
164
|
+
return session;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// src/routes/chats/index.ts
|
|
168
|
+
var repoNameCache = /* @__PURE__ */ new Map();
|
|
169
|
+
function deriveRepoName(cwd) {
|
|
170
|
+
if (!cwd) return "Unknown";
|
|
171
|
+
const cached = repoNameCache.get(cwd);
|
|
172
|
+
if (cached) return cached;
|
|
173
|
+
let name = "Unknown";
|
|
174
|
+
try {
|
|
175
|
+
const url = execSync2("git remote get-url origin 2>/dev/null", {
|
|
176
|
+
cwd,
|
|
177
|
+
encoding: "utf-8",
|
|
178
|
+
timeout: 3e3
|
|
179
|
+
}).trim();
|
|
180
|
+
const match = url.match(/[:/]([^/]+\/[^/.]+?)(?:\.git)?$/);
|
|
181
|
+
if (match?.[1]) name = match[1];
|
|
182
|
+
} catch {
|
|
183
|
+
try {
|
|
184
|
+
const root = execSync2("git rev-parse --show-toplevel 2>/dev/null", {
|
|
185
|
+
cwd,
|
|
186
|
+
encoding: "utf-8",
|
|
187
|
+
timeout: 3e3
|
|
188
|
+
}).trim();
|
|
189
|
+
name = root.split("/").filter(Boolean).pop() || "Unknown";
|
|
190
|
+
} catch {
|
|
191
|
+
const segments = cwd.split("/").filter(Boolean);
|
|
192
|
+
name = segments[segments.length - 1] || "Unknown";
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
repoNameCache.set(cwd, name);
|
|
196
|
+
return name;
|
|
197
|
+
}
|
|
198
|
+
async function findSessionFile(sessionId) {
|
|
199
|
+
const projectsBase = path2.join(os2.homedir(), ".claude", "projects");
|
|
200
|
+
try {
|
|
201
|
+
const dirs = await readdir(projectsBase);
|
|
202
|
+
for (const dir of dirs) {
|
|
203
|
+
const filePath = path2.join(projectsBase, dir, `${sessionId}.jsonl`);
|
|
204
|
+
try {
|
|
205
|
+
await stat(filePath);
|
|
206
|
+
return filePath;
|
|
207
|
+
} catch {
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
var chats = new Hono();
|
|
215
|
+
chats.post("/new", async (c) => {
|
|
216
|
+
const { prompt, model, permission, repo } = await c.req.json();
|
|
217
|
+
console.log(`[POST /new] prompt="${prompt?.slice(0, 50)}" model=${model} repo=${repo}`);
|
|
218
|
+
if (!prompt) return c.json({ error: "Prompt is required" }, 400);
|
|
219
|
+
try {
|
|
220
|
+
const { session, tempId } = await createChat({ model, permission, repo, prompt });
|
|
221
|
+
await sendMessage({ prompt, session });
|
|
222
|
+
return streamSSE(c, async (stream) => {
|
|
223
|
+
let finalSessionId = "";
|
|
224
|
+
console.log(`[POST /new] SSE stream started, tempId=${tempId}`);
|
|
225
|
+
const sendPerm = async (req) => {
|
|
226
|
+
try {
|
|
227
|
+
await stream.writeSSE({
|
|
228
|
+
data: JSON.stringify({
|
|
229
|
+
type: "permission_request",
|
|
230
|
+
...req,
|
|
231
|
+
sessionId: finalSessionId || "pending"
|
|
232
|
+
})
|
|
233
|
+
});
|
|
234
|
+
} catch (e) {
|
|
235
|
+
console.error("Failed to send permission SSE:", e);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
setPermissionCallback(tempId, sendPerm);
|
|
239
|
+
try {
|
|
240
|
+
for await (const msg of session.stream()) {
|
|
241
|
+
if (msg.session_id && !finalSessionId) {
|
|
242
|
+
finalSessionId = msg.session_id;
|
|
243
|
+
console.log(`[POST /new] sessionId received: ${finalSessionId}`);
|
|
244
|
+
activeSessions.add(finalSessionId);
|
|
245
|
+
setPermissionCallback(tempId, null);
|
|
246
|
+
setPermissionCallback(finalSessionId, sendPerm);
|
|
247
|
+
}
|
|
248
|
+
const text = getAssistantText(msg);
|
|
249
|
+
if (text)
|
|
250
|
+
await stream.writeSSE({
|
|
251
|
+
data: JSON.stringify({
|
|
252
|
+
response: text.trim(),
|
|
253
|
+
sessionId: msg.session_id
|
|
254
|
+
})
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
if (finalSessionId) {
|
|
258
|
+
sessions.set(finalSessionId, session);
|
|
259
|
+
console.log(`[POST /new] session stored: ${finalSessionId}`);
|
|
260
|
+
}
|
|
261
|
+
} catch (err) {
|
|
262
|
+
console.error(err);
|
|
263
|
+
await stream.writeSSE({
|
|
264
|
+
data: JSON.stringify({ error: "Stream error" })
|
|
265
|
+
});
|
|
266
|
+
} finally {
|
|
267
|
+
setPermissionCallback(tempId, null);
|
|
268
|
+
if (finalSessionId) {
|
|
269
|
+
setPermissionCallback(finalSessionId, null);
|
|
270
|
+
activeSessions.delete(finalSessionId);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
} catch (error) {
|
|
275
|
+
console.error(error);
|
|
276
|
+
return c.json({ error: "Failed to create new chat" }, 400);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
chats.post("/:id/message", async (c) => {
|
|
280
|
+
const { prompt, model, permission } = await c.req.json();
|
|
281
|
+
const sessionId = c.req.param("id");
|
|
282
|
+
console.log(`[POST /:id/message] sessionId=${sessionId} prompt="${prompt?.slice(0, 50)}"`);
|
|
283
|
+
if (!prompt || !sessionId || /[\/\\]/.test(sessionId))
|
|
284
|
+
return c.json({ error: "Invalid request" }, 400);
|
|
285
|
+
try {
|
|
286
|
+
const session = await getSession({ sessionId, model, permission });
|
|
287
|
+
if (!session) return c.json({ error: "Session is required" }, 400);
|
|
288
|
+
await sendMessage({ prompt, session });
|
|
289
|
+
return streamSSE(c, async (stream) => {
|
|
290
|
+
activeSessions.add(sessionId);
|
|
291
|
+
setPermissionCallback(sessionId, async (req) => {
|
|
292
|
+
try {
|
|
293
|
+
await stream.writeSSE({
|
|
294
|
+
data: JSON.stringify({
|
|
295
|
+
type: "permission_request",
|
|
296
|
+
...req,
|
|
297
|
+
sessionId
|
|
298
|
+
})
|
|
299
|
+
});
|
|
300
|
+
} catch (e) {
|
|
301
|
+
console.error("Failed to send permission SSE:", e);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
try {
|
|
305
|
+
for await (const msg of session.stream()) {
|
|
306
|
+
const text = getAssistantText(msg);
|
|
307
|
+
if (text)
|
|
308
|
+
await stream.writeSSE({
|
|
309
|
+
data: JSON.stringify({
|
|
310
|
+
response: text?.trim() || null,
|
|
311
|
+
sessionId: msg.session_id
|
|
312
|
+
})
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
} catch (err) {
|
|
316
|
+
console.error(err);
|
|
317
|
+
await stream.writeSSE({
|
|
318
|
+
data: JSON.stringify({ error: "Stream error" })
|
|
319
|
+
});
|
|
320
|
+
} finally {
|
|
321
|
+
setPermissionCallback(sessionId, null);
|
|
322
|
+
activeSessions.delete(sessionId);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
} catch (error) {
|
|
326
|
+
console.error(error);
|
|
327
|
+
return c.json({ error: "Failed to send message" }, 400);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
chats.post("/:id/permission", async (c) => {
|
|
331
|
+
const { requestId, behavior, updatedPermissions } = await c.req.json();
|
|
332
|
+
if (!requestId || !behavior)
|
|
333
|
+
return c.json({ error: "requestId and behavior are required" }, 400);
|
|
334
|
+
const resolve = pendingPermissions.get(requestId);
|
|
335
|
+
if (!resolve)
|
|
336
|
+
return c.json({ error: "No pending permission request found" }, 404);
|
|
337
|
+
if (behavior === "allow") {
|
|
338
|
+
resolve({
|
|
339
|
+
behavior: "allow",
|
|
340
|
+
...updatedPermissions ? { updatedPermissions } : {}
|
|
341
|
+
});
|
|
342
|
+
} else {
|
|
343
|
+
resolve({ behavior: "deny", message: "User denied permission" });
|
|
344
|
+
}
|
|
345
|
+
return c.json({ ok: true });
|
|
346
|
+
});
|
|
347
|
+
chats.get("/", async (c) => {
|
|
348
|
+
try {
|
|
349
|
+
const projectsBase = path2.join(os2.homedir(), ".claude", "projects");
|
|
350
|
+
const dirs = await readdir(projectsBase);
|
|
351
|
+
const allChats = [];
|
|
352
|
+
for (const dir of dirs) {
|
|
353
|
+
const dirPath = path2.join(projectsBase, dir);
|
|
354
|
+
try {
|
|
355
|
+
const s = await stat(dirPath);
|
|
356
|
+
if (!s.isDirectory()) continue;
|
|
357
|
+
} catch {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const files = (await readdir(dirPath)).filter(
|
|
361
|
+
(f) => f.endsWith(".jsonl")
|
|
362
|
+
);
|
|
363
|
+
for (const file of files) {
|
|
364
|
+
const filePath = path2.join(dirPath, file);
|
|
365
|
+
const content = await readFile(filePath, "utf-8");
|
|
366
|
+
const lines = content.split("\n").filter(Boolean);
|
|
367
|
+
const sessionId = file.replace(".jsonl", "");
|
|
368
|
+
let preview = "";
|
|
369
|
+
let timestamp = "";
|
|
370
|
+
let cwd = "";
|
|
371
|
+
let gitBranch = "";
|
|
372
|
+
for (const line of lines) {
|
|
373
|
+
try {
|
|
374
|
+
const obj = JSON.parse(line);
|
|
375
|
+
if (obj.type === "user") {
|
|
376
|
+
if (!cwd && obj.cwd) cwd = obj.cwd;
|
|
377
|
+
if (!gitBranch && obj.gitBranch) gitBranch = obj.gitBranch;
|
|
378
|
+
if (!obj.isMeta) {
|
|
379
|
+
const msgContent = obj.message.content;
|
|
380
|
+
if (typeof msgContent === "string") {
|
|
381
|
+
preview = msgContent.slice(0, 100);
|
|
382
|
+
} else {
|
|
383
|
+
preview = msgContent.filter((b) => b.type === "text").map((b) => b.text).join("").slice(0, 100);
|
|
384
|
+
}
|
|
385
|
+
timestamp = obj.timestamp;
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
} catch {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (!timestamp) continue;
|
|
394
|
+
allChats.push({
|
|
395
|
+
sessionId,
|
|
396
|
+
text: preview,
|
|
397
|
+
timestamp,
|
|
398
|
+
isActive: activeSessions.has(sessionId),
|
|
399
|
+
repo: deriveRepoName(cwd),
|
|
400
|
+
cwd,
|
|
401
|
+
gitBranch
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return c.json({ result: allChats });
|
|
406
|
+
} catch (error) {
|
|
407
|
+
console.error("[GET /chats] error:", error);
|
|
408
|
+
return c.json({ result: [] });
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
chats.get("/:id", async (c) => {
|
|
412
|
+
const sessionId = c.req.param("id");
|
|
413
|
+
if (!sessionId || /[\/\\]/.test(sessionId))
|
|
414
|
+
return c.json({ error: "Invalid session id" }, 400);
|
|
415
|
+
try {
|
|
416
|
+
const file = await findSessionFile(sessionId);
|
|
417
|
+
if (!file) return c.json({ error: "Chat not found" }, 404);
|
|
418
|
+
const content = await readFile(file, "utf-8");
|
|
419
|
+
const lines = content.split("\n").filter(Boolean);
|
|
420
|
+
const parsed = lines.map((line) => JSON.parse(line));
|
|
421
|
+
let model;
|
|
422
|
+
let permissionMode;
|
|
423
|
+
for (const obj of [...parsed].reverse()) {
|
|
424
|
+
if (!model && obj.type === "assistant") model = obj.message?.model;
|
|
425
|
+
if (!permissionMode && obj.type === "user" && !obj.isMeta)
|
|
426
|
+
permissionMode = obj.permissionMode;
|
|
427
|
+
if (model && permissionMode) break;
|
|
428
|
+
}
|
|
429
|
+
const messages = parsed.filter(
|
|
430
|
+
(obj) => obj.type === "user" && !obj.isMeta || obj.type === "assistant"
|
|
431
|
+
).flatMap((obj) => {
|
|
432
|
+
const content2 = obj.message.content;
|
|
433
|
+
if (typeof content2 === "string") {
|
|
434
|
+
return [
|
|
435
|
+
{
|
|
436
|
+
role: obj.message.role,
|
|
437
|
+
type: "text",
|
|
438
|
+
text: content2.trim(),
|
|
439
|
+
timestamp: obj.timestamp
|
|
440
|
+
}
|
|
441
|
+
];
|
|
442
|
+
}
|
|
443
|
+
return content2.filter(
|
|
444
|
+
(b) => ["text", "tool_use", "tool_result"].includes(b.type)
|
|
445
|
+
).map((b) => {
|
|
446
|
+
if (b.type === "text") {
|
|
447
|
+
return {
|
|
448
|
+
role: obj.message.role,
|
|
449
|
+
type: "text",
|
|
450
|
+
text: b.text.trim(),
|
|
451
|
+
timestamp: obj.timestamp
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
if (b.type === "tool_use") {
|
|
455
|
+
return {
|
|
456
|
+
role: obj.message.role,
|
|
457
|
+
type: "tool_use",
|
|
458
|
+
name: b.name,
|
|
459
|
+
input: b.input,
|
|
460
|
+
timestamp: obj.timestamp
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
if (b.type === "tool_result") {
|
|
464
|
+
const resultText = Array.isArray(b.content) ? b.content.filter((r) => r.type === "text").map((r) => r.text).join("") : b.content ?? "";
|
|
465
|
+
return {
|
|
466
|
+
role: obj.message.role,
|
|
467
|
+
type: "tool_result",
|
|
468
|
+
text: resultText.trim(),
|
|
469
|
+
timestamp: obj.timestamp
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
}).filter(Boolean);
|
|
473
|
+
});
|
|
474
|
+
return c.json({ messages, model, permissionMode });
|
|
475
|
+
} catch {
|
|
476
|
+
return c.json({ error: "Chat not found" }, 404);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// src/routes/git/index.ts
|
|
481
|
+
import { Hono as Hono2 } from "hono";
|
|
482
|
+
import { exec } from "child_process";
|
|
483
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
484
|
+
import path3 from "path";
|
|
485
|
+
import { promisify } from "util";
|
|
486
|
+
var execAsync = promisify(exec);
|
|
487
|
+
var _repoRoot = null;
|
|
488
|
+
async function getRepoRoot() {
|
|
489
|
+
if (_repoRoot === null) {
|
|
490
|
+
try {
|
|
491
|
+
const { stdout } = await execAsync("git rev-parse --show-toplevel");
|
|
492
|
+
_repoRoot = stdout.trim();
|
|
493
|
+
} catch {
|
|
494
|
+
_repoRoot = false;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return _repoRoot || null;
|
|
498
|
+
}
|
|
499
|
+
var git = new Hono2();
|
|
500
|
+
git.get("/status", async (c) => {
|
|
501
|
+
try {
|
|
502
|
+
const root = await getRepoRoot();
|
|
503
|
+
if (!root) return c.json({ files: [], error: "Not a git repository" });
|
|
504
|
+
const { stdout: statusOut } = await execAsync("git status --porcelain", {
|
|
505
|
+
cwd: root
|
|
506
|
+
});
|
|
507
|
+
const [{ stdout: unstagedNum }, { stdout: stagedNum }] = await Promise.all([
|
|
508
|
+
execAsync("git diff --numstat", { cwd: root }).catch(() => ({
|
|
509
|
+
stdout: ""
|
|
510
|
+
})),
|
|
511
|
+
execAsync("git diff --cached --numstat", { cwd: root }).catch(() => ({
|
|
512
|
+
stdout: ""
|
|
513
|
+
}))
|
|
514
|
+
]);
|
|
515
|
+
const parseNumstat = (raw) => {
|
|
516
|
+
const map = /* @__PURE__ */ new Map();
|
|
517
|
+
for (const line of raw.split("\n").filter(Boolean)) {
|
|
518
|
+
const [add, del, file] = line.split(" ");
|
|
519
|
+
if (file) {
|
|
520
|
+
map.set(file, {
|
|
521
|
+
additions: add === "-" ? 0 : parseInt(add, 10),
|
|
522
|
+
deletions: del === "-" ? 0 : parseInt(del, 10)
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return map;
|
|
527
|
+
};
|
|
528
|
+
const unstagedStats = parseNumstat(unstagedNum);
|
|
529
|
+
const stagedStats = parseNumstat(stagedNum);
|
|
530
|
+
const files = [];
|
|
531
|
+
for (const line of statusOut.split("\n").filter(Boolean)) {
|
|
532
|
+
const xy = line.slice(0, 2);
|
|
533
|
+
const filePath = line.slice(3).trim();
|
|
534
|
+
let status = "M";
|
|
535
|
+
let staged = false;
|
|
536
|
+
if (xy === "??") {
|
|
537
|
+
status = "?";
|
|
538
|
+
} else if (xy === "!!") {
|
|
539
|
+
continue;
|
|
540
|
+
} else {
|
|
541
|
+
const indexStatus = xy[0];
|
|
542
|
+
const worktreeStatus = xy[1];
|
|
543
|
+
if (indexStatus !== " " && indexStatus !== "?") {
|
|
544
|
+
staged = true;
|
|
545
|
+
if (indexStatus === "A") status = "A";
|
|
546
|
+
else if (indexStatus === "D") status = "D";
|
|
547
|
+
else if (indexStatus === "R") status = "R";
|
|
548
|
+
else status = "M";
|
|
549
|
+
} else {
|
|
550
|
+
if (worktreeStatus === "D") status = "D";
|
|
551
|
+
else status = "M";
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
const stats = staged ? stagedStats.get(filePath) : unstagedStats.get(filePath);
|
|
555
|
+
let additions = stats?.additions ?? 0;
|
|
556
|
+
let deletions = stats?.deletions ?? 0;
|
|
557
|
+
if (status === "?") {
|
|
558
|
+
try {
|
|
559
|
+
const content = await readFile2(path3.join(root, filePath), "utf-8");
|
|
560
|
+
additions = content.split("\n").length;
|
|
561
|
+
} catch {
|
|
562
|
+
additions = 0;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
files.push({ path: filePath, status, staged, additions, deletions });
|
|
566
|
+
}
|
|
567
|
+
return c.json({ files });
|
|
568
|
+
} catch (error) {
|
|
569
|
+
console.error("git status error:", error);
|
|
570
|
+
return c.json({ files: [] });
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
git.get("/diff", async (c) => {
|
|
574
|
+
const filePath = c.req.query("file");
|
|
575
|
+
if (!filePath) return c.json({ error: "file parameter required" }, 400);
|
|
576
|
+
if (filePath.includes("..")) return c.json({ error: "Invalid path" }, 400);
|
|
577
|
+
const root = await getRepoRoot();
|
|
578
|
+
if (!root) return c.json({ error: "Not a git repository" }, 400);
|
|
579
|
+
const safePath = filePath.replace(/'/g, "'\\''");
|
|
580
|
+
try {
|
|
581
|
+
let oldContent = "";
|
|
582
|
+
let newContent = "";
|
|
583
|
+
try {
|
|
584
|
+
const { stdout } = await execAsync(`git show 'HEAD:${safePath}'`, {
|
|
585
|
+
cwd: root,
|
|
586
|
+
maxBuffer: 10 * 1024 * 1024
|
|
587
|
+
});
|
|
588
|
+
oldContent = stdout;
|
|
589
|
+
} catch {
|
|
590
|
+
oldContent = "";
|
|
591
|
+
}
|
|
592
|
+
try {
|
|
593
|
+
newContent = await readFile2(path3.join(root, filePath), "utf-8");
|
|
594
|
+
} catch (e) {
|
|
595
|
+
console.error("readFile error for", filePath, e);
|
|
596
|
+
newContent = "";
|
|
597
|
+
}
|
|
598
|
+
return c.json({ path: filePath, oldContent, newContent });
|
|
599
|
+
} catch (error) {
|
|
600
|
+
console.error("git diff error:", error);
|
|
601
|
+
return c.json({ error: "Failed to get diff" }, 500);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// src/routes/gh/index.ts
|
|
606
|
+
import { Hono as Hono3 } from "hono";
|
|
607
|
+
import { execSync as execSync3 } from "child_process";
|
|
608
|
+
var gh = new Hono3();
|
|
609
|
+
gh.get("/repos", async (c) => {
|
|
610
|
+
const fields = "--json name,nameWithOwner,description,url,isPrivate,updatedAt --limit 100";
|
|
611
|
+
try {
|
|
612
|
+
const personal = JSON.parse(
|
|
613
|
+
execSync3(`gh repo list ${fields}`, { encoding: "utf-8" })
|
|
614
|
+
);
|
|
615
|
+
let orgRepos = [];
|
|
616
|
+
try {
|
|
617
|
+
const orgs = execSync3("gh api user/orgs --jq '.[].login'", { encoding: "utf-8" }).trim().split("\n").filter(Boolean);
|
|
618
|
+
for (const org of orgs) {
|
|
619
|
+
const repos = JSON.parse(
|
|
620
|
+
execSync3(`gh repo list ${org} ${fields}`, { encoding: "utf-8" })
|
|
621
|
+
);
|
|
622
|
+
orgRepos.push(...repos);
|
|
623
|
+
}
|
|
624
|
+
} catch {
|
|
625
|
+
}
|
|
626
|
+
const username = execSync3("gh api user --jq '.login'", { encoding: "utf-8" }).trim();
|
|
627
|
+
const all = [...personal, ...orgRepos].sort(
|
|
628
|
+
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
629
|
+
);
|
|
630
|
+
return c.json({ username, repos: all });
|
|
631
|
+
} catch (error) {
|
|
632
|
+
return c.json([]);
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// src/server.ts
|
|
637
|
+
import { bearerAuth } from "hono/bearer-auth";
|
|
638
|
+
var app = new Hono4();
|
|
639
|
+
var token = process.env.AUTH_TOKEN;
|
|
640
|
+
if (!token) {
|
|
641
|
+
console.error("AUTH_TOKEN is not set. Run `anywhere-ai init` first.");
|
|
642
|
+
process.exit(1);
|
|
643
|
+
}
|
|
644
|
+
app.use("*", cors());
|
|
645
|
+
app.get("/health", async (c) => {
|
|
646
|
+
return c.json({ message: "server is healthy" }, 200);
|
|
647
|
+
});
|
|
648
|
+
app.use("/v1/*", bearerAuth({ token }));
|
|
649
|
+
app.route("/v1/chats", chats);
|
|
650
|
+
app.route("/v1/git", git);
|
|
651
|
+
app.route("/v1/gh", gh);
|
|
652
|
+
serve(
|
|
653
|
+
{
|
|
654
|
+
fetch: app.fetch,
|
|
655
|
+
hostname: "0.0.0.0",
|
|
656
|
+
port: parseInt(process.env.PORT || "3847")
|
|
657
|
+
},
|
|
658
|
+
(info) => {
|
|
659
|
+
console.log("Server is running");
|
|
660
|
+
}
|
|
661
|
+
);
|