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.
- package/fingerprint-source.js +17 -7
- package/lib/config-dir.js +23 -6
- package/package.json +1 -1
- package/server.js +12 -6
- package/server.json +2 -2
package/fingerprint-source.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
26
|
-
export function isPackagedInstall(baseDir) {
|
|
25
|
+
function readLayout(baseDir) {
|
|
27
26
|
const key = resolve(baseDir);
|
|
28
27
|
if (!cache.has(key)) {
|
|
29
|
-
let
|
|
28
|
+
let layout = null;
|
|
30
29
|
try {
|
|
31
30
|
const marker = join(key, LAYOUT_MARKER);
|
|
32
|
-
|
|
33
|
-
} catch {
|
|
34
|
-
cache.set(key,
|
|
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
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
|
-
|
|
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
|
|
214
|
-
|
|
215
|
-
"user's
|
|
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
|
+
"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.
|
|
16
|
+
"version": "0.3.7",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
}
|