@agentcairn/mcp-server 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -2
- package/dist/authn.d.ts +38 -0
- package/dist/authn.js +86 -0
- package/dist/authn.js.map +1 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +25 -0
- package/dist/bin.js.map +1 -0
- package/dist/context.d.ts +49 -0
- package/dist/context.js +79 -0
- package/dist/context.js.map +1 -0
- package/dist/dashboard.d.ts +29 -0
- package/dist/dashboard.js +365 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/events.d.ts +120 -0
- package/dist/events.js +161 -0
- package/dist/events.js.map +1 -0
- package/dist/http.d.ts +35 -0
- package/dist/http.js +348 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/security.d.ts +49 -0
- package/dist/security.js +143 -0
- package/dist/security.js.map +1 -0
- package/dist/serve.d.ts +2 -0
- package/dist/serve.js +60 -0
- package/dist/serve.js.map +1 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.js +14 -0
- package/dist/server.js.map +1 -0
- package/dist/tools.d.ts +8 -0
- package/dist/tools.js +199 -0
- package/dist/tools.js.map +1 -0
- package/dist/util.d.ts +13 -0
- package/dist/util.js +49 -0
- package/dist/util.js.map +1 -0
- package/package.json +55 -4
- package/ui/css/app.css +361 -0
- package/ui/favicon.svg +1 -0
- package/ui/index.html +17 -0
- package/ui/js/api.js +37 -0
- package/ui/js/app.js +190 -0
- package/ui/js/charts.js +133 -0
- package/ui/js/dom.js +247 -0
- package/ui/js/drawer.js +39 -0
- package/ui/js/json.js +37 -0
- package/ui/js/store.js +77 -0
- package/ui/js/views/access.js +129 -0
- package/ui/js/views/auth.js +102 -0
- package/ui/js/views/eventDrawer.js +106 -0
- package/ui/js/views/graph.js +344 -0
- package/ui/js/views/live.js +203 -0
- package/ui/js/views/memoryDrawer.js +37 -0
- package/ui/js/views/project.js +94 -0
- package/ui/js/views/projects.js +77 -0
- package/ui/js/views/query.js +107 -0
- package/ui/js/views/system.js +73 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http';
|
|
2
|
+
export declare const SESSION_COOKIE = "aib_session";
|
|
3
|
+
export interface Session {
|
|
4
|
+
username: string;
|
|
5
|
+
expires: number;
|
|
6
|
+
}
|
|
7
|
+
/** Dashboard logins live in memory: a restart signs everyone out, which is fine for an admin panel. */
|
|
8
|
+
export declare class SessionStore {
|
|
9
|
+
private readonly clock;
|
|
10
|
+
private readonly sessions;
|
|
11
|
+
constructor(clock?: () => number);
|
|
12
|
+
create(username: string): {
|
|
13
|
+
id: string;
|
|
14
|
+
maxAgeSeconds: number;
|
|
15
|
+
};
|
|
16
|
+
get(id: string | undefined): Session | undefined;
|
|
17
|
+
destroy(id: string | undefined): void;
|
|
18
|
+
clear(): void;
|
|
19
|
+
}
|
|
20
|
+
export declare function parseCookies(req: IncomingMessage): Record<string, string>;
|
|
21
|
+
export declare function sessionCookie(id: string, maxAgeSeconds: number, secure: boolean): string;
|
|
22
|
+
export declare const clearedSessionCookie: (secure: boolean) => string;
|
|
23
|
+
/** A cross-site page must not be able to drive the dashboard with the admin's cookie. */
|
|
24
|
+
export declare function sameOrigin(req: IncomingMessage): boolean;
|
|
25
|
+
/** Fixed-window limiter for login and setup-code guesses. */
|
|
26
|
+
export declare class RateLimiter {
|
|
27
|
+
private readonly max;
|
|
28
|
+
private readonly windowMs;
|
|
29
|
+
private readonly clock;
|
|
30
|
+
private readonly hits;
|
|
31
|
+
constructor(max: number, windowMs: number, clock?: () => number);
|
|
32
|
+
/** Records an attempt; returns false once the key is over the limit. */
|
|
33
|
+
attempt(key: string): boolean;
|
|
34
|
+
reset(key: string): void;
|
|
35
|
+
retryAfterSeconds(key: string): number;
|
|
36
|
+
}
|
|
37
|
+
export declare function generateSetupCode(): string;
|
|
38
|
+
/**
|
|
39
|
+
* The passcode printed in the startup log while no admin exists. It lives
|
|
40
|
+
* only in memory: a restart prints a new one, and creating the admin ends it.
|
|
41
|
+
*/
|
|
42
|
+
export declare class SetupGate {
|
|
43
|
+
private code;
|
|
44
|
+
open(): string;
|
|
45
|
+
close(): void;
|
|
46
|
+
get isOpen(): boolean;
|
|
47
|
+
check(given: string): boolean;
|
|
48
|
+
}
|
|
49
|
+
export declare function setupBanner(code: string, port: number): string;
|
package/dist/security.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { header } from './util.js';
|
|
3
|
+
// ---- sessions --------------------------------------------------------------
|
|
4
|
+
export const SESSION_COOKIE = 'aib_session';
|
|
5
|
+
const SESSION_MS = 12 * 60 * 60 * 1000;
|
|
6
|
+
/** Dashboard logins live in memory: a restart signs everyone out, which is fine for an admin panel. */
|
|
7
|
+
export class SessionStore {
|
|
8
|
+
clock;
|
|
9
|
+
sessions = new Map();
|
|
10
|
+
constructor(clock = Date.now) {
|
|
11
|
+
this.clock = clock;
|
|
12
|
+
}
|
|
13
|
+
create(username) {
|
|
14
|
+
const id = randomBytes(32).toString('base64url');
|
|
15
|
+
this.sessions.set(id, { username, expires: this.clock() + SESSION_MS });
|
|
16
|
+
for (const [k, s] of this.sessions)
|
|
17
|
+
if (s.expires < this.clock())
|
|
18
|
+
this.sessions.delete(k);
|
|
19
|
+
return { id, maxAgeSeconds: SESSION_MS / 1000 };
|
|
20
|
+
}
|
|
21
|
+
get(id) {
|
|
22
|
+
if (!id)
|
|
23
|
+
return undefined;
|
|
24
|
+
const s = this.sessions.get(id);
|
|
25
|
+
if (!s)
|
|
26
|
+
return undefined;
|
|
27
|
+
if (s.expires < this.clock()) {
|
|
28
|
+
this.sessions.delete(id);
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
return s;
|
|
32
|
+
}
|
|
33
|
+
destroy(id) {
|
|
34
|
+
if (id)
|
|
35
|
+
this.sessions.delete(id);
|
|
36
|
+
}
|
|
37
|
+
clear() {
|
|
38
|
+
this.sessions.clear();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function parseCookies(req) {
|
|
42
|
+
const out = {};
|
|
43
|
+
for (const part of (req.headers.cookie ?? '').split(';')) {
|
|
44
|
+
const i = part.indexOf('=');
|
|
45
|
+
if (i > 0)
|
|
46
|
+
out[part.slice(0, i).trim()] = decodeURIComponent(part.slice(i + 1).trim());
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
export function sessionCookie(id, maxAgeSeconds, secure) {
|
|
51
|
+
return `${SESSION_COOKIE}=${id}; HttpOnly; SameSite=Strict; Path=/; Max-Age=${maxAgeSeconds}${secure ? '; Secure' : ''}`;
|
|
52
|
+
}
|
|
53
|
+
export const clearedSessionCookie = (secure) => `${SESSION_COOKIE}=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0${secure ? '; Secure' : ''}`;
|
|
54
|
+
/** A cross-site page must not be able to drive the dashboard with the admin's cookie. */
|
|
55
|
+
export function sameOrigin(req) {
|
|
56
|
+
const origin = header(req, 'origin');
|
|
57
|
+
if (!origin)
|
|
58
|
+
return true;
|
|
59
|
+
try {
|
|
60
|
+
return new URL(origin).host === header(req, 'host');
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// ---- rate limiting ---------------------------------------------------------
|
|
67
|
+
/** Fixed-window limiter for login and setup-code guesses. */
|
|
68
|
+
export class RateLimiter {
|
|
69
|
+
max;
|
|
70
|
+
windowMs;
|
|
71
|
+
clock;
|
|
72
|
+
hits = new Map();
|
|
73
|
+
constructor(max, windowMs, clock = Date.now) {
|
|
74
|
+
this.max = max;
|
|
75
|
+
this.windowMs = windowMs;
|
|
76
|
+
this.clock = clock;
|
|
77
|
+
}
|
|
78
|
+
/** Records an attempt; returns false once the key is over the limit. */
|
|
79
|
+
attempt(key) {
|
|
80
|
+
const now = this.clock();
|
|
81
|
+
for (const [k, v] of this.hits)
|
|
82
|
+
if (v.resetAt < now)
|
|
83
|
+
this.hits.delete(k);
|
|
84
|
+
const entry = this.hits.get(key) ?? { count: 0, resetAt: now + this.windowMs };
|
|
85
|
+
entry.count++;
|
|
86
|
+
this.hits.set(key, entry);
|
|
87
|
+
return entry.count <= this.max;
|
|
88
|
+
}
|
|
89
|
+
reset(key) {
|
|
90
|
+
this.hits.delete(key);
|
|
91
|
+
}
|
|
92
|
+
retryAfterSeconds(key) {
|
|
93
|
+
const e = this.hits.get(key);
|
|
94
|
+
return e ? Math.max(1, Math.ceil((e.resetAt - this.clock()) / 1000)) : 0;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// ---- one-time setup code ---------------------------------------------------
|
|
98
|
+
const ALPHABET = '23456789ABCDEFGHJKMNPQRSTUVWXYZ';
|
|
99
|
+
export function generateSetupCode() {
|
|
100
|
+
const bytes = randomBytes(12);
|
|
101
|
+
const chars = [...bytes].map(b => ALPHABET[b % ALPHABET.length]).join('');
|
|
102
|
+
return `${chars.slice(0, 4)}-${chars.slice(4, 8)}-${chars.slice(8, 12)}`;
|
|
103
|
+
}
|
|
104
|
+
const normalizeCode = (s) => s.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
|
105
|
+
/**
|
|
106
|
+
* The passcode printed in the startup log while no admin exists. It lives
|
|
107
|
+
* only in memory: a restart prints a new one, and creating the admin ends it.
|
|
108
|
+
*/
|
|
109
|
+
export class SetupGate {
|
|
110
|
+
code = null;
|
|
111
|
+
open() {
|
|
112
|
+
this.code = generateSetupCode();
|
|
113
|
+
return this.code;
|
|
114
|
+
}
|
|
115
|
+
close() {
|
|
116
|
+
this.code = null;
|
|
117
|
+
}
|
|
118
|
+
get isOpen() {
|
|
119
|
+
return this.code !== null;
|
|
120
|
+
}
|
|
121
|
+
check(given) {
|
|
122
|
+
if (!this.code)
|
|
123
|
+
return false;
|
|
124
|
+
const a = Buffer.from(normalizeCode(given).padEnd(32, '_'));
|
|
125
|
+
const b = Buffer.from(normalizeCode(this.code).padEnd(32, '_'));
|
|
126
|
+
return a.length === b.length && timingSafeEqual(a, b) && normalizeCode(given).length === 12;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export function setupBanner(code, port) {
|
|
130
|
+
const lines = [
|
|
131
|
+
'Agent Cairn: first-time setup',
|
|
132
|
+
'',
|
|
133
|
+
`Open http://<this-server>:${port}/ui/ and enter this one-time code:`,
|
|
134
|
+
'',
|
|
135
|
+
` ${code}`,
|
|
136
|
+
'',
|
|
137
|
+
'It works until the admin account is created, or the server restarts.',
|
|
138
|
+
];
|
|
139
|
+
const width = Math.max(...lines.map(l => l.length)) + 4;
|
|
140
|
+
const bar = '─'.repeat(width);
|
|
141
|
+
return ['┌' + bar + '┐', ...lines.map(l => `│ ${l.padEnd(width - 2)}│`), '└' + bar + '┘'].join('\n');
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=security.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.js","sourceRoot":"","sources":["../src/security.ts"],"names":[],"mappings":"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"}
|
package/dist/serve.d.ts
ADDED
package/dist/serve.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { countAdmins, ensureSchema, hasDefaultCredentials, loadConfig, loadEnvironment, openBrain } from '@agentcairn/core';
|
|
3
|
+
import { createBrainHttp } from './http.js';
|
|
4
|
+
import { setupBanner } from './security.js';
|
|
5
|
+
const log = (message) => console.error(`cairn: ${message}`);
|
|
6
|
+
async function main() {
|
|
7
|
+
loadEnvironment();
|
|
8
|
+
const env = process.env;
|
|
9
|
+
const token = env.CAIRN_TOKEN || undefined;
|
|
10
|
+
const noAuth = env.CAIRN_NO_AUTH === '1';
|
|
11
|
+
if (noAuth)
|
|
12
|
+
log('warning: running WITHOUT authentication (CAIRN_NO_AUTH=1).');
|
|
13
|
+
const config = loadConfig();
|
|
14
|
+
if (hasDefaultCredentials(config))
|
|
15
|
+
log('warning: Neo4j is using the default credentials.');
|
|
16
|
+
const brain = openBrain(config);
|
|
17
|
+
const port = Number(env.CAIRN_PORT || 8787);
|
|
18
|
+
const host = env.CAIRN_HOST || '0.0.0.0';
|
|
19
|
+
const { server, setup } = createBrainHttp(brain, {
|
|
20
|
+
token,
|
|
21
|
+
noAuth,
|
|
22
|
+
log,
|
|
23
|
+
port,
|
|
24
|
+
capture: env.CAIRN_CAPTURE === 'meta' ? 'meta' : 'full',
|
|
25
|
+
eventCapacity: Number(env.CAIRN_EVENT_CAPACITY) || undefined,
|
|
26
|
+
trustProxy: env.CAIRN_TRUST_PROXY === '1',
|
|
27
|
+
secureCookies: env.CAIRN_COOKIE_SECURE === '1',
|
|
28
|
+
});
|
|
29
|
+
server.listen(port, host, () => log(`listening on http://${host}:${port} (dashboard /ui/, MCP /mcp, neo4j ${config.uri})`));
|
|
30
|
+
// Neo4j may still be starting; everything here is retried lazily on first use.
|
|
31
|
+
void (async () => {
|
|
32
|
+
try {
|
|
33
|
+
await ensureSchema(brain);
|
|
34
|
+
if (env.CAIRN_RESET_ADMIN === '1') {
|
|
35
|
+
await brain.run('MATCH (a:Admin) DETACH DELETE a');
|
|
36
|
+
await brain.run("MATCH (m:BrainMeta {id: 'setup'}) DELETE m");
|
|
37
|
+
log('CAIRN_RESET_ADMIN=1: all dashboard accounts were removed. Unset it after this start.');
|
|
38
|
+
}
|
|
39
|
+
log('schema ready');
|
|
40
|
+
if ((await countAdmins(brain)) === 0 && !setup.isOpen)
|
|
41
|
+
console.error(`\n${setupBanner(setup.open(), port)}\n`);
|
|
42
|
+
else if (!token && !noAuth)
|
|
43
|
+
log('dashboard account exists; create access tokens under Access in the dashboard.');
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
log(`schema not ready yet (${error.message}); will retry on first request`);
|
|
47
|
+
}
|
|
48
|
+
})();
|
|
49
|
+
const shutdown = () => {
|
|
50
|
+
server.close(() => void brain.close().finally(() => process.exit(0)));
|
|
51
|
+
setTimeout(() => process.exit(0), 5000).unref();
|
|
52
|
+
};
|
|
53
|
+
process.on('SIGINT', shutdown);
|
|
54
|
+
process.on('SIGTERM', shutdown);
|
|
55
|
+
}
|
|
56
|
+
main().catch(error => {
|
|
57
|
+
log(`failed to start: ${error instanceof Error ? error.message : String(error)}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
});
|
|
60
|
+
//# sourceMappingURL=serve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve.js","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Brain } from '@agentcairn/core';
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { type Scope } from './context.js';
|
|
4
|
+
export declare const SERVER_NAME = "cairn";
|
|
5
|
+
export declare const SERVER_VERSION = "0.1.0";
|
|
6
|
+
/** `scope` may be a folder (stdio mode) or a client-supplied project hint (remote mode). */
|
|
7
|
+
export declare function createBrainServer(brain: Brain, scope: string | Scope, options?: {
|
|
8
|
+
agent?: string;
|
|
9
|
+
onProject?: (projectId: string) => void;
|
|
10
|
+
}): McpServer;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { BrainContext } from './context.js';
|
|
3
|
+
import { SERVER_INSTRUCTIONS, registerTools } from './tools.js';
|
|
4
|
+
export const SERVER_NAME = 'cairn';
|
|
5
|
+
export const SERVER_VERSION = '0.1.0';
|
|
6
|
+
/** `scope` may be a folder (stdio mode) or a client-supplied project hint (remote mode). */
|
|
7
|
+
export function createBrainServer(brain, scope, options = {}) {
|
|
8
|
+
const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }, { instructions: SERVER_INSTRUCTIONS });
|
|
9
|
+
const resolved = typeof scope === 'string' ? { cwd: scope } : scope;
|
|
10
|
+
const ctx = new BrainContext(brain, resolved, () => server.server.getClientVersion()?.name ?? options.agent, options.onProject);
|
|
11
|
+
registerTools(server, ctx);
|
|
12
|
+
return server;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AACnE,OAAO,EAAE,YAAY,EAAc,MAAM,cAAc,CAAA;AACvD,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/D,MAAM,CAAC,MAAM,WAAW,GAAG,OAAO,CAAA;AAClC,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAA;AAErC,4FAA4F;AAC5F,MAAM,UAAU,iBAAiB,CAC/B,KAAY,EACZ,KAAqB,EACrB,UAAuE,EAAE;IAEzE,MAAM,MAAM,GAAG,IAAI,SAAS,CAC1B,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,EAAE,EAC9C,EAAE,YAAY,EAAE,mBAAmB,EAAE,CACtC,CAAA;IACD,MAAM,QAAQ,GAAU,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAA;IAC1E,MAAM,GAAG,GAAG,IAAI,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,gBAAgB,EAAE,EAAE,IAAI,IAAI,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,CAAC,CAAA;IAC/H,aAAa,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC1B,OAAO,MAAM,CAAA;AACf,CAAC"}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Memory, type Staleness } from '@agentcairn/core';
|
|
2
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import type { BrainContext } from './context.js';
|
|
4
|
+
/** Turns any failure into a message the agent can act on; never throws. */
|
|
5
|
+
export declare function describeError(error: unknown): string;
|
|
6
|
+
export declare function formatMemory(m: Memory, staleness?: Staleness, score?: number | null): string;
|
|
7
|
+
export declare const SERVER_INSTRUCTIONS = "Agent Cairn is the shared, persistent memory of this project, written by agents like you.\n- Start of a session: call cairn_briefing before exploring the repo and trust it over re-reading files. If it returns an onboarding prompt, follow it.\n- After finishing a goal or process: call cairn_checkpoint.\n- When you learn something durable (a decision and its reason, a convention, a trap): call cairn_remember. Replace outdated memories with supersedes (ids shown as #abc12345).\n- Look things up with cairn_recall instead of searching the repo for context that is probably recorded.";
|
|
8
|
+
export declare function registerTools(server: McpServer, ctx: BrainContext): void;
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { MAX_BRIEFING_TOKENS, MEMORY_KINDS, MemoryValidationError, MIN_BRIEFING_TOKENS, ProjectMissingError, ProjectNotFoundError, RemoteConflictError, SupersedeError, UnresolvedProjectError, buildBriefing, checkConnection, checkStaleness, checkpoint, countActiveByKind, forgetMemory, hasDefaultCredentials, listAreas, loadConfig, recall, remember, schemaVersion, shortId, } from '@agentcairn/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
const ok = (text) => ({ content: [{ type: 'text', text }] });
|
|
4
|
+
const fail = (text) => ({ content: [{ type: 'text', text }], isError: true });
|
|
5
|
+
/** Turns any failure into a message the agent can act on; never throws. */
|
|
6
|
+
export function describeError(error) {
|
|
7
|
+
if (error instanceof MemoryValidationError ||
|
|
8
|
+
error instanceof SupersedeError ||
|
|
9
|
+
error instanceof UnresolvedProjectError ||
|
|
10
|
+
error instanceof ProjectNotFoundError ||
|
|
11
|
+
error instanceof ProjectMissingError ||
|
|
12
|
+
error instanceof RemoteConflictError) {
|
|
13
|
+
return error.message;
|
|
14
|
+
}
|
|
15
|
+
const e = error;
|
|
16
|
+
if (e.code === 'Neo.ClientError.Security.Unauthorized') {
|
|
17
|
+
return 'Neo4j rejected the credentials. Check NEO4J_USER and NEO4J_PASSWORD.';
|
|
18
|
+
}
|
|
19
|
+
if (e.code === 'ServiceUnavailable' || e.code === 'SessionExpired') {
|
|
20
|
+
return `Cannot reach Neo4j at ${loadConfig().uri}. Check that it is running and NEO4J_URI is right.`;
|
|
21
|
+
}
|
|
22
|
+
return `Unexpected error: ${e.message ?? String(error)}`;
|
|
23
|
+
}
|
|
24
|
+
const guard = (handler) => async (args) => {
|
|
25
|
+
try {
|
|
26
|
+
return await handler(args);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
return fail(describeError(error));
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
export function formatMemory(m, staleness, score) {
|
|
33
|
+
const meta = [m.kind + (m.area ? `/${m.area}` : ''), `importance ${m.importance}`, m.createdAt.slice(0, 10)];
|
|
34
|
+
if (m.status === 'superseded')
|
|
35
|
+
meta.push('superseded');
|
|
36
|
+
if (score != null)
|
|
37
|
+
meta.push(`score ${score.toFixed(2)}`);
|
|
38
|
+
const stale = staleness?.status === 'stale' ? `\n⚠ may be stale (changed: ${staleness.changedPaths.slice(0, 3).join(', ')})` : '';
|
|
39
|
+
const paths = m.paths.length ? `\npaths: ${m.paths.join(', ')}` : '';
|
|
40
|
+
return `#${shortId(m.id)} [${meta.join(', ')}] ${m.title}\n${m.body}${paths}${stale}`;
|
|
41
|
+
}
|
|
42
|
+
const kindSchema = z.enum(MEMORY_KINDS);
|
|
43
|
+
const commitShaSchema = z.string().optional().describe('Set automatically by hooks; leave empty');
|
|
44
|
+
const authorSchema = z.string().optional().describe('Set automatically by hooks; leave empty');
|
|
45
|
+
const pathsSchema = z.array(z.string()).max(50).optional().describe('Repo-relative files or folders this concerns; used to detect staleness');
|
|
46
|
+
const areaSchema = z.string().optional().describe('Module or subsystem this is about');
|
|
47
|
+
export const SERVER_INSTRUCTIONS = `Agent Cairn is the shared, persistent memory of this project, written by agents like you.
|
|
48
|
+
- Start of a session: call cairn_briefing before exploring the repo and trust it over re-reading files. If it returns an onboarding prompt, follow it.
|
|
49
|
+
- After finishing a goal or process: call cairn_checkpoint.
|
|
50
|
+
- When you learn something durable (a decision and its reason, a convention, a trap): call cairn_remember. Replace outdated memories with supersedes (ids shown as #abc12345).
|
|
51
|
+
- Look things up with cairn_recall instead of searching the repo for context that is probably recorded.`;
|
|
52
|
+
export function registerTools(server, ctx) {
|
|
53
|
+
server.registerTool('cairn_briefing', {
|
|
54
|
+
title: 'Project briefing',
|
|
55
|
+
description: 'Start here. Returns a compact, token-budgeted briefing of this project (overview, conventions, gotchas, open threads, recent checkpoints, decisions), with stale items flagged. If the project has no memories yet, returns onboarding instructions instead.',
|
|
56
|
+
inputSchema: {
|
|
57
|
+
max_tokens: z
|
|
58
|
+
.number()
|
|
59
|
+
.int()
|
|
60
|
+
.min(MIN_BRIEFING_TOKENS)
|
|
61
|
+
.max(MAX_BRIEFING_TOKENS)
|
|
62
|
+
.optional()
|
|
63
|
+
.describe('Token budget for the briefing (default 1500)'),
|
|
64
|
+
},
|
|
65
|
+
}, guard(async ({ max_tokens }) => {
|
|
66
|
+
const project = await ctx.project({ write: false });
|
|
67
|
+
const briefing = await buildBriefing(ctx.brain, project.projectId, { maxTokens: max_tokens, cwd: ctx.cwd });
|
|
68
|
+
return ok(briefing.text);
|
|
69
|
+
}));
|
|
70
|
+
server.registerTool('cairn_recall', {
|
|
71
|
+
title: 'Search project memory',
|
|
72
|
+
description: 'Search this project\'s memories by text, kind and area. Without a query, lists the most important and recent ones. Use it for details the briefing left out.',
|
|
73
|
+
inputSchema: {
|
|
74
|
+
query: z.string().optional().describe('Words to search for in titles and bodies'),
|
|
75
|
+
kind: kindSchema.optional(),
|
|
76
|
+
area: areaSchema,
|
|
77
|
+
limit: z.number().int().min(1).max(50).optional().describe('Default 10'),
|
|
78
|
+
include_superseded: z.boolean().optional().describe('Also return memories that were replaced'),
|
|
79
|
+
},
|
|
80
|
+
}, guard(async ({ query, kind, area, limit, include_superseded }) => {
|
|
81
|
+
const project = await ctx.project({ write: false });
|
|
82
|
+
const found = await recall(ctx.brain, project.projectId, {
|
|
83
|
+
query,
|
|
84
|
+
kind,
|
|
85
|
+
area,
|
|
86
|
+
limit,
|
|
87
|
+
includeSuperseded: include_superseded,
|
|
88
|
+
});
|
|
89
|
+
if (found.length === 0)
|
|
90
|
+
return ok('No matching memories.');
|
|
91
|
+
const staleness = ctx.cwd ? await checkStaleness(ctx.cwd, found) : undefined;
|
|
92
|
+
return ok(found.map(m => formatMemory(m, staleness?.get(m.id), m.score)).join('\n\n'));
|
|
93
|
+
}));
|
|
94
|
+
server.registerTool('cairn_remember', {
|
|
95
|
+
title: 'Record a memory',
|
|
96
|
+
description: 'Store one durable fact about this project: a decision (with its reason), convention, gotcha, overview, open thread or glossary term. One fact per memory. Use supersedes to replace outdated memories.',
|
|
97
|
+
inputSchema: {
|
|
98
|
+
kind: kindSchema,
|
|
99
|
+
title: z.string().min(1).max(200),
|
|
100
|
+
body: z.string().min(1).max(20_000).describe('Short and specific: the why and the non-obvious, no pasted code'),
|
|
101
|
+
area: areaSchema,
|
|
102
|
+
paths: pathsSchema,
|
|
103
|
+
importance: z.number().int().min(1).max(5).optional().describe('1 minor to 5 critical (default 3)'),
|
|
104
|
+
supersedes: z.array(z.string()).max(20).optional().describe('Ids (#abc12345) of memories this replaces'),
|
|
105
|
+
commit_sha: commitShaSchema,
|
|
106
|
+
author: authorSchema,
|
|
107
|
+
},
|
|
108
|
+
}, guard(async ({ commit_sha, author, ...input }) => {
|
|
109
|
+
const project = await ctx.project({ write: true });
|
|
110
|
+
const m = await remember(ctx.brain, project.projectId, {
|
|
111
|
+
...input,
|
|
112
|
+
...(await ctx.provenance({ commitSha: commit_sha, author })),
|
|
113
|
+
});
|
|
114
|
+
const replaced = m.supersedes.length ? ` (replaced ${m.supersedes.map(shortId).map(s => `#${s}`).join(', ')})` : '';
|
|
115
|
+
return ok(`Remembered #${shortId(m.id)}: ${m.title}${replaced}`);
|
|
116
|
+
}));
|
|
117
|
+
server.registerTool('cairn_checkpoint', {
|
|
118
|
+
title: 'Checkpoint a finished goal',
|
|
119
|
+
description: 'Call after finishing a goal or process. Records what was done, stores each decision as its own memory, and keeps what is next as an open thread.',
|
|
120
|
+
inputSchema: {
|
|
121
|
+
goal: z.string().min(1).max(200).describe('What you were doing'),
|
|
122
|
+
outcome: z.string().min(1).max(20_000).describe('What changed and how it ended'),
|
|
123
|
+
decisions: z
|
|
124
|
+
.array(z.object({
|
|
125
|
+
title: z.string().min(1).max(200),
|
|
126
|
+
body: z.string().min(1).max(20_000),
|
|
127
|
+
area: areaSchema,
|
|
128
|
+
paths: pathsSchema,
|
|
129
|
+
}))
|
|
130
|
+
.max(20)
|
|
131
|
+
.optional()
|
|
132
|
+
.describe('Choices worth keeping, each with its reason'),
|
|
133
|
+
next: z.string().optional().describe('What remains to be done'),
|
|
134
|
+
resolves: z.array(z.string()).max(20).optional().describe('Ids (#abc12345) of open threads this finished'),
|
|
135
|
+
area: areaSchema,
|
|
136
|
+
paths: pathsSchema,
|
|
137
|
+
commit_sha: commitShaSchema,
|
|
138
|
+
author: authorSchema,
|
|
139
|
+
},
|
|
140
|
+
}, guard(async ({ commit_sha, author, ...input }) => {
|
|
141
|
+
const project = await ctx.project({ write: true });
|
|
142
|
+
const r = await checkpoint(ctx.brain, project.projectId, {
|
|
143
|
+
...input,
|
|
144
|
+
...(await ctx.provenance({ commitSha: commit_sha, author })),
|
|
145
|
+
});
|
|
146
|
+
const parts = [`Checkpoint #${shortId(r.checkpoint.id)} recorded.`];
|
|
147
|
+
if (r.decisions.length)
|
|
148
|
+
parts.push(`${r.decisions.length} decision(s) stored.`);
|
|
149
|
+
if (r.thread)
|
|
150
|
+
parts.push(`Open thread #${shortId(r.thread.id)} created.`);
|
|
151
|
+
if (r.checkpoint.supersedes.length)
|
|
152
|
+
parts.push(`Resolved ${r.checkpoint.supersedes.length} thread(s).`);
|
|
153
|
+
return ok(parts.join(' '));
|
|
154
|
+
}));
|
|
155
|
+
server.registerTool('cairn_areas', {
|
|
156
|
+
title: 'List areas',
|
|
157
|
+
description: 'Lists the areas (modules or subsystems) memories have been filed under, with counts.',
|
|
158
|
+
inputSchema: {},
|
|
159
|
+
}, guard(async () => {
|
|
160
|
+
const project = await ctx.project({ write: false });
|
|
161
|
+
const areas = await listAreas(ctx.brain, project.projectId);
|
|
162
|
+
return ok(areas.length ? areas.map(a => `${a.name}: ${a.count}`).join('\n') : 'No areas yet.');
|
|
163
|
+
}));
|
|
164
|
+
server.registerTool('cairn_forget', {
|
|
165
|
+
title: 'Delete a memory',
|
|
166
|
+
description: 'Permanently deletes one memory by id (#abc12345). Memories it had replaced become active again. Use for memories that were wrong, not for outdated ones: supersede those.',
|
|
167
|
+
inputSchema: { id: z.string().min(6).describe('Memory id or unique prefix, e.g. abc12345') },
|
|
168
|
+
}, guard(async ({ id }) => {
|
|
169
|
+
const project = await ctx.project({ write: false });
|
|
170
|
+
const gone = await forgetMemory(ctx.brain, project.projectId, id.replace(/^#/, ''));
|
|
171
|
+
if (!gone)
|
|
172
|
+
return fail(`No memory matches "${id}" in this project.`);
|
|
173
|
+
const restored = gone.restored.length ? ` Restored ${gone.restored.length} earlier memory(ies).` : '';
|
|
174
|
+
return ok(`Deleted #${shortId(gone.id)}: ${gone.title}.${restored}`);
|
|
175
|
+
}));
|
|
176
|
+
server.registerTool('cairn_status', {
|
|
177
|
+
title: 'Brain status',
|
|
178
|
+
description: 'Health check: Neo4j connection, which project this folder maps to and how, and memory counts.',
|
|
179
|
+
inputSchema: {},
|
|
180
|
+
}, guard(async () => {
|
|
181
|
+
const config = loadConfig();
|
|
182
|
+
const info = await checkConnection(ctx.brain);
|
|
183
|
+
await ctx.ready();
|
|
184
|
+
const project = await ctx.project({ write: false });
|
|
185
|
+
const counts = await countActiveByKind(ctx.brain, project.projectId);
|
|
186
|
+
const total = [...counts.values()].reduce((a, b) => a + b, 0);
|
|
187
|
+
const lines = [
|
|
188
|
+
`Neo4j: ${info.agent} at ${config.uri} (schema v${await schemaVersion(ctx.brain)})`,
|
|
189
|
+
`Project: ${project.projectId} (via ${project.source === 'link' ? 'explicit project id' : 'git remote'}${project.isNew ? ', no memories yet' : ''})`,
|
|
190
|
+
`Active memories: ${total}${total ? ` (${[...counts].map(([k, n]) => `${k} ${n}`).join(', ')})` : ''}`,
|
|
191
|
+
];
|
|
192
|
+
if (ctx.cwd)
|
|
193
|
+
lines.splice(2, 0, `Folder: ${ctx.cwd}`);
|
|
194
|
+
if (hasDefaultCredentials(config))
|
|
195
|
+
lines.push('Warning: Neo4j is using the default credentials.');
|
|
196
|
+
return ok(lines.join('\n'));
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,YAAY,EACZ,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,aAAa,EACb,eAAe,EACf,cAAc,EACd,UAAU,EACV,iBAAiB,EACjB,YAAY,EACZ,qBAAqB,EACrB,SAAS,EACT,UAAU,EACV,MAAM,EACN,QAAQ,EACR,aAAa,EACb,OAAO,GAGR,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAKvB,MAAM,EAAE,GAAG,CAAC,IAAY,EAAc,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAA;AAChF,MAAM,IAAI,GAAG,CAAC,IAAY,EAAc,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAA;AAEjG,2EAA2E;AAC3E,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,IACE,KAAK,YAAY,qBAAqB;QACtC,KAAK,YAAY,cAAc;QAC/B,KAAK,YAAY,sBAAsB;QACvC,KAAK,YAAY,oBAAoB;QACrC,KAAK,YAAY,mBAAmB;QACpC,KAAK,YAAY,mBAAmB,EACpC,CAAC;QACD,OAAO,KAAK,CAAC,OAAO,CAAA;IACtB,CAAC;IACD,MAAM,CAAC,GAAG,KAA4C,CAAA;IACtD,IAAI,CAAC,CAAC,IAAI,KAAK,uCAAuC,EAAE,CAAC;QACvD,OAAO,sEAAsE,CAAA;IAC/E,CAAC;IACD,IAAI,CAAC,CAAC,IAAI,KAAK,oBAAoB,IAAI,CAAC,CAAC,IAAI,KAAK,gBAAgB,EAAE,CAAC;QACnE,OAAO,yBAAyB,UAAU,EAAE,CAAC,GAAG,oDAAoD,CAAA;IACtG,CAAC;IACD,OAAO,qBAAqB,CAAC,CAAC,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAA;AAC1D,CAAC;AAED,MAAM,KAAK,GACT,CAAI,OAAyC,EAAE,EAAE,CACjD,KAAK,EAAE,IAAO,EAAuB,EAAE;IACrC,IAAI,CAAC;QACH,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAA;IACnC,CAAC;AACH,CAAC,CAAA;AAEH,MAAM,UAAU,YAAY,CAAC,CAAS,EAAE,SAAqB,EAAE,KAAqB;IAClF,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,cAAc,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;IAC5G,IAAI,CAAC,CAAC,MAAM,KAAK,YAAY;QAAE,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;IACtD,IAAI,KAAK,IAAI,IAAI;QAAE,IAAI,CAAC,IAAI,CAAC,SAAS,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IACzD,MAAM,KAAK,GACT,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,8BAA8B,SAAS,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAA;IACrH,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;IACpE,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,IAAI,GAAG,KAAK,GAAG,KAAK,EAAE,CAAA;AACvF,CAAC;AAED,MAAM,UAAU,GAAG,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;AACvC,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yCAAyC,CAAC,CAAA;AACjG,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yCAAyC,CAAC,CAAA;AAC9F,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,wEAAwE,CAAC,CAAA;AAC7I,MAAM,UAAU,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,mCAAmC,CAAC,CAAA;AAEtF,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;;wGAIqE,CAAA;AAExG,MAAM,UAAU,aAAa,CAAC,MAAiB,EAAE,GAAiB;IAChE,MAAM,CAAC,YAAY,CACjB,gBAAgB,EAChB;QACE,KAAK,EAAE,kBAAkB;QACzB,WAAW,EACT,8PAA8P;QAChQ,WAAW,EAAE;YACX,UAAU,EAAE,CAAC;iBACV,MAAM,EAAE;iBACR,GAAG,EAAE;iBACL,GAAG,CAAC,mBAAmB,CAAC;iBACxB,GAAG,CAAC,mBAAmB,CAAC;iBACxB,QAAQ,EAAE;iBACV,QAAQ,CAAC,8CAA8C,CAAC;SAC5D;KACF,EACD,KAAK,CAAC,KAAK,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE;QAC7B,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAA;QACnD,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC,CAAA;QAC3G,OAAO,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;IAC1B,CAAC,CAAC,CACH,CAAA;IAED,MAAM,CAAC,YAAY,CACjB,cAAc,EACd;QACE,KAAK,EAAE,uBAAuB;QAC9B,WAAW,EACT,8JAA8J;QAChK,WAAW,EAAE;YACX,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,0CAA0C,CAAC;YACjF,IAAI,EAAE,UAAU,CAAC,QAAQ,EAAE;YAC3B,IAAI,EAAE,UAAU;YAChB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,YAAY,CAAC;YACxE,kBAAkB,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yCAAyC,CAAC;SAC/F;KACF,EACD,KAAK,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,kBAAkB,EAAE,EAAE,EAAE;QAC/D,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAA;QACnD,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,EAAE;YACvD,KAAK;YACL,IAAI;YACJ,IAAI;YACJ,KAAK;YACL,iBAAiB,EAAE,kBAAkB;SACtC,CAAC,CAAA;QACF,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC,uBAAuB,CAAC,CAAA;QAC1D,MAAM,SAAS,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,cAAc,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QAC5E,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,EAAE,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAA;IACxF,CAAC,CAAC,CACH,CAAA;IAED,MAAM,CAAC,YAAY,CACjB,gBAAgB,EAChB;QACE,KAAK,EAAE,iBAAiB;QACxB,WAAW,EACT,wMAAwM;QAC1M,WAAW,EAAE;YACX,IAAI,EAAE,UAAU;YAChB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;YACjC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,iEAAiE,CAAC;YAC/G,IAAI,EAAE,UAAU;YAChB,KAAK,EAAE,WAAW;YAClB,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,mCAAmC,CAAC;YACnG,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,2CAA2C,CAAC;YACxG,UAAU,EAAE,eAAe;YAC3B,MAAM,EAAE,YAAY;SACrB;KACF,EACD,KAAK,CAAC,KAAK,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,EAAE,EAAE;QAC/C,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QAClD,MAAM,CAAC,GAAG,MAAM,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,EAAE;YACrD,GAAG,KAAK;YACR,GAAG,CAAC,MAAM,GAAG,CAAC,UAAU,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC;SAC7D,CAAC,CAAA;QACF,MAAM,QAAQ,GAAG,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAA;QACnH,OAAO,EAAE,CAAC,eAAe,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,QAAQ,EAAE,CAAC,CAAA;IAClE,CAAC,CAAC,CACH,CAAA;IAED,MAAM,CAAC,YAAY,CACjB,kBAAkB,EAClB;QACE,KAAK,EAAE,4BAA4B;QACnC,WAAW,EACT,kJAAkJ;QACpJ,WAAW,EAAE;YACX,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,qBAAqB,CAAC;YAChE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,+BAA+B,CAAC;YAChF,SAAS,EAAE,CAAC;iBACT,KAAK,CACJ,CAAC,CAAC,MAAM,CAAC;gBACP,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;gBACjC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC;gBACnC,IAAI,EAAE,UAAU;gBAChB,KAAK,EAAE,WAAW;aACnB,CAAC,CACH;iBACA,GAAG,CAAC,EAAE,CAAC;iBACP,QAAQ,EAAE;iBACV,QAAQ,CAAC,6CAA6C,CAAC;YAC1D,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yBAAyB,CAAC;YAC/D,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,+CAA+C,CAAC;YAC1G,IAAI,EAAE,UAAU;YAChB,KAAK,EAAE,WAAW;YAClB,UAAU,EAAE,eAAe;YAC3B,MAAM,EAAE,YAAY;SACrB;KACF,EACD,KAAK,CAAC,KAAK,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,EAAE,EAAE;QAC/C,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QAClD,MAAM,CAAC,GAAG,MAAM,UAAU,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,EAAE;YACvD,GAAG,KAAK;YACR,GAAG,CAAC,MAAM,GAAG,CAAC,UAAU,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC;SAC7D,CAAC,CAAA;QACF,MAAM,KAAK,GAAG,CAAC,eAAe,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,EAAE,CAAC,YAAY,CAAC,CAAA;QACnE,IAAI,CAAC,CAAC,SAAS,CAAC,MAAM;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,MAAM,sBAAsB,CAAC,CAAA;QAC/E,IAAI,CAAC,CAAC,MAAM;YAAE,KAAK,CAAC,IAAI,CAAC,gBAAgB,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,WAAW,CAAC,CAAA;QACzE,IAAI,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC,MAAM;YAAE,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC,MAAM,aAAa,CAAC,CAAA;QACvG,OAAO,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;IAC5B,CAAC,CAAC,CACH,CAAA;IAED,MAAM,CAAC,YAAY,CACjB,aAAa,EACb;QACE,KAAK,EAAE,YAAY;QACnB,WAAW,EAAE,sFAAsF;QACnG,WAAW,EAAE,EAAE;KAChB,EACD,KAAK,CAAC,KAAK,IAAI,EAAE;QACf,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAA;QACnD,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,CAAC,CAAA;QAC3D,OAAO,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAA;IAChG,CAAC,CAAC,CACH,CAAA;IAED,MAAM,CAAC,YAAY,CACjB,cAAc,EACd;QACE,KAAK,EAAE,iBAAiB;QACxB,WAAW,EACT,2KAA2K;QAC7K,WAAW,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,2CAA2C,CAAC,EAAE;KAC7F,EACD,KAAK,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE;QACrB,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAA;QACnD,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAA;QACnF,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC,sBAAsB,EAAE,oBAAoB,CAAC,CAAA;QACpE,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,aAAa,IAAI,CAAC,QAAQ,CAAC,MAAM,uBAAuB,CAAC,CAAC,CAAC,EAAE,CAAA;QACrG,OAAO,EAAE,CAAC,YAAY,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,IAAI,CAAC,KAAK,IAAI,QAAQ,EAAE,CAAC,CAAA;IACtE,CAAC,CAAC,CACH,CAAA;IAED,MAAM,CAAC,YAAY,CACjB,cAAc,EACd;QACE,KAAK,EAAE,cAAc;QACrB,WAAW,EAAE,+FAA+F;QAC5G,WAAW,EAAE,EAAE;KAChB,EACD,KAAK,CAAC,KAAK,IAAI,EAAE;QACf,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;QAC3B,MAAM,IAAI,GAAG,MAAM,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;QAC7C,MAAM,GAAG,CAAC,KAAK,EAAE,CAAA;QACjB,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAA;QACnD,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,CAAC,CAAA;QACpE,MAAM,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;QAC7D,MAAM,KAAK,GAAG;YACZ,UAAU,IAAI,CAAC,KAAK,OAAO,MAAM,CAAC,GAAG,aAAa,MAAM,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG;YACnF,YAAY,OAAO,CAAC,SAAS,SAAS,OAAO,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,YAAY,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,EAAE,GAAG;YACpJ,oBAAoB,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;SACvG,CAAA;QACD,IAAI,GAAG,CAAC,GAAG;YAAE,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,WAAW,GAAG,CAAC,GAAG,EAAE,CAAC,CAAA;QACrD,IAAI,qBAAqB,CAAC,MAAM,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,kDAAkD,CAAC,CAAA;QACjG,OAAO,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;IAC7B,CAAC,CAAC,CACH,CAAA;AACH,CAAC"}
|
package/dist/util.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
export declare class HttpError extends Error {
|
|
3
|
+
readonly status: number;
|
|
4
|
+
readonly code: string;
|
|
5
|
+
readonly extra: Record<string, unknown>;
|
|
6
|
+
constructor(status: number, code: string, message: string, extra?: Record<string, unknown>);
|
|
7
|
+
}
|
|
8
|
+
export declare function send(res: ServerResponse, status: number, body: unknown, headers?: Record<string, string>): void;
|
|
9
|
+
export declare function readJson(req: IncomingMessage, limit: number): Promise<unknown>;
|
|
10
|
+
export declare const header: (req: IncomingMessage, name: string) => string | undefined;
|
|
11
|
+
export declare const asRecord: (v: unknown) => Record<string, unknown>;
|
|
12
|
+
/** Client address; X-Forwarded-For is believed only when the operator says a proxy is in front. */
|
|
13
|
+
export declare function clientIp(req: IncomingMessage, trustProxy: boolean): string | undefined;
|
package/dist/util.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export class HttpError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
code;
|
|
4
|
+
extra;
|
|
5
|
+
constructor(status, code, message, extra = {}) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.extra = extra;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function send(res, status, body, headers = {}) {
|
|
13
|
+
const text = JSON.stringify(body);
|
|
14
|
+
res.writeHead(status, { 'content-type': 'application/json', 'content-length': Buffer.byteLength(text), ...headers });
|
|
15
|
+
res.end(text);
|
|
16
|
+
}
|
|
17
|
+
export async function readJson(req, limit) {
|
|
18
|
+
const chunks = [];
|
|
19
|
+
let size = 0;
|
|
20
|
+
for await (const chunk of req) {
|
|
21
|
+
size += chunk.length;
|
|
22
|
+
if (size > limit)
|
|
23
|
+
throw new HttpError(413, 'too_large', `Request body exceeds ${limit} bytes.`);
|
|
24
|
+
chunks.push(chunk);
|
|
25
|
+
}
|
|
26
|
+
if (size === 0)
|
|
27
|
+
return undefined;
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw new HttpError(400, 'bad_json', 'Request body is not valid JSON.');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export const header = (req, name) => {
|
|
36
|
+
const v = req.headers[name];
|
|
37
|
+
return (Array.isArray(v) ? v[0] : v)?.trim() || undefined;
|
|
38
|
+
};
|
|
39
|
+
export const asRecord = (v) => v && typeof v === 'object' && !Array.isArray(v) ? v : {};
|
|
40
|
+
/** Client address; X-Forwarded-For is believed only when the operator says a proxy is in front. */
|
|
41
|
+
export function clientIp(req, trustProxy) {
|
|
42
|
+
if (trustProxy) {
|
|
43
|
+
const forwarded = header(req, 'x-forwarded-for')?.split(',')[0]?.trim();
|
|
44
|
+
if (forwarded)
|
|
45
|
+
return forwarded;
|
|
46
|
+
}
|
|
47
|
+
return req.socket.remoteAddress?.replace(/^::ffff:/, '');
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=util.js.map
|
package/dist/util.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,SAAU,SAAQ,KAAK;IAEvB;IACA;IAEA;IAJX,YACW,MAAc,EACd,IAAY,EACrB,OAAe,EACN,QAAiC,EAAE;QAE5C,KAAK,CAAC,OAAO,CAAC,CAAA;QALL,WAAM,GAAN,MAAM,CAAQ;QACd,SAAI,GAAJ,IAAI,CAAQ;QAEZ,UAAK,GAAL,KAAK,CAA8B;IAG9C,CAAC;CACF;AAED,MAAM,UAAU,IAAI,CAAC,GAAmB,EAAE,MAAc,EAAE,IAAa,EAAE,UAAkC,EAAE;IAC3G,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;IACjC,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,GAAG,OAAO,EAAE,CAAC,CAAA;IACpH,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;AACf,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,GAAoB,EAAE,KAAa;IAChE,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,IAAI,IAAI,GAAG,CAAC,CAAA;IACZ,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,EAAE,CAAC;QAC9B,IAAI,IAAK,KAAgB,CAAC,MAAM,CAAA;QAChC,IAAI,IAAI,GAAG,KAAK;YAAE,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,EAAE,wBAAwB,KAAK,SAAS,CAAC,CAAA;QAC/F,MAAM,CAAC,IAAI,CAAC,KAAe,CAAC,CAAA;IAC9B,CAAC;IACD,IAAI,IAAI,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IAChC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAA;IAC3D,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,UAAU,EAAE,iCAAiC,CAAC,CAAA;IACzE,CAAC;AACH,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,GAAoB,EAAE,IAAY,EAAsB,EAAE;IAC/E,MAAM,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3B,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,SAAS,CAAA;AAC3D,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,CAAU,EAA2B,EAAE,CAC9D,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAE,CAA6B,CAAC,CAAC,CAAC,EAAE,CAAA;AAEvF,mGAAmG;AACnG,MAAM,UAAU,QAAQ,CAAC,GAAoB,EAAE,UAAmB;IAChE,IAAI,UAAU,EAAE,CAAC;QACf,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,EAAE,iBAAiB,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAA;QACvE,IAAI,SAAS;YAAE,OAAO,SAAS,CAAA;IACjC,CAAC;IACD,OAAO,GAAG,CAAC,MAAM,CAAC,aAAa,EAAE,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAA;AAC1D,CAAC"}
|