agserver 1.0.0
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/README.md +448 -0
- package/package.json +20 -0
- package/schema/contract.md +109 -0
- package/server/auth.mjs +72 -0
- package/server/chat.mjs +256 -0
- package/server/cli.mjs +138 -0
- package/server/files.mjs +165 -0
- package/server/git.mjs +328 -0
- package/server/index.mjs +225 -0
- package/server/projects.mjs +220 -0
package/README.md
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
# AGServer
|
|
2
|
+
|
|
3
|
+
AI-assisted git project browser server. Run it against any folder of git repos and connect your client app to browse code, view git history, and chat with an AI agent via `kiro-cli`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Node.js >= 18
|
|
10
|
+
- `git` on PATH
|
|
11
|
+
- `kiro-cli` installed and authenticated (for chat endpoints)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g agserver
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Start the server
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Serve a specific folder
|
|
27
|
+
agserver ~/my-projects
|
|
28
|
+
|
|
29
|
+
# Custom port
|
|
30
|
+
agserver ~/my-projects --port 9000
|
|
31
|
+
|
|
32
|
+
# Bind to all interfaces (LAN access)
|
|
33
|
+
agserver ~/my-projects --public
|
|
34
|
+
|
|
35
|
+
# Provide your own API key
|
|
36
|
+
agserver ~/my-projects --key my-secret-key-at-least-24-chars
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
On first run the server prints the auto-generated API key — copy it, you'll need it in your client:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
agserver
|
|
43
|
+
projects root : /Users/you/my-projects
|
|
44
|
+
port : 8765
|
|
45
|
+
kiro-cli : 1.2.3 ✓
|
|
46
|
+
[agserver] running
|
|
47
|
+
[agserver] host=127.0.0.1 port=8765 apiVersion=1.0.0
|
|
48
|
+
[agserver] api key: <generated-key-shown-here> ← copy this
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Connecting from your client
|
|
54
|
+
|
|
55
|
+
Every request to `/api/*` must include the header:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
x-agserver-key: <your-api-key>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The server also returns `x-agserver-version` on every response. Check it matches your client's expected major version.
|
|
62
|
+
|
|
63
|
+
### Version compatibility check (recommended)
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
const res = await fetch('http://localhost:8765/health');
|
|
67
|
+
const { apiVersion } = await res.json();
|
|
68
|
+
const [serverMajor] = apiVersion.split('.');
|
|
69
|
+
const [clientMajor] = CLIENT_API_VERSION.split('.');
|
|
70
|
+
if (serverMajor !== clientMajor) throw new Error('AGServer version mismatch');
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Base client helper (JavaScript)
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
const BASE = 'http://localhost:8765';
|
|
77
|
+
const KEY = 'your-api-key-here';
|
|
78
|
+
|
|
79
|
+
async function api(path, opts = {}) {
|
|
80
|
+
const res = await fetch(`${BASE}${path}`, {
|
|
81
|
+
...opts,
|
|
82
|
+
headers: { 'x-agserver-key': KEY, 'Content-Type': 'application/json', ...opts.headers },
|
|
83
|
+
});
|
|
84
|
+
if (!res.ok) throw new Error(`AGServer ${res.status}: ${await res.text()}`);
|
|
85
|
+
return res.json();
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Endpoints
|
|
92
|
+
|
|
93
|
+
### GET /health
|
|
94
|
+
No auth required. Use this to verify the server is reachable and check its version.
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
GET http://localhost:8765/health
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"ok": true,
|
|
103
|
+
"apiVersion": "1.0.0",
|
|
104
|
+
"host": "127.0.0.1",
|
|
105
|
+
"port": 8765,
|
|
106
|
+
"privateIpOnly": false,
|
|
107
|
+
"time": "2026-01-01T00:00:00.000Z"
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### GET /api/project-list
|
|
114
|
+
Returns all git repos discovered one level deep under the projects root.
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
GET /api/project-list
|
|
118
|
+
x-agserver-key: <key>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"version": 1,
|
|
124
|
+
"title": "AGClient",
|
|
125
|
+
"groups": ["All Project", "AGProject"],
|
|
126
|
+
"projects": [
|
|
127
|
+
{
|
|
128
|
+
"id": "project-myrepo",
|
|
129
|
+
"name": "myrepo",
|
|
130
|
+
"path": "myrepo",
|
|
131
|
+
"summary": "fix: login bug (main)",
|
|
132
|
+
"status": "online",
|
|
133
|
+
"lastChange": "2026-01-01",
|
|
134
|
+
"group": "AGProject",
|
|
135
|
+
"unreadCount": 3,
|
|
136
|
+
"initials": "MY",
|
|
137
|
+
"avatarColor": "#2563EB"
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`status` is one of `"online"` | `"busy"` (uncommitted changes) | `"warning"` (merge conflicts).
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
### GET /api/project-branches?projectId=
|
|
148
|
+
Returns branches and tags for a project.
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
GET /api/project-branches?projectId=project-myrepo
|
|
152
|
+
x-agserver-key: <key>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"version": 1,
|
|
158
|
+
"projects": [{
|
|
159
|
+
"projectId": "project-myrepo",
|
|
160
|
+
"projectName": "myrepo",
|
|
161
|
+
"branches": [
|
|
162
|
+
{
|
|
163
|
+
"id": "branch-feat-login",
|
|
164
|
+
"name": "feat/login",
|
|
165
|
+
"brief": "Add login flow",
|
|
166
|
+
"tag": "FEAT",
|
|
167
|
+
"color": "#2563EB",
|
|
168
|
+
"icon": "alt-route",
|
|
169
|
+
"lastChange": "2 days ago",
|
|
170
|
+
"group": "active",
|
|
171
|
+
"commitCount": 4
|
|
172
|
+
}
|
|
173
|
+
],
|
|
174
|
+
"tags": [
|
|
175
|
+
{ "id": "tag-v1-0-0", "name": "v1.0.0", "lastChange": "3 weeks ago", "subject": "Release v1.0.0" }
|
|
176
|
+
]
|
|
177
|
+
}]
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`group` is `"active"` or `"stale"` based on `AGSERVER_ACTIVE_BRANCH_DAYS` (default 30).
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
### GET /api/branch-workspace?projectId=&branchId=
|
|
186
|
+
Returns the full workspace data for a branch — git graph, change summary, file browser config.
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
GET /api/branch-workspace?projectId=project-myrepo&branchId=branch-feat-login
|
|
190
|
+
x-agserver-key: <key>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"version": 1,
|
|
196
|
+
"workspaces": [{
|
|
197
|
+
"projectId": "project-myrepo",
|
|
198
|
+
"branchId": "branch-feat-login",
|
|
199
|
+
"tabs": {
|
|
200
|
+
"git": { "graph": { "selectedBranch": "feat/login", "baseBranch": "main", "script": "..." } },
|
|
201
|
+
"changes": { "chips": ["All 3", "Modified 2", "New 1", "Delete 0"], "tree": [...] },
|
|
202
|
+
"chat": { "mode": "text-placeholder" },
|
|
203
|
+
"files": { "rootPath": "", "initialDepth": 2 }
|
|
204
|
+
}
|
|
205
|
+
}]
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The `git.graph.script` is a `@gitgraph/js` imperative script — pass it to a `GitgraphJS.createGitgraph` container to render the graph.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
### GET /api/git/commit?projectId=&branchId=&commitId=
|
|
214
|
+
Returns commit metadata and its diff tree. Use `commitId=working-tree` for uncommitted changes.
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
GET /api/git/commit?projectId=project-myrepo&branchId=branch-feat-login&commitId=abc1234
|
|
218
|
+
x-agserver-key: <key>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"version": 1,
|
|
224
|
+
"projectId": "project-myrepo",
|
|
225
|
+
"branchId": "branch-feat-login",
|
|
226
|
+
"commit": {
|
|
227
|
+
"id": "abc1234",
|
|
228
|
+
"hash": "abc1234...",
|
|
229
|
+
"shortHash": "abc1234",
|
|
230
|
+
"subject": "fix: handle null user",
|
|
231
|
+
"author": "Jane",
|
|
232
|
+
"authoredAt": "2026-01-01",
|
|
233
|
+
"isWorkingTree": false
|
|
234
|
+
},
|
|
235
|
+
"changes": {
|
|
236
|
+
"chips": ["All 2", "Modified 2", "New 0", "Delete 0"],
|
|
237
|
+
"tree": [{ "id": "c1", "level": 0, "type": "file", "name": "auth.ts", "path": "src/auth.ts", "status": "M" }],
|
|
238
|
+
"patch": "diff --git ...",
|
|
239
|
+
"patchTruncated": false
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
### GET /api/git/commit-file?projectId=&branchId=&commitId=&path=
|
|
247
|
+
Returns the content of a specific file at a commit.
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
GET /api/git/commit-file?projectId=project-myrepo&branchId=branch-feat-login&commitId=abc1234&path=src/auth.ts
|
|
251
|
+
x-agserver-key: <key>
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
```json
|
|
255
|
+
{
|
|
256
|
+
"path": "src/auth.ts",
|
|
257
|
+
"content": "export function login() { ... }",
|
|
258
|
+
"size": 1024,
|
|
259
|
+
"binary": false,
|
|
260
|
+
"truncated": false
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
For images: `"image": true`, content is base64. For PDFs: `"pdf": true`, content is base64. For LFS pointers that can't be smudged: `"lfs": true`, content is empty.
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
### GET /api/git/commit-file-diff?projectId=&branchId=&commitId=&path=
|
|
269
|
+
Returns the unified diff for a single file at a commit.
|
|
270
|
+
|
|
271
|
+
```json
|
|
272
|
+
{ "diff": "--- a/src/auth.ts\n+++ b/src/auth.ts\n...", "truncated": false }
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
### GET /api/files/tree?projectId=&branchId=&path=&levels=
|
|
278
|
+
Browse the file tree. `path` defaults to repo root, `levels` is 1–4 (default 2).
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
GET /api/files/tree?projectId=project-myrepo&branchId=branch-feat-login&path=src&levels=2
|
|
282
|
+
x-agserver-key: <key>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
```json
|
|
286
|
+
{
|
|
287
|
+
"projectId": "project-myrepo",
|
|
288
|
+
"branchId": "branch-feat-login",
|
|
289
|
+
"path": "src",
|
|
290
|
+
"levels": 2,
|
|
291
|
+
"usingLiveFs": true,
|
|
292
|
+
"nodes": [
|
|
293
|
+
{ "name": "components", "path": "src/components", "type": "dir", "hasChildren": true },
|
|
294
|
+
{ "name": "auth.ts", "path": "src/auth.ts", "type": "file", "size": 1024, "ext": ".ts", "hasChildren": false }
|
|
295
|
+
]
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
`usingLiveFs: true` means the current checked-out branch is being served from disk. `false` means reading from git history.
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
### GET /api/files/content?projectId=&branchId=&path=
|
|
304
|
+
Returns the content of any file in the repo.
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
GET /api/files/content?projectId=project-myrepo&branchId=branch-feat-login&path=src/auth.ts
|
|
308
|
+
x-agserver-key: <key>
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Same response shape as `commit-file`.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
### GET /api/files/pdf?projectId=&branchId=&path=&commitId=
|
|
316
|
+
Returns raw `application/pdf` bytes. Use directly as a PDF source URL (include the key as a query param is not supported — use a proxy or fetch + blob URL on the client).
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
### POST /api/chat/stream
|
|
321
|
+
Streams an AI chat response via `kiro-cli`. Response is NDJSON — one JSON object per line.
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
POST /api/chat/stream
|
|
325
|
+
x-agserver-key: <key>
|
|
326
|
+
Content-Type: application/json
|
|
327
|
+
|
|
328
|
+
{
|
|
329
|
+
"context": {
|
|
330
|
+
"projectId": "project-myrepo",
|
|
331
|
+
"branchId": "branch-feat-login",
|
|
332
|
+
"filePath": "src/auth.ts",
|
|
333
|
+
"fileContent": "...",
|
|
334
|
+
"fileDiff": "..."
|
|
335
|
+
},
|
|
336
|
+
"messages": [
|
|
337
|
+
{ "role": "user", "content": "Why does login fail for null users?" }
|
|
338
|
+
],
|
|
339
|
+
"attachments": [
|
|
340
|
+
{ "path": "src/types.ts", "content": "..." }
|
|
341
|
+
]
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Response stream (each line is a JSON object):
|
|
346
|
+
```
|
|
347
|
+
{"delta":"The issue is"}
|
|
348
|
+
{"delta":" on line 42..."}
|
|
349
|
+
{"done":true,"credits":0.003,"elapsed":"1.2s"}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
On error:
|
|
353
|
+
```
|
|
354
|
+
{"error":"kiro-cli exited with code 1: ...","done":true}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Client reading example:
|
|
358
|
+
```js
|
|
359
|
+
const res = await fetch(`${BASE}/api/chat/stream`, {
|
|
360
|
+
method: 'POST',
|
|
361
|
+
headers: { 'x-agserver-key': KEY, 'Content-Type': 'application/json' },
|
|
362
|
+
body: JSON.stringify({ context, messages, attachments }),
|
|
363
|
+
});
|
|
364
|
+
const reader = res.body.getReader();
|
|
365
|
+
const decoder = new TextDecoder();
|
|
366
|
+
let buf = '';
|
|
367
|
+
while (true) {
|
|
368
|
+
const { done, value } = await reader.read();
|
|
369
|
+
if (done) break;
|
|
370
|
+
buf += decoder.decode(value, { stream: true });
|
|
371
|
+
const lines = buf.split('\n');
|
|
372
|
+
buf = lines.pop();
|
|
373
|
+
for (const line of lines) {
|
|
374
|
+
if (!line.trim()) continue;
|
|
375
|
+
const msg = JSON.parse(line);
|
|
376
|
+
if (msg.delta) process.stdout.write(msg.delta);
|
|
377
|
+
if (msg.done) console.log('\n[done]', msg);
|
|
378
|
+
if (msg.error) console.error('[error]', msg.error);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
### POST /api/chat/message
|
|
386
|
+
Same as `/api/chat/stream` but waits for the full response and returns it in one JSON object.
|
|
387
|
+
|
|
388
|
+
```json
|
|
389
|
+
{ "content": "The issue is on line 42...", "credits": 0.003, "elapsed": "1.2s" }
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
### POST /api/worktrees/dispose
|
|
395
|
+
Cleans up git worktrees created by chat sessions. Optionally scoped to one project.
|
|
396
|
+
|
|
397
|
+
```json
|
|
398
|
+
// request body (optional)
|
|
399
|
+
{ "projectId": "project-myrepo" }
|
|
400
|
+
|
|
401
|
+
// response
|
|
402
|
+
{ "disposed": { "project-myrepo": [{ "dir": "...", "branch": "feat/login", "pushed": false }] } }
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## Error responses
|
|
408
|
+
|
|
409
|
+
All errors follow the same shape:
|
|
410
|
+
|
|
411
|
+
```json
|
|
412
|
+
{ "error": "error_code", "message": "optional detail" }
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
| Code | HTTP | Meaning |
|
|
416
|
+
|---|---|---|
|
|
417
|
+
| `invalid_api_key` | 401 | Missing or wrong `x-agserver-key` |
|
|
418
|
+
| `forbidden_remote_ip` | 403 | IP blocked by `PRIVATE_IP_ONLY` |
|
|
419
|
+
| `not_found` | 404 | Unknown route |
|
|
420
|
+
| `project_not_found` | 404 | `projectId` doesn't match any repo |
|
|
421
|
+
| `branch_not_found` | 404 | `branchId` doesn't resolve |
|
|
422
|
+
| `commit_not_found` | 404 | `commitId` not in repo |
|
|
423
|
+
| `file_not_found` | 404 | Path not found at that ref |
|
|
424
|
+
| `missing_project_id` | 400 | Required query param missing |
|
|
425
|
+
| `path_outside_repo` | 400 | Path traversal attempt blocked |
|
|
426
|
+
| `path_blocked` | 400 | Path points to an ignored directory |
|
|
427
|
+
| `server_error` | 500 | Unexpected internal error |
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## Environment variables
|
|
432
|
+
|
|
433
|
+
| Variable | Default | Description |
|
|
434
|
+
|---|---|---|
|
|
435
|
+
| `AGSERVER_PORT` | `8765` | Listen port |
|
|
436
|
+
| `AGSERVER_HOST` | `127.0.0.1` | Listen host |
|
|
437
|
+
| `AGSERVER_API_KEY` | auto-generated | Auth key (min 24 chars) |
|
|
438
|
+
| `AGSERVER_PROJECTS_ROOT` | cwd | Root folder scanned for git repos |
|
|
439
|
+
| `AGSERVER_ALLOW_WEAK_KEY` | `0` | Set `1` to allow short keys in dev |
|
|
440
|
+
| `AGSERVER_PRIVATE_IP_ONLY` | `0` | Set `1` to restrict to LAN subnet |
|
|
441
|
+
| `AGSERVER_ACTIVE_BRANCH_DAYS` | `30` | Days before a branch is considered stale |
|
|
442
|
+
| `AGSERVER_KIRO_CLI` | `kiro-cli` | Path to kiro-cli binary |
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Security note
|
|
447
|
+
|
|
448
|
+
The chat endpoints (`/api/chat/*`) give an AI agent full tool access to your machine via `kiro-cli --trust-all-tools`. Treat your API key like a root password — never expose it in client-side code or commit it to source control.
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agserver",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "AI-assisted git project browser server — run from any folder like http-server",
|
|
6
|
+
"main": "server/index.mjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"agserver": "server/cli.mjs"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18.0.0"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"start": "node server/cli.mjs",
|
|
15
|
+
"dev": "node --watch server/index.mjs"
|
|
16
|
+
},
|
|
17
|
+
"keywords": ["git", "kiro", "ai", "project-browser", "agserver"],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"dependencies": {}
|
|
20
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# AGServer — API Contract
|
|
2
|
+
|
|
3
|
+
## Versioning
|
|
4
|
+
|
|
5
|
+
API version: `1.0.0` (semver)
|
|
6
|
+
|
|
7
|
+
The server advertises its version in every response via the `x-agserver-version` header.
|
|
8
|
+
The `/health` endpoint also returns `apiVersion` in the JSON body.
|
|
9
|
+
|
|
10
|
+
Compatibility rule: client and server are compatible if they share the same **major** version.
|
|
11
|
+
The client should warn (but not block) on minor/patch mismatches.
|
|
12
|
+
|
|
13
|
+
## Auth
|
|
14
|
+
|
|
15
|
+
All `/api/*` endpoints require the header:
|
|
16
|
+
```
|
|
17
|
+
x-agserver-key: <AGSERVER_API_KEY>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Endpoints
|
|
21
|
+
|
|
22
|
+
### GET /health
|
|
23
|
+
No auth required.
|
|
24
|
+
```json
|
|
25
|
+
{ "ok": true, "apiVersion": "1.0.0", "host": "...", "port": 8765, "time": "..." }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### GET /api/project-list
|
|
29
|
+
Returns all git repos auto-discovered under `AGSERVER_PROJECTS_ROOT`.
|
|
30
|
+
```json
|
|
31
|
+
{ "version": 1, "title": "AGClient", "groups": ["All Project", "AGProject"], "projects": [...] }
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### GET /api/project-branches?projectId=
|
|
35
|
+
```json
|
|
36
|
+
{ "version": 1, "projects": [{ "projectId", "projectName", "branches": [...], "tags": [...] }] }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### GET /api/branch-workspace?projectId=&branchId=
|
|
40
|
+
```json
|
|
41
|
+
{ "version": 1, "workspaces": [{ "projectId", "branchId", "tabs": { "git", "changes", "chat", "files" } }] }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### GET /api/git/commit?projectId=&branchId=&commitId=
|
|
45
|
+
```json
|
|
46
|
+
{ "version": 1, "projectId", "branchId", "commit": { "id", "hash", "shortHash", "subject", "author", "authoredAt", "isWorkingTree" }, "changes": { "chips", "tree", "patch", "patchTruncated" } }
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### GET /api/git/commit-file?projectId=&branchId=&commitId=&path=
|
|
50
|
+
```json
|
|
51
|
+
{ "path", "content", "size", "binary", "truncated", "image?", "pdf?", "lfs?" }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### GET /api/git/commit-file-diff?projectId=&branchId=&commitId=&path=
|
|
55
|
+
```json
|
|
56
|
+
{ "diff": "...", "truncated": false }
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### GET /api/files/tree?projectId=&branchId=&path=&levels=
|
|
60
|
+
```json
|
|
61
|
+
{ "projectId", "branchId", "path", "levels", "nodes": [{ "name", "path", "type", "hasChildren", "size?", "ext?", "children?" }] }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### GET /api/files/content?projectId=&branchId=&path=
|
|
65
|
+
```json
|
|
66
|
+
{ "path", "content", "size", "binary", "truncated", "image?", "pdf?", "lfs?" }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### GET /api/files/pdf?projectId=&branchId=&path=&commitId=
|
|
70
|
+
Returns raw `application/pdf` bytes.
|
|
71
|
+
|
|
72
|
+
### POST /api/chat/stream
|
|
73
|
+
NDJSON stream. Each line: `{ "delta": "..." }` or `{ "done": true, "credits?", "elapsed?" }` or `{ "error": "..." }`.
|
|
74
|
+
|
|
75
|
+
### POST /api/chat/message
|
|
76
|
+
```json
|
|
77
|
+
{ "content": "...", "credits?", "elapsed?" }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### POST /api/worktrees/dispose
|
|
81
|
+
```json
|
|
82
|
+
{ "disposed": { "<projectId>": [...] } }
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### GET /api/project-settings?projectId=
|
|
86
|
+
```json
|
|
87
|
+
{ "projectId": "...", "settings": { "worktreeEnabled": false } }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### POST /api/project-settings?projectId=
|
|
91
|
+
Body: `{ "worktreeEnabled": true }`
|
|
92
|
+
```json
|
|
93
|
+
{ "projectId": "...", "settings": { "worktreeEnabled": true } }
|
|
94
|
+
```
|
|
95
|
+
Settings are persisted in `<AGSERVER_PROJECTS_ROOT>/.agserver-settings.json`.
|
|
96
|
+
By default `worktreeEnabled` is `false` — kiro-cli runs directly in the repo directory.
|
|
97
|
+
Set to `true` to enable the git worktree isolation model for that project.
|
|
98
|
+
|
|
99
|
+
## Environment Variables
|
|
100
|
+
|
|
101
|
+
| Variable | Default | Description |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| `AGSERVER_PORT` | `8765` | Listen port |
|
|
104
|
+
| `AGSERVER_HOST` | `127.0.0.1` | Listen host |
|
|
105
|
+
| `AGSERVER_API_KEY` | auto-generated | Auth key |
|
|
106
|
+
| `AGSERVER_PROJECTS_ROOT` | `/Volumes/Agentic/AGProject` | Root dir scanned for git repos |
|
|
107
|
+
| `AGSERVER_ALLOW_WEAK_KEY` | `0` | Set `1` for dev |
|
|
108
|
+
| `AGSERVER_PRIVATE_IP_ONLY` | `0` | Restrict to LAN IPs |
|
|
109
|
+
| `AGSERVER_ACTIVE_BRANCH_DAYS` | `30` | Days threshold for active/stale branches |
|
package/server/auth.mjs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
|
|
3
|
+
export function normalizeIp(ip) {
|
|
4
|
+
if (!ip) return '';
|
|
5
|
+
if (ip.startsWith('::ffff:')) return ip.slice(7);
|
|
6
|
+
if (ip === '::1') return '127.0.0.1';
|
|
7
|
+
return ip;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isPrivateIp(ip) {
|
|
11
|
+
const n = normalizeIp(ip);
|
|
12
|
+
if (!n) return false;
|
|
13
|
+
if (n === '127.0.0.1') return true;
|
|
14
|
+
if (n.startsWith('10.')) return true;
|
|
15
|
+
if (n.startsWith('192.168.')) return true;
|
|
16
|
+
if (n.startsWith('169.254.')) return true;
|
|
17
|
+
if (n.startsWith('172.')) {
|
|
18
|
+
const second = Number(n.split('.')[1] || '0');
|
|
19
|
+
return second >= 16 && second <= 31;
|
|
20
|
+
}
|
|
21
|
+
if (n === '::1') return true;
|
|
22
|
+
if (n.startsWith('fe80:') || n.startsWith('fd') || n.startsWith('fc')) return true;
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function listLanIps() {
|
|
27
|
+
const interfaces = os.networkInterfaces();
|
|
28
|
+
const ips = [];
|
|
29
|
+
Object.values(interfaces).forEach((entries) => {
|
|
30
|
+
(entries || []).forEach((entry) => {
|
|
31
|
+
if (entry.family !== 'IPv4' || entry.internal) return;
|
|
32
|
+
ips.push(entry.address);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
return ips;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sameSubnet24(a, b) {
|
|
39
|
+
const aa = a.split('.');
|
|
40
|
+
const bb = b.split('.');
|
|
41
|
+
if (aa.length !== 4 || bb.length !== 4) return false;
|
|
42
|
+
return aa[0] === bb[0] && aa[1] === bb[1] && aa[2] === bb[2];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isAllowedRemoteIp(ip) {
|
|
46
|
+
const n = normalizeIp(ip);
|
|
47
|
+
if (n === '127.0.0.1') return true;
|
|
48
|
+
if (!isPrivateIp(n)) return false;
|
|
49
|
+
const localIps = listLanIps();
|
|
50
|
+
if (localIps.length === 0) return true;
|
|
51
|
+
if (n.includes(':')) return true;
|
|
52
|
+
return localIps.some((localIp) => sameSubnet24(localIp, n));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function isWeakApiKey(value) {
|
|
56
|
+
return !value || value.length < 24 || value === 'chatagent-local-dev-key';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function assertAuthorized(req, res, { API_KEY, PRIVATE_IP_ONLY, jsonFn }) {
|
|
60
|
+
const remote = normalizeIp(req.socket.remoteAddress || '');
|
|
61
|
+
if (PRIVATE_IP_ONLY && !isAllowedRemoteIp(remote)) {
|
|
62
|
+
jsonFn(res, 403, { error: 'forbidden_remote_ip', message: 'Request blocked: only local/private subnet clients are allowed.' });
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
const key = req.headers['x-agserver-key'];
|
|
66
|
+
if (key !== API_KEY) {
|
|
67
|
+
console.log(`[auth-fail] key=${key ? 'present' : 'missing'} from=${req.socket.remoteAddress} url=${req.url}`);
|
|
68
|
+
jsonFn(res, 401, { error: 'invalid_api_key', message: 'Missing or invalid API key.' });
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
}
|