aimeat 1.20.6 → 1.22.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/dist/.env.example +9 -0
- package/dist/locales/en.json +17 -1
- package/dist/locales/fi.json +17 -3
- package/dist/public/css/views/profile.css +55 -0
- package/dist/public/js/services/memory.js +9 -2
- package/dist/public/views/profile/landing-page.js +16 -1
- package/dist/public/views/profile/organisms-tab.js +456 -220
- package/dist/src/cli/connect/api-client.d.ts +24 -0
- package/dist/src/cli/connect/api-client.d.ts.map +1 -1
- package/dist/src/cli/connect/api-client.js +29 -33
- package/dist/src/cli/connect/api-client.js.map +1 -1
- package/dist/src/cli/connect/config.d.ts.map +1 -1
- package/dist/src/cli/connect/config.js +4 -1
- package/dist/src/cli/connect/config.js.map +1 -1
- package/dist/src/cli/connect/mcp/local-server.d.ts +25 -0
- package/dist/src/cli/connect/mcp/local-server.d.ts.map +1 -0
- package/dist/src/cli/connect/mcp/local-server.js +411 -0
- package/dist/src/cli/connect/mcp/local-server.js.map +1 -0
- package/dist/src/cli/connect/mcp/poller.d.ts +13 -0
- package/dist/src/cli/connect/mcp/poller.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/poller.js +3 -2
- package/dist/src/cli/connect/mcp/poller.js.map +1 -1
- package/dist/src/cli/connect/mcp/server.d.ts +41 -0
- package/dist/src/cli/connect/mcp/server.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/server.js +47 -21
- package/dist/src/cli/connect/mcp/server.js.map +1 -1
- package/dist/src/cli/connect/tunnel-client.d.ts +109 -0
- package/dist/src/cli/connect/tunnel-client.d.ts.map +1 -0
- package/dist/src/cli/connect/tunnel-client.js +462 -0
- package/dist/src/cli/connect/tunnel-client.js.map +1 -0
- package/dist/src/config.d.ts +4 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +9 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/generated/api-types.d.ts +139 -0
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/index.js +72 -7
- package/dist/src/index.js.map +1 -1
- package/dist/src/models/schemas.d.ts +1 -1
- package/dist/src/routes/agent-tasks.d.ts.map +1 -1
- package/dist/src/routes/agent-tasks.js +6 -1
- package/dist/src/routes/agent-tasks.js.map +1 -1
- package/dist/src/routes/connect-tunnel.d.ts +17 -0
- package/dist/src/routes/connect-tunnel.d.ts.map +1 -0
- package/dist/src/routes/connect-tunnel.js +31 -0
- package/dist/src/routes/connect-tunnel.js.map +1 -0
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +5 -0
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/server-bootstrap/service-init.d.ts +2 -0
- package/dist/src/server-bootstrap/service-init.d.ts.map +1 -1
- package/dist/src/server-bootstrap/service-init.js +11 -0
- package/dist/src/server-bootstrap/service-init.js.map +1 -1
- package/dist/src/server.d.ts +2 -0
- package/dist/src/server.d.ts.map +1 -1
- package/dist/src/server.js +1 -0
- package/dist/src/server.js.map +1 -1
- package/dist/src/services/config-schema.d.ts.map +1 -1
- package/dist/src/services/config-schema.js +5 -0
- package/dist/src/services/config-schema.js.map +1 -1
- package/dist/src/services/connect-tunnel.d.ts +123 -0
- package/dist/src/services/connect-tunnel.d.ts.map +1 -0
- package/dist/src/services/connect-tunnel.js +403 -0
- package/dist/src/services/connect-tunnel.js.map +1 -0
- package/dist/src/services/event-bus.d.ts +14 -0
- package/dist/src/services/event-bus.d.ts.map +1 -1
- package/dist/src/services/event-bus.js +25 -0
- package/dist/src/services/event-bus.js.map +1 -1
- package/package.json +1 -1
|
@@ -6,12 +6,36 @@ export interface ApiResponse {
|
|
|
6
6
|
message: string;
|
|
7
7
|
};
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Pluggable request transport. `path` is always node-relative (`/v1/...`);
|
|
11
|
+
* absolute URLs never reach a transport (they go direct — see `send()`).
|
|
12
|
+
* Returns the HTTP status and the parsed (envelope) body.
|
|
13
|
+
*/
|
|
14
|
+
export interface Transport {
|
|
15
|
+
request(method: string, path: string, opts?: {
|
|
16
|
+
body?: unknown;
|
|
17
|
+
query?: Record<string, string>;
|
|
18
|
+
}): Promise<{
|
|
19
|
+
status: number;
|
|
20
|
+
body: unknown;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
9
23
|
export declare class AimeatClient {
|
|
10
24
|
private baseUrl;
|
|
11
25
|
private token;
|
|
26
|
+
private transport;
|
|
12
27
|
constructor(baseUrl: string, token?: string);
|
|
13
28
|
static fromConfig(): Promise<AimeatClient>;
|
|
29
|
+
/** Route subsequent requests through `t` (e.g. the tunnel). `null` restores direct fetch. */
|
|
30
|
+
setTransport(t: Transport | null): void;
|
|
31
|
+
hasTransport(): boolean;
|
|
14
32
|
private headers;
|
|
33
|
+
/**
|
|
34
|
+
* Single dispatch point for every verb. Node-relative paths go through the
|
|
35
|
+
* transport when one is set; absolute URLs (presigned uploads etc.) and the
|
|
36
|
+
* default case use direct fetch with `Connection: close`.
|
|
37
|
+
*/
|
|
38
|
+
private send;
|
|
15
39
|
get(path: string): Promise<ApiResponse>;
|
|
16
40
|
post(path: string, body?: unknown): Promise<ApiResponse>;
|
|
17
41
|
put(path: string, body?: unknown): Promise<ApiResponse>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../../../../src/cli/connect/api-client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../../../../src/cli/connect/api-client.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3C;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAC9I;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,SAAS,CAA0B;gBAE/B,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM;WAK9B,UAAU,IAAI,OAAO,CAAC,YAAY,CAAC;IAQhD,6FAA6F;IAC7F,YAAY,CAAC,CAAC,EAAE,SAAS,GAAG,IAAI,GAAG,IAAI;IACvC,YAAY,IAAI,OAAO;IAEvB,OAAO,CAAC,OAAO;IAMf;;;;OAIG;YACW,IAAI;IAcZ,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IACvC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC;IACxD,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC;IACvD,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC;IACzD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAEhD,UAAU,IAAI,MAAM;IACpB,aAAa,IAAI,MAAM,GAAG,IAAI;IAC9B,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;CAC1B"}
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file api-client.ts
|
|
3
3
|
* @description HTTP client for AIMEAT node API with automatic auth header injection.
|
|
4
|
-
* @structure Wraps fetch, response-envelope handling, and stored-token based client
|
|
4
|
+
* @structure Wraps fetch, response-envelope handling, and stored-token based client
|
|
5
|
+
* construction. `Transport` is the pluggable seam: the default transport does a
|
|
6
|
+
* direct `fetch` with `Connection: close` (one-shot CLI behavior); the serve
|
|
7
|
+
* daemon swaps in a tunnel transport (`ConnectTunnelClient.forward`) so every
|
|
8
|
+
* MCP tool call flows over the single persistent WS without per-tool changes.
|
|
5
9
|
* @usage Imported by `aimeat connect` subcommands and MCP tools.
|
|
6
|
-
* @version-history
|
|
10
|
+
* @version-history
|
|
11
|
+
* v1.9.4 — 2026-05-28 — Update connector guidance and close one-shot CLI HTTP connections.
|
|
12
|
+
* v2.0.0 — 2026-06-10 — Phase 4: Transport seam (direct fetch default, tunnel override).
|
|
7
13
|
*/
|
|
8
14
|
import { getToken } from './keychain.js';
|
|
9
15
|
import { loadConfig } from './config.js';
|
|
10
16
|
export class AimeatClient {
|
|
11
17
|
baseUrl;
|
|
12
18
|
token = null;
|
|
19
|
+
transport = null;
|
|
13
20
|
constructor(baseUrl, token) {
|
|
14
21
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
15
22
|
this.token = token ?? null;
|
|
@@ -23,49 +30,38 @@ export class AimeatClient {
|
|
|
23
30
|
throw new Error('No stored token. Run: npx aimeat connect');
|
|
24
31
|
return new AimeatClient(config.node_url, token);
|
|
25
32
|
}
|
|
33
|
+
/** Route subsequent requests through `t` (e.g. the tunnel). `null` restores direct fetch. */
|
|
34
|
+
setTransport(t) { this.transport = t; }
|
|
35
|
+
hasTransport() { return this.transport !== null; }
|
|
26
36
|
headers() {
|
|
27
37
|
const h = { 'Content-Type': 'application/json', Connection: 'close' };
|
|
28
38
|
if (this.token)
|
|
29
39
|
h['Authorization'] = `Bearer ${this.token}`;
|
|
30
40
|
return h;
|
|
31
41
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
async
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
43
|
-
});
|
|
44
|
-
return res.json();
|
|
45
|
-
}
|
|
46
|
-
async put(path, body) {
|
|
42
|
+
/**
|
|
43
|
+
* Single dispatch point for every verb. Node-relative paths go through the
|
|
44
|
+
* transport when one is set; absolute URLs (presigned uploads etc.) and the
|
|
45
|
+
* default case use direct fetch with `Connection: close`.
|
|
46
|
+
*/
|
|
47
|
+
async send(method, path, body) {
|
|
48
|
+
if (this.transport && !path.startsWith('http')) {
|
|
49
|
+
const r = await this.transport.request(method, path, { body });
|
|
50
|
+
return r.body;
|
|
51
|
+
}
|
|
47
52
|
const url = path.startsWith('http') ? path : `${this.baseUrl}${path}`;
|
|
48
53
|
const res = await fetch(url, {
|
|
49
|
-
method
|
|
54
|
+
method,
|
|
50
55
|
headers: this.headers(),
|
|
51
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
56
|
+
body: body !== undefined && body !== null ? JSON.stringify(body) : undefined,
|
|
52
57
|
});
|
|
53
58
|
return res.json();
|
|
54
59
|
}
|
|
55
|
-
async
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
61
|
-
});
|
|
62
|
-
return res.json();
|
|
63
|
-
}
|
|
64
|
-
async delete(path) {
|
|
65
|
-
const url = path.startsWith('http') ? path : `${this.baseUrl}${path}`;
|
|
66
|
-
const res = await fetch(url, { method: 'DELETE', headers: this.headers() });
|
|
67
|
-
return res.json();
|
|
68
|
-
}
|
|
60
|
+
async get(path) { return this.send('GET', path); }
|
|
61
|
+
async post(path, body) { return this.send('POST', path, body); }
|
|
62
|
+
async put(path, body) { return this.send('PUT', path, body); }
|
|
63
|
+
async patch(path, body) { return this.send('PATCH', path, body); }
|
|
64
|
+
async delete(path) { return this.send('DELETE', path); }
|
|
69
65
|
getBaseUrl() { return this.baseUrl; }
|
|
70
66
|
getTokenValue() { return this.token; }
|
|
71
67
|
setToken(t) { this.token = t; }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-client.js","sourceRoot":"","sources":["../../../../src/cli/connect/api-client.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"api-client.js","sourceRoot":"","sources":["../../../../src/cli/connect/api-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../../src/cli/connect/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../../src/cli/connect/config.ts"],"names":[],"mappings":"AAsCA,wBAAgB,YAAY,IAAI,MAAM,CAAuB;AAI7D,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IAKd,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,eAAe,GAAG,eAAe,GAAG,cAAc,GAAG,cAAc,CAAC;CAChF;AAED,wBAAgB,UAAU,IAAI,mBAAmB,GAAG,IAAI,CAIvD;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAG5D;AAID,MAAM,WAAW,oBAAoB;IACnC,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;;;OASG;IACH,IAAI,CAAC,EAAE,YAAY,GAAG,aAAa,GAAG,aAAa,GAAG,aAAa,GAAG,aAAa,CAAC;IACpF,wDAAwD;IACxD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,8EAA8E;IAC9E,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,iEAAiE;IACjE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,cAAc,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IACnC,yGAAyG;IACzG,UAAU,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;IAChC,wDAAwD;IACxD,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAMD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI,CAQ7E;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,oBAAoB,GAAG,IAAI,CAIpF;AAID,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,oBAAoB,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC,CAyB5D;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAQhG"}
|
|
@@ -23,13 +23,16 @@
|
|
|
23
23
|
* v1.0.0 -- original single-agent config
|
|
24
24
|
* v1.1.0 -- 2026-05-28 -- Security warning on wake.command (executes via exec)
|
|
25
25
|
* v2.0.0 -- 2026-05-29 -- Per-agent config layout + runner block + loadAllAgents
|
|
26
|
+
* v2.1.0 -- 2026-06-10 -- AIMEAT_HOME env override for the connector home dir
|
|
26
27
|
*/
|
|
27
28
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
28
29
|
import { join } from 'node:path';
|
|
29
30
|
import { homedir } from 'node:os';
|
|
30
31
|
import { parse, stringify } from 'yaml';
|
|
31
32
|
import { listAllTokens } from './keychain.js';
|
|
32
|
-
|
|
33
|
+
// AIMEAT_HOME overrides the connector home (~/.aimeat) — used by tests to run
|
|
34
|
+
// against a throwaway directory and by operators running isolated daemons.
|
|
35
|
+
const CONFIG_DIR = process.env.AIMEAT_HOME ?? join(homedir(), '.aimeat');
|
|
33
36
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.yaml');
|
|
34
37
|
export function getConfigDir() { return CONFIG_DIR; }
|
|
35
38
|
export function loadConfig() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../../src/cli/connect/config.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../../src/cli/connect/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C,8EAA8E;AAC9E,2EAA2E;AAC3E,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC,CAAC;AACzE,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;AAEpD,MAAM,UAAU,YAAY,KAAa,OAAO,UAAU,CAAC,CAAC,CAAC;AAsB7D,MAAM,UAAU,UAAU;IACxB,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,MAAM,GAAG,GAAG,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAC/C,OAAO,KAAK,CAAC,GAAG,CAAwB,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,MAA2B;IACpD,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACxE,aAAa,CAAC,WAAW,EAAE,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC;AACzD,CAAC;AA6ED,SAAS,WAAW,CAAC,KAAa;IAChC,OAAO,IAAI,CAAC,UAAU,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,OAAO,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,MAAM,CAAC,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;IACpC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAChC,IAAI,CAAC;QACH,OAAO,KAAK,CAAC,YAAY,CAAC,CAAC,EAAE,OAAO,CAAC,CAAyB,CAAC;IACjE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAa,EAAE,MAA4B;IAC5E,MAAM,GAAG,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAC/B,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1D,aAAa,CAAC,kBAAkB,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC;AACvE,CAAC;AAWD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,MAAM,WAAW,GAAG,MAAM,aAAa,EAAE,CAAC;IAC1C,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAExC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,GAAG,GAAkB,EAAE,CAAC;IAE9B,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAC/B,IAAI,QAAQ,GAAG,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9C,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,0EAA0E;YAC1E,2EAA2E;YAC3E,MAAM,SAAS,GAAG,MAAM,IAAI,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,CAAC;YACvF,QAAQ,GAAG;gBACT,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,QAAQ,EAAE,SAAS,IAAI,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,mBAAmB;gBAC/E,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,SAAS;gBAC1C,aAAa,EAAE,SAAS,CAAC,CAAC,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,SAAS;aAC7D,CAAC;QACJ,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC1F,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,KAAa,EAAE,KAAc;IACjE,MAAM,GAAG,GAAG,MAAM,aAAa,EAAE,CAAC;IAClC,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC;IACpF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,UAAU,KAAK,mCAAmC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAC;IAC5I,CAAC;IACD,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { AgentRegistry } from '../agent-registry.js';
|
|
3
|
+
export declare const SERVE_DISCOVERY_SCHEMA_VERSION = 1;
|
|
4
|
+
export interface ServeDiscoveryAgent {
|
|
5
|
+
agent: string;
|
|
6
|
+
owner: string;
|
|
7
|
+
node_url: string;
|
|
8
|
+
/** How this agent's API calls reach the node right now. */
|
|
9
|
+
transport: 'tunnel' | 'direct' | 'auth_failed';
|
|
10
|
+
}
|
|
11
|
+
export interface ServeDiscovery {
|
|
12
|
+
schema_version: number;
|
|
13
|
+
port: number;
|
|
14
|
+
pid: number;
|
|
15
|
+
agents: ServeDiscoveryAgent[];
|
|
16
|
+
started_at: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function serveDiscoveryPath(): string;
|
|
19
|
+
export interface ServeDaemonOptions {
|
|
20
|
+
registry: AgentRegistry;
|
|
21
|
+
/** Fresh, fully tool-registered MCP server (one per Streamable-HTTP session). */
|
|
22
|
+
buildMcp: () => McpServer;
|
|
23
|
+
}
|
|
24
|
+
export declare function runServeDaemon(opts: ServeDaemonOptions): Promise<void>;
|
|
25
|
+
//# sourceMappingURL=local-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-server.d.ts","sourceRoot":"","sources":["../../../../../src/cli/connect/mcp/local-server.ts"],"names":[],"mappings":"AA0CA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE,OAAO,KAAK,EAAE,aAAa,EAAmB,MAAM,sBAAsB,CAAC;AAK3E,eAAO,MAAM,8BAA8B,IAAI,CAAC;AAEhD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,2DAA2D;IAC3D,SAAS,EAAE,QAAQ,GAAG,QAAQ,GAAG,aAAa,CAAC;CAChD;AAED,MAAM,WAAW,cAAc;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAqFD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,aAAa,CAAC;IACxB,iFAAiF;IACjF,QAAQ,EAAE,MAAM,SAAS,CAAC;CAC3B;AAQD,wBAAsB,cAAc,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoQ5E"}
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file local-server.ts
|
|
3
|
+
* @description Loopback serve daemon for `aimeat connect serve --http` (Phase 4
|
|
4
|
+
* of the Connector Forward Tunnel). Holds ONE persistent tunnel WS per agent
|
|
5
|
+
* to its AIMEAT node and exposes everything a local crew needs on
|
|
6
|
+
* `127.0.0.1:<ephemeral>`:
|
|
7
|
+
* - `/v1/mcp` — local Streamable-HTTP MCP endpoint (fresh McpServer per
|
|
8
|
+
* session, same tool surface as stdio serve; no auth — the loopback bind
|
|
9
|
+
* is the trust boundary).
|
|
10
|
+
* - `ALL /v1/*` — REST proxy: method/path/query/body forwarded over the
|
|
11
|
+
* agent's tunnel (`X-Aimeat-Agent` header picks the agent; defaults to
|
|
12
|
+
* the registry's primary). Falls back to direct HTTP when degraded.
|
|
13
|
+
* - `GET /local/tasks/next?wait=ms[&agent=name]` — long-poll fed by the
|
|
14
|
+
* tunnel's `deliver`/`backlog` frames: realtime task delivery for
|
|
15
|
+
* synchronous clients, no upstream polling.
|
|
16
|
+
* - `GET /local/status`, `POST /local/shutdown` — introspection + clean stop.
|
|
17
|
+
* Writes the discovery file `<AIMEAT_HOME>/serve.json` (schema_version, port,
|
|
18
|
+
* pid, agents, started_at) atomically on start, removes it on clean exit, and
|
|
19
|
+
* stale-detects a previous daemon by pid.
|
|
20
|
+
*
|
|
21
|
+
* Graceful degradation: if an agent's node has the tunnel disabled / too old
|
|
22
|
+
* (`unsupported`/`unreachable`), that agent keeps the direct-fetch transport
|
|
23
|
+
* and the legacy upstream poll loop. `auth_failed` surfaces the
|
|
24
|
+
* "Run: aimeat connect" guidance and starts nothing (a dead token must not
|
|
25
|
+
* hot-loop). With the tunnel online there is NO upstream poll: `deliver`/
|
|
26
|
+
* `backlog` drive the same wake adapter + task-runner hook the poller used.
|
|
27
|
+
* @structure
|
|
28
|
+
* - AgentChannel — per-agent push state: task queue + long-poll waiters,
|
|
29
|
+
* seen-id dedup (live deliver vs backlog), wake/task-runner dispatch.
|
|
30
|
+
* - runServeDaemon() — tunnel startup per agent, Express loopback server,
|
|
31
|
+
* discovery-file lifecycle, signal handling.
|
|
32
|
+
* @usage Called by mcp/server.ts `runServe()` when `--http`/`--daemon` is set.
|
|
33
|
+
* @version-history
|
|
34
|
+
* v1.0.0 — 2026-06-10 — Phase 4: initial loopback daemon (local MCP + REST
|
|
35
|
+
* proxy + long-poll push surface + discovery file + degraded fallback).
|
|
36
|
+
*/
|
|
37
|
+
import express from 'express';
|
|
38
|
+
import { randomBytes } from 'node:crypto';
|
|
39
|
+
import { writeFileSync, renameSync, existsSync, readFileSync, unlinkSync, mkdirSync } from 'node:fs';
|
|
40
|
+
import { join } from 'node:path';
|
|
41
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
42
|
+
import { ConnectTunnelClient } from '../tunnel-client.js';
|
|
43
|
+
import { getToken } from '../keychain.js';
|
|
44
|
+
import { getConfigDir } from '../config.js';
|
|
45
|
+
import { wakeAgent } from './wakeup.js';
|
|
46
|
+
import { startPollerForAgent, legacyWakeAdapter } from './poller.js';
|
|
47
|
+
import { launchTaskRunner, isRunner } from '../task-runner.js';
|
|
48
|
+
export const SERVE_DISCOVERY_SCHEMA_VERSION = 1;
|
|
49
|
+
export function serveDiscoveryPath() {
|
|
50
|
+
return join(getConfigDir(), 'serve.json');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Per-agent push state. Fed by the tunnel's `deliver` (live) and `backlog`
|
|
54
|
+
* (on-connect snapshot) frames; tasks are deduped by id across both sources so
|
|
55
|
+
* a live-pushed-then-backlogged task fires the wake/runner/long-poll exactly
|
|
56
|
+
* once per daemon lifetime. (Storage stays the source of truth — a consumer
|
|
57
|
+
* that missed a long-poll window can always list tasks via the REST proxy.)
|
|
58
|
+
*/
|
|
59
|
+
class AgentChannel {
|
|
60
|
+
entry;
|
|
61
|
+
transportMode = 'direct';
|
|
62
|
+
tunnel = null;
|
|
63
|
+
seenTaskIds = new Set();
|
|
64
|
+
seenMessageIds = new Set();
|
|
65
|
+
queue = [];
|
|
66
|
+
waiters = [];
|
|
67
|
+
constructor(entry) {
|
|
68
|
+
this.entry = entry;
|
|
69
|
+
}
|
|
70
|
+
handleTask(payload, via) {
|
|
71
|
+
const task = payload;
|
|
72
|
+
const id = typeof task?.id === 'string' ? task.id : null;
|
|
73
|
+
if (!task || !id || this.seenTaskIds.has(id))
|
|
74
|
+
return;
|
|
75
|
+
this.seenTaskIds.add(id);
|
|
76
|
+
// Same side effects the poll loop used to produce on a new queued task.
|
|
77
|
+
void wakeAgent(legacyWakeAdapter(this.entry), 'task_new', `task ${id} via ${via}`);
|
|
78
|
+
if (isRunner(this.entry)) {
|
|
79
|
+
launchTaskRunner(this.entry, {
|
|
80
|
+
id,
|
|
81
|
+
title: typeof task.title === 'string' ? task.title : undefined,
|
|
82
|
+
description: typeof task.description === 'string' ? task.description : undefined,
|
|
83
|
+
}).catch(err => {
|
|
84
|
+
console.error(`[serve:${this.entry.agent}] runner launch failed for ${id}: ${err.message}`);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const item = { task, via, receivedAt: new Date().toISOString() };
|
|
88
|
+
const waiter = this.waiters.shift();
|
|
89
|
+
if (waiter)
|
|
90
|
+
waiter(item);
|
|
91
|
+
else
|
|
92
|
+
this.queue.push(item);
|
|
93
|
+
}
|
|
94
|
+
handleMessages(messages) {
|
|
95
|
+
let fresh = 0;
|
|
96
|
+
for (const m of messages) {
|
|
97
|
+
const id = typeof m?.id === 'string' ? m.id : null;
|
|
98
|
+
if (!id || this.seenMessageIds.has(id))
|
|
99
|
+
continue;
|
|
100
|
+
this.seenMessageIds.add(id);
|
|
101
|
+
fresh++;
|
|
102
|
+
}
|
|
103
|
+
if (fresh > 0) {
|
|
104
|
+
void wakeAgent(legacyWakeAdapter(this.entry), 'message_new', `${fresh} new message(s)`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Long-poll: next undelivered task, or null after `waitMs` with none. */
|
|
108
|
+
nextTask(waitMs) {
|
|
109
|
+
const queued = this.queue.shift();
|
|
110
|
+
if (queued)
|
|
111
|
+
return Promise.resolve(queued);
|
|
112
|
+
if (waitMs <= 0)
|
|
113
|
+
return Promise.resolve(null);
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
const waiter = (item) => { clearTimeout(timer); resolve(item); };
|
|
116
|
+
const timer = setTimeout(() => {
|
|
117
|
+
const i = this.waiters.indexOf(waiter);
|
|
118
|
+
if (i >= 0)
|
|
119
|
+
this.waiters.splice(i, 1);
|
|
120
|
+
resolve(null);
|
|
121
|
+
}, waitMs);
|
|
122
|
+
this.waiters.push(waiter);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
drainWaiters() {
|
|
126
|
+
for (const w of this.waiters.splice(0))
|
|
127
|
+
w(null);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Is the pid recorded in an existing discovery file still alive? */
|
|
131
|
+
function pidAlive(pid) {
|
|
132
|
+
try {
|
|
133
|
+
process.kill(pid, 0);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
return err.code === 'EPERM';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export async function runServeDaemon(opts) {
|
|
141
|
+
const { registry, buildMcp } = opts;
|
|
142
|
+
const discoveryFile = serveDiscoveryPath();
|
|
143
|
+
// Stale-detect: a live daemon owns the discovery file; a dead pid is overwritten.
|
|
144
|
+
if (existsSync(discoveryFile)) {
|
|
145
|
+
try {
|
|
146
|
+
const prev = JSON.parse(readFileSync(discoveryFile, 'utf-8'));
|
|
147
|
+
if (prev.pid && prev.pid !== process.pid && pidAlive(prev.pid)) {
|
|
148
|
+
console.error(`Another serve daemon appears to be running (pid ${prev.pid}, port ${prev.port}).`);
|
|
149
|
+
console.error(`Stop it first, or delete ${discoveryFile} if it is stale.`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch { /* unreadable file — treat as stale and overwrite */ }
|
|
154
|
+
}
|
|
155
|
+
const startedAt = new Date().toISOString();
|
|
156
|
+
const channels = new Map();
|
|
157
|
+
// ── One tunnel client per agent, with graceful degradation ──
|
|
158
|
+
for (const entry of registry.list()) {
|
|
159
|
+
const ch = new AgentChannel(entry);
|
|
160
|
+
channels.set(entry.agent, ch);
|
|
161
|
+
const tunnel = new ConnectTunnelClient({
|
|
162
|
+
nodeUrl: entry.config.node_url,
|
|
163
|
+
getToken: () => getToken(entry.agent, entry.owner),
|
|
164
|
+
label: `tunnel:${entry.agent}`,
|
|
165
|
+
onDeliver: (kind, payload) => {
|
|
166
|
+
if (kind === 'task_assigned')
|
|
167
|
+
ch.handleTask(payload, 'deliver');
|
|
168
|
+
},
|
|
169
|
+
onBacklog: ({ tasks, messages }) => {
|
|
170
|
+
for (const t of tasks)
|
|
171
|
+
ch.handleTask(t, 'backlog');
|
|
172
|
+
ch.handleMessages(messages);
|
|
173
|
+
},
|
|
174
|
+
onAuthFailure: () => {
|
|
175
|
+
// Token died mid-session: fall back to direct fetch so already-running
|
|
176
|
+
// tool calls fail with the node's own 401 (clear guidance) instead of
|
|
177
|
+
// "Tunnel not connected".
|
|
178
|
+
ch.transportMode = 'auth_failed';
|
|
179
|
+
entry.client.setTransport(null);
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
const outcome = await tunnel.start();
|
|
183
|
+
if (outcome === 'online') {
|
|
184
|
+
ch.tunnel = tunnel;
|
|
185
|
+
ch.transportMode = 'tunnel';
|
|
186
|
+
entry.client.setTransport({ request: (m, p, o) => tunnel.forward(m, p, o ?? {}) });
|
|
187
|
+
console.error(`[serve] ${entry.agent}@${entry.owner}: tunnel online — API + realtime delivery over one WS (no upstream polling)`);
|
|
188
|
+
}
|
|
189
|
+
else if (outcome === 'auth_failed') {
|
|
190
|
+
ch.transportMode = 'auth_failed';
|
|
191
|
+
// Guidance already printed by the tunnel client; do NOT start a poller —
|
|
192
|
+
// it would hot-fail against the same dead credential.
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
ch.transportMode = 'direct';
|
|
196
|
+
console.error(`[serve] ${entry.agent}@${entry.owner}: tunnel ${outcome} — degraded to direct HTTP + poll loop`);
|
|
197
|
+
startPollerForAgent(entry);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// ── Loopback HTTP server ──
|
|
201
|
+
const app = express();
|
|
202
|
+
app.use(express.json({ limit: '25mb' }));
|
|
203
|
+
const resolveAgent = (req) => {
|
|
204
|
+
const header = req.headers['x-aimeat-agent'];
|
|
205
|
+
const q = req.query.agent;
|
|
206
|
+
const name = (typeof header === 'string' && header)
|
|
207
|
+
|| (typeof q === 'string' && q)
|
|
208
|
+
|| undefined;
|
|
209
|
+
return registry.resolve(name || undefined);
|
|
210
|
+
};
|
|
211
|
+
// ── Local MCP (Streamable HTTP) — mirrors the node's session plumbing, no auth ──
|
|
212
|
+
const transports = new Map();
|
|
213
|
+
app.post('/v1/mcp', async (req, res) => {
|
|
214
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
215
|
+
if (sessionId && transports.has(sessionId)) {
|
|
216
|
+
await transports.get(sessionId).handleRequest(req, res, req.body);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (sessionId) {
|
|
220
|
+
const body = req.body;
|
|
221
|
+
const method = Array.isArray(body) ? body[0]?.method : body?.method;
|
|
222
|
+
if (method !== 'initialize') {
|
|
223
|
+
res.status(404).json({
|
|
224
|
+
jsonrpc: '2.0',
|
|
225
|
+
error: { code: -32600, message: 'Session not found. Please re-initialize.' },
|
|
226
|
+
id: (Array.isArray(body) ? body[0]?.id : body?.id) ?? null,
|
|
227
|
+
});
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const transport = new StreamableHTTPServerTransport({
|
|
232
|
+
sessionIdGenerator: () => `local-${randomBytes(16).toString('hex')}`,
|
|
233
|
+
});
|
|
234
|
+
const mcp = buildMcp();
|
|
235
|
+
transport.onclose = () => {
|
|
236
|
+
if (transport.sessionId)
|
|
237
|
+
transports.delete(transport.sessionId);
|
|
238
|
+
};
|
|
239
|
+
await mcp.connect(transport);
|
|
240
|
+
await transport.handleRequest(req, res, req.body);
|
|
241
|
+
if (transport.sessionId)
|
|
242
|
+
transports.set(transport.sessionId, transport);
|
|
243
|
+
});
|
|
244
|
+
app.get('/v1/mcp', async (req, res) => {
|
|
245
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
246
|
+
if (!sessionId || !transports.has(sessionId)) {
|
|
247
|
+
res.status(400).json({ error: 'Missing or invalid mcp-session-id header' });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
await transports.get(sessionId).handleRequest(req, res);
|
|
251
|
+
});
|
|
252
|
+
app.delete('/v1/mcp', async (req, res) => {
|
|
253
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
254
|
+
if (!sessionId || !transports.has(sessionId)) {
|
|
255
|
+
res.status(404).json({ error: 'Session not found' });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
await transports.get(sessionId).close();
|
|
259
|
+
transports.delete(sessionId);
|
|
260
|
+
res.status(200).json({ closed: true });
|
|
261
|
+
});
|
|
262
|
+
// ── Push surface: long-poll fed by deliver/backlog (realtime, no spin) ──
|
|
263
|
+
app.get('/local/tasks/next', async (req, res) => {
|
|
264
|
+
let entry;
|
|
265
|
+
try {
|
|
266
|
+
entry = resolveAgent(req);
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
res.status(400).json({ ok: false, error: { code: 'UNKNOWN_AGENT', message: err.message } });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const rawWait = typeof req.query.wait === 'string' ? parseInt(req.query.wait, 10) : NaN;
|
|
273
|
+
const waitMs = Math.min(Math.max(Number.isFinite(rawWait) ? rawWait : 25_000, 0), 120_000);
|
|
274
|
+
const item = await channels.get(entry.agent).nextTask(waitMs);
|
|
275
|
+
if (!item) {
|
|
276
|
+
res.status(204).end();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
res.json({
|
|
280
|
+
ok: true,
|
|
281
|
+
data: { agent: entry.agent, owner: entry.owner, via: item.via, received_at: item.receivedAt, task: item.task },
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
app.get('/local/status', (_req, res) => {
|
|
285
|
+
res.json({
|
|
286
|
+
ok: true,
|
|
287
|
+
data: {
|
|
288
|
+
pid: process.pid,
|
|
289
|
+
started_at: startedAt,
|
|
290
|
+
agents: registry.list().map(e => ({
|
|
291
|
+
agent: e.agent,
|
|
292
|
+
owner: e.owner,
|
|
293
|
+
node_url: e.config.node_url,
|
|
294
|
+
transport: channels.get(e.agent).transportMode,
|
|
295
|
+
tunnel_status: channels.get(e.agent).tunnel?.getStatus() ?? null,
|
|
296
|
+
})),
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
app.post('/local/shutdown', (_req, res) => {
|
|
301
|
+
res.json({ ok: true, data: { stopping: true } });
|
|
302
|
+
setTimeout(() => { void shutdown(0); }, 50);
|
|
303
|
+
});
|
|
304
|
+
// ── REST proxy: everything else under /v1 funnels into the tunnel ──
|
|
305
|
+
app.all('/v1/*splat', async (req, res) => {
|
|
306
|
+
let entry;
|
|
307
|
+
try {
|
|
308
|
+
entry = resolveAgent(req);
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
res.status(400).json({ ok: false, error: { code: 'UNKNOWN_AGENT', message: err.message } });
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const ch = channels.get(entry.agent);
|
|
315
|
+
const query = {};
|
|
316
|
+
for (const [k, v] of Object.entries(req.query)) {
|
|
317
|
+
if (k === 'agent')
|
|
318
|
+
continue; // local routing parameter, not for the node
|
|
319
|
+
query[k] = Array.isArray(v) ? String(v[0]) : String(v);
|
|
320
|
+
}
|
|
321
|
+
const hasBody = req.method !== 'GET' && req.method !== 'HEAD'
|
|
322
|
+
&& req.body !== undefined && req.body !== null
|
|
323
|
+
&& (typeof req.body !== 'object' || Object.keys(req.body).length > 0);
|
|
324
|
+
const body = hasBody ? req.body : undefined;
|
|
325
|
+
try {
|
|
326
|
+
if (ch.tunnel?.isOnline()) {
|
|
327
|
+
const r = await ch.tunnel.forward(req.method, req.path, { query, body });
|
|
328
|
+
res.status(r.status).json(r.body);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
// Degraded (or tunnel mid-reconnect): direct HTTP with the stored token.
|
|
332
|
+
const url = new URL(entry.config.node_url.replace(/\/+$/, '') + req.path);
|
|
333
|
+
for (const [k, v] of Object.entries(query))
|
|
334
|
+
url.searchParams.set(k, v);
|
|
335
|
+
const token = await getToken(entry.agent, entry.owner);
|
|
336
|
+
const headers = { 'Content-Type': 'application/json', Connection: 'close' };
|
|
337
|
+
if (token)
|
|
338
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
339
|
+
const r = await fetch(url, { method: req.method, headers, body: body !== undefined ? JSON.stringify(body) : undefined });
|
|
340
|
+
const text = await r.text();
|
|
341
|
+
let parsed;
|
|
342
|
+
try {
|
|
343
|
+
parsed = text ? JSON.parse(text) : null;
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
parsed = text;
|
|
347
|
+
}
|
|
348
|
+
res.status(r.status).json(parsed);
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
res.status(502).json({ ok: false, error: { code: 'PROXY_ERROR', message: err.message } });
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
// ── Start, write discovery, wire shutdown ──
|
|
355
|
+
const server = await new Promise((resolve) => {
|
|
356
|
+
const s = app.listen(0, '127.0.0.1', () => resolve(s));
|
|
357
|
+
});
|
|
358
|
+
const port = server.address().port;
|
|
359
|
+
const writeDiscovery = () => {
|
|
360
|
+
const doc = {
|
|
361
|
+
schema_version: SERVE_DISCOVERY_SCHEMA_VERSION,
|
|
362
|
+
port,
|
|
363
|
+
pid: process.pid,
|
|
364
|
+
started_at: startedAt,
|
|
365
|
+
agents: registry.list().map(e => ({
|
|
366
|
+
agent: e.agent,
|
|
367
|
+
owner: e.owner,
|
|
368
|
+
node_url: e.config.node_url,
|
|
369
|
+
transport: channels.get(e.agent).transportMode,
|
|
370
|
+
})),
|
|
371
|
+
};
|
|
372
|
+
mkdirSync(getConfigDir(), { recursive: true });
|
|
373
|
+
const tmp = `${discoveryFile}.tmp-${process.pid}`;
|
|
374
|
+
writeFileSync(tmp, JSON.stringify(doc, null, 2), 'utf-8');
|
|
375
|
+
renameSync(tmp, discoveryFile); // atomic replace on the same volume
|
|
376
|
+
};
|
|
377
|
+
writeDiscovery();
|
|
378
|
+
let shuttingDown = false;
|
|
379
|
+
async function shutdown(code) {
|
|
380
|
+
if (shuttingDown)
|
|
381
|
+
return;
|
|
382
|
+
shuttingDown = true;
|
|
383
|
+
try {
|
|
384
|
+
if (existsSync(discoveryFile))
|
|
385
|
+
unlinkSync(discoveryFile);
|
|
386
|
+
}
|
|
387
|
+
catch { /* best effort */ }
|
|
388
|
+
for (const ch of channels.values()) {
|
|
389
|
+
ch.drainWaiters();
|
|
390
|
+
try {
|
|
391
|
+
await ch.tunnel?.close();
|
|
392
|
+
}
|
|
393
|
+
catch { /* ignore */ }
|
|
394
|
+
}
|
|
395
|
+
await new Promise((resolve) => {
|
|
396
|
+
const t = setTimeout(() => resolve(), 1_000);
|
|
397
|
+
server.close(() => { clearTimeout(t); resolve(); });
|
|
398
|
+
});
|
|
399
|
+
process.exit(code);
|
|
400
|
+
}
|
|
401
|
+
process.on('SIGINT', () => { void shutdown(0); });
|
|
402
|
+
process.on('SIGTERM', () => { void shutdown(0); });
|
|
403
|
+
const tunnelCount = [...channels.values()].filter(c => c.transportMode === 'tunnel').length;
|
|
404
|
+
const summary = registry.list()
|
|
405
|
+
.map(e => `${e.agent}@${e.owner} [${channels.get(e.agent).transportMode}]`)
|
|
406
|
+
.join(', ');
|
|
407
|
+
console.error(`AIMEAT serve daemon listening on http://127.0.0.1:${port} (MCP: /v1/mcp, proxy: /v1/*, push: /local/tasks/next)`);
|
|
408
|
+
console.error(`[serve] discovery: ${discoveryFile}`);
|
|
409
|
+
console.error(`[serve] ${registry.size()} agent(s): ${summary} — ${tunnelCount} over tunnel`);
|
|
410
|
+
}
|
|
411
|
+
//# sourceMappingURL=local-server.js.map
|