phaedo-mcp 0.3.4 → 0.3.5

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 CHANGED
@@ -22,12 +22,46 @@ when your phone is offline, is encrypted too.
22
22
  - **the Phaedo phone app** — your full profile lives there. The app isn't open
23
23
  to the public yet; [phaedo.so](https://phaedo.so) has a link to hear when it is.
24
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).
26
- 2. **Node.js**, free from [nodejs.org](https://nodejs.org) (the LTS button).
25
+ profile on this computer from how you work in Claude Code (see *Install*).
26
+ 2. **Node.js**, free from [nodejs.org](https://nodejs.org) (the LTS button), for
27
+ Claude Code and other apps. Claude Desktop brings its own.
27
28
 
28
29
  With neither, there is no profile, and the server says so plainly rather than
29
30
  guessing. You can still try Phaedo with a made-up example profile.
30
31
 
32
+ ## Install
33
+
34
+ **Claude Code** (and Cowork) — run these two commands in Terminal:
35
+
36
+ ```bash
37
+ claude plugin marketplace add phaedo-labs/phaedo-mcp
38
+ ```
39
+
40
+ ```bash
41
+ claude plugin install phaedo@phaedo
42
+ ```
43
+
44
+ Then start a new Claude Code session. That's all: Phaedo loads your profile at the
45
+ start of each session, checks with you before Claude merges, deploys or publishes,
46
+ and learns from how you work. Without the phone app, it builds a starter profile from
47
+ your own sessions (for example, that you want short answers without headers). It
48
+ starts thin and grows as you work. In Claude Code, type `/phaedo:status` to see how
49
+ far along it is, and `/phaedo:pair` to pair the phone app.
50
+
51
+ **Claude Desktop** — download
52
+ [phaedo.mcpb](https://github.com/phaedo-labs/phaedo-mcp/raw/main/desktop/phaedo.mcpb)
53
+ and double-click it. Claude Desktop shows what it installs and asks you to confirm.
54
+ Node.js comes with Claude Desktop, so nothing else is needed. Desktop uses the same
55
+ profile and pairing as the Claude Code plugin on this computer.
56
+
57
+ **Other apps** (Cursor and others) — see *Use your own profile in other apps* below,
58
+ or the manual setup under *Reference*.
59
+
60
+ What Phaedo reads stays on this computer: each Claude Code session is reduced to
61
+ counts (like "asked for a shorter answer, 4 times"), and the text itself is
62
+ discarded. If you pair the phone app later, your full profile replaces the starter
63
+ one and keeps what it learned.
64
+
31
65
  ## Try it with the example profile (no phone needed)
32
66
 
33
67
  This loads a made-up person's profile, so you can see what Phaedo adds before you
@@ -66,32 +100,7 @@ example, delete the `env` line in Claude Desktop, or in Claude Code run
66
100
  `claude mcp remove -s user phaedo` and add it again without `-e …`. The server
67
101
  will then tell you there's no profile until you pair your phone.
68
102
 
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
-
94
- ## Use your own profile (with the phone app)
103
+ ## Use your own profile in other apps (with the phone app)
95
104
 
96
105
  1. **Get this folder.** On [the GitHub page](https://github.com/phaedo-labs/phaedo-mcp),
97
106
  click **Code → Download ZIP**, unzip it, and open the `mcp` folder inside.
@@ -102,7 +111,9 @@ phone app later, your full profile replaces the starter one and keeps what it le
102
111
  - Linux: run `bash installers/linux/install-phaedo-mcp.sh`.
103
112
 
104
113
  It adds Phaedo to Claude Desktop, Cursor and Claude Code — keeping your other
105
- settings, and backing each file up first — then starts pairing.
114
+ settings, and backing each file up first — then starts pairing. (In Claude Code
115
+ and Claude Desktop, the plugin and the `.mcpb` above are simpler; use one route
116
+ per app, not both.)
106
117
  3. **Pair with your phone.** Open Phaedo on your phone. On the same Wi-Fi, approve
107
118
  **"Phaedo MCP Server"** and check that the code matches the one in Terminal.
108
119
  On a different network, tap **Pair with code** and type the code Terminal shows.
@@ -7,6 +7,7 @@
7
7
  // policies or delegation signals its own way would be the third time.
8
8
 
9
9
  import { join } from 'path';
10
+ import { configDir } from './lib/config-dir.js';
10
11
  import { readFileSync, existsSync } from 'fs';
11
12
  import { loadFingerprint, loadFingerprintPreferCache } from './fingerprint-source.js';
12
13
  import { readServingClientId } from './phone-source.js';
@@ -24,7 +25,7 @@ export function buildConsultOpts(baseDir, env = process.env, { warn = (m) => pro
24
25
  // the server (mcp/consult-policies.json — what policy-editor.html produces). Zero
25
26
  // config: drop the file in and restart the client. (Portable vault policies in
26
27
  // phaedo_consult_policies always apply too, regardless of this file.)
27
- const policiesPath = env.PHAEDO_CONSULT_POLICIES || join(baseDir, 'consult-policies.json');
28
+ const policiesPath = env.PHAEDO_CONSULT_POLICIES || join(configDir(baseDir, env), 'consult-policies.json');
28
29
  if (existsSync(policiesPath)) {
29
30
  try { opts.policies = JSON.parse(readFileSync(policiesPath, 'utf8')); }
30
31
  catch (e) { warn(`[phaedo-mcp] ignoring malformed consult policies (${policiesPath}): ${e.message}\n`); }
@@ -18,6 +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
22
 
22
23
  // §4.1 protocol-identity normalization (consumer defense-in-depth, 2026-06-16).
23
24
  // A stored fingerprint from a producer that predates identity stamping (or whose
@@ -95,7 +96,7 @@ function resolveSourcePath(baseDir) {
95
96
  const env = process.env.PHAEDO_FINGERPRINT;
96
97
  if (env === SAMPLE_KEYWORD) return SAMPLE_PATH;
97
98
  if (env) return resolve(process.cwd(), env);
98
- return resolve(baseDir, 'fingerprint.json');
99
+ return resolve(configDir(baseDir), 'fingerprint.json');
99
100
  }
100
101
 
101
102
  // Read by the assistant (a tool error) and, through it, by the person — so plain words, and
@@ -108,11 +109,26 @@ export const NO_PROFILE_MESSAGE =
108
109
  'is installed (see the README). To try a made-up example profile instead, set ' +
109
110
  'PHAEDO_FINGERPRINT=sample in this server\'s settings.';
110
111
 
112
+ // Inside the Claude plugin the Claude Code hook is already installed — the plugin is the hook —
113
+ // so telling the person to install it would send them looking for a step that doesn't exist.
114
+ // Claude Code exports CLAUDE_PLUGIN_ROOT to a plugin's MCP server and hooks, and to nothing else.
115
+ export const PLUGIN_NO_PROFILE_MESSAGE =
116
+ 'No Phaedo profile is set up yet, so Phaedo has not changed how the assistant responds. It ' +
117
+ 'starts one from your own Claude Code sessions on this computer: after a session or two, it ' +
118
+ 'loads at the start of the next one. For your full profile, pair the Phaedo phone app with ' +
119
+ '/phaedo:pair (the app is not open to the public yet; https://phaedo.so to hear when it is).';
120
+
121
+ export function noProfileMessage(env = process.env) {
122
+ return env.CLAUDE_PLUGIN_ROOT ? PLUGIN_NO_PROFILE_MESSAGE : NO_PROFILE_MESSAGE;
123
+ }
124
+
111
125
  // The same news for the server's log (Claude Desktop's MCP log shows stderr), one line.
112
126
  const NO_PROFILE_LOG =
113
127
  'no profile is set up on this computer, so Phaedo will not change how the assistant responds. ' +
114
128
  'Pair the Phaedo phone app, install the Claude Code hook to start one from your own sessions, ' +
115
129
  'or set PHAEDO_FINGERPRINT=sample to try the example.';
130
+ const PLUGIN_NO_PROFILE_LOG =
131
+ 'no profile yet; one starts from your Claude Code sessions on this computer, or pair the phone app with /phaedo:pair.';
116
132
 
117
133
  // STARTER PROFILE — the phone-less path.
118
134
  //
@@ -172,7 +188,7 @@ export async function profileStatusLine(baseDir) {
172
188
  if (env === SAMPLE_KEYWORD) return 'using the made-up example profile (PHAEDO_FINGERPRINT=sample).';
173
189
  if (env) return null;
174
190
  if (hasPairingRecord(baseDir)) return null;
175
- if (existsSync(resolve(baseDir, 'fingerprint.json'))) return null;
191
+ if (existsSync(resolve(configDir(baseDir), 'fingerprint.json'))) return null;
176
192
  const starter = await loadStarterProfile();
177
193
  if (starter) {
178
194
  const { learned, areas, sessions, stage } = await summarizeStarter(starter);
@@ -180,7 +196,7 @@ export async function profileStatusLine(baseDir) {
180
196
  `${learned} thing${learned === 1 ? '' : 's'} learned across ${areas} area${areas === 1 ? '' : 's'}` +
181
197
  `${sessions ? ` from ${sessions} session${sessions === 1 ? '' : 's'}` : ''} — ${stage}`;
182
198
  }
183
- return NO_PROFILE_LOG;
199
+ return process.env.CLAUDE_PLUGIN_ROOT ? PLUGIN_NO_PROFILE_LOG : NO_PROFILE_LOG;
184
200
  }
185
201
 
186
202
  // Returns the vault payload:
@@ -273,7 +289,7 @@ export async function loadFingerprint(baseDir) {
273
289
  if (!process.env.PHAEDO_FINGERPRINT && err?.code === 'ENOENT') {
274
290
  const starter = await loadStarterProfile();
275
291
  if (starter) return starter;
276
- throw new PhaedoError('decryption_failed', NO_PROFILE_MESSAGE);
292
+ throw new PhaedoError('decryption_failed', noProfileMessage());
277
293
  }
278
294
  throw new PhaedoError('decryption_failed', `Could not read the Phaedo profile at ${path} (${err?.code || err?.message || err}).`);
279
295
  }
@@ -0,0 +1,43 @@
1
+ // Where this install keeps the subject's own config files: pairing.json, identity.json,
2
+ // consult-policies.json, escalation-policy.json and fingerprint.json.
3
+ //
4
+ // A clone or the npm package keeps them beside server.js, as it always has. A packaged install
5
+ // can't: a Claude Code plugin's folder is replaced on every update (`${CLAUDE_PLUGIN_ROOT}` is a
6
+ // per-version cache directory), and a Claude Desktop bundle's folder belongs to Claude Desktop.
7
+ // A pairing written there is lost on the next update. So both packages ship an
8
+ // `install-layout.json` marker beside server.js, and keep these files in the state dir
9
+ // (~/.phaedo-mcp, or PHAEDO_MCP_STATE_DIR) instead — the same folder for both, so pairing once
10
+ // from either covers the other.
11
+ //
12
+ // A file marker rather than an environment variable, because the plugin's hooks and its
13
+ // `pair.js` / `pull.js` run in processes that don't all get the plugin's environment (commands
14
+ // Claude runs through its Bash tool see no CLAUDE_PLUGIN_ROOT). The marker travels with the
15
+ // files. PHAEDO_CONFIG_DIR still overrides both.
16
+
17
+ import { existsSync, readFileSync } from 'node:fs';
18
+ import { homedir } from 'node:os';
19
+ import { join, resolve } from 'node:path';
20
+
21
+ export const LAYOUT_MARKER = 'install-layout.json';
22
+
23
+ const cache = new Map();
24
+
25
+ /** True when the package at `baseDir` is a packaged install (plugin or Desktop bundle). */
26
+ export function isPackagedInstall(baseDir) {
27
+ const key = resolve(baseDir);
28
+ if (!cache.has(key)) {
29
+ let packaged = false;
30
+ try {
31
+ 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);
35
+ }
36
+ return cache.get(key);
37
+ }
38
+
39
+ export function configDir(baseDir, env = process.env) {
40
+ if (env.PHAEDO_CONFIG_DIR) return resolve(env.PHAEDO_CONFIG_DIR);
41
+ if (isPackagedInstall(baseDir)) return env.PHAEDO_MCP_STATE_DIR || join(homedir(), '.phaedo-mcp');
42
+ return baseDir;
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phaedo-mcp",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "private": false,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -92,7 +92,7 @@
92
92
  "unsetup": "node install.mjs --uninstall",
93
93
  "pair": "node pair.js",
94
94
  "pull": "node pull.js",
95
- "test": "node test-smoke.mjs && node test-no-profile.mjs && node test-phone-boundary.mjs && node test-cache.mjs && node test-pairing.mjs && node test-revoke.mjs && node test-install.mjs && node test-consult.mjs && node test-consult-policy.mjs && node test-amount-bands.mjs && node test-resolver-revision.mjs && node test-ship-actions.mjs && node test-ship-hook.mjs && node test-ship-readout.mjs && node test-action-binding.mjs && node test-autonomy-promotion.mjs && node test-ap2.mjs && node test-delegation-sync.mjs && node test-delegation-relay.mjs && node test-capture-sync.mjs && node test-capture-hook.mjs && node test-stop-capture.mjs && node test-capture-local.mjs && node test-capture-a11y.mjs && node test-vendored-capture.mjs && node test-escalation.mjs && node test-domain.mjs && node test-receipts.mjs && node test-receipts-sync.mjs && node test-agreement.mjs && node test-calibration.mjs && node test-identity.mjs && node test-signing.mjs && node test-sign-request.mjs && node test-signing-audit.mjs && node test-delegation-domain-fragmentation.mjs && node test-stale-authority.mjs && node test-config-home-fold.mjs && node test-precedence-and-ceiling.mjs && node test-package-closure.mjs",
95
+ "test": "node test-smoke.mjs && node test-no-profile.mjs && node test-phone-boundary.mjs && node test-cache.mjs && node test-pairing.mjs && node test-revoke.mjs && node test-install.mjs && node test-consult.mjs && node test-consult-policy.mjs && node test-amount-bands.mjs && node test-resolver-revision.mjs && node test-ship-actions.mjs && node test-ship-hook.mjs && node test-ship-readout.mjs && node test-action-binding.mjs && node test-autonomy-promotion.mjs && node test-ap2.mjs && node test-delegation-sync.mjs && node test-delegation-relay.mjs && node test-capture-sync.mjs && node test-capture-hook.mjs && node test-stop-capture.mjs && node test-capture-local.mjs && node test-capture-a11y.mjs && node test-vendored-capture.mjs && node test-escalation.mjs && node test-domain.mjs && node test-receipts.mjs && node test-receipts-sync.mjs && node test-agreement.mjs && node test-calibration.mjs && node test-identity.mjs && node test-signing.mjs && node test-sign-request.mjs && node test-signing-audit.mjs && node test-delegation-domain-fragmentation.mjs && node test-stale-authority.mjs && node test-config-home-fold.mjs && node test-precedence-and-ceiling.mjs && node test-package-closure.mjs && node test-plugin.mjs",
96
96
  "receipts": "node receipts.js list",
97
97
  "agreement": "node agreement.js",
98
98
  "ship-readout": "node ship-readout.js",
package/pair.js CHANGED
@@ -12,14 +12,15 @@
12
12
  import { fileURLToPath } from 'url';
13
13
  import { dirname, resolve } from 'path';
14
14
  import { pair } from './pairing.js';
15
+ import { configDir } from './lib/config-dir.js';
15
16
 
16
17
  const BASE_DIR = dirname(fileURLToPath(import.meta.url));
17
18
  const identityPath = process.env.PHAEDO_IDENTITY
18
19
  ? resolve(process.cwd(), process.env.PHAEDO_IDENTITY)
19
- : resolve(BASE_DIR, 'identity.json');
20
+ : resolve(configDir(BASE_DIR), 'identity.json');
20
21
  const pairingPath = process.env.PHAEDO_PAIRING
21
22
  ? resolve(process.cwd(), process.env.PHAEDO_PAIRING)
22
- : resolve(BASE_DIR, 'pairing.json');
23
+ : resolve(configDir(BASE_DIR), 'pairing.json');
23
24
 
24
25
  pair({ identityPath, pairingPath, log: (m) => process.stdout.write(m + '\n') })
25
26
  .then(() => process.exit(0))
package/phone-source.js CHANGED
@@ -23,6 +23,7 @@
23
23
  // after the one-time pairing.
24
24
 
25
25
  import { readFile } from 'fs/promises';
26
+ import { configDir } from './lib/config-dir.js';
26
27
  import { existsSync, readFileSync } from 'fs';
27
28
  import { resolve } from 'path';
28
29
  import { Phaedo, b64uEnc, b64uDec, Capabilities } from './lib/phaedo-crypto.js';
@@ -55,7 +56,7 @@ const APPROVE_TIMEOUT_MS = 120000; // human-in-the-loop: /v1/authorize
55
56
  export async function loadPairingRecord(baseDir) {
56
57
  const path = process.env.PHAEDO_PAIRING
57
58
  ? resolve(process.cwd(), process.env.PHAEDO_PAIRING)
58
- : resolve(baseDir, 'pairing.json');
59
+ : resolve(configDir(baseDir), 'pairing.json');
59
60
  let raw;
60
61
  try {
61
62
  raw = await readFile(path, 'utf8');
@@ -78,7 +79,7 @@ export async function loadPairingRecord(baseDir) {
78
79
  // True iff a pairing record is configured (→ use the phone source over local JSON).
79
80
  export function hasPairingRecord(baseDir) {
80
81
  if (process.env.PHAEDO_PAIRING) return true;
81
- return existsSync(resolve(baseDir, 'pairing.json'));
82
+ return existsSync(resolve(configDir(baseDir), 'pairing.json'));
82
83
  }
83
84
 
84
85
  // This server's OWN paired-client identifier — the identity §10.5 `agents` scoping
@@ -96,7 +97,7 @@ export function hasPairingRecord(baseDir) {
96
97
  export function readServingClientId(baseDir) {
97
98
  const path = process.env.PHAEDO_PAIRING
98
99
  ? resolve(process.cwd(), process.env.PHAEDO_PAIRING)
99
- : resolve(baseDir, 'pairing.json');
100
+ : resolve(configDir(baseDir), 'pairing.json');
100
101
  try {
101
102
  const id = JSON.parse(readFileSync(path, 'utf8'))?.client_id;
102
103
  return typeof id === 'string' && id.trim() ? id.trim() : null;
package/server.js CHANGED
@@ -17,6 +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
21
  import { hasPairingRecord, loadPairingRecord } from './phone-source.js';
21
22
  import { depositDelegationSuggestions } from './delegation-sync.js';
22
23
  import { depositReceiptsDigest, drainAndApplyMarks } from './receipts-sync.js';
@@ -178,7 +179,7 @@ const ESCALATION_WINDOW_MS = (() => {
178
179
  // over the stakes-gated default. Local file (PHAEDO_ESCALATION_POLICY, else
179
180
  // mcp/escalation-policy.json beside the server) overrides; the portable vault policy
180
181
  // (phaedo_fingerprint.escalation_policy) is merged in per-call (loadEscalationPolicy).
181
- const ESCALATION_POLICY_PATH = process.env.PHAEDO_ESCALATION_POLICY || join(BASE_DIR, 'escalation-policy.json');
182
+ const ESCALATION_POLICY_PATH = process.env.PHAEDO_ESCALATION_POLICY || join(configDir(BASE_DIR), 'escalation-policy.json');
182
183
  let ESCALATION_POLICY_FILE = null;
183
184
  if (existsSync(ESCALATION_POLICY_PATH)) {
184
185
  try { ESCALATION_POLICY_FILE = JSON.parse(readFileSync(ESCALATION_POLICY_PATH, 'utf8')); }
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.4",
5
+ "version": "0.3.5",
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.4",
16
+ "version": "0.3.5",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  }