@hexis-ai/engram-server 0.1.6 → 0.2.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/adapters/memory-key-store.d.ts +1 -1
- package/dist/adapters/memory-key-store.js +1 -1
- package/dist/adapters/postgres-key-store.d.ts +1 -1
- package/dist/adapters/postgres-key-store.js +2 -2
- package/dist/admin.js +11 -5
- package/dist/context.d.ts +26 -0
- package/dist/context.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/key-store.d.ts +4 -4
- package/dist/routes/helpers.d.ts +22 -0
- package/dist/routes/helpers.js +32 -0
- package/dist/routes/persons.d.ts +13 -0
- package/dist/routes/persons.js +67 -0
- package/dist/routes/search.d.ts +9 -0
- package/dist/routes/search.js +40 -0
- package/dist/routes/sessions.d.ts +11 -0
- package/dist/routes/sessions.js +51 -0
- package/dist/schemas.d.ts +100 -0
- package/dist/schemas.js +108 -0
- package/dist/server.d.ts +7 -21
- package/dist/server.js +17 -156
- package/package.json +4 -4
|
@@ -20,5 +20,5 @@ export declare class InMemoryKeyStore implements KeyStore {
|
|
|
20
20
|
listKeys(workspaceId: string): Promise<ApiKeyInfo[]>;
|
|
21
21
|
revokeKey(workspaceId: string, keyId: string): Promise<void>;
|
|
22
22
|
resolveKey(rawKey: string): Promise<KeyResolution | null>;
|
|
23
|
-
|
|
23
|
+
registerKey(workspaceId: string, rawKey: string, name?: string): Promise<void>;
|
|
24
24
|
}
|
|
@@ -77,7 +77,7 @@ export class InMemoryKeyStore {
|
|
|
77
77
|
this.keys.set(id, { ...row, lastUsedAt: new Date().toISOString() });
|
|
78
78
|
return { workspaceId: row.workspaceId, keyId: row.id };
|
|
79
79
|
}
|
|
80
|
-
async
|
|
80
|
+
async registerKey(workspaceId, rawKey, name) {
|
|
81
81
|
if (!this.workspaces.has(workspaceId))
|
|
82
82
|
throw new Error("workspace_not_found");
|
|
83
83
|
const hash = hashKey(rawKey);
|
|
@@ -22,5 +22,5 @@ export declare class PostgresKeyStore implements KeyStore {
|
|
|
22
22
|
listKeys(workspaceId: string): Promise<ApiKeyInfo[]>;
|
|
23
23
|
revokeKey(workspaceId: string, keyId: string): Promise<void>;
|
|
24
24
|
resolveKey(rawKey: string): Promise<KeyResolution | null>;
|
|
25
|
-
|
|
25
|
+
registerKey(workspaceId: string, rawKey: string, name?: string): Promise<void>;
|
|
26
26
|
}
|
|
@@ -106,7 +106,7 @@ export class PostgresKeyStore {
|
|
|
106
106
|
`.catch(() => undefined);
|
|
107
107
|
return { workspaceId: row.workspace_id, keyId: row.id };
|
|
108
108
|
}
|
|
109
|
-
async
|
|
109
|
+
async registerKey(workspaceId, rawKey, name) {
|
|
110
110
|
const ws = await this.getWorkspace(workspaceId);
|
|
111
111
|
if (!ws)
|
|
112
112
|
throw new Error("workspace_not_found");
|
|
@@ -114,7 +114,7 @@ export class PostgresKeyStore {
|
|
|
114
114
|
const id = crypto.randomUUID();
|
|
115
115
|
await this.sql `
|
|
116
116
|
INSERT INTO engram_api_keys (id, workspace_id, key_hash, prefix, name)
|
|
117
|
-
VALUES (${id}, ${workspaceId}, ${hash}, ${keyPrefix(rawKey)}, ${name ??
|
|
117
|
+
VALUES (${id}, ${workspaceId}, ${hash}, ${keyPrefix(rawKey)}, ${name ?? null})
|
|
118
118
|
ON CONFLICT (key_hash) DO NOTHING
|
|
119
119
|
`;
|
|
120
120
|
}
|
package/dist/admin.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { isValidWorkspaceId } from "./key-store";
|
|
3
|
+
import { createWorkspaceSchema, issueKeySchema, parseJsonBody } from "./schemas";
|
|
3
4
|
/**
|
|
4
5
|
* Build the admin sub-router. Mount under `/admin/v1`.
|
|
5
6
|
*
|
|
@@ -18,9 +19,9 @@ export function createAdminRouter(opts) {
|
|
|
18
19
|
await next();
|
|
19
20
|
});
|
|
20
21
|
app.post("/workspaces", async (c) => {
|
|
21
|
-
const body =
|
|
22
|
-
if (body
|
|
23
|
-
return
|
|
22
|
+
const body = await parseJsonBody(c, createWorkspaceSchema);
|
|
23
|
+
if (body instanceof Response)
|
|
24
|
+
return body;
|
|
24
25
|
if (body.id !== undefined && !isValidWorkspaceId(body.id)) {
|
|
25
26
|
return c.json({ error: "invalid_workspace_id" }, 400);
|
|
26
27
|
}
|
|
@@ -67,9 +68,14 @@ export function createAdminRouter(opts) {
|
|
|
67
68
|
const ws = await opts.keyStore.getWorkspace(workspaceId);
|
|
68
69
|
if (!ws)
|
|
69
70
|
return c.json({ error: "workspace_not_found" }, 404);
|
|
70
|
-
|
|
71
|
+
// The body is optional here — an empty POST issues a key with no name.
|
|
72
|
+
const raw = await c.req.json().catch(() => ({}));
|
|
73
|
+
const parsed = issueKeySchema.safeParse(raw);
|
|
74
|
+
if (!parsed.success) {
|
|
75
|
+
return c.json({ error: "invalid_body", issues: parsed.error.issues }, 400);
|
|
76
|
+
}
|
|
71
77
|
const key = await opts.keyStore.issueKey(workspaceId, {
|
|
72
|
-
...(
|
|
78
|
+
...(parsed.data.name !== undefined ? { name: parsed.data.name } : {}),
|
|
73
79
|
});
|
|
74
80
|
return c.json(key);
|
|
75
81
|
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { StorageAdapter } from "./storage";
|
|
2
|
+
/**
|
|
3
|
+
* The per-request workspace context. Produced by an `AuthResolver` from a
|
|
4
|
+
* raw API key and stashed on the Hono context so route handlers can reach
|
|
5
|
+
* the tenant-scoped storage adapter without re-resolving the key.
|
|
6
|
+
*/
|
|
7
|
+
export interface WorkspaceContext {
|
|
8
|
+
workspaceId: string;
|
|
9
|
+
/** The storage adapter scoped to this workspace. */
|
|
10
|
+
storage: StorageAdapter;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Resolve an API key (raw `Authorization: Bearer <key>` or `X-Api-Key`
|
|
14
|
+
* token) into a workspace context. Throwing or returning null short-circuits
|
|
15
|
+
* the request to 401.
|
|
16
|
+
*/
|
|
17
|
+
export interface AuthResolver {
|
|
18
|
+
(apiKey: string): Promise<WorkspaceContext | null> | WorkspaceContext | null;
|
|
19
|
+
}
|
|
20
|
+
/** Hono environment shared by `createServer` and every `/v1` route module. */
|
|
21
|
+
export interface Env {
|
|
22
|
+
Variables: {
|
|
23
|
+
ctx: WorkspaceContext;
|
|
24
|
+
request_id: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { createServer, type CreateServerOptions
|
|
1
|
+
export { createServer, type CreateServerOptions } from "./server";
|
|
2
|
+
export { type AuthResolver, type WorkspaceContext } from "./context";
|
|
2
3
|
export { foldEvents, type StorageAdapter, type SessionRow, } from "./storage";
|
|
3
4
|
export { InMemoryAdapter } from "./adapters/memory";
|
|
4
5
|
export { PostgresAdapter, type PostgresAdapterOptions, type SqlClient, } from "./adapters/postgres";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { createServer
|
|
1
|
+
export { createServer } from "./server";
|
|
2
|
+
export {} from "./context";
|
|
2
3
|
export { foldEvents, } from "./storage";
|
|
3
4
|
export { InMemoryAdapter } from "./adapters/memory";
|
|
4
5
|
export { PostgresAdapter, } from "./adapters/postgres";
|
package/dist/key-store.d.ts
CHANGED
|
@@ -50,11 +50,11 @@ export interface KeyStore {
|
|
|
50
50
|
*/
|
|
51
51
|
resolveKey(rawKey: string): Promise<KeyResolution | null>;
|
|
52
52
|
/**
|
|
53
|
-
* Register a
|
|
54
|
-
*
|
|
55
|
-
*
|
|
53
|
+
* Register a caller-supplied raw key under a workspace, instead of minting
|
|
54
|
+
* a random one via `issueKey`. Used by the dev entrypoint to seed a known,
|
|
55
|
+
* fixed key. Idempotent on (workspace_id, key_hash).
|
|
56
56
|
*/
|
|
57
|
-
|
|
57
|
+
registerKey(workspaceId: string, rawKey: string, name?: string): Promise<void>;
|
|
58
58
|
}
|
|
59
59
|
export declare const KEY_PREFIX = "eng_";
|
|
60
60
|
export declare function generateRawKey(): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import type { Session } from "@hexis-ai/engram-core";
|
|
3
|
+
import type { PersonMap } from "@hexis-ai/engram-sdk";
|
|
4
|
+
import type { StorageAdapter } from "../storage";
|
|
5
|
+
/** Resolved server config threaded into each `/v1` route module. */
|
|
6
|
+
export interface RouteConfig {
|
|
7
|
+
/** Generates session ids when the client doesn't supply one. */
|
|
8
|
+
newSessionId: () => string;
|
|
9
|
+
/** Default `?limit=` when the client omits it. */
|
|
10
|
+
defaultListLimit: number;
|
|
11
|
+
/** Hard cap on `?limit=` (and the search corpus size). */
|
|
12
|
+
maxListLimit: number;
|
|
13
|
+
}
|
|
14
|
+
/** Parse and clamp a `?limit=` query param into `[1, max]`, falling back to `def`. */
|
|
15
|
+
export declare function clampLimit(c: Context, def: number, max: number): number;
|
|
16
|
+
/**
|
|
17
|
+
* Build a deduped person_id → PersonInfo map covering every person id
|
|
18
|
+
* referenced by `sessions` (participants ∪ viewable_by). Returns {} if
|
|
19
|
+
* there are no session rows. Persons that exist as ids in sessions but
|
|
20
|
+
* have no engram_persons row are simply absent from the map.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolvePersonMap(storage: StorageAdapter, sessions: Pick<Session, "participants" | "viewable_by">[]): Promise<PersonMap>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Parse and clamp a `?limit=` query param into `[1, max]`, falling back to `def`. */
|
|
2
|
+
export function clampLimit(c, def, max) {
|
|
3
|
+
const raw = c.req.query("limit");
|
|
4
|
+
if (!raw)
|
|
5
|
+
return def;
|
|
6
|
+
const n = parseInt(raw, 10);
|
|
7
|
+
if (isNaN(n) || n < 1)
|
|
8
|
+
return def;
|
|
9
|
+
return Math.min(n, max);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Build a deduped person_id → PersonInfo map covering every person id
|
|
13
|
+
* referenced by `sessions` (participants ∪ viewable_by). Returns {} if
|
|
14
|
+
* there are no session rows. Persons that exist as ids in sessions but
|
|
15
|
+
* have no engram_persons row are simply absent from the map.
|
|
16
|
+
*/
|
|
17
|
+
export async function resolvePersonMap(storage, sessions) {
|
|
18
|
+
const ids = new Set();
|
|
19
|
+
for (const s of sessions) {
|
|
20
|
+
for (const id of s.participants ?? [])
|
|
21
|
+
ids.add(id);
|
|
22
|
+
for (const id of s.viewable_by ?? [])
|
|
23
|
+
ids.add(id);
|
|
24
|
+
}
|
|
25
|
+
if (ids.size === 0)
|
|
26
|
+
return {};
|
|
27
|
+
const list = await storage.getPersons([...ids]);
|
|
28
|
+
const map = {};
|
|
29
|
+
for (const p of list)
|
|
30
|
+
map[p.id] = p;
|
|
31
|
+
return map;
|
|
32
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { Env } from "../context";
|
|
3
|
+
import { type RouteConfig } from "./helpers";
|
|
4
|
+
/**
|
|
5
|
+
* Person routes. Mount under `/v1`:
|
|
6
|
+
* POST /v1/persons create a person (server allocates the id)
|
|
7
|
+
* PUT /v1/persons/:id upsert at a host-supplied id
|
|
8
|
+
* PATCH /v1/persons/:id patch profile fields
|
|
9
|
+
* GET /v1/persons/:id fetch one person
|
|
10
|
+
* GET /v1/persons list / free-text search persons
|
|
11
|
+
* GET /v1/persons/:id/sessions sessions this person participates in / can view
|
|
12
|
+
*/
|
|
13
|
+
export declare function personsRoutes(cfg: RouteConfig): Hono<Env>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { parseJsonBody, personCreateSchema, personUpdateSchema } from "../schemas";
|
|
3
|
+
import { clampLimit, resolvePersonMap } from "./helpers";
|
|
4
|
+
/**
|
|
5
|
+
* Person routes. Mount under `/v1`:
|
|
6
|
+
* POST /v1/persons create a person (server allocates the id)
|
|
7
|
+
* PUT /v1/persons/:id upsert at a host-supplied id
|
|
8
|
+
* PATCH /v1/persons/:id patch profile fields
|
|
9
|
+
* GET /v1/persons/:id fetch one person
|
|
10
|
+
* GET /v1/persons list / free-text search persons
|
|
11
|
+
* GET /v1/persons/:id/sessions sessions this person participates in / can view
|
|
12
|
+
*/
|
|
13
|
+
export function personsRoutes(cfg) {
|
|
14
|
+
const app = new Hono();
|
|
15
|
+
app.post("/persons", async (c) => {
|
|
16
|
+
const body = await parseJsonBody(c, personCreateSchema);
|
|
17
|
+
if (body instanceof Response)
|
|
18
|
+
return body;
|
|
19
|
+
const p = await c.var.ctx.storage.createPerson(body);
|
|
20
|
+
return c.json(p, 201);
|
|
21
|
+
});
|
|
22
|
+
app.put("/persons/:id", async (c) => {
|
|
23
|
+
const id = c.req.param("id");
|
|
24
|
+
const body = await parseJsonBody(c, personCreateSchema);
|
|
25
|
+
if (body instanceof Response)
|
|
26
|
+
return body;
|
|
27
|
+
const p = await c.var.ctx.storage.upsertPerson(id, body);
|
|
28
|
+
return c.json(p);
|
|
29
|
+
});
|
|
30
|
+
app.patch("/persons/:id", async (c) => {
|
|
31
|
+
const id = c.req.param("id");
|
|
32
|
+
const body = await parseJsonBody(c, personUpdateSchema);
|
|
33
|
+
if (body instanceof Response)
|
|
34
|
+
return body;
|
|
35
|
+
const p = await c.var.ctx.storage.updatePerson(id, body);
|
|
36
|
+
if (!p)
|
|
37
|
+
return c.json({ error: "person_not_found" }, 404);
|
|
38
|
+
return c.json(p);
|
|
39
|
+
});
|
|
40
|
+
app.get("/persons/:id", async (c) => {
|
|
41
|
+
const id = c.req.param("id");
|
|
42
|
+
const p = await c.var.ctx.storage.getPerson(id);
|
|
43
|
+
if (!p)
|
|
44
|
+
return c.json({ error: "person_not_found" }, 404);
|
|
45
|
+
return c.json(p);
|
|
46
|
+
});
|
|
47
|
+
app.get("/persons", async (c) => {
|
|
48
|
+
const limit = clampLimit(c, cfg.defaultListLimit, cfg.maxListLimit);
|
|
49
|
+
const q = c.req.query("q") || undefined;
|
|
50
|
+
const persons = await c.var.ctx.storage.listPersons({ limit, q });
|
|
51
|
+
return c.json({ persons });
|
|
52
|
+
});
|
|
53
|
+
app.get("/persons/:id/sessions", async (c) => {
|
|
54
|
+
const id = c.req.param("id");
|
|
55
|
+
const limit = clampLimit(c, cfg.defaultListLimit, cfg.maxListLimit);
|
|
56
|
+
const channel = c.req.query("channel") || undefined;
|
|
57
|
+
const scope = c.req.query("scope") === "viewable" ? "viewable" : "participant";
|
|
58
|
+
const sessions = await c.var.ctx.storage.sessionsForPerson(id, {
|
|
59
|
+
limit,
|
|
60
|
+
channel,
|
|
61
|
+
scope,
|
|
62
|
+
});
|
|
63
|
+
const persons = await resolvePersonMap(c.var.ctx.storage, sessions);
|
|
64
|
+
return c.json({ sessions, persons });
|
|
65
|
+
});
|
|
66
|
+
return app;
|
|
67
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { Env } from "../context";
|
|
3
|
+
import { type RouteConfig } from "./helpers";
|
|
4
|
+
/**
|
|
5
|
+
* Search route. Mount under `/v1`:
|
|
6
|
+
* POST /v1/search score the workspace corpus against a query session
|
|
7
|
+
* (an existing session id, or an inline session)
|
|
8
|
+
*/
|
|
9
|
+
export declare function searchRoutes(cfg: RouteConfig): Hono<Env>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { buildIndex, search } from "@hexis-ai/engram-core";
|
|
3
|
+
import { parseJsonBody, searchRequestSchema } from "../schemas";
|
|
4
|
+
import { resolvePersonMap } from "./helpers";
|
|
5
|
+
/**
|
|
6
|
+
* Search route. Mount under `/v1`:
|
|
7
|
+
* POST /v1/search score the workspace corpus against a query session
|
|
8
|
+
* (an existing session id, or an inline session)
|
|
9
|
+
*/
|
|
10
|
+
export function searchRoutes(cfg) {
|
|
11
|
+
const app = new Hono();
|
|
12
|
+
app.post("/search", async (c) => {
|
|
13
|
+
const body = await parseJsonBody(c, searchRequestSchema);
|
|
14
|
+
if (body instanceof Response)
|
|
15
|
+
return body;
|
|
16
|
+
const corpus = await c.var.ctx.storage.listSessions({
|
|
17
|
+
limit: cfg.maxListLimit,
|
|
18
|
+
});
|
|
19
|
+
let queryQuery;
|
|
20
|
+
if ("sessionId" in body.query) {
|
|
21
|
+
const q = await c.var.ctx.storage.getSession(body.query.sessionId);
|
|
22
|
+
if (!q)
|
|
23
|
+
return c.json({ error: "query_session_not_found" }, 404);
|
|
24
|
+
queryQuery = q;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
queryQuery = body.query.session;
|
|
28
|
+
}
|
|
29
|
+
const index = buildIndex(corpus);
|
|
30
|
+
const opts = (body.options ?? {});
|
|
31
|
+
const queryParticipants = opts.queryParticipants ?? queryQuery.participants ?? [];
|
|
32
|
+
const results = search(queryQuery.steps, index, {
|
|
33
|
+
...opts,
|
|
34
|
+
queryParticipants,
|
|
35
|
+
});
|
|
36
|
+
const persons = await resolvePersonMap(c.var.ctx.storage, results.map((r) => r.session));
|
|
37
|
+
return c.json({ results, persons });
|
|
38
|
+
});
|
|
39
|
+
return app;
|
|
40
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { Env } from "../context";
|
|
3
|
+
import { type RouteConfig } from "./helpers";
|
|
4
|
+
/**
|
|
5
|
+
* Session routes. Mount under `/v1`:
|
|
6
|
+
* POST /v1/sessions create a session
|
|
7
|
+
* POST /v1/sessions/:id/events append events
|
|
8
|
+
* GET /v1/sessions/:id fetch one session + its persons map
|
|
9
|
+
* GET /v1/sessions list recent sessions + persons map
|
|
10
|
+
*/
|
|
11
|
+
export declare function sessionsRoutes(cfg: RouteConfig): Hono<Env>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { eventBatchSchema, parseJsonBody, sessionInitSchema } from "../schemas";
|
|
3
|
+
import { clampLimit, resolvePersonMap } from "./helpers";
|
|
4
|
+
/**
|
|
5
|
+
* Session routes. Mount under `/v1`:
|
|
6
|
+
* POST /v1/sessions create a session
|
|
7
|
+
* POST /v1/sessions/:id/events append events
|
|
8
|
+
* GET /v1/sessions/:id fetch one session + its persons map
|
|
9
|
+
* GET /v1/sessions list recent sessions + persons map
|
|
10
|
+
*/
|
|
11
|
+
export function sessionsRoutes(cfg) {
|
|
12
|
+
const app = new Hono();
|
|
13
|
+
app.post("/sessions", async (c) => {
|
|
14
|
+
const body = await parseJsonBody(c, sessionInitSchema);
|
|
15
|
+
if (body instanceof Response)
|
|
16
|
+
return body;
|
|
17
|
+
const id = body.id ?? cfg.newSessionId();
|
|
18
|
+
const createdAt = new Date().toISOString();
|
|
19
|
+
await c.var.ctx.storage.createSession({ ...body, id, createdAt });
|
|
20
|
+
return c.json({ id });
|
|
21
|
+
});
|
|
22
|
+
app.post("/sessions/:id/events", async (c) => {
|
|
23
|
+
const id = c.req.param("id");
|
|
24
|
+
const body = await parseJsonBody(c, eventBatchSchema);
|
|
25
|
+
if (body instanceof Response)
|
|
26
|
+
return body;
|
|
27
|
+
try {
|
|
28
|
+
await c.var.ctx.storage.appendEvents(id, body.events);
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
return c.json({ error: "session_not_found", message: e.message }, 404);
|
|
32
|
+
}
|
|
33
|
+
return c.body(null, 204);
|
|
34
|
+
});
|
|
35
|
+
app.get("/sessions/:id", async (c) => {
|
|
36
|
+
const id = c.req.param("id");
|
|
37
|
+
const s = await c.var.ctx.storage.getSession(id);
|
|
38
|
+
if (!s)
|
|
39
|
+
return c.json({ error: "session_not_found" }, 404);
|
|
40
|
+
const persons = await resolvePersonMap(c.var.ctx.storage, [s]);
|
|
41
|
+
return c.json({ session: s, persons });
|
|
42
|
+
});
|
|
43
|
+
app.get("/sessions", async (c) => {
|
|
44
|
+
const limit = clampLimit(c, cfg.defaultListLimit, cfg.maxListLimit);
|
|
45
|
+
const channel = c.req.query("channel") || undefined;
|
|
46
|
+
const sessions = await c.var.ctx.storage.listSessions({ limit, channel });
|
|
47
|
+
const persons = await resolvePersonMap(c.var.ctx.storage, sessions);
|
|
48
|
+
return c.json({ sessions, persons });
|
|
49
|
+
});
|
|
50
|
+
return app;
|
|
51
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schemas for every request body the server accepts. These are the
|
|
3
|
+
* trust boundary: handlers parse with `parseJsonBody` and never cast raw
|
|
4
|
+
* JSON straight onto a wire type.
|
|
5
|
+
*
|
|
6
|
+
* The wire types themselves live in `@hexis-ai/engram-sdk` (dependency-free,
|
|
7
|
+
* reusable by language ports). Each schema is `satisfies z.ZodType<…>` so a
|
|
8
|
+
* drift between a schema and its wire type fails the build here.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import type { Context } from "hono";
|
|
12
|
+
export declare const sessionInitSchema: z.ZodObject<{
|
|
13
|
+
id: z.ZodOptional<z.ZodString>;
|
|
14
|
+
title: z.ZodOptional<z.ZodString>;
|
|
15
|
+
channel: z.ZodOptional<z.ZodString>;
|
|
16
|
+
participants: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
17
|
+
viewable_by: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
18
|
+
}, z.core.$strip>;
|
|
19
|
+
export declare const sessionEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
20
|
+
type: z.ZodLiteral<"step">;
|
|
21
|
+
seq: z.ZodNumber;
|
|
22
|
+
at: z.ZodString;
|
|
23
|
+
tool: z.ZodString;
|
|
24
|
+
resources: z.ZodArray<z.ZodString>;
|
|
25
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
26
|
+
type: z.ZodLiteral<"participant">;
|
|
27
|
+
seq: z.ZodNumber;
|
|
28
|
+
at: z.ZodString;
|
|
29
|
+
personId: z.ZodString;
|
|
30
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
31
|
+
type: z.ZodLiteral<"title">;
|
|
32
|
+
seq: z.ZodNumber;
|
|
33
|
+
at: z.ZodString;
|
|
34
|
+
title: z.ZodString;
|
|
35
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
36
|
+
type: z.ZodLiteral<"end">;
|
|
37
|
+
seq: z.ZodNumber;
|
|
38
|
+
at: z.ZodString;
|
|
39
|
+
}, z.core.$strip>], "type">;
|
|
40
|
+
export declare const eventBatchSchema: z.ZodObject<{
|
|
41
|
+
events: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
42
|
+
type: z.ZodLiteral<"step">;
|
|
43
|
+
seq: z.ZodNumber;
|
|
44
|
+
at: z.ZodString;
|
|
45
|
+
tool: z.ZodString;
|
|
46
|
+
resources: z.ZodArray<z.ZodString>;
|
|
47
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
48
|
+
type: z.ZodLiteral<"participant">;
|
|
49
|
+
seq: z.ZodNumber;
|
|
50
|
+
at: z.ZodString;
|
|
51
|
+
personId: z.ZodString;
|
|
52
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
53
|
+
type: z.ZodLiteral<"title">;
|
|
54
|
+
seq: z.ZodNumber;
|
|
55
|
+
at: z.ZodString;
|
|
56
|
+
title: z.ZodString;
|
|
57
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
58
|
+
type: z.ZodLiteral<"end">;
|
|
59
|
+
seq: z.ZodNumber;
|
|
60
|
+
at: z.ZodString;
|
|
61
|
+
}, z.core.$strip>], "type">>;
|
|
62
|
+
}, z.core.$strip>;
|
|
63
|
+
export declare const personCreateSchema: z.ZodObject<{
|
|
64
|
+
display_name: z.ZodOptional<z.ZodString>;
|
|
65
|
+
}, z.core.$strip>;
|
|
66
|
+
export declare const personUpdateSchema: z.ZodObject<{
|
|
67
|
+
display_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
68
|
+
}, z.core.$strip>;
|
|
69
|
+
export declare const searchRequestSchema: z.ZodObject<{
|
|
70
|
+
query: z.ZodUnion<readonly [z.ZodObject<{
|
|
71
|
+
sessionId: z.ZodString;
|
|
72
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
73
|
+
session: z.ZodObject<{
|
|
74
|
+
steps: z.ZodArray<z.ZodObject<{
|
|
75
|
+
tool: z.ZodString;
|
|
76
|
+
resources: z.ZodArray<z.ZodString>;
|
|
77
|
+
}, z.core.$strip>>;
|
|
78
|
+
participants: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
79
|
+
}, z.core.$strip>;
|
|
80
|
+
}, z.core.$strip>]>;
|
|
81
|
+
options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
82
|
+
}, z.core.$strip>;
|
|
83
|
+
export declare const createWorkspaceSchema: z.ZodObject<{
|
|
84
|
+
id: z.ZodOptional<z.ZodString>;
|
|
85
|
+
name: z.ZodOptional<z.ZodString>;
|
|
86
|
+
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
87
|
+
issueKey: z.ZodOptional<z.ZodBoolean>;
|
|
88
|
+
keyName: z.ZodOptional<z.ZodString>;
|
|
89
|
+
}, z.core.$strip>;
|
|
90
|
+
export declare const issueKeySchema: z.ZodObject<{
|
|
91
|
+
name: z.ZodOptional<z.ZodString>;
|
|
92
|
+
}, z.core.$strip>;
|
|
93
|
+
/**
|
|
94
|
+
* Read and validate a JSON request body. Returns the parsed value, or a
|
|
95
|
+
* `Response` (400) the caller should return as-is:
|
|
96
|
+
*
|
|
97
|
+
* const body = await parseJsonBody(c, sessionInitSchema);
|
|
98
|
+
* if (body instanceof Response) return body;
|
|
99
|
+
*/
|
|
100
|
+
export declare function parseJsonBody<T>(c: Context, schema: z.ZodType<T>): Promise<T | Response>;
|
package/dist/schemas.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schemas for every request body the server accepts. These are the
|
|
3
|
+
* trust boundary: handlers parse with `parseJsonBody` and never cast raw
|
|
4
|
+
* JSON straight onto a wire type.
|
|
5
|
+
*
|
|
6
|
+
* The wire types themselves live in `@hexis-ai/engram-sdk` (dependency-free,
|
|
7
|
+
* reusable by language ports). Each schema is `satisfies z.ZodType<…>` so a
|
|
8
|
+
* drift between a schema and its wire type fails the build here.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
// --- Sessions --------------------------------------------------------
|
|
12
|
+
export const sessionInitSchema = z.object({
|
|
13
|
+
id: z.string().min(1).optional(),
|
|
14
|
+
title: z.string().optional(),
|
|
15
|
+
channel: z.string().optional(),
|
|
16
|
+
participants: z.array(z.string()).optional(),
|
|
17
|
+
viewable_by: z.array(z.string()).optional(),
|
|
18
|
+
});
|
|
19
|
+
const stepEventSchema = z.object({
|
|
20
|
+
type: z.literal("step"),
|
|
21
|
+
seq: z.number().int().nonnegative(),
|
|
22
|
+
at: z.string(),
|
|
23
|
+
tool: z.string(),
|
|
24
|
+
resources: z.array(z.string()),
|
|
25
|
+
});
|
|
26
|
+
const participantEventSchema = z.object({
|
|
27
|
+
type: z.literal("participant"),
|
|
28
|
+
seq: z.number().int().nonnegative(),
|
|
29
|
+
at: z.string(),
|
|
30
|
+
personId: z.string(),
|
|
31
|
+
});
|
|
32
|
+
const titleEventSchema = z.object({
|
|
33
|
+
type: z.literal("title"),
|
|
34
|
+
seq: z.number().int().nonnegative(),
|
|
35
|
+
at: z.string(),
|
|
36
|
+
title: z.string(),
|
|
37
|
+
});
|
|
38
|
+
const endEventSchema = z.object({
|
|
39
|
+
type: z.literal("end"),
|
|
40
|
+
seq: z.number().int().nonnegative(),
|
|
41
|
+
at: z.string(),
|
|
42
|
+
});
|
|
43
|
+
export const sessionEventSchema = z.discriminatedUnion("type", [
|
|
44
|
+
stepEventSchema,
|
|
45
|
+
participantEventSchema,
|
|
46
|
+
titleEventSchema,
|
|
47
|
+
endEventSchema,
|
|
48
|
+
]);
|
|
49
|
+
export const eventBatchSchema = z.object({
|
|
50
|
+
events: z.array(sessionEventSchema),
|
|
51
|
+
});
|
|
52
|
+
// --- Persons ---------------------------------------------------------
|
|
53
|
+
export const personCreateSchema = z.object({
|
|
54
|
+
display_name: z.string().optional(),
|
|
55
|
+
});
|
|
56
|
+
export const personUpdateSchema = z.object({
|
|
57
|
+
display_name: z.string().nullable().optional(),
|
|
58
|
+
});
|
|
59
|
+
// --- Search ----------------------------------------------------------
|
|
60
|
+
const searchQuerySchema = z.union([
|
|
61
|
+
z.object({ sessionId: z.string().min(1) }),
|
|
62
|
+
z.object({
|
|
63
|
+
session: z.object({
|
|
64
|
+
steps: z.array(z.object({ tool: z.string(), resources: z.array(z.string()) })),
|
|
65
|
+
participants: z.array(z.string()).optional(),
|
|
66
|
+
}),
|
|
67
|
+
}),
|
|
68
|
+
]);
|
|
69
|
+
export const searchRequestSchema = z.object({
|
|
70
|
+
query: searchQuerySchema,
|
|
71
|
+
// `options` is forwarded as-is to engram-core's pure `search()`, which
|
|
72
|
+
// tolerates partial/absent options. Only the query (which drives storage
|
|
73
|
+
// lookups) needs strict validation here.
|
|
74
|
+
options: z.record(z.string(), z.unknown()).optional(),
|
|
75
|
+
});
|
|
76
|
+
// --- Admin -----------------------------------------------------------
|
|
77
|
+
export const createWorkspaceSchema = z.object({
|
|
78
|
+
id: z.string().optional(),
|
|
79
|
+
name: z.string().optional(),
|
|
80
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
81
|
+
issueKey: z.boolean().optional(),
|
|
82
|
+
keyName: z.string().optional(),
|
|
83
|
+
});
|
|
84
|
+
export const issueKeySchema = z.object({
|
|
85
|
+
name: z.string().optional(),
|
|
86
|
+
});
|
|
87
|
+
// --- Helper ----------------------------------------------------------
|
|
88
|
+
/**
|
|
89
|
+
* Read and validate a JSON request body. Returns the parsed value, or a
|
|
90
|
+
* `Response` (400) the caller should return as-is:
|
|
91
|
+
*
|
|
92
|
+
* const body = await parseJsonBody(c, sessionInitSchema);
|
|
93
|
+
* if (body instanceof Response) return body;
|
|
94
|
+
*/
|
|
95
|
+
export async function parseJsonBody(c, schema) {
|
|
96
|
+
let raw;
|
|
97
|
+
try {
|
|
98
|
+
raw = await c.req.json();
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return c.json({ error: "invalid_json" }, 400);
|
|
102
|
+
}
|
|
103
|
+
const result = schema.safeParse(raw);
|
|
104
|
+
if (!result.success) {
|
|
105
|
+
return c.json({ error: "invalid_body", issues: result.error.issues }, 400);
|
|
106
|
+
}
|
|
107
|
+
return result.data;
|
|
108
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,20 +1,8 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import type {
|
|
2
|
+
import type { AuthResolver, Env } from "./context";
|
|
3
3
|
import { type AdminOptions } from "./admin";
|
|
4
|
-
/**
|
|
5
|
-
* Resolve an API key (raw `Authorization: Bearer <key>` token) into a
|
|
6
|
-
* workspace context. Throwing or returning null short-circuits to 401.
|
|
7
|
-
*/
|
|
8
|
-
export interface AuthResolver {
|
|
9
|
-
(apiKey: string): Promise<WorkspaceContext | null> | WorkspaceContext | null;
|
|
10
|
-
}
|
|
11
|
-
export interface WorkspaceContext {
|
|
12
|
-
workspaceId: string;
|
|
13
|
-
/** The storage adapter scoped to this workspace. */
|
|
14
|
-
storage: StorageAdapter;
|
|
15
|
-
}
|
|
16
4
|
export interface CreateServerOptions {
|
|
17
|
-
/** Resolves Bearer tokens into workspace contexts. */
|
|
5
|
+
/** Resolves Bearer / X-Api-Key tokens into workspace contexts. */
|
|
18
6
|
auth: AuthResolver;
|
|
19
7
|
/**
|
|
20
8
|
* Generates session ids. Defaults to `crypto.randomUUID()`.
|
|
@@ -32,11 +20,9 @@ export interface CreateServerOptions {
|
|
|
32
20
|
*/
|
|
33
21
|
admin?: AdminOptions;
|
|
34
22
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
23
|
+
/**
|
|
24
|
+
* Build the engram HTTP app. Wiring only: this sets up cross-cutting
|
|
25
|
+
* middleware (request id + access log), the workspace auth gate, and mounts
|
|
26
|
+
* the `/v1` route modules (`routes/*`) and the optional `/admin/v1` router.
|
|
27
|
+
*/
|
|
41
28
|
export declare function createServer(opts: CreateServerOptions): Hono<Env>;
|
|
42
|
-
export {};
|
package/dist/server.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import { buildIndex, search, } from "@hexis-ai/engram-core";
|
|
3
2
|
import { log, newRequestId } from "./logger";
|
|
4
3
|
import { createAdminRouter } from "./admin";
|
|
4
|
+
import { sessionsRoutes } from "./routes/sessions";
|
|
5
|
+
import { personsRoutes } from "./routes/persons";
|
|
6
|
+
import { searchRoutes } from "./routes/search";
|
|
7
|
+
/**
|
|
8
|
+
* Build the engram HTTP app. Wiring only: this sets up cross-cutting
|
|
9
|
+
* middleware (request id + access log), the workspace auth gate, and mounts
|
|
10
|
+
* the `/v1` route modules (`routes/*`) and the optional `/admin/v1` router.
|
|
11
|
+
*/
|
|
5
12
|
export function createServer(opts) {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
13
|
+
const cfg = {
|
|
14
|
+
newSessionId: opts.newSessionId ?? (() => crypto.randomUUID()),
|
|
15
|
+
defaultListLimit: opts.defaultListLimit ?? 100,
|
|
16
|
+
maxListLimit: opts.maxListLimit ?? 500,
|
|
17
|
+
};
|
|
9
18
|
const app = new Hono();
|
|
10
19
|
// Request id + access log middleware. Runs for every route.
|
|
11
20
|
app.use("*", async (c, next) => {
|
|
@@ -54,6 +63,7 @@ export function createServer(opts) {
|
|
|
54
63
|
if (opts.admin) {
|
|
55
64
|
app.route("/admin/v1", createAdminRouter(opts.admin));
|
|
56
65
|
}
|
|
66
|
+
// Workspace auth gate — every `/v1/*` route runs behind this.
|
|
57
67
|
app.use("/v1/*", async (c, next) => {
|
|
58
68
|
const apiKey = c.req.header("x-api-key") ??
|
|
59
69
|
c.req.header("authorization")?.match(/^Bearer\s+(.+)$/i)?.[1];
|
|
@@ -68,157 +78,8 @@ export function createServer(opts) {
|
|
|
68
78
|
// Identity probe — echoes the workspace the caller's key resolves to.
|
|
69
79
|
// Used by clients (e.g. engram-web) to label which tenant they're viewing.
|
|
70
80
|
app.get("/v1/me", (c) => c.json({ workspaceId: c.var.ctx.workspaceId }));
|
|
71
|
-
|
|
72
|
-
app.
|
|
73
|
-
|
|
74
|
-
if (body === null)
|
|
75
|
-
return c.json({ error: "invalid_json" }, 400);
|
|
76
|
-
const id = body.id ?? newId();
|
|
77
|
-
const createdAt = new Date().toISOString();
|
|
78
|
-
await c.var.ctx.storage.createSession({
|
|
79
|
-
...body,
|
|
80
|
-
id,
|
|
81
|
-
createdAt,
|
|
82
|
-
});
|
|
83
|
-
return c.json({ id });
|
|
84
|
-
});
|
|
85
|
-
app.post("/v1/sessions/:id/events", async (c) => {
|
|
86
|
-
const id = c.req.param("id");
|
|
87
|
-
const body = (await c.req.json().catch(() => null));
|
|
88
|
-
if (!body || !Array.isArray(body.events)) {
|
|
89
|
-
return c.json({ error: "events_array_required" }, 400);
|
|
90
|
-
}
|
|
91
|
-
try {
|
|
92
|
-
await c.var.ctx.storage.appendEvents(id, body.events);
|
|
93
|
-
}
|
|
94
|
-
catch (e) {
|
|
95
|
-
return c.json({ error: "session_not_found", message: e.message }, 404);
|
|
96
|
-
}
|
|
97
|
-
return c.body(null, 204);
|
|
98
|
-
});
|
|
99
|
-
app.get("/v1/sessions/:id", async (c) => {
|
|
100
|
-
const id = c.req.param("id");
|
|
101
|
-
const s = await c.var.ctx.storage.getSession(id);
|
|
102
|
-
if (!s)
|
|
103
|
-
return c.json({ error: "session_not_found" }, 404);
|
|
104
|
-
const persons = await resolvePersonMap(c.var.ctx.storage, [s]);
|
|
105
|
-
return c.json({ session: s, persons });
|
|
106
|
-
});
|
|
107
|
-
app.get("/v1/sessions", async (c) => {
|
|
108
|
-
const limit = clampLimit(c, defaultLimit, maxLimit);
|
|
109
|
-
const channel = c.req.query("channel") || undefined;
|
|
110
|
-
const sessions = await c.var.ctx.storage.listSessions({ limit, channel });
|
|
111
|
-
const persons = await resolvePersonMap(c.var.ctx.storage, sessions);
|
|
112
|
-
return c.json({ sessions, persons });
|
|
113
|
-
});
|
|
114
|
-
app.post("/v1/search", async (c) => {
|
|
115
|
-
const body = (await c.req.json().catch(() => null));
|
|
116
|
-
if (!body || !body.query)
|
|
117
|
-
return c.json({ error: "query_required" }, 400);
|
|
118
|
-
const corpus = await c.var.ctx.storage.listSessions({ limit: maxLimit });
|
|
119
|
-
let queryQuery;
|
|
120
|
-
if ("sessionId" in body.query) {
|
|
121
|
-
const q = await c.var.ctx.storage.getSession(body.query.sessionId);
|
|
122
|
-
if (!q)
|
|
123
|
-
return c.json({ error: "query_session_not_found" }, 404);
|
|
124
|
-
queryQuery = q;
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
queryQuery = body.query.session;
|
|
128
|
-
}
|
|
129
|
-
const index = buildIndex(corpus);
|
|
130
|
-
const opts = body.options ?? {};
|
|
131
|
-
const queryParticipants = opts.queryParticipants ?? queryQuery.participants ?? [];
|
|
132
|
-
const results = search(queryQuery.steps, index, {
|
|
133
|
-
...opts,
|
|
134
|
-
queryParticipants,
|
|
135
|
-
});
|
|
136
|
-
const persons = await resolvePersonMap(c.var.ctx.storage, results.map((r) => r.session));
|
|
137
|
-
return c.json({ results, persons });
|
|
138
|
-
});
|
|
139
|
-
// --- Persons ---------------------------------------------------------
|
|
140
|
-
app.post("/v1/persons", async (c) => {
|
|
141
|
-
const body = (await c.req.json().catch(() => null));
|
|
142
|
-
if (body === null)
|
|
143
|
-
return c.json({ error: "invalid_json" }, 400);
|
|
144
|
-
const p = await c.var.ctx.storage.createPerson(body);
|
|
145
|
-
return c.json(p, 201);
|
|
146
|
-
});
|
|
147
|
-
app.put("/v1/persons/:id", async (c) => {
|
|
148
|
-
const id = c.req.param("id");
|
|
149
|
-
const body = (await c.req.json().catch(() => null));
|
|
150
|
-
if (body === null)
|
|
151
|
-
return c.json({ error: "invalid_json" }, 400);
|
|
152
|
-
const p = await c.var.ctx.storage.upsertPerson(id, body);
|
|
153
|
-
return c.json(p);
|
|
154
|
-
});
|
|
155
|
-
app.patch("/v1/persons/:id", async (c) => {
|
|
156
|
-
const id = c.req.param("id");
|
|
157
|
-
const body = (await c.req.json().catch(() => null));
|
|
158
|
-
if (body === null)
|
|
159
|
-
return c.json({ error: "invalid_json" }, 400);
|
|
160
|
-
const p = await c.var.ctx.storage.updatePerson(id, body);
|
|
161
|
-
if (!p)
|
|
162
|
-
return c.json({ error: "person_not_found" }, 404);
|
|
163
|
-
return c.json(p);
|
|
164
|
-
});
|
|
165
|
-
app.get("/v1/persons/:id", async (c) => {
|
|
166
|
-
const id = c.req.param("id");
|
|
167
|
-
const p = await c.var.ctx.storage.getPerson(id);
|
|
168
|
-
if (!p)
|
|
169
|
-
return c.json({ error: "person_not_found" }, 404);
|
|
170
|
-
return c.json(p);
|
|
171
|
-
});
|
|
172
|
-
app.get("/v1/persons", async (c) => {
|
|
173
|
-
const limit = clampLimit(c, defaultLimit, maxLimit);
|
|
174
|
-
const q = c.req.query("q") || undefined;
|
|
175
|
-
const persons = await c.var.ctx.storage.listPersons({ limit, q });
|
|
176
|
-
return c.json({ persons });
|
|
177
|
-
});
|
|
178
|
-
app.get("/v1/persons/:id/sessions", async (c) => {
|
|
179
|
-
const id = c.req.param("id");
|
|
180
|
-
const limit = clampLimit(c, defaultLimit, maxLimit);
|
|
181
|
-
const channel = c.req.query("channel") || undefined;
|
|
182
|
-
const scopeRaw = c.req.query("scope");
|
|
183
|
-
const scope = scopeRaw === "viewable" ? "viewable" : "participant";
|
|
184
|
-
const sessions = await c.var.ctx.storage.sessionsForPerson(id, {
|
|
185
|
-
limit,
|
|
186
|
-
channel,
|
|
187
|
-
scope,
|
|
188
|
-
});
|
|
189
|
-
const persons = await resolvePersonMap(c.var.ctx.storage, sessions);
|
|
190
|
-
return c.json({ sessions, persons });
|
|
191
|
-
});
|
|
81
|
+
app.route("/v1", sessionsRoutes(cfg));
|
|
82
|
+
app.route("/v1", personsRoutes(cfg));
|
|
83
|
+
app.route("/v1", searchRoutes(cfg));
|
|
192
84
|
return app;
|
|
193
85
|
}
|
|
194
|
-
function clampLimit(c, def, max) {
|
|
195
|
-
const raw = c.req.query("limit");
|
|
196
|
-
if (!raw)
|
|
197
|
-
return def;
|
|
198
|
-
const n = parseInt(raw, 10);
|
|
199
|
-
if (isNaN(n) || n < 1)
|
|
200
|
-
return def;
|
|
201
|
-
return Math.min(n, max);
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* Build a deduped person_id → PersonInfo map covering every person id
|
|
205
|
-
* referenced by `sessions` (participants ∪ viewable_by). Returns {} if
|
|
206
|
-
* there are no session rows. Persons that exist as ids in sessions but
|
|
207
|
-
* have no engram_persons row are simply absent from the map.
|
|
208
|
-
*/
|
|
209
|
-
async function resolvePersonMap(storage, sessions) {
|
|
210
|
-
const ids = new Set();
|
|
211
|
-
for (const s of sessions) {
|
|
212
|
-
for (const id of s.participants ?? [])
|
|
213
|
-
ids.add(id);
|
|
214
|
-
for (const id of s.viewable_by ?? [])
|
|
215
|
-
ids.add(id);
|
|
216
|
-
}
|
|
217
|
-
if (ids.size === 0)
|
|
218
|
-
return {};
|
|
219
|
-
const list = await storage.getPersons([...ids]);
|
|
220
|
-
const map = {};
|
|
221
|
-
for (const p of list)
|
|
222
|
-
map[p.id] = p;
|
|
223
|
-
return map;
|
|
224
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hexis-ai/engram-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Engram server: ingest agent session events, persist via a pluggable adapter, expose search.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"engram",
|
|
@@ -41,7 +41,6 @@
|
|
|
41
41
|
],
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
44
|
-
"prepack": "bun run build",
|
|
45
44
|
"pack": "bun run build && bun pm pack",
|
|
46
45
|
"test": "bun test",
|
|
47
46
|
"type-check": "tsc --noEmit",
|
|
@@ -49,8 +48,9 @@
|
|
|
49
48
|
},
|
|
50
49
|
"dependencies": {
|
|
51
50
|
"@hexis-ai/engram-core": "^0.1.5",
|
|
52
|
-
"@hexis-ai/engram-sdk": "^0.
|
|
53
|
-
"hono": "^4.6.0"
|
|
51
|
+
"@hexis-ai/engram-sdk": "^0.2.0",
|
|
52
|
+
"hono": "^4.6.0",
|
|
53
|
+
"zod": "^4.0.0"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"postgres": "^3.4.0"
|