@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.
@@ -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[]>;
@@ -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.9.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.8.0",
53
+ "@hexis-ai/engram-sdk": "^0.9.0",
54
54
  "hono": "^4.6.0",
55
55
  "zod": "^4.0.0"
56
56
  },