Logo: the app had no icon at all — public/ held only .gitkeep and the page
emitted no <link rel="icon">, so browsers requested /favicon.ico, got a 404
and showed a blank tab. app/icon.svg is picked up automatically by the App
Router; public/logo.svg is a currentColor variant for in-app use.
The mark is three retrieval signals converging on a single memory, which is
what the search actually does (vector + full-text + tags fused by RRF) and
what the product does (many sessions, one store). Checked at 16px: the outer
strokes are held at equal opacity because asymmetry read as a rendering
artifact rather than as ranking.
Tokens page: reframed so a bearer token is the exception rather than the
default. A token is a credential to store and rotate; the plugin just signs
you in. The install hint renders only when PLUGIN_MARKETPLACE_URL is set —
a copyable command pointing nowhere is worse than no command.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The memory list page threw a Next.js server-side exception whenever a
project filter was applied. The filter built a raw Drizzle `sql` fragment
that interpolated a JS string[] into `ANY(${accessibleIds}::uuid[])`,
which doesn't bind as a Postgres array literal — the same array-binding
bug class already fixed in #1 (commits 3019446, b3f7e60) for
memory.list/snippet.list. The search path avoided it by using the `pg`
tag; the plain list path did not.
Fix: resolve the typed project key to a single project id from the
user's accessible set (owned ∪ shared, owned winning on key collision to
match project.identify and the search path), then filter with a plain
`eq(memories.projectId, resolvedId)` — fully parameterized, no raw array
interpolation. Returns empty when the key matches no readable project.
This also makes the list view's collision semantics consistent with the
search view.
Feature: replace the plain "Project key…" text input with a type-ahead
combobox (_project-combobox.tsx) populated with the user's accessible
project keys, narrowing as they type; selecting a suggestion applies the
filter immediately. Free-typed keys still submit. getAccessibleProjects
is hoisted to the page and reused for both the dropdown options and the
list WHERE clause (no extra query on the list path).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Projects page filtered with eq(projects.userId, userId), so a user
with an rw (or ro) share on someone else's project never saw it in the
list — even though project.identify already returned {shared, access}
for the same project. Switch to getAccessibleProjects(userId,
groupNames) (owner ∪ group-shared, the same helper search/memories use)
and aggregate counts over that id set, and label non-owned rows with a
'shared · ro|rw' badge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace hardcoded memory.dnspegasus.net references throughout README
with the generic memory.example.com placeholder (matches .env.example).
In tokens-manager.tsx, the claude-mcp-add snippet shown to users now
derives the host from PUBLIC_URL via a server-side prop instead of a
hardcoded literal, so any deployer sees their own URL in the snippet.
Prepares the repo for public mirroring.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces "per-machine X-Project-Key header" as the default way to tell
Claude Code which shared-memory project a repo belongs to. Commit a
single-line `.shared-memory-project` text file at the repo root; every
collaborator's Claude Code reads it at session start and attaches all
memories + snippets to the same shared project. No per-machine config
required.
Changes:
- New file convention documented in README (resolution order, format,
authoring snippet, rationale for plain-text over JSON).
- project.identify tool description now leads with "check
.shared-memory-project at the repo root", with inference as fallback.
- memory.write description mentions the file as the canonical source for
project keys.
- Project detail page in the Web UI shows a copy-paste `echo > file`
command so users see exactly what to add to their repo.
- Added .shared-memory-project to this repo (content: `shared-memory`).
Resolution precedence is: explicit tool arg → .shared-memory-project →
X-Project-Key header → inference. The file beats the header because
repo context is more specific than machine context.
This is a soft convention — Claude has to read the file. Directive tool
descriptions make this very likely; a Claude Code skill would make it
bulletproof, deferred until we see whether the description alone is
enough.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes Phase 4f from the deferred backlog. Renders every audit_log row
that pertains to a project — memory writes/updates/deletes, snippet
puts/deletes, share grants/changes/revocations, project.identify
collisions — newest first, capped at 150 rows.
Query is a three-leg UNION ALL joining audit_log against memories,
snippets, and the project itself. Avoids relying on payload->>projectKey
which isn't populated consistently across all action shapes.
Each row renders as "<actor> <verb-phrase>" with entity links where
applicable, plus a compact relative timestamp. memory.update entries
also show scope/project transitions inline when those changed. Share
events surface the group name and access level with the existing
tone-coded Badge.
Auth: viewable by anyone with read access to the project (owner +
ro/rw group members). Same guard as the project detail page.
Linked from the project page header so it's discoverable without typing
the URL. No nav-bar link — it's a per-project artifact, not a global
view.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. lib/snippets.ts + lib/memory-actions.ts (×2): shared-project key
lookups were querying `projects` by key with no visibility scope. Since
`projects.key` is unique per user (not global), the unscoped match
could resolve another user's project entirely. Restricted the lookups
to `readableProjectIds(userId, groupNames)` — own + shared only.
2. lib/access.ts getAccessibleProjects: a stray `if (existing) continue`
inside the share-collapse loop short-circuited on the first match,
killing the rw-beats-ro upgrade path. Two-group cases where one share
was ro and another rw on the same project were incorrectly resolved
as ro. Replaced with explicit owner/rw skip.
3. lib/mcp/tools.ts snippetPut: removed a dead `void exists` block that
looked like an authorization pre-flight but was actually a no-op —
real write authorization lives inside putSnippet, called next. Added
a comment at the call site documenting where the check is.
4. memory.delete + snippet.delete: previously had no optimistic-lock
CAS, so a concurrent peer edit could be silently overwritten by a
delete on a stale view. Added optional `version` to MemoryDeleteInput
(new) and SnippetDeleteInput (extended); UPDATE WHERE now CASes on
version; 0-row response surfaces CONCURRENT_EDIT_ERROR. Web detail
pages pass `version` through hidden form inputs. When the caller
doesn't supply a version, falls back to the version we just read in
the same handler for in-handler consistency.
5. lib/mcp/tools.ts memorySearch re-fetch: missing `isNull(deletedAt)`
on the post-search row hydration left a TOCTOU window where a row
soft-deleted between the search and the re-fetch would be returned.
Visibility is still enforced by searchMemories itself.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds project-level sharing via the new project_shares table plus the
infrastructure that makes multi-user editing safe and visible.
Authorization (lib/access.ts):
- getAccessibleProjects / getProjectAccess centralise the predicate
used by every read and write path.
- readableProjectIds / writableProjectIds drive listing-style queries.
- Web UI Server Actions and pages source group memberships from the
user_groups table so authorization works without depending on
Agent A's session callback shape.
Optimistic locking:
- memories + snippets gain version + last_edited_by columns. Every
UPDATE bumps version and stamps the editor; UPDATE WHERE clauses
require the caller's pre-fetched version, surfacing a clear
"refresh and try again" error on lost-write races rather than
silently clobbering.
- MemoryUpdateInput / SnippetPutInput accept an optional version
token.
MCP tools:
- memory.write / .update / .delete / .get / .list / .search,
snippet.put / .get / .list / .delete now respect shared-project
access (read = owner | any share, write = owner | rw share).
- project, defaults to ctx.defaultProjectKey from the X-Project-Key
header (populated by the MCP route — Agent A's wiring).
- project.identify returns shared projects you have access to and
prefers an owned project on key collision, audit-logging the
collision so an operator can debug it.
- Tool descriptions for memory.update, memory.write, snippet.put,
and project.identify updated with the co-edit / shared-project
notes.
Web UI:
- Project detail page: ownership badge, shared-with-N-groups badge,
owner-only "Manage sharing" section (add/flip/remove shares via
lib/share-actions.ts). Add-share is constrained to groups the
granter is already in.
- "Shared" chips on memory cards in /memories and /dashboard.
- "Last edited by ..." on memory + snippet detail pages, shown only
when the last editor isn't the row's original author so the chip
stays informative.
- Read-only viewers (ro shares) lose Edit/Delete affordances on
memories and snippets.
Migration 0004_project_shares.sql adds project_shares + the two new
columns on memories and snippets; it depends on Agent A's
0003_groups.sql for the groups, user_groups, and memory_access enum.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Foundational work for the upcoming group-scoped sharing feature.
Schema (migration 0003_groups.sql + drizzle schema):
- memory_access enum ('ro' | 'rw') reserved for Agent B's project_shares
- groups (id, oidc_iss, name, display_name, …) keyed by (oidc_iss, name)
so different IdPs can both have e.g. "platform" without colliding
- user_groups (user_id, group_id, synced_at) PK (user_id, group_id)
Auth (auth.ts + lib/auth/sync-groups.ts):
- jwt callback now syncs `profile.groups` after upserting the user
- syncUserGroupsFromClaim runs in a single tx: upserts each group,
inserts new memberships, deletes ones no longer in the claim
- missing/empty claim → user has zero groups (wipe memberships)
- EntraID GUID-vs-name edge case: we treat whatever strings the claim
emits as names verbatim; groups overage (>200 groups → no claim)
is documented as unsupported in v1
UserContext + JWT (lib/mcp/context.ts, lib/auth/jwt.ts):
- AuthenticatedClaims.groups surfaced from verified JWT payload
- UserContext.groups: string[] — live from OIDC token claim, falls
back to DB snapshot for CLI (HMAC) tokens which carry no claim
- UserContext.defaultProjectKey: optional, set from header
MCP route (app/api/mcp/route.ts):
- reads X-Project-Key header, validates against ProjectKey Zod schema,
400 on invalid; empty/missing leaves defaultProjectKey undefined
- auto-upserts the header-supplied project so first-use works without
a separate project.identify call
Tools (lib/mcp/tools.ts):
- withDefaultProject helper injects ctx.defaultProjectKey when the
caller omits `project`. Per-tool defaultScope hint avoids breaking
snippet.put (user-scope default) while making memory.write
(project-scope default) honor the header
- applied to memory.write/list/search/update and all snippet.* tools
Web UI:
- /settings/groups debug page lists current memberships with synced_at
and a clear empty state pointing at README troubleshooting
- /settings/tokens grows a "Pin to project" dropdown; selected key is
baked into the generated `claude mcp add` snippet as
`--header "X-Project-Key: <key>"`. The JWT itself stays
identity-only — pinning is purely a UX shortcut
- settings landing page links to /settings/groups
- README troubleshooting bullet covers the empty-groups path for
Authentik / EntraID / Keycloak
Refactor:
- extracted resolveProjectId + upsertProject from memory-actions.ts
into lib/projects.ts so the MCP route can reuse upsertProject
Verification:
- pnpm typecheck clean
- SKIP_ENV_VALIDATION=true pnpm build clean; /settings/groups in route table
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Snippets are exact-name-keyed templates (PR formats, checklists, style
rules) — prescriptive artifacts the user wants applied consistently.
Distinct from memories, which are searched descriptive prose.
- Migration 0002 adds scope/project_id/deleted_at to snippets, partial
unique indexes per scope, and a scope/project CHECK constraint
mirroring memories_scope_project_chk.
- New @shared-memory/schemas: SnippetName/Put/Get/List/Delete inputs
with shared scope-project refinement.
- lib/snippets.ts: get/put/list/softDelete helpers used by both the
Web Server Actions and the MCP tool handlers.
- Four new MCP tools (snippet.put/get/list/delete) with directive
descriptions contrasting against memory.* (exact-name lookup vs
search; templates vs facts).
- /snippets pages: list, new, [name] with edit + delete and a
scope-picker when a name lives in multiple scopes.
- Nav: Snippets link between Memories and Projects.
Design note: when a name exists in both user and project scope,
snippet.get without an explicit scope prefers project (if project key
given) then falls back to user. The detail page shows a picker when
the name is ambiguous and no scope query param was supplied.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends both the Web UI edit form and the memory.update MCP tool so
existing memories can be reclassified between user-global and
project-attached scopes without delete+rewrite. Schema refines enforce
the user/project consistency invariants; audit log captures from/to
scope and projectKey on transitions.
Server components can't pass function props (onSubmit handlers etc.) to
client-rendered HTML elements. The handler here was a no-op leftover —
removing it fixes the 500 'Event handlers cannot be passed to Client
Component props' on /memories/[id].
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the debug /me + /connect pages with a real authed app shell.
Pages
- / — anonymous landing; redirects to /dashboard once signed in
- /dashboard — recent memories + top projects, quick "new memory" action
- /memories — searchable list with hybrid (vector+FTS+tags) scoring;
per-result rank breakdown shown inline
- /memories/[id] — view + inline edit toggle + delete
- /memories/new — create form with project autocomplete
- /projects — list with memory counts and last-activity
- /projects/[key] — that project's memories
- /settings — read-only Authentik profile + link to tokens
- /settings/tokens — list / create / revoke CLI tokens
Old URLs preserved as redirects:
- /me → /dashboard
- /connect → /settings/tokens
Stack additions
- Tailwind v4 with CSS-first @theme tokens (dark only for now)
- App shell in app/(authed)/ — auth guard + top nav with global search box
- Lightweight UI primitives in app/_components/ui/ (Button, Input, Card,
Badge, EmptyState, Container, PageHeader)
- Search logic extracted from MCP tool into lib/memories.ts so Web UI and
MCP both call the same RRF code path
- Memory CRUD via Server Actions in lib/memory-actions.ts; audit_log
rows are tagged actor='web' to distinguish from MCP writes
Per-token revoke
- New cli_tokens table (id, user_id, jti unique, name, created_at,
last_used_at, expires_at, revoked_at) — migration 0001_cli_tokens.sql
- mintCliToken now records jti + name; verifyCliToken enforces revocation
for tracked tokens. Legacy tokens minted before this change (no jti)
are accepted on signature alone until they expire naturally.
- /settings/tokens lists active + revoked tokens with one-click revoke
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>