phaedo-mcp 0.3.2 → 0.3.4
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/README.md +37 -7
- package/capture-local.js +17 -0
- package/fingerprint-source.js +99 -5
- package/package.json +1 -1
- package/pull.js +11 -2
- package/server.js +7 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -18,12 +18,15 @@ when your phone is offline, is encrypted too.
|
|
|
18
18
|
|
|
19
19
|
## What you need
|
|
20
20
|
|
|
21
|
-
1. **
|
|
22
|
-
|
|
21
|
+
1. **A profile**, from either:
|
|
22
|
+
- **the Phaedo phone app** — your full profile lives there. The app isn't open
|
|
23
|
+
to the public yet; [phaedo.so](https://phaedo.so) has a link to hear when it is.
|
|
24
|
+
- **your own Claude Code sessions** — no phone needed. Phaedo builds a starter
|
|
25
|
+
profile on this computer from how you work in Claude Code (see below).
|
|
23
26
|
2. **Node.js**, free from [nodejs.org](https://nodejs.org) (the LTS button).
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
With neither, there is no profile, and the server says so plainly rather than
|
|
29
|
+
guessing. You can still try Phaedo with a made-up example profile.
|
|
27
30
|
|
|
28
31
|
## Try it with the example profile (no phone needed)
|
|
29
32
|
|
|
@@ -63,6 +66,31 @@ example, delete the `env` line in Claude Desktop, or in Claude Code run
|
|
|
63
66
|
`claude mcp remove -s user phaedo` and add it again without `-e …`. The server
|
|
64
67
|
will then tell you there's no profile until you pair your phone.
|
|
65
68
|
|
|
69
|
+
## Start a profile from your Claude Code sessions (no phone needed)
|
|
70
|
+
|
|
71
|
+
If you use Claude Code, Phaedo can learn from how you work there and build a
|
|
72
|
+
starter profile on this computer — for example, that you want short answers
|
|
73
|
+
without headers. It starts thin and grows as you work.
|
|
74
|
+
|
|
75
|
+
1. **Get this folder** as in step 1 below (Code → Download ZIP, unzip, open the
|
|
76
|
+
`mcp` folder).
|
|
77
|
+
2. **Install the Claude Code hook.** In Terminal, from the `mcp` folder:
|
|
78
|
+
```bash
|
|
79
|
+
npm install
|
|
80
|
+
```
|
|
81
|
+
```bash
|
|
82
|
+
npm run setup:code-hook
|
|
83
|
+
```
|
|
84
|
+
This adds Phaedo to your AI apps and turns on three things in Claude Code:
|
|
85
|
+
loading your profile when a session starts, checking with you before it merges
|
|
86
|
+
or publishes, and learning from your sessions.
|
|
87
|
+
3. **Quit and reopen Claude Code**, then keep working. The first sessions build the
|
|
88
|
+
profile, and each later session picks up what's been learned.
|
|
89
|
+
|
|
90
|
+
What it reads stays on this computer: each session is reduced to counts (like "asked
|
|
91
|
+
for a shorter answer, 4 times"), and the text itself is discarded. If you pair the
|
|
92
|
+
phone app later, your full profile replaces the starter one and keeps what it learned.
|
|
93
|
+
|
|
66
94
|
## Use your own profile (with the phone app)
|
|
67
95
|
|
|
68
96
|
1. **Get this folder.** On [the GitHub page](https://github.com/phaedo-labs/phaedo-mcp),
|
|
@@ -219,9 +247,11 @@ if you prefer.)*
|
|
|
219
247
|
|
|
220
248
|
**Phase 1a — local JSON.** With no pairing record, reads a **vault payload**
|
|
221
249
|
from `PHAEDO_FINGERPRINT` (a path, or the word `sample` for the shipped
|
|
222
|
-
`mcp/sample-fingerprint.json`), then `mcp/fingerprint.json`. With neither,
|
|
223
|
-
|
|
224
|
-
|
|
250
|
+
`mcp/sample-fingerprint.json`), then `mcp/fingerprint.json`. With neither, a
|
|
251
|
+
**starter profile**: the Claude Code capture state on this computer folded onto an
|
|
252
|
+
empty fingerprint (id `starter-local`), served only if it learned something. With
|
|
253
|
+
nothing captured, there is **no profile**: every tool returns a plain-language
|
|
254
|
+
`decryption_failed` error saying so. The sample is never served unless asked for by name — until 0.3.1 it
|
|
225
255
|
was the silent fallback, so a fresh `npx phaedo-mcp` injected a made-up person's
|
|
226
256
|
style without saying so. The file looks like this:
|
|
227
257
|
```json
|
package/capture-local.js
CHANGED
|
@@ -53,6 +53,23 @@ async function localSnapshots(now) {
|
|
|
53
53
|
return out;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// How many captured sessions the local state holds, across every surface: the live ones plus
|
|
57
|
+
// those evicted into the baseline (their counts are still in it). For the starter-profile
|
|
58
|
+
// readout only; 0 on any failure — it describes, it never gates.
|
|
59
|
+
export async function localCaptureSessionCount() {
|
|
60
|
+
try {
|
|
61
|
+
const stateMod = await loadCaptureModule('capture/state.js');
|
|
62
|
+
const state = stateMod.loadState(stateMod.defaultStatePath());
|
|
63
|
+
let n = 0;
|
|
64
|
+
for (const s of Object.values(state.surfaces || {})) {
|
|
65
|
+
n += Object.keys((s && s.sessions) || {}).length + (Array.isArray(s && s.evicted) ? s.evicted.length : 0);
|
|
66
|
+
}
|
|
67
|
+
return n;
|
|
68
|
+
} catch {
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
56
73
|
// Fold the local capture state into a vault payload's fingerprint and return the
|
|
57
74
|
// folded payload (or the input unchanged when there is nothing to fold / the
|
|
58
75
|
// modules are unavailable). Pure with respect to the input object.
|
package/fingerprint-source.js
CHANGED
|
@@ -5,15 +5,17 @@
|
|
|
5
5
|
// Phase 1b (phone): if a pairing record is configured (PHAEDO_PAIRING or
|
|
6
6
|
// mcp/pairing.json), authorize → pull /v1/profile → unwrap the §7.2 envelope
|
|
7
7
|
// from the paired phone (phone-source.js). Spec-faithful; no plaintext at rest.
|
|
8
|
-
// Phase 1a (local): otherwise read a local vault-payload JSON. With neither,
|
|
9
|
-
//
|
|
8
|
+
// Phase 1a (local): otherwise read a local vault-payload JSON. With neither, a STARTER
|
|
9
|
+
// profile from this computer's own capture (loadStarterProfile), else no profile — tools
|
|
10
|
+
// say so (NO_PROFILE_MESSAGE). The shipped sample is opt-in only.
|
|
10
11
|
|
|
11
12
|
import { readFile } from 'fs/promises';
|
|
13
|
+
import { existsSync } from 'fs';
|
|
12
14
|
import { resolve, dirname } from 'path';
|
|
13
15
|
import { fileURLToPath } from 'url';
|
|
14
16
|
import { hasPairingRecord, loadPairingRecord, loadFingerprintFromPhone, PhoneError } from './phone-source.js';
|
|
15
17
|
import { defaultStateDir, writeCache, readCache, clearCache } from './cache.js';
|
|
16
|
-
import { foldLocalCapture, isLocalCaptureOwner } from './capture-local.js';
|
|
18
|
+
import { foldLocalCapture, isLocalCaptureOwner, localCaptureSessionCount } from './capture-local.js';
|
|
17
19
|
import { Capabilities, AuthoredSource } from './lib/phaedo-crypto.js';
|
|
18
20
|
import { PhaedoError } from './errors.js';
|
|
19
21
|
|
|
@@ -78,6 +80,7 @@ export function foldConfigHome(vd) {
|
|
|
78
80
|
// Resolution order for the local fixture (1a):
|
|
79
81
|
// 1. PHAEDO_FINGERPRINT env var (absolute or cwd-relative path, or `sample`)
|
|
80
82
|
// 2. ./fingerprint.json next to the server
|
|
83
|
+
// 3. a starter profile folded from this computer's capture state (see loadStarterProfile)
|
|
81
84
|
// — nothing else. There is deliberately NO silent fallback to the shipped sample.
|
|
82
85
|
//
|
|
83
86
|
// NO PROFILE IS AN ANSWER, NOT A DEMO. Until 2026-10-20 an unpaired install with no file fell
|
|
@@ -100,8 +103,85 @@ function resolveSourcePath(baseDir) {
|
|
|
100
103
|
export const NO_PROFILE_MESSAGE =
|
|
101
104
|
'No Phaedo profile is set up on this computer, so there is nothing to load and Phaedo has ' +
|
|
102
105
|
'not changed how the assistant responds. A profile comes from the Phaedo phone app, which is ' +
|
|
103
|
-
'not open to the public yet (https://phaedo.so to hear when it is).
|
|
104
|
-
'
|
|
106
|
+
'not open to the public yet (https://phaedo.so to hear when it is). Without the phone, Phaedo ' +
|
|
107
|
+
'can start one from your own Claude Code sessions on this computer once its Claude Code hook ' +
|
|
108
|
+
'is installed (see the README). To try a made-up example profile instead, set ' +
|
|
109
|
+
'PHAEDO_FINGERPRINT=sample in this server\'s settings.';
|
|
110
|
+
|
|
111
|
+
// The same news for the server's log (Claude Desktop's MCP log shows stderr), one line.
|
|
112
|
+
const NO_PROFILE_LOG =
|
|
113
|
+
'no profile is set up on this computer, so Phaedo will not change how the assistant responds. ' +
|
|
114
|
+
'Pair the Phaedo phone app, install the Claude Code hook to start one from your own sessions, ' +
|
|
115
|
+
'or set PHAEDO_FINGERPRINT=sample to try the example.';
|
|
116
|
+
|
|
117
|
+
// STARTER PROFILE — the phone-less path.
|
|
118
|
+
//
|
|
119
|
+
// Someone without the phone app has no fingerprint to pull, but if they turned on the Claude
|
|
120
|
+
// Code capture hook, this computer already holds their own signal counts (capture-local.js, the
|
|
121
|
+
// tier-3 owner). Folding those onto an EMPTY fingerprint gives a profile made only of what they
|
|
122
|
+
// did — never the sample's invented traits, which is what this branch fell back to until 0.3.0.
|
|
123
|
+
// It is thin by construction (persona_strength 0; the projection's confidence section says
|
|
124
|
+
// "treat all signals as preliminary"), and that is the honest label for it.
|
|
125
|
+
//
|
|
126
|
+
// Served only when the fold learned something: zero signals is still "no profile". The id is a
|
|
127
|
+
// constant so receipts and the agreement metric stay on one subject across loads; pairing a
|
|
128
|
+
// phone later replaces it (the phone inherits the cumulative counts — capture-local.js §5).
|
|
129
|
+
export const STARTER_FINGERPRINT_ID = 'starter-local';
|
|
130
|
+
|
|
131
|
+
async function loadStarterProfile(now = new Date().toISOString()) {
|
|
132
|
+
if (!await isLocalCaptureOwner(false)) return null;
|
|
133
|
+
const base = { phaedo_fingerprint: { fingerprint_id: STARTER_FINGERPRINT_ID, updated_at: now, layers: {} } };
|
|
134
|
+
const vd = await foldLocalCapture(base, { now });
|
|
135
|
+
const layers = (vd && vd.phaedo_fingerprint && vd.phaedo_fingerprint.layers) || {};
|
|
136
|
+
if (!Object.values(layers).some((l) => Array.isArray(l?.signals) && l.signals.length)) return null;
|
|
137
|
+
return markStarter(foldConfigHome(normalizeIdentity(vd)));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// HOW FAR ALONG a starter profile is, in words a person can act on. "Learned" counts distinct
|
|
141
|
+
// traits (layer + field), not observations: ten corrections about length are one thing learned.
|
|
142
|
+
// The stages are about what to do next, which is the point of saying it at all: early on, keep
|
|
143
|
+
// working; once it is a solid start, the phone app is what adds the rest (its interview and the
|
|
144
|
+
// decisions the subject approves, which sessions cannot show). Cut-offs are deliberately coarse
|
|
145
|
+
// and are not a confidence measure — the projection's own confidence section is that.
|
|
146
|
+
export const STARTER_STAGES = [
|
|
147
|
+
{ min: 8, text: 'a solid start. The Phaedo phone app adds what sessions can\'t show (https://phaedo.so).' },
|
|
148
|
+
{ min: 3, text: 'taking shape; it keeps learning as you work.' },
|
|
149
|
+
{ min: 0, text: 'just getting started; it fills in as you work.' },
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
export async function summarizeStarter(vd) {
|
|
153
|
+
const layers = (vd && vd.phaedo_fingerprint && vd.phaedo_fingerprint.layers) || {};
|
|
154
|
+
const traits = new Set();
|
|
155
|
+
let areas = 0;
|
|
156
|
+
for (const [id, layer] of Object.entries(layers)) {
|
|
157
|
+
const sigs = (layer && Array.isArray(layer.signals)) ? layer.signals : [];
|
|
158
|
+
if (sigs.length) areas++;
|
|
159
|
+
for (const sg of sigs) if (sg && sg.field) traits.add(`${id}.${sg.field}`);
|
|
160
|
+
}
|
|
161
|
+
const learned = traits.size;
|
|
162
|
+
const sessions = await localCaptureSessionCount();
|
|
163
|
+
const stage = STARTER_STAGES.find((st) => learned >= st.min).text;
|
|
164
|
+
return { learned, areas, sessions, stage };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** One line for the server log describing where the profile will come from, or null when there
|
|
168
|
+
* is nothing a person needs told. Never touches the phone: a pull at startup would put an
|
|
169
|
+
* "authorize" card on it with nobody asking. */
|
|
170
|
+
export async function profileStatusLine(baseDir) {
|
|
171
|
+
const env = process.env.PHAEDO_FINGERPRINT;
|
|
172
|
+
if (env === SAMPLE_KEYWORD) return 'using the made-up example profile (PHAEDO_FINGERPRINT=sample).';
|
|
173
|
+
if (env) return null;
|
|
174
|
+
if (hasPairingRecord(baseDir)) return null;
|
|
175
|
+
if (existsSync(resolve(baseDir, 'fingerprint.json'))) return null;
|
|
176
|
+
const starter = await loadStarterProfile();
|
|
177
|
+
if (starter) {
|
|
178
|
+
const { learned, areas, sessions, stage } = await summarizeStarter(starter);
|
|
179
|
+
return `no phone paired; using a starter profile from your own sessions on this computer: ` +
|
|
180
|
+
`${learned} thing${learned === 1 ? '' : 's'} learned across ${areas} area${areas === 1 ? '' : 's'}` +
|
|
181
|
+
`${sessions ? ` from ${sessions} session${sessions === 1 ? '' : 's'}` : ''} — ${stage}`;
|
|
182
|
+
}
|
|
183
|
+
return NO_PROFILE_LOG;
|
|
184
|
+
}
|
|
105
185
|
|
|
106
186
|
// Returns the vault payload:
|
|
107
187
|
// { phaedo_fingerprint, phaedo_behavioral_signals, phaedo_linguistic_profile,
|
|
@@ -131,6 +211,18 @@ function stamp(vd, cached) {
|
|
|
131
211
|
return vd;
|
|
132
212
|
}
|
|
133
213
|
|
|
214
|
+
// A starter profile is read fresh every load (live), but it did not come from a phone —
|
|
215
|
+
// `pull.js` must not say it did.
|
|
216
|
+
function markStarter(vd) {
|
|
217
|
+
try {
|
|
218
|
+
Object.defineProperty(vd, SOURCE, {
|
|
219
|
+
value: { live: true, cachedAt: null, ageMs: 0, starter: true },
|
|
220
|
+
enumerable: false, configurable: true, writable: true,
|
|
221
|
+
});
|
|
222
|
+
} catch { /* frozen object — provenance is best-effort, never fatal */ }
|
|
223
|
+
return vd;
|
|
224
|
+
}
|
|
225
|
+
|
|
134
226
|
/** Provenance of a loaded fingerprint. Absent marker → a live read. */
|
|
135
227
|
export function sourceOf(vd) {
|
|
136
228
|
const s = vd && vd[SOURCE];
|
|
@@ -179,6 +271,8 @@ export async function loadFingerprint(baseDir) {
|
|
|
179
271
|
// Only the default location may be absent quietly. A path the user NAMED that doesn't
|
|
180
272
|
// read is their mistake to see, not "no profile".
|
|
181
273
|
if (!process.env.PHAEDO_FINGERPRINT && err?.code === 'ENOENT') {
|
|
274
|
+
const starter = await loadStarterProfile();
|
|
275
|
+
if (starter) return starter;
|
|
182
276
|
throw new PhaedoError('decryption_failed', NO_PROFILE_MESSAGE);
|
|
183
277
|
}
|
|
184
278
|
throw new PhaedoError('decryption_failed', `Could not read the Phaedo profile at ${path} (${err?.code || err?.message || err}).`);
|
package/package.json
CHANGED
package/pull.js
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
9
|
import { dirname } from 'path';
|
|
10
|
-
import { loadFingerprint, sourceOf } from './fingerprint-source.js';
|
|
10
|
+
import { loadFingerprint, sourceOf, summarizeStarter } from './fingerprint-source.js';
|
|
11
11
|
|
|
12
12
|
const BASE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
|
|
14
14
|
loadFingerprint(BASE_DIR)
|
|
15
|
-
.then((vd) => {
|
|
15
|
+
.then(async (vd) => {
|
|
16
16
|
const fp = (vd && vd.phaedo_fingerprint) || {};
|
|
17
17
|
const layers = fp.layers ? Object.keys(fp.layers).length : 0;
|
|
18
18
|
const src = sourceOf(vd);
|
|
@@ -21,6 +21,15 @@ loadFingerprint(BASE_DIR)
|
|
|
21
21
|
// profile went unnoticed while the agent answered authorization questions from it. The
|
|
22
22
|
// whole point of this command is to verify the pairing works, so it has to distinguish
|
|
23
23
|
// "your phone answered" from "your phone didn't, here's an old copy".
|
|
24
|
+
if (src.starter) {
|
|
25
|
+
const { learned, areas, sessions, stage } = await summarizeStarter(vd);
|
|
26
|
+
process.stdout.write(
|
|
27
|
+
`✓ No phone paired — serving a starter profile from your own sessions on this computer:\n` +
|
|
28
|
+
` ${learned} thing(s) learned across ${areas} area(s)${sessions ? ` from ${sessions} session(s)` : ''} — ${stage}\n` +
|
|
29
|
+
` Pair the Phaedo phone app (npm run pair) to replace it with your full profile.\n`
|
|
30
|
+
);
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
24
33
|
if (src.live) {
|
|
25
34
|
process.stdout.write(
|
|
26
35
|
`✓ Read your profile from the phone — fingerprint ${fp.fingerprint_id || '(no id)'}, ${layers} layer(s).\n`
|
package/server.js
CHANGED
|
@@ -16,7 +16,7 @@ import { z } from 'zod';
|
|
|
16
16
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
17
17
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
18
18
|
|
|
19
|
-
import { loadFingerprint, sourceOf } from './fingerprint-source.js';
|
|
19
|
+
import { loadFingerprint, sourceOf, profileStatusLine } from './fingerprint-source.js';
|
|
20
20
|
import { hasPairingRecord, loadPairingRecord } from './phone-source.js';
|
|
21
21
|
import { depositDelegationSuggestions } from './delegation-sync.js';
|
|
22
22
|
import { depositReceiptsDigest, drainAndApplyMarks } from './receipts-sync.js';
|
|
@@ -892,6 +892,12 @@ async function main() {
|
|
|
892
892
|
await server.connect(transport);
|
|
893
893
|
// stdio transport owns stdout for protocol framing — never console.log here.
|
|
894
894
|
process.stderr.write('[phaedo-mcp] ready on stdio\n');
|
|
895
|
+
// Where the profile will come from, in one line, after the handshake so startup never waits
|
|
896
|
+
// on it. stderr is what Claude Desktop's MCP log shows — the one place a person debugging
|
|
897
|
+
// "why isn't Phaedo doing anything" will look. Best-effort: a failure here says nothing.
|
|
898
|
+
profileStatusLine(BASE_DIR)
|
|
899
|
+
.then((line) => { if (line) process.stderr.write(`[phaedo-mcp] ${line}\n`); })
|
|
900
|
+
.catch(() => {});
|
|
895
901
|
}
|
|
896
902
|
|
|
897
903
|
main().catch((err) => {
|
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.4",
|
|
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.4",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
}
|