phaedo-mcp 0.3.5 → 0.3.7

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.
@@ -18,7 +18,7 @@ import { defaultStateDir, writeCache, readCache, clearCache } from './cache.js';
18
18
  import { foldLocalCapture, isLocalCaptureOwner, localCaptureSessionCount } from './capture-local.js';
19
19
  import { Capabilities, AuthoredSource } from './lib/phaedo-crypto.js';
20
20
  import { PhaedoError } from './errors.js';
21
- import { configDir } from './lib/config-dir.js';
21
+ import { configDir, isDirectoryEdition } from './lib/config-dir.js';
22
22
 
23
23
  // §4.1 protocol-identity normalization (consumer defense-in-depth, 2026-06-16).
24
24
  // A stored fingerprint from a producer that predates identity stamping (or whose
@@ -118,7 +118,14 @@ export const PLUGIN_NO_PROFILE_MESSAGE =
118
118
  'loads at the start of the next one. For your full profile, pair the Phaedo phone app with ' +
119
119
  '/phaedo:pair (the app is not open to the public yet; https://phaedo.so to hear when it is).';
120
120
 
121
- export function noProfileMessage(env = process.env) {
121
+ // The directory edition learns nothing from sessions, so it can't promise a starter profile.
122
+ export const DIRECTORY_NO_PROFILE_MESSAGE =
123
+ 'No Phaedo profile is set up yet, so Phaedo has not changed how the assistant responds. Your ' +
124
+ 'profile comes from the Phaedo phone app: pair it with /phaedo:pair (the app is not open to the ' +
125
+ 'public yet; https://phaedo.so to hear when it is).';
126
+
127
+ export function noProfileMessage(env = process.env, baseDir = null) {
128
+ if (baseDir && isDirectoryEdition(baseDir)) return DIRECTORY_NO_PROFILE_MESSAGE;
122
129
  return env.CLAUDE_PLUGIN_ROOT ? PLUGIN_NO_PROFILE_MESSAGE : NO_PROFILE_MESSAGE;
123
130
  }
124
131
 
@@ -127,6 +134,7 @@ const NO_PROFILE_LOG =
127
134
  'no profile is set up on this computer, so Phaedo will not change how the assistant responds. ' +
128
135
  'Pair the Phaedo phone app, install the Claude Code hook to start one from your own sessions, ' +
129
136
  'or set PHAEDO_FINGERPRINT=sample to try the example.';
137
+ const DIRECTORY_NO_PROFILE_LOG = 'no profile yet; pair the Phaedo phone app with /phaedo:pair.';
130
138
  const PLUGIN_NO_PROFILE_LOG =
131
139
  'no profile yet; one starts from your Claude Code sessions on this computer, or pair the phone app with /phaedo:pair.';
132
140
 
@@ -144,7 +152,8 @@ const PLUGIN_NO_PROFILE_LOG =
144
152
  // phone later replaces it (the phone inherits the cumulative counts — capture-local.js §5).
145
153
  export const STARTER_FINGERPRINT_ID = 'starter-local';
146
154
 
147
- async function loadStarterProfile(now = new Date().toISOString()) {
155
+ async function loadStarterProfile(now = new Date().toISOString(), baseDir = null) {
156
+ if (baseDir && isDirectoryEdition(baseDir)) return null; // nothing is learned from sessions there
148
157
  if (!await isLocalCaptureOwner(false)) return null;
149
158
  const base = { phaedo_fingerprint: { fingerprint_id: STARTER_FINGERPRINT_ID, updated_at: now, layers: {} } };
150
159
  const vd = await foldLocalCapture(base, { now });
@@ -189,13 +198,14 @@ export async function profileStatusLine(baseDir) {
189
198
  if (env) return null;
190
199
  if (hasPairingRecord(baseDir)) return null;
191
200
  if (existsSync(resolve(configDir(baseDir), 'fingerprint.json'))) return null;
192
- const starter = await loadStarterProfile();
201
+ const starter = await loadStarterProfile(undefined, baseDir);
193
202
  if (starter) {
194
203
  const { learned, areas, sessions, stage } = await summarizeStarter(starter);
195
204
  return `no phone paired; using a starter profile from your own sessions on this computer: ` +
196
205
  `${learned} thing${learned === 1 ? '' : 's'} learned across ${areas} area${areas === 1 ? '' : 's'}` +
197
206
  `${sessions ? ` from ${sessions} session${sessions === 1 ? '' : 's'}` : ''} — ${stage}`;
198
207
  }
208
+ if (isDirectoryEdition(baseDir)) return DIRECTORY_NO_PROFILE_LOG;
199
209
  return process.env.CLAUDE_PLUGIN_ROOT ? PLUGIN_NO_PROFILE_LOG : NO_PROFILE_LOG;
200
210
  }
201
211
 
@@ -287,9 +297,9 @@ export async function loadFingerprint(baseDir) {
287
297
  // Only the default location may be absent quietly. A path the user NAMED that doesn't
288
298
  // read is their mistake to see, not "no profile".
289
299
  if (!process.env.PHAEDO_FINGERPRINT && err?.code === 'ENOENT') {
290
- const starter = await loadStarterProfile();
300
+ const starter = await loadStarterProfile(undefined, baseDir);
291
301
  if (starter) return starter;
292
- throw new PhaedoError('decryption_failed', noProfileMessage());
302
+ throw new PhaedoError('decryption_failed', noProfileMessage(process.env, baseDir));
293
303
  }
294
304
  throw new PhaedoError('decryption_failed', `Could not read the Phaedo profile at ${path} (${err?.code || err?.message || err}).`);
295
305
  }
@@ -304,7 +314,7 @@ export async function loadFingerprint(baseDir) {
304
314
  // persistence, idempotent). Owner-gated: a paired setup never reaches this
305
315
  // branch, and an explicit PHAEDO_FINGERPRINT ("use THIS file") stays verbatim —
306
316
  // it is the hermetic-test escape hatch.
307
- if (!process.env.PHAEDO_FINGERPRINT && await isLocalCaptureOwner(hasPairingRecord(baseDir))) {
317
+ if (!process.env.PHAEDO_FINGERPRINT && !isDirectoryEdition(baseDir) && await isLocalCaptureOwner(hasPairingRecord(baseDir))) {
308
318
  return foldConfigHome(normalizeIdentity(await foldLocalCapture(vd)));
309
319
  }
310
320
  return foldConfigHome(normalizeIdentity(vd));
package/lib/config-dir.js CHANGED
@@ -22,20 +22,37 @@ export const LAYOUT_MARKER = 'install-layout.json';
22
22
 
23
23
  const cache = new Map();
24
24
 
25
- /** True when the package at `baseDir` is a packaged install (plugin or Desktop bundle). */
26
- export function isPackagedInstall(baseDir) {
25
+ function readLayout(baseDir) {
27
26
  const key = resolve(baseDir);
28
27
  if (!cache.has(key)) {
29
- let packaged = false;
28
+ let layout = null;
30
29
  try {
31
30
  const marker = join(key, LAYOUT_MARKER);
32
- packaged = existsSync(marker) && JSON.parse(readFileSync(marker, 'utf8'))?.config === 'state-dir';
33
- } catch { packaged = false; } // an unreadable marker → the old, beside-the-server layout
34
- cache.set(key, packaged);
31
+ if (existsSync(marker)) layout = JSON.parse(readFileSync(marker, 'utf8'));
32
+ } catch { layout = null; } // an unreadable marker → the old, beside-the-server layout
33
+ cache.set(key, layout && typeof layout === 'object' ? layout : null);
35
34
  }
36
35
  return cache.get(key);
37
36
  }
38
37
 
38
+ /** True when the package at `baseDir` is a packaged install (plugin or Desktop bundle). */
39
+ export function isPackagedInstall(baseDir) {
40
+ return readLayout(baseDir)?.config === 'state-dir';
41
+ }
42
+
43
+ // THE DIRECTORY EDITION. Anthropic's directory policy says listed software must not read
44
+ // Claude's chat history or "use conversation content to build user profiles", nor "make any
45
+ // changes to Claude's behavior across sessions". The plugin built for the directory therefore
46
+ // leaves out every transcript reader (the session hooks and the capture modules), and this
47
+ // flag turns off the two server paths that would still use conversation-derived data or apply
48
+ // the profile unasked: the starter profile folded from local capture counts, and the "load the
49
+ // profile at session start" instruction. The profile then comes only from the paired phone app,
50
+ // and loads when the user asks for it. Everything else — consult, escalate, sign, the merge
51
+ // check — is the same server.
52
+ export function isDirectoryEdition(baseDir) {
53
+ return readLayout(baseDir)?.edition === 'directory';
54
+ }
55
+
39
56
  export function configDir(baseDir, env = process.env) {
40
57
  if (env.PHAEDO_CONFIG_DIR) return resolve(env.PHAEDO_CONFIG_DIR);
41
58
  if (isPackagedInstall(baseDir)) return env.PHAEDO_MCP_STATE_DIR || join(homedir(), '.phaedo-mcp');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phaedo-mcp",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "private": false,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
package/server.js CHANGED
@@ -17,7 +17,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
17
17
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
18
18
 
19
19
  import { loadFingerprint, sourceOf, profileStatusLine } from './fingerprint-source.js';
20
- import { configDir } from './lib/config-dir.js';
20
+ import { configDir, isDirectoryEdition } from './lib/config-dir.js';
21
21
  import { hasPairingRecord, loadPairingRecord } from './phone-source.js';
22
22
  import { depositDelegationSuggestions } from './delegation-sync.js';
23
23
  import { depositReceiptsDigest, drainAndApplyMarks } from './receipts-sync.js';
@@ -195,9 +195,13 @@ const PKG_VERSION = JSON.parse(readFileSync(join(BASE_DIR, 'package.json'), 'utf
195
195
  // tool descriptions and nothing saying "load the profile first" or "consult before you ship" —
196
196
  // so whether Phaedo was used at all depended on the model guessing. Short on purpose: clients
197
197
  // put this in the model's context on every turn.
198
+ // The directory edition loads the profile only when asked (lib/config-dir.js explains why).
199
+ const DIRECTORY_EDITION = isDirectoryEdition(BASE_DIR);
198
200
  const SERVER_INSTRUCTIONS = [
199
201
  'Phaedo holds the user\'s profile: how they like answers written, and how they want decisions handled when an AI acts for them.',
200
- '- At the start of a session, call phaedo_request_injection once and treat the returned projection as background on the user\'s style.',
202
+ DIRECTORY_EDITION
203
+ ? '- When the user asks to load their Phaedo profile, call phaedo_request_injection and treat the returned projection as background on the user\'s style. Don\'t load it unasked.'
204
+ : '- At the start of a session, call phaedo_request_injection once and treat the returned projection as background on the user\'s style.',
201
205
  '- Before a consequential action for the user (merging, deploying, publishing, sending a message, spending money, deleting, changing an account or setting), call phaedo_consult and follow its signal. If you are acting on your own and the decision should be the user\'s, use phaedo_escalate and do not proceed until it resolves.',
202
206
  '- When the user reacts to an action you consulted on, call phaedo_record_outcome with its receipt_id: record what they chose, never what you inferred.',
203
207
  '- If a tool says no Phaedo profile is set up, tell the user once, plainly, and carry on normally. Do not retry.',
@@ -209,10 +213,12 @@ server.registerTool(
209
213
  'phaedo_request_injection',
210
214
  {
211
215
  title: 'Request a Phaedo fingerprint injection',
212
- description:
213
- "Returns the user's Phaedo cognitive-fingerprint projection as a system-prompt " +
214
- 'block to insert at the start of an AI session, so the assistant responds in the ' +
215
- "user's style. Call once at session start and prepend the returned `projection`.",
216
+ description: DIRECTORY_EDITION
217
+ ? "Returns the user's Phaedo profile as a block to use as background on their style. " +
218
+ 'Call it only when the user asks to load their Phaedo profile.'
219
+ : "Returns the user's Phaedo cognitive-fingerprint projection as a system-prompt " +
220
+ 'block to insert at the start of an AI session, so the assistant responds in the ' +
221
+ "user's style. Call once at session start and prepend the returned `projection`.",
216
222
  inputSchema: {
217
223
  session_context: z
218
224
  .object({
package/server.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.phaedo-labs/phaedo-mcp",
4
4
  "description": "Make AI apps answer your way, check with you before acting, and get approvals on your phone.",
5
- "version": "0.3.5",
5
+ "version": "0.3.7",
6
6
  "repository": {
7
7
  "url": "https://github.com/phaedo-labs/phaedo-mcp",
8
8
  "source": "github",
@@ -13,7 +13,7 @@
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "phaedo-mcp",
16
- "version": "0.3.5",
16
+ "version": "0.3.7",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  }