@hexis-ai/engram-server 0.9.0 → 0.10.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.d.ts +1 -0
- package/dist/adapters/memory.js +6 -0
- package/dist/adapters/postgres.d.ts +1 -0
- package/dist/adapters/postgres.js +13 -0
- package/dist/routes/aliases.d.ts +24 -0
- package/dist/routes/aliases.js +41 -0
- package/dist/server.js +3 -0
- package/dist/storage.d.ts +10 -0
- package/package.json +2 -2
|
@@ -49,6 +49,7 @@ export declare class InMemoryAdapter implements StorageAdapter {
|
|
|
49
49
|
name: string;
|
|
50
50
|
} & AliasUpsert): Promise<AliasInfo | null>;
|
|
51
51
|
listAliases(personId: string): Promise<AliasInfo[]>;
|
|
52
|
+
findAliasesByName(name: string): Promise<AliasInfo[]>;
|
|
52
53
|
upsertIdentity(ref: string, input: IdentityUpsert): Promise<IdentityInfo | null>;
|
|
53
54
|
getIdentityByRef(ref: string): Promise<IdentityInfo | null>;
|
|
54
55
|
listIdentitiesByPerson(personId: string): Promise<IdentityInfo[]>;
|
package/dist/adapters/memory.js
CHANGED
|
@@ -287,6 +287,12 @@ export class InMemoryAdapter {
|
|
|
287
287
|
matches.sort((a, b) => b.last_used.localeCompare(a.last_used));
|
|
288
288
|
return matches;
|
|
289
289
|
}
|
|
290
|
+
async findAliasesByName(name) {
|
|
291
|
+
const lower = name.toLowerCase();
|
|
292
|
+
const matches = [...this.aliases.values()].filter((a) => a.name.toLowerCase() === lower);
|
|
293
|
+
matches.sort((a, b) => b.last_used.localeCompare(a.last_used));
|
|
294
|
+
return matches;
|
|
295
|
+
}
|
|
290
296
|
// --- Identities ---------------------------------------------------
|
|
291
297
|
async upsertIdentity(ref, input) {
|
|
292
298
|
if (!this.persons.has(input.person_id))
|
|
@@ -62,6 +62,7 @@ export declare class PostgresAdapter implements StorageAdapter {
|
|
|
62
62
|
name: string;
|
|
63
63
|
} & AliasUpsert): Promise<AliasInfo | null>;
|
|
64
64
|
listAliases(personId: string): Promise<AliasInfo[]>;
|
|
65
|
+
findAliasesByName(name: string): Promise<AliasInfo[]>;
|
|
65
66
|
upsertIdentity(ref: string, input: IdentityUpsert): Promise<IdentityInfo | null>;
|
|
66
67
|
getIdentityByRef(ref: string): Promise<IdentityInfo | null>;
|
|
67
68
|
listIdentitiesByPerson(personId: string): Promise<IdentityInfo[]>;
|
|
@@ -367,6 +367,19 @@ export class PostgresAdapter {
|
|
|
367
367
|
`;
|
|
368
368
|
return rows.map(toAliasInfo);
|
|
369
369
|
}
|
|
370
|
+
async findAliasesByName(name) {
|
|
371
|
+
// The `name_lower` column is a STORED generated lower(name) backed
|
|
372
|
+
// by `idx_engram_aliases_name_lower (workspace_id, name_lower)`, so
|
|
373
|
+
// this lookup is index-only regardless of casing in the input.
|
|
374
|
+
const rows = await this.sql `
|
|
375
|
+
SELECT person_id, name, caller, usage_count, last_used, created_at, updated_at
|
|
376
|
+
FROM engram_aliases
|
|
377
|
+
WHERE workspace_id = ${this.workspaceId}
|
|
378
|
+
AND name_lower = ${name.toLowerCase()}
|
|
379
|
+
ORDER BY last_used DESC
|
|
380
|
+
`;
|
|
381
|
+
return rows.map(toAliasInfo);
|
|
382
|
+
}
|
|
370
383
|
// --- Identities ---------------------------------------------------
|
|
371
384
|
async upsertIdentity(ref, input) {
|
|
372
385
|
// Pre-check rather than rely on the FK so unknown persons return
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { Env } from "../context";
|
|
3
|
+
import { type RouteConfig } from "./helpers";
|
|
4
|
+
/**
|
|
5
|
+
* Workspace-wide alias lookup. Mount under `/v1`:
|
|
6
|
+
* GET /v1/aliases?name=<name>
|
|
7
|
+
*
|
|
8
|
+
* The personal-scoped endpoints (`/v1/persons/:id/aliases`) cover
|
|
9
|
+
* "what aliases does this person have?". This top-level route covers
|
|
10
|
+
* the inverse — "which person(s) does this name resolve to?" — which
|
|
11
|
+
* hosts use to ground a free-text "who is X?" against the alias
|
|
12
|
+
* registry without pre-knowing the owning person.
|
|
13
|
+
*
|
|
14
|
+
* The match is case-insensitive (the alias table is keyed by
|
|
15
|
+
* `lower(name)`). Multiple persons in the same workspace can share an
|
|
16
|
+
* alias, so the response is an array ordered by `last_used` desc; the
|
|
17
|
+
* caller picks the most recently active match (or applies its own
|
|
18
|
+
* tie-breaker like caller/usage_count).
|
|
19
|
+
*
|
|
20
|
+
* Returns 400 when `?name=` is missing — alias lookups have no useful
|
|
21
|
+
* "list all aliases" semantics, so dropping the query param is treated
|
|
22
|
+
* as a request error rather than a full scan.
|
|
23
|
+
*/
|
|
24
|
+
export declare function aliasesRoutes(_cfg: RouteConfig): Hono<Env>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { resolvePersonMap } from "./helpers";
|
|
3
|
+
/**
|
|
4
|
+
* Workspace-wide alias lookup. Mount under `/v1`:
|
|
5
|
+
* GET /v1/aliases?name=<name>
|
|
6
|
+
*
|
|
7
|
+
* The personal-scoped endpoints (`/v1/persons/:id/aliases`) cover
|
|
8
|
+
* "what aliases does this person have?". This top-level route covers
|
|
9
|
+
* the inverse — "which person(s) does this name resolve to?" — which
|
|
10
|
+
* hosts use to ground a free-text "who is X?" against the alias
|
|
11
|
+
* registry without pre-knowing the owning person.
|
|
12
|
+
*
|
|
13
|
+
* The match is case-insensitive (the alias table is keyed by
|
|
14
|
+
* `lower(name)`). Multiple persons in the same workspace can share an
|
|
15
|
+
* alias, so the response is an array ordered by `last_used` desc; the
|
|
16
|
+
* caller picks the most recently active match (or applies its own
|
|
17
|
+
* tie-breaker like caller/usage_count).
|
|
18
|
+
*
|
|
19
|
+
* Returns 400 when `?name=` is missing — alias lookups have no useful
|
|
20
|
+
* "list all aliases" semantics, so dropping the query param is treated
|
|
21
|
+
* as a request error rather than a full scan.
|
|
22
|
+
*/
|
|
23
|
+
export function aliasesRoutes(_cfg) {
|
|
24
|
+
const app = new Hono();
|
|
25
|
+
app.get("/aliases", async (c) => {
|
|
26
|
+
const name = c.req.query("name");
|
|
27
|
+
if (!name) {
|
|
28
|
+
return c.json({
|
|
29
|
+
error: "name_required",
|
|
30
|
+
message: "GET /v1/aliases requires a ?name= query parameter",
|
|
31
|
+
}, 400);
|
|
32
|
+
}
|
|
33
|
+
const aliases = await c.var.ctx.storage.findAliasesByName(name);
|
|
34
|
+
// Inline the persons map so the caller can render a "candidate
|
|
35
|
+
// picker" UI without a second round-trip per match. Same envelope
|
|
36
|
+
// shape as the session list endpoints.
|
|
37
|
+
const persons = await resolvePersonMap(c.var.ctx.storage, aliases.map((a) => ({ participants: [a.person_id], viewable_by: [] })));
|
|
38
|
+
return c.json({ aliases, persons });
|
|
39
|
+
});
|
|
40
|
+
return app;
|
|
41
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { log, newRequestId } from "./logger";
|
|
3
3
|
import { createAdminRouter } from "./admin";
|
|
4
|
+
import { aliasesRoutes } from "./routes/aliases";
|
|
4
5
|
import { sessionsRoutes } from "./routes/sessions";
|
|
5
6
|
import { personsRoutes } from "./routes/persons";
|
|
6
7
|
import { identitiesRoutes } from "./routes/identities";
|
|
@@ -61,6 +62,7 @@ export function createServer(opts) {
|
|
|
61
62
|
personSessions: "GET /v1/persons/:id/sessions",
|
|
62
63
|
personAliases: "GET /v1/persons/:id/aliases",
|
|
63
64
|
upsertAlias: "PUT /v1/persons/:id/aliases/:name",
|
|
65
|
+
findAliasesByName: "GET /v1/aliases?name=…",
|
|
64
66
|
identityByRef: "GET/PUT /v1/identities/:ref",
|
|
65
67
|
personIdentities: "GET /v1/persons/:id/identities",
|
|
66
68
|
},
|
|
@@ -88,6 +90,7 @@ export function createServer(opts) {
|
|
|
88
90
|
app.route("/v1", sessionsRoutes(cfg));
|
|
89
91
|
app.route("/v1", personsRoutes(cfg));
|
|
90
92
|
app.route("/v1", identitiesRoutes(cfg));
|
|
93
|
+
app.route("/v1", aliasesRoutes(cfg));
|
|
91
94
|
app.route("/v1", searchRoutes(cfg));
|
|
92
95
|
return app;
|
|
93
96
|
}
|
package/dist/storage.d.ts
CHANGED
|
@@ -90,6 +90,16 @@ export interface StorageAdapter {
|
|
|
90
90
|
} & AliasUpsert): Promise<AliasInfo | null>;
|
|
91
91
|
/** A person's aliases, ordered newest-used-first. */
|
|
92
92
|
listAliases(personId: string): Promise<AliasInfo[]>;
|
|
93
|
+
/**
|
|
94
|
+
* Workspace-wide lookup by alias name (case-insensitive). Returns
|
|
95
|
+
* every row whose `lower(name)` matches `lower(name)` across all
|
|
96
|
+
* persons in this workspace. Ordered by `last_used` desc — the
|
|
97
|
+
* caller can pick the most recently active match without re-sorting.
|
|
98
|
+
*
|
|
99
|
+
* Hosts use this to resolve a free-text "who is X?" query into a
|
|
100
|
+
* person id without pre-knowing which person owns the alias.
|
|
101
|
+
*/
|
|
102
|
+
findAliasesByName(name: string): Promise<AliasInfo[]>;
|
|
93
103
|
/**
|
|
94
104
|
* Upsert by `ref` (e.g. `slack:U12345`). Idempotent: writing the same
|
|
95
105
|
* ref multiple times converges. If the ref points to a different
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hexis-ai/engram-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Engram server: ingest agent session events, persist via a pluggable adapter, expose search.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"engram",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@hexis-ai/engram-core": "^0.1.5",
|
|
53
|
-
"@hexis-ai/engram-sdk": "^0.
|
|
53
|
+
"@hexis-ai/engram-sdk": "^0.9.0",
|
|
54
54
|
"hono": "^4.6.0",
|
|
55
55
|
"zod": "^4.0.0"
|
|
56
56
|
},
|