From f769daa48a467f41b4e8d09e09775dc4fcef1df8 Mon Sep 17 00:00:00 2001 From: jknapp Date: Mon, 18 May 2026 09:24:11 -0700 Subject: [PATCH] feat(project.identify): setupHint when key wasn't read from the file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Claude calls project.identify with `source` ∈ {explicit, header, inferred, undefined}, the response now includes a `setupHint` field with a short message + a copy-pasteable command to create the `.shared-memory-project` file. When `source: 'file'` is passed, no hint is emitted (the user already has the file). Hint only fires on owned-project responses — you can't ask a viewer of a shared project to commit to a repo they don't own. Tool description tells Claude: "Pass `source` based on how you resolved the key. If the response carries a setupHint, briefly relay its message and command to the user." This gives us a one-prompt nudge per session without auto-creating files or being pushy — the user decides. ProjectIdentifyInput in @shared-memory/schemas gains an optional `source: 'file' | 'explicit' | 'header' | 'inferred'` field. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/web/lib/mcp/tools.ts | 31 ++++++++++++++++++++++++++++++- packages/schemas/src/index.ts | 12 ++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/apps/web/lib/mcp/tools.ts b/apps/web/lib/mcp/tools.ts index 8da2ee3..0ec4e00 100644 --- a/apps/web/lib/mcp/tools.ts +++ b/apps/web/lib/mcp/tools.ts @@ -156,7 +156,7 @@ function withDefaultProject( const projectIdentify: ToolDef = { name: "project.identify", description: - "Call ONCE near the start of every session that has a project context — a repo you're working in, a service you're debugging, etc. — to register or look up that project so subsequent project-scoped memories attach correctly. **Look up the key in this order:** (1) `.shared-memory-project` at the repo root (single-line text file with just the project key; walk up from cwd to find it — same lookup style as `.gitignore` / `.nvmrc`). (2) If no file, fall back to a stable inference: repo name, git remote slug, or working directory basename. The file convention exists so teams sharing a repo all hit the same shared project automatically — prefer it over guessing. Returns shared projects you have access to in addition to your own; when an owned and a shared project would both match the same key, the owned one wins (a server-side warning is logged so the collision is debuggable). Skip if the work is purely scratch / not tied to a specific codebase.", + "Call ONCE near the start of every session that has a project context — a repo you're working in, a service you're debugging, etc. — to register or look up that project so subsequent project-scoped memories attach correctly. **Look up the key in this order:** (1) `.shared-memory-project` at the repo root (single-line text file with just the project key; walk up from cwd to find it — same lookup style as `.gitignore` / `.nvmrc`). (2) If no file, fall back to a stable inference: repo name, git remote slug, or working directory basename. The file convention exists so teams sharing a repo all hit the same shared project automatically — prefer it over guessing. **Pass `source` based on how you resolved the key** — 'file', 'explicit', 'header', or 'inferred' — so the server can decide whether to suggest the user create `.shared-memory-project`. If the response carries a `setupHint`, briefly relay its `message` and `command` to the user (one short sentence, don't over-explain — they'll decide). Returns shared projects you have access to in addition to your own; when an owned and a shared project would both match the same key, the owned one wins (a server-side warning is logged so the collision is debuggable). Skip if the work is purely scratch / not tied to a specific codebase.", inputSchema: { type: "object", properties: { @@ -169,6 +169,12 @@ const projectIdentify: ToolDef = { type: "string", description: "Human-readable name shown in the Web UI. Optional.", }, + source: { + type: "string", + enum: ["file", "explicit", "header", "inferred"], + description: + "How you resolved the project key. Pass 'file' when you read it from `.shared-memory-project`, 'explicit' when the user named it, 'header' when you used the X-Project-Key default, or 'inferred' when you guessed from repo/cwd. Anything but 'file' may surface a setupHint in the response.", + }, }, required: ["key"], }, @@ -259,6 +265,7 @@ const projectIdentify: ToolDef = { shared: false, access: "owner" as const, readOnly: false, + ...buildSetupHint(parsed.data.key, parsed.data.source), }, `project ${ownedRow[0].key} (${ownedRow[0].id})`, ); @@ -308,12 +315,34 @@ const projectIdentify: ToolDef = { shared: false, access: "owner" as const, readOnly: false, + ...buildSetupHint(p.key, parsed.data.source), }, `project ${p.key} (${p.id})`, ); }, }; +/** + * Returns a `setupHint` field when the caller resolved the project key + * by anything OTHER than reading `.shared-memory-project`. Surfaces a + * copy-pasteable command + a short message Claude is told to relay to + * the user. Spreading `{}` from a "no hint needed" branch is the + * cleanest way to conditionally add the field without nullish noise. + */ +function buildSetupHint( + projectKey: string, + source: ProjectIdentifyInput["source"], +): { setupHint?: { message: string; command: string } } { + if (source === "file") return {}; + return { + setupHint: { + message: + "This repo doesn't appear to have a `.shared-memory-project` file. Committing one ties every collaborator's Claude Code to the same shared project automatically — no per-machine config. Want me to commit it?", + command: `echo "${projectKey}" > .shared-memory-project`, + }, + }; +} + const memoryWrite: ToolDef = { name: "memory.write", description: diff --git a/packages/schemas/src/index.ts b/packages/schemas/src/index.ts index 7984e2d..8b9a96c 100644 --- a/packages/schemas/src/index.ts +++ b/packages/schemas/src/index.ts @@ -105,6 +105,18 @@ export type MemorySearchInput = z.infer; export const ProjectIdentifyInput = z.object({ key: ProjectKey, display_name: z.string().min(1).max(200).optional(), + /** + * How the caller resolved this project key. The server uses this to + * decide whether to include a `setupHint` in the response suggesting + * the user commit a `.shared-memory-project` file: + * - 'file' — already from .shared-memory-project; no hint needed + * - 'explicit' — user named the project in-conversation; hint shown + * - 'header' — X-Project-Key fallback; hint shown + * - 'inferred' — guessed from repo/cwd; hint shown + * + * Omitting the field is treated as 'inferred'. + */ + source: z.enum(["file", "explicit", "header", "inferred"]).optional(), }); export type ProjectIdentifyInput = z.infer;